Exploring Property Records

SDS 236

Ben Baumer

4/17/23

Property ownership

Massachusetts Property Tax Parcels

  • Data are separated by town
  • Don’t want to have to re-download it
  • Set cache location
library(tidyverse)
cache_dir <- here::here("data")
cache_dir
[1] "/home/runner/work/sds236/sds236/data"

Towns in Massachusetts

  • Download the spreadsheet about the towns
download.file(
  url = "https://www.mass.gov/doc/massgis-parcel-data-download-links-table/download", 
  destfile = "/tmp/towns.xlsx"
)
  • Set some additional information
ma_towns <- readxl::read_excel("/tmp/towns.xlsx") |>
  janitor::clean_names() |>
  mutate(
    zip_filename = basename(shapefile_download_url),
    local_zip_filename = fs::path(cache_dir, zip_filename),
    in_cache_dir = FALSE,
    is_unzipped = FALSE
  )

Town info

ma_towns |>
  select(town_name, shapefile_download_url)
# A tibble: 351 × 2
   town_name shapefile_download_url                                             
   <chr>     <chr>                                                              
 1 ABINGTON  http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 2 ACTON     http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 3 ACUSHNET  http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 4 ADAMS     http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 5 AGAWAM    http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 6 ALFORD    http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 7 AMESBURY  http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 8 AMHERST   http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
 9 ANDOVER   http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
10 AQUINNAH  http://download.massgis.digital.mass.gov/shapefiles/l3parcels/L3_S…
# ℹ 341 more rows

Your turn: Towns

  • Download the ma_towns data frame
  • Inspect it

Download town shapefiles

  • Function will download a ZIP file for specified towns
download_town <- function(town_db, towns) {
  town_db |>
    filter(town_name %in% towns) |>
    filter(!in_cache_dir) |>
    select(x = shapefile_download_url, y = local_zip_filename) |>
    purrr::pwalk(function(x, y) download.file(url = x, destfile = y))
}
  • Sample usage
download_town(ma_towns, "NORTHAMPTON")

Unzip town parcels

  • Unzipped file names are not the same!
safe_unzip_one <- function(path_zip) {  
  if (!file.exists(path_zip)) {
    return(NA)
  }
  path_zip |>
    unzip(exdir = cache_dir) |>
    dirname() |>
    unique()
}
  • Make the function vectorized
safe_unzip <- function(path_zip) {
  path_zip |>
    purrr::map_chr(safe_unzip_one)
}

Query the cache

update_cache <- function(town_db) {
  zipped <- fs::dir_ls(cache_dir, type = "file", regexp = "*.zip")
  unzipped <- fs::dir_ls(cache_dir, type = "directory", regexp = "L3*")
  
  message("Updating town cache information...")
  town_db |>
    mutate(
      in_cache_dir = local_zip_filename %in% zipped,
      dsn = safe_unzip(local_zip_filename),
      is_unzipped = dsn %in% unzipped
    )
}

Update cache information

ma_towns |>
  filter(in_cache_dir)
# A tibble: 0 × 10
# ℹ 10 variables: town_name <chr>, town_id <dbl>, shapefile_download_url <chr>,
#   file_gdb_download_url <chr>, assessed_fiscal_year <dbl>, note <chr>,
#   zip_filename <chr>, local_zip_filename <fs::path>, in_cache_dir <lgl>,
#   is_unzipped <lgl>
ma_towns <- update_cache(ma_towns)
ma_towns |>
  filter(in_cache_dir)
# A tibble: 1 × 11
  town_name   town_id shapefile_download_url               file_gdb_download_url
  <chr>         <dbl> <chr>                                <chr>                
1 NORTHAMPTON     214 http://download.massgis.digital.mas… http://download.mass…
# ℹ 7 more variables: assessed_fiscal_year <dbl>, note <chr>,
#   zip_filename <chr>, local_zip_filename <fs::path>, in_cache_dir <lgl>,
#   is_unzipped <lgl>, dsn <chr>

Your turn: Download shapefiles

  • Download the data for Northampton and at least one other town
  • Update your cache

