# Load required packages
library(tidyverse)
library(tidycensus)
library(janitor)
library(sf)
library(tigris)
library(scales)
library(ggnewscale)
library(patchwork)
library(RColorBrewer)
library(viridisLite)
library(units)
library(knitr)
library(caret)Vacancy and Land Use - Sharswood
URBS 4000: Urban Studies Thesis
Project Title: Mixed by Design
Part 1: Vacancy Data
Vacant Property Indicators from Open Data Philly
- Data Source: https://opendataphilly.org/datasets/vacant-property-indicators/
- Date Downloaded: Oct 20, 2025
- Department: Office of Innovation and Technology in cooperation with Department of Licenses and Inspections, Office of Property Assessment, Philadelphia Land Bank, and Philadelphia Water Department.
- Description: The location of properties across Philadelphia that are likely to be a vacant lot or vacant building based on an assessment of City of Philadelphia administrative datasets.
- Types of GeoJSON Files: Buildings, Land, and Points
Step 1: Load Raw Data as csv files
- Philadelphia census tracts, block groups, and neighborhood boundaries
# Load spatial data
census_tracts <- tracts(state = "PA", county = "Philadelphia", year = 2020, class = "sf", cb = TRUE, progress = FALSE)
block_grps <- block_groups(state = "PA", county = "Philadelphia", year = 2020, class = "sf",cb = TRUE, progress = FALSE)
# Filter Block Groups for Sharswood
ids <- c("421010138001","421010138002","421010139001",
"421010139002","421010139003")
sharswood_bg_sf <- block_grps |>
filter(GEOID %in% ids) |>
st_transform(2272) # PA State Plane (in US Survey Feet)Step 2: Get Philadelphia Demographic Data using tidycensus
# Load all available variables for ACS 5-year 2022
acs_vars_2022 <- load_variables(2022, "acs5", cache = TRUE)# Get tract-level demographic data from 2022 ACS 5-Yr Estimates for Philadelphia
phl_tract_2022_data <- get_acs(
geography = "tract",
variables = c(
total_pop = "B01003_001",
median_income = "B19013_001",
poverty = "B17001_001",
White = "B03002_003",
Black = "B03002_004",
Hispanic = "B03002_012"
),
state = "PA",
county = "Philadelphia",
year = 2022,
survey = "acs5",
output = "wide",
geometry = TRUE
)
# Clean the county names to remove state name and "County"
phl_tract_2022_clean <- phl_tract_2022_data |>
separate(
NAME,
into = c("tract_name", "county_name", "state_name"),
sep = "; "
) |>
mutate(
tract_name = str_remove(tract_name, "Census Tract "),
county_name = str_remove(county_name, " County")
)
phl_tract_2022_summary <- phl_tract_2022_clean |>
select(GEOID, tract_name, county_name, total_popE, median_incomeE)Step 3: Filter Data for Sharswood
# Filter Census Tracts for Sharswood
sharswood_2022_tract_sf <- phl_tract_2022_summary |>
filter(tract_name %in% c("138", "139"))
sharswood_2022_tract <- sharswood_2022_tract_sf |>
st_drop_geometry()Sharswood Land Parcels
- Data Source: https://opendataphilly.org/datasets/department-of-records-property-parcels/
- Date Downloaded: Oct 19, 2025
- Department: Department of Records
- File Type: GeoJSON
- Description: Department of Records (DOR) boundaries of real estate property parcels derived from legal recorded deed documents
# Filter Property Parcels for Sharswood
prop_parcels <- st_read("data/DOR_Parcel.geojson") |>
st_transform(st_crs(sharswood_bg_sf))
sharswood_parcels <- st_filter(prop_parcels, sharswood_bg_sf, .predicate = st_intersects)
st_write(
sharswood_parcels, "data/sharswood_parcels.geojson",
driver = "GeoJSON",
delete_dsn = TRUE,
layer_options = c("RFC7946=YES","COORDINATE_PRECISION=6","WRITE_BBOX=NO")
)Vacancy Data for Sharswood
# Spatially Filter Vacancy Data for Sharswood
vacant_bldgs <- st_read("data/Vacant_Indicators_Bldg.geojson")
vacant_land <- st_read("data/Vacant_Indicators_Land.geojson")
vacant_pts <- st_read("data/Vacant_Indicators_Points.geojson")
sharswood_land_parcels <- st_read("data/sharswood_parcels.geojson")
sharswood_zip <- 19121
sharswood_vacant_bldgs <- vacant_bldgs |>
st_transform(st_crs(sharswood_bg_sf)) |>
filter(zipcode == sharswood_zip) |>
mutate(bldg_desc = str_squish(bldg_desc)) |>
filter(
!is.na(bldg_desc), # filter out NAs
!str_detect(bldg_desc, regex("\\bschool\\b", ignore_case = TRUE)), # filter out Girard College
!str_detect(bldg_desc, regex("\\bland\\b", ignore_case = TRUE)) # filter out Land
) |>
mutate(
# Remove "MASONRY" and tidy up
bldg_desc_clean = str_squish(str_remove(bldg_desc,
regex("MASONRY", ignore_case = TRUE))),
# Create broader categories
bldg_type = case_when(
str_detect(bldg_desc_clean,
regex("APT\\s*2-4", ignore_case = TRUE)) ~ "Apt 2-4 units",
str_detect(bldg_desc_clean,
regex("ROW\\s*\\d", ignore_case = TRUE)) ~ "Rowhouse",
str_detect(bldg_desc_clean,
regex("ROW\\s*CONV", ignore_case = TRUE)) ~ "Rowhouse Convert/Apt",
str_detect(bldg_desc_clean,
regex("SEMI", ignore_case = TRUE)) ~ "Semi-Detached",
str_detect(bldg_desc_clean,
regex("STORE", ignore_case = TRUE)) ~ "Store",
str_detect(bldg_desc_clean,
regex("STR/OFF\\+APTS", ignore_case = TRUE)) ~ "Store/Office + Apts",
str_detect(bldg_desc_clean,
regex("HSE\\s+WORSHIP", ignore_case = TRUE)) ~ "Hse of Worship",
TRUE ~ "Other")
) |>
st_filter(sharswood_bg_sf, .predicate = st_intersects)
sharswood_vacant_land <- vacant_land |>
st_transform(st_crs(sharswood_bg_sf)) |>
filter(ZIPCODE == sharswood_zip,
BLDG_DESC == "VAC LAND RES < ACRE") |>
st_filter(sharswood_bg_sf, .predicate = st_intersects)
sharswood_vacant_pts <- vacant_pts |>
st_transform(st_crs(sharswood_bg_sf)) |>
filter(ZIPCODE == sharswood_zip) |>
st_filter(sharswood_bg_sf, .predicate = st_within)# Vacancy in Table Format
sharswood_vacant_bldgs_df <- sharswood_vacant_bldgs |>
st_drop_geometry() |>
select(objectid, address, owner1, bldg_desc, bldg_type, zoningbasedistrict)
sharswood_vacant_land_df <- sharswood_vacant_land |>
st_drop_geometry() |>
select(OBJECTID, ADDRESS, OWNER1, BLDG_DESC, ZONINGBASEDISTRICT)
sharswood_vacant_pts_df <- sharswood_vacant_pts |>
st_drop_geometry() |>
select(OBJECTID, ADDRESS, OWNER1, BLDG_DESC, ZONINGBASEDISTRICT)# Count Vacant Land by Owner
sharswood_vacant_land_owner <- sharswood_vacant_land |>
mutate(owner_std = str_to_title(str_squish(OWNER1))) |>
add_count(owner_std, name = "owner_n") |>
mutate(
owner_grp = case_when(
is.na(owner_std) | owner_std == "" ~ "Unknown",
owner_n == 1 ~ "Individual Owners",
TRUE ~ owner_std
)
) |>
select(-owner_n)
sharswood_vacant_land_owner_df <- sharswood_vacant_land_owner |>
st_drop_geometry() |>
select(OBJECTID, ADDRESS, OWNER1, BLDG_DESC, owner_grp, ZONINGBASEDISTRICT)Step 4: Plot Maps of Vacant Buildings, Land, and Points in Sharswood
# Build palettes that expand to any number of categories
n_bldg <- dplyr::n_distinct(sharswood_vacant_bldgs$bldg_type)
# Blue → teal → green
bldg_cols <- colorRampPalette(
c("#084081", "#2b8cbe", "#4eb3d3", "#7bccc4", "#43a2ca", "#2ca25f", "#238b45", "#006d2c")
)(n_bldg)
n_land <- dplyr::n_distinct(sharswood_vacant_land$ZONINGBASEDISTRICT)
land_cols <- colorRampPalette(brewer.pal(3, "Reds"))(n_land)
ggplot() +
geom_sf(data = sharswood_land_parcels, fill = NA, color = "gray60", linewidth = 0.4) +
geom_sf(data = sharswood_vacant_bldgs,
aes(fill = bldg_type),
color = "grey10", linewidth = 0.08) +
scale_fill_manual(values = bldg_cols, name = "Vacant Buildings - Type") +
guides(fill = guide_legend(order = 1)) +
new_scale_fill() +
geom_sf(data = sharswood_vacant_land,
aes(fill = ZONINGBASEDISTRICT),
color = "grey30", linewidth = 0.2, alpha = 0.65) +
scale_fill_manual(values = land_cols, name = "Vacant Land - Zoning") +
guides(fill = guide_legend(order = 2)) +
labs(
title = "Vacant Land and Buildings in Sharswood (2025)",
subtitle = "Data Source: Philadelphia Office of Innovation and Technology",
caption = "Vacant Land updated 3/24/25; Vacant Buildings updated 7/7/25",
) +
theme_void() +
theme(
legend.position = "right",
legend.title = element_text(size = 11, face = "bold"),
legend.text = element_text(size = 9),
plot.title = element_text(face = "bold"),
plot.caption = element_text(hjust = 0, size = 9, margin = margin(t = 6))
)Zoning Codes:
Source: Philadelphia Zoning Code Quick Guide (Dec 2022), https://www.phila.gov/media/20220909084529/ZONING-QUICK-GUIDE_PCPC_9_9_22.pdf
- CMX-2: Commercial Mixed-Use (small-scale)
- RM-1: Residential Multifamily (low-rise)
- RSA-5: Residential Single-Family Attached (rowhomes)
# Vacant Land by Ownership
n_land_owner <- dplyr::n_distinct(sharswood_vacant_land_owner$owner_grp)
land_owner_cols <- colorRampPalette(brewer.pal(8, "Set1"))(n_land_owner)
ggplot() +
geom_sf(data = sharswood_land_parcels, fill = NA, color = "gray60", linewidth = 0.4) +
geom_sf(data = sharswood_vacant_land_owner,
aes(fill = owner_grp),
color = "grey30", linewidth = 0.2, alpha = 0.65) +
scale_fill_manual(values = land_owner_cols, name = "Vacant Land - Ownership") +
guides(fill = guide_legend(order = 2)) +
labs(
title = "Vacant Land in Sharswood by Ownership (2025)",
subtitle = "Data Source: Philadelphia Office of Innovation and Technology",
caption = "Vacant Land updated 3/24/25"
) +
theme_void() +
theme(
legend.position = "right",
legend.title = element_text(size = 11, face = "bold"),
legend.text = element_text(size = 9),
plot.title = element_text(face = "bold"),
plot.caption = element_text(hjust = 0, size = 9, margin = margin(t = 6))
)Part 2: Land-Use Data
- Data Source: https://opendataphilly.org/datasets/land-use/
- Date Downloaded: Nov 12, 2025
- Department: Philadelphia Department of Planning and Development
- File Type: GeoJSON
- Metadata Link: https://metadata.phila.gov/#home/datasetdetails/5543864420583086178c4e74/representationdetails/6679cce2eae40a02d29eb194/
# Color Palette from PCPC
landuse_pal <- c(
"Residential Low" = "#ffffcc", # 255,255,204
"Residential Medium" = "#f5ff12", # 245,255,18
"Residential High" = "#ffaa00", # 255,170,0
"Commercial Consumer" = "#e60000", # 230,0,0
"Commercial Business/Professional" = "#a80000", # 168,0,0
"Mixed Commercial/Residential" = "#e89579", # 232,149,121
"Industrial" = "#8c00ff", # 140,0,255
"Civic/Institution" = "#0070ff", # 0,112,255
"Transportation" = "#cccccc", # 204,204,204
"Culture/Recreation" = "#00ffe6", # 0,255,230
"Active Recreation" = "#9bccb3", # 155,204,179
"Park/Open Space" = "#63cc00", # 99,204,0
"Cemetery" = "#6b7e93", # 107,126,147
"Water" = "#bfe8ff", # 191,232,255
"Vacant" = "#a87000", # 168,112,0
"Other/Unknown" = "#d2d2d2" # 210,210,210
)