Read town parcel shapefiles

  • Zip files contain both shapefiles and assessor data
  • Linked by LOC_ID
library(sf)
read_town_parcels <- function(dsn) {
  if (!file.exists(dsn)) {
    return(NA)
  }
  parcel_layers <- dsn |>
    st_layers() |>
    pluck("name")
  
  parcel_layer_name <- parcel_layers |>
    str_subset("TaxPar")
  assess_layer_name <- parcel_layers |>
    str_subset("Assess")

  parcels <- dsn |>
    st_read(layer = parcel_layer_name) |>
    st_transform(4326)
  
  assess <- dsn |>
    st_read(layer = assess_layer_name)
  
  parcels |>
    left_join(assess, by = c("LOC_ID"))
}

Example: Northampton

noho <- ma_towns |>
  filter(town_name == "NORTHAMPTON") |>
  pull(dsn) |>
  read_town_parcels()
Reading layer `M214TaxPar_CY22_FY23' from data source 
  `/home/runner/work/sds236/sds236/data/L3_SHP_M214_Northampton' 
  using driver `ESRI Shapefile'
Simple feature collection with 10692 features and 12 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 97757.07 ymin: 893258.2 xmax: 110641.8 ymax: 903377.7
Projected CRS: NAD83 / Massachusetts Mainland
Reading layer `M214Assess_CY22_FY23' from data source 
  `/home/runner/work/sds236/sds236/data/L3_SHP_M214_Northampton' 
  using driver `ESRI Shapefile'

What data do we have?

noho |>
  filter(str_detect(OWNER1, "BENJAMIN S BAUMER"))
Simple feature collection with 1 feature and 48 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -72.66645 ymin: 42.32264 xmax: -72.666 ymax: 42.32314
Geodetic CRS:  WGS 84
  SHAPE_Leng SHAPE_Area  MAP_PAR_ID          LOC_ID POLY_TYPE MAP_NO SOURCE
1   146.6726   1192.173 30A-023-001 M_103870_897591       FEE   <NA> ASSESS
  PLAN_ID LAST_EDIT BND_CHK NO_MATCH TOWN_ID.x     PROP_ID BLDG_VAL LAND_VAL
1    <NA>  20120327    <NA>        N       214 30A-023-001   266800   110400
  OTHER_VAL TOTAL_VAL   FY LOT_SIZE  LS_DATE LS_PRICE USE_CODE SITE_ADDR
1      6370    377200 2023    0.257 20131213   244500      101      <NA>
  ADDR_NUM      FULL_STR LOCATION        CITY  ZIP
1       48 LEXINGTON AVE     <NA> NORTHAMPTON <NA>
                             OWNER1         OWN_ADDR OWN_CITY OWN_STATE OWN_ZIP
1 MESCON CORY E & BENJAMIN S BAUMER 48 LEXINGTON AVE FLORENCE      <NA>   01062
  OWN_CO LS_BOOK LS_PAGE REG_ID ZONING YEAR_BUILT BLD_AREA UNITS RES_AREA
1   <NA>   11544     153   <NA>   <NA>          0        0     1     1995
           STYLE STORIES NUM_ROOMS LOT_UNITS CAMA_ID TOWN_ID.y
1 1:CONVENTIONAL     1.5         0         A       0       214
                        geometry
1 MULTIPOLYGON (((-72.66623 4...

Your turn: Read town data

  • Read the property tax parcel data for Northampton…
  • …and at least one other town

What do we know?

Who are the richest landowners?

noho |>
  as_tibble() |>
  group_by(OWNER1) |>
  summarize(
    num_parcels = n(),
    acreage = sum(LOT_SIZE, na.rm = TRUE),
    value = sum(TOTAL_VAL)
  ) |>
  arrange(desc(value)) |>
  print(n = 15)
# A tibble: 9,526 × 4
   OWNER1                         num_parcels acreage     value
   <chr>                                <int>   <dbl>     <dbl>
 1 SMITH COLLEGE                          156  195.   479268360
 2 NORTHAMPTON CITY OF                    137 3317.   227874212
 3 UNITED STATES VETERANS                   1  104.    67692900
 4 NORTHAMPTON HOUSING AUTHORITY           22   38.3   58892200
 5 MASSACHUSETTS COMMONWEALTH OF           19  116.    38760310
 6 COOLEY DICKINSON HOSPITAL INC            4   43.2   21038300
 7 HATHAWAY FARMS TOWNHOMES                 1   18.0   20302700
 8 L-3 COMMUNICATIONS CORP                  1   13.6   17639800
 9 COCA COLA COMPANY THE                    1   21.8   17624200
10 OXBOW PROFESSIONAL PARK LLC              3   15.1   14189200
11 NORTHAMPTON STATE HOSPITAL               2  114.    13182300
12 MEADOWBROOK PRESERVATION                 1   26.5   12897200
13 D'AMOUR PAUL H     ET AL                 1   12.2   12571807
14 LATHROP COMMUNITY INC                    5   82.2   11674700
15 TARLIN LLOYD D & JACOB RABINOV           1    7.58  11287200
# ℹ 9,511 more rows

Land use codes

Where do the millionaires live?

noho |>
  as_tibble() |>
  filter(USE_CODE == 101, TOTAL_VAL > 1e6) |>
  select(OWNER1, LOT_SIZE, TOTAL_VAL) |>
  arrange(desc(TOTAL_VAL)) |>
  print(n = 15)
# A tibble: 36 × 3
   OWNER1                                  LOT_SIZE TOTAL_VAL
   <chr>                                      <dbl>     <dbl>
 1 SPROULL ROBERT F & LEE S AND               5.7     2073100
 2 BENNETT ELIZABETH FRASER                   1.43    1684500
 3 KELLEY, JOHN E & KATRINA FRALICK KELLEY    8.05    1591100
 4 SIERROS KONSTANTINOS N &                   6.65    1468400
 5 GRADY TIMOTHY & JESSICA ANNE               0.916   1464700
 6 JONAS ROBERT A &                           0.19    1344100
 7 WEINSTEIN PETER J & KATHERINE J            0.294   1333600
 8 WILLIAMS BARBARA S &                       0.308   1274400
 9 SMITH DIANNA G TRUSTEE                     8.62    1272900
10 SHARPE ALAN NATHANSON                      6.18    1262600
11 NIELDS KATRYNA &                           1.62    1234900
12 SILBERSTEIN HARVEY & JULIE                 0.863   1222900
13 GREEN SCOTT T &                            2.05    1208800
14 WIRTH PETER K & MICHELLE L                13.7     1187500
15 EPSTEIN NOAH J & RACHEL B                  0.845   1162700
# ℹ 21 more rows

Make a map

library(leaflet)
millionaires <- noho |>
  filter(USE_CODE == 101, TOTAL_VAL > 1e6) |>
  mutate(
    popup = paste0(
      "ID: ", LOC_ID, "</br>", OWNER1, "</br>",
      "Lot size: ", LOT_SIZE, " acres</br>",
      "Assessed value: $", 
      format(round(TOTAL_VAL / 1000), big.mark = ",", scientific = FALSE), "k"
    )
  ) |>
  leaflet() |>
  addTiles() |>
  addPolygons(weight = 1, popup = ~popup)

Million dollar homes

millionaires

Eric Suher

Nu-Way

Affordable housing?

Empty building lots

noho |>
  filter(USE_CODE == 101, LAND_VAL > 0, BLDG_VAL == 0) |>  
  mutate(
    popup = paste0(
      "ID: ", LOC_ID, "</br>", OWNER1, "</br>",
      "Lot size: ", LOT_SIZE, " acres</br>",
      "Assessed value: $", 
      format(round(TOTAL_VAL / 1000), big.mark = ",", scientific = FALSE), "k"
    )
  ) |>
  leaflet() |>
  addTiles() |>
  addPolygons(weight = 1, popup = ~popup)

Your turn: brainstorm

  • Brainstorm some ideas for investigation with property tax data