From 1ef0f9ee58dc594e2f5783bcdd7f5bc44caa50b3 Mon Sep 17 00:00:00 2001 From: Nissim Lebovits <111617674+nlebovits@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:12:09 -0500 Subject: [PATCH] Built site for gh-pages --- .nojekyll | 2 +- assets/permits_per_year.png | Bin 0 -> 43663 bytes final.html | 754 ++++++++++++++++++------------------ index.html | 754 ++++++++++++++++++------------------ 4 files changed, 771 insertions(+), 739 deletions(-) create mode 100644 assets/permits_per_year.png diff --git a/.nojekyll b/.nojekyll index 4be31a9..6db9be4 100644 --- a/.nojekyll +++ b/.nojekyll @@ -1 +1 @@ -140c791b \ No newline at end of file +eb4d990e \ No newline at end of file diff --git a/assets/permits_per_year.png b/assets/permits_per_year.png new file mode 100644 index 0000000000000000000000000000000000000000..2cd745a8b199a5b61cf03d11b3a9dcfbd5098231 GIT binary patch literal 43663 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 suppressMessages( tmap_animation(tm, "assets/permits_animation.gif", delay = 50) -) +) + +bar_graph <- ggplot(building_permits %>% filter(!year %in% c(2024)), aes(x = as.factor(year))) + + geom_bar(fill = palette[1], color = NA, alpha = 0.7) + + labs(title = "Permits per Year", + y = "Count") + + theme_minimal() + + theme(axis.title.x = element_blank(), + aspect.ratio = .75) + +# Ensure the 'assets' directory exists +if (!dir.exists("assets")) { + dir.create("assets") +} + +# Save the plot +ggsave(bar_graph, filename = "assets/permits_per_year.png") +
+
+

Philadelphia Building Permits, 2013 - 2023
-
-
-Show the code -
ggplot(building_permits %>% filter(!year %in% c(2024)), aes(x = as.factor(year))) +
-  geom_bar(fill = palette[1], color = NA, alpha = 0.7) +
-  labs(title = "Permits per Year",
-       y = "Count") +
-  theme_minimal() +
-  theme(axis.title.x = element_blank(),
-        aspect.ratio = .75)
-
-
-

+
+
+

+

We note a significant uptick in new construction permits as we approach 2021, followed by a sharp decline. It is generally acknowledged that this trend was due to the expiration of a tax abatement program for developers.

@@ -9365,36 +9375,36 @@

Show the code -
perms_x_dist <- st_join(building_permits, council_dists)
-
-perms_x_dist_sum <- perms_x_dist %>%
-                  st_drop_geometry() %>%
-                  group_by(DISTRICT, year) %>%
-                  summarize(permits_count = n())
-
-perms_x_dist_mean = perms_x_dist_sum %>%
-                      group_by(year) %>%
-                      summarize(permits_count = mean(permits_count)) %>%
-                      mutate(DISTRICT = "Average")
-
-perms_x_dist_sum <- bind_rows(perms_x_dist_sum, perms_x_dist_mean) %>%
-                        mutate(color = ifelse(DISTRICT != "Average", 0, 1))
-
-ggplotly(
-ggplot(perms_x_dist_sum %>% filter(year > 2013, year < 2024), aes(x = year, y = permits_count, color = as.character(color), group = interaction(DISTRICT, color))) +
-  geom_line(lwd = 0.7) +
-  labs(title = "Permits per Year by Council District",
-       y = "Total Permits") +
-  # facet_wrap(~DISTRICT) +
-  theme_minimal() +
-  theme(axis.title.x = element_blank(),
-        legend.position = "none") +
-  scale_color_manual(values = c(palette[5], palette[1]))
-)
+
perms_x_dist <- st_join(building_permits, council_dists)
+
+perms_x_dist_sum <- perms_x_dist %>%
+                  st_drop_geometry() %>%
+                  group_by(DISTRICT, year) %>%
+                  summarize(permits_count = n())
+
+perms_x_dist_mean = perms_x_dist_sum %>%
+                      group_by(year) %>%
+                      summarize(permits_count = mean(permits_count)) %>%
+                      mutate(DISTRICT = "Average")
+
+perms_x_dist_sum <- bind_rows(perms_x_dist_sum, perms_x_dist_mean) %>%
+                        mutate(color = ifelse(DISTRICT != "Average", 0, 1))
+
+ggplotly(
+ggplot(perms_x_dist_sum %>% filter(year > 2013, year < 2024), aes(x = year, y = permits_count, color = as.character(color), group = interaction(DISTRICT, color))) +
+  geom_line(lwd = 0.7) +
+  labs(title = "Permits per Year by Council District",
+       y = "Total Permits") +
+  # facet_wrap(~DISTRICT) +
+  theme_minimal() +
+  theme(axis.title.x = element_blank(),
+        legend.position = "none") +
+  scale_color_manual(values = c(palette[5], palette[1]))
+)
-
- +
+

@@ -9404,26 +9414,26 @@

Show the code -
permits_bg_long <- permits_bg %>%
-                    filter(!year %in% c(2024)) %>%
-                    st_drop_geometry() %>%
-                    pivot_longer(
-                      cols = c(starts_with("lag")),
-                      names_to = "Variable",
-                      values_to = "Value"
-                    )
-
-
-ggscatter(permits_bg_long, x = "permits_count", y = "Value", facet.by = "Variable",
-   add = "reg.line",
-   add.params = list(color = palette[3]),
-   conf.int = TRUE, alpha = 0.2
-   ) + 
-  stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01, size = 3) +
-  labs(title = "Correlation of `permits_count` and Engineered Features",
-       x = "Value",
-       y = "Permits Count") +
-  theme_minimal()
+
permits_bg_long <- permits_bg %>%
+                    filter(!year %in% c(2024)) %>%
+                    st_drop_geometry() %>%
+                    pivot_longer(
+                      cols = c(starts_with("lag")),
+                      names_to = "Variable",
+                      values_to = "Value"
+                    )
+
+
+ggscatter(permits_bg_long, x = "permits_count", y = "Value", facet.by = "Variable",
+   add = "reg.line",
+   add.params = list(color = palette[3]),
+   conf.int = TRUE, alpha = 0.2
+   ) + 
+  stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01, size = 3) +
+  labs(title = "Correlation of `permits_count` and Engineered Features",
+       x = "Value",
+       y = "Permits Count") +
+  theme_minimal()

@@ -9437,28 +9447,28 @@

<
Show the code -
med_inc <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "med_inc", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Med. Inc. ($)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Median Income")
-  
-race <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "percent_nonwhite", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Nonwhite (%)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Race")
-  
-rent_burd <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "ext_rent_burden", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Rent Burden (%)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Extreme Rent Burden")
-  
-tmap_arrange(med_inc, race, rent_burd)
+
med_inc <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "med_inc", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Med. Inc. ($)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Median Income")
+  
+race <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "percent_nonwhite", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Nonwhite (%)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Race")
+  
+rent_burd <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "ext_rent_burden", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Rent Burden (%)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Extreme Rent Burden")
+  
+tmap_arrange(med_inc, race, rent_burd)

@@ -9480,25 +9490,25 @@

Show the code -
corr_vars <- c("total_pop",
-               "med_inc",
-               "percent_nonwhite",
-               "percent_renters",
-               "rent_burden",
-               "ext_rent_burden")
-
-corr_dat <- permits_bg %>% select(all_of(corr_vars), permits_count) %>% select(where(is.numeric)) %>% st_drop_geometry() %>% unique() %>% na.omit()
-
-corr <- round(cor(corr_dat), 2)
-p.mat <- cor_pmat(corr_dat)
-
-ggcorrplot(corr, p.mat = p.mat, hc.order = FALSE,
-    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
-  annotate(
-  geom = "rect",
-  xmin = .5, xmax = 7.5, ymin = 6.5, ymax = 7.5,
-  fill = "transparent", color = "red", alpha = 0.5
-)
+
corr_vars <- c("total_pop",
+               "med_inc",
+               "percent_nonwhite",
+               "percent_renters",
+               "rent_burden",
+               "ext_rent_burden")
+
+corr_dat <- permits_bg %>% select(all_of(corr_vars), permits_count) %>% select(where(is.numeric)) %>% st_drop_geometry() %>% unique() %>% na.omit()
+
+corr <- round(cor(corr_dat), 2)
+p.mat <- cor_pmat(corr_dat)
+
+ggcorrplot(corr, p.mat = p.mat, hc.order = FALSE,
+    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
+  annotate(
+  geom = "rect",
+  xmin = .5, xmax = 7.5, ymin = 6.5, ymax = 7.5,
+  fill = "transparent", color = "red", alpha = 0.5
+)

@@ -9511,13 +9521,13 @@

Show the code -
ols <- lm(permits_count ~ ., data = permits_bg %>% filter(year < 2024) %>% select(-c(mapname, geoid10, year)) %>% st_drop_geometry())
-vif(ols) %>%
-  data.frame() %>%
-  clean_names() %>%
-  select(-df) %>%
-  arrange(desc(gvif)) %>%
-  kablerize()
+
ols <- lm(permits_count ~ ., data = permits_bg %>% filter(year < 2024) %>% select(-c(mapname, geoid10, year)) %>% st_drop_geometry())
+vif(ols) %>%
+  data.frame() %>%
+  clean_names() %>%
+  select(-df) %>%
+  arrange(desc(gvif)) %>%
+  kablerize()
@@ -9967,15 +9977,15 @@

Show the code -
ggplot(permits_bg %>% st_drop_geometry %>% filter(!year %in% c(2024)), aes(x = permits_count)) +
-  geom_histogram(fill = palette[1], color = NA, alpha = 0.7) +
-  labs(title = "Permits per Block Group per Year",
-       subtitle = "Log-Transformed",
-       y = "Count") +
-  scale_x_log10() +
-  facet_wrap(~year) +
-  theme_minimal() +
-  theme(axis.title.x = element_blank())
+
ggplot(permits_bg %>% st_drop_geometry %>% filter(!year %in% c(2024)), aes(x = permits_count)) +
+  geom_histogram(fill = palette[1], color = NA, alpha = 0.7) +
+  labs(title = "Permits per Block Group per Year",
+       subtitle = "Log-Transformed",
+       y = "Count") +
+  scale_x_log10() +
+  facet_wrap(~year) +
+  theme_minimal() +
+  theme(axis.title.x = element_blank())

@@ -9989,69 +9999,75 @@

Show the code -
lisa <- permits_bg %>% 
-  filter(year == 2023) %>%
-  mutate(nb = st_contiguity(geometry),
-                         wt = st_weights(nb),
-                         permits_lag = st_lag(permits_count, nb, wt),
-          moran = local_moran(permits_count, nb, wt)) %>% 
-  tidyr::unnest(moran) %>% 
-  mutate(pysal = ifelse(p_folded_sim <= 0.1, as.character(pysal), NA),
-         hotspot = case_when(
-           pysal == "High-High" ~ "Yes",
-           TRUE ~ "No"
-         ))
-
-# 
-# palette <- c("High-High" = "#B20016", 
-#              "Low-Low" = "#1C4769", 
-#              "Low-High" = "#24975E", 
-#              "High-Low" = "#EACA97")
-
-morans_i <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "Moran's I"),
-  "Local Moran's I (2023)")
-
-p_value <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "P-Value"),
-  "Moran's I P-Value (2023)")
-
-sig_hotspots <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(mono_5_green[1], mono_5_green[5]), textNA = "Not a Hotspot", title = "Hotspot?"),
-  "Construction Hotspots (2023)")
-
-tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
+
lisa <- permits_bg %>% 
+  filter(year == 2023) %>%
+  mutate(nb = st_contiguity(geometry),
+                         wt = st_weights(nb),
+                         permits_lag = st_lag(permits_count, nb, wt),
+          moran = local_moran(permits_count, nb, wt)) %>% 
+  tidyr::unnest(moran) %>% 
+  mutate(pysal = ifelse(p_folded_sim <= 0.1, as.character(pysal), NA),
+         hotspot = case_when(
+           pysal == "High-High" ~ "Yes",
+           TRUE ~ "No"
+         ))
+
+# 
+# palette <- c("High-High" = "#B20016", 
+#              "Low-Low" = "#1C4769", 
+#              "Low-High" = "#24975E", 
+#              "High-Low" = "#EACA97")
+
+morans_i <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "Moran's I") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Local Moran's I (2023)")
+
+p_value <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "P-Value") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Moran's I P-Value (2023)")
+
+sig_hotspots <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(mono_5_green[1], mono_5_green[5]), textNA = "Not a Hotspot", title = "Hotspot?") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Construction Hotspots (2023)")
+
+tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
-

+

Emergeging hotspots…? If I can get it to work.

Show the code -
# Prepare the data
-permits_data <- permits_bg %>%
-  filter(year < 2024,
-         year > 2012) %>%
-  select(permits_count, geoid10, year) %>%
-  na.omit()
-
-# Create spacetime object
-stc <- as_spacetime(permits_data,
-                    .loc_col = "geoid10",
-                    .time_col = "year")
-
-# Run emerging hotspot analysis
-ehsa <- emerging_hotspot_analysis(
-  x = stc,
-  .var = "permits_count",
-  k = 1,
-  nsim = 25
-)
-
-# Analyze the result
-count(ehsa, classification)
+
# Prepare the data
+permits_data <- permits_bg %>%
+  filter(year < 2024,
+         year > 2012) %>%
+  select(permits_count, geoid10, year) %>%
+  na.omit()
+
+# Create spacetime object
+stc <- as_spacetime(permits_data,
+                    .loc_col = "geoid10",
+                    .time_col = "year")
+
+# Run emerging hotspot analysis
+ehsa <- emerging_hotspot_analysis(
+  x = stc,
+  .var = "permits_count",
+  k = 1,
+  nsim = 25
+)
+
+# Analyze the result
+count(ehsa, classification)
       classification    n
@@ -10069,17 +10085,17 @@ 

Show the code -
suppressMessages(
-ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: OLS",
-       subtitle = "2022 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: OLS",
+       subtitle = "2022 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10088,21 +10104,21 @@

Show the code -
ols_preds_map <- tmap_theme(tm_shape(ols_preds) +
-        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: OLS")
-
-ols_error_map <- tmap_theme(tm_shape(ols_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: OLS")
-
-tmap_arrange(ols_preds_map, ols_error_map)
+
ols_preds_map <- tmap_theme(tm_shape(ols_preds) +
+        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: OLS")
+
+ols_error_map <- tmap_theme(tm_shape(ols_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: OLS")
+
+tmap_arrange(ols_preds_map, ols_error_map)

@@ -10115,17 +10131,17 @@

Show the code -
suppressMessages(
-ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: RF",
-       subtitle = "2022 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: RF",
+       subtitle = "2022 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10134,21 +10150,21 @@

Show the code -
test_preds_map <- tmap_theme(tm_shape(rf_test_preds) +
-        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: RF Test")
-
-test_error_map <- tmap_theme(tm_shape(rf_test_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: RF Test") 
-
-tmap_arrange(test_preds_map, test_error_map)
+
test_preds_map <- tmap_theme(tm_shape(rf_test_preds) +
+        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: RF Test")
+
+test_error_map <- tmap_theme(tm_shape(rf_test_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: RF Test") 
+
+tmap_arrange(test_preds_map, test_error_map)

@@ -10159,43 +10175,43 @@

5 Model Testing

-

Model training, validation, and testing involved three steps. First, we partitioned our data into training, validation, and testing sets. We used data from 2013 through 2021 for initial model training. Next, we evaluated our models’ ability to accurately predict 2022 construction permits using our validation set, which consisted of all permits in 2022. We carried out additional feature engineering and model tuning, iterating based on the results of these training and testing splits. We sought to minimize both the mean absolute error (MAE) of our best model and the distribution of absolute error. Finally, when we were satisfied with the results of our best model, we evaluated it again by training it on all data from 2013 through 2022 and validating it on data from 2023 (all but the last two weeks, which we consider negligible for our purposes), which the model had never “seen” before. As Kuhn and Johnson write in Applied Predictive Modeling (2013), “Ideally, the model should be evaluated on samples that were not used to build or fine-tune the model, so that they provide an unbiased sense of model effectiveness.”

+

Model training, validation, and testing involved three steps. First, we partitioned our data into training, validation, and testing sets. We used data from 2013 through 2021 for initial model training. Next, we evaluated our models’ ability to accurately predict 2022 construction permits using our validation set, which consisted of all permits in 2022. We carried out additional feature engineering and model tuning, iterating based on the results of these training and testing splits. We sought to minimize both the mean absolute error (MAE) of our best model and the distribution of absolute error. Finally, when we were satisfied with the results of our best model, we evaluated it again by training it on all data from 2013 through 2022 and validating it on data from 2023 (all but the last two weeks, which we consider negligible for our purposes), which the model had never “seen” before. As Kuhn and Johnson write in Applied Predictive Modeling (2013), “Ideally, the model should be evaluated on samples that were not used to build or fine-tune the model, so that they provide an unbiased sense of model effectiveness.” (Code for all of these steps is available on GitHub.)

Again, testing confirms the strength of our model; based on 2023 data, our random forest model produces a MAE of 2.19. We note again that the range of model error is relatively narrow. Generally, we see that where the model predicts there to be more permits, there is also higher error. This spatial trend is also seen in the distribution of absolute errors clustering in a handful of block groups with high permit counts.

Show the code -
val_preds_map <- tmap_theme(tm_shape(rf_val_preds) +
-        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: RF Validate")
-
-val_error_map <- tmap_theme(tm_shape(rf_val_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: RF Validate")
-
-tmap_arrange(val_preds_map, val_error_map)
+
val_preds_map <- tmap_theme(tm_shape(rf_val_preds) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: RF Validate")
+
+val_error_map <- tmap_theme(tm_shape(rf_val_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: RF Validate")
+
+tmap_arrange(val_preds_map, val_error_map)

Show the code -
suppressMessages(
-ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: RF",
-       subtitle = "2023 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: RF",
+       subtitle = "2023 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10209,24 +10225,24 @@

Show the code -
rf_val_preds_long <- rf_val_preds %>%
-  pivot_longer(cols = c(rent_burden, percent_nonwhite, total_pop, med_inc),
-               names_to = "variable", values_to = "value") %>%
-  mutate(variable = case_when(
-    variable == "med_inc" ~ "Median Income ($)",
-    variable == "percent_nonwhite" ~ "Nonwhite (%)",
-    variable == "rent_burden" ~ "Rent Burden (%)",
-    TRUE ~ "Total Pop."
-  ))
-
-ggplot(rf_val_preds_long, aes(x = value, y = abs_error)) +
-  geom_point(alpha = 0.2) +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  facet_wrap(~ variable, scales = "free_x") +
-  labs(title = "Generalizability of Absolute Error",
-       x = "Value",
-       y = "Absolute Error") +
-  theme_minimal()
+
rf_val_preds_long <- rf_val_preds %>%
+  pivot_longer(cols = c(rent_burden, percent_nonwhite, total_pop, med_inc),
+               names_to = "variable", values_to = "value") %>%
+  mutate(variable = case_when(
+    variable == "med_inc" ~ "Median Income ($)",
+    variable == "percent_nonwhite" ~ "Nonwhite (%)",
+    variable == "rent_burden" ~ "Rent Burden (%)",
+    TRUE ~ "Total Pop."
+  ))
+
+ggplot(rf_val_preds_long, aes(x = value, y = abs_error)) +
+  geom_point(alpha = 0.2) +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  facet_wrap(~ variable, scales = "free_x") +
+  labs(title = "Generalizability of Absolute Error",
+       x = "Value",
+       y = "Absolute Error") +
+  theme_minimal()

@@ -10236,14 +10252,14 @@

Show the code -
suppressMessages(
-  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
-    geom_boxplot(fill = NA, color = palette[3], alpha = 0.7) +
-    labs(title = "MAE by Council District",
-         y = "Mean Absolute Error",
-         x = "Council District") +
-    theme_minimal()
-)
+
suppressMessages(
+  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
+    geom_boxplot(fill = NA, color = palette[3], alpha = 0.7) +
+    labs(title = "MAE by Council District",
+         y = "Mean Absolute Error",
+         x = "Council District") +
+    theme_minimal()
+)

@@ -10256,29 +10272,29 @@

Show the code -
filtered_zoning <- zoning %>%
-                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
-                            CODE != "I2",
-                            !str_detect(CODE, "SP")) %>%
-                     st_join(., rf_val_preds %>% select(rf_val_preds))
-
-
-zoning_map <- tmap_theme(tm_shape(filtered_zoning) +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey", title = "Zoning Code", palette = zoning_palette) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE,
-            legend.height = 0.4),
-  "Restrictive Zoning")
-  
-mismatch <- tmap_theme(tm_shape(filtered_zoning) +
-        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher", title = "Predicted New Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Development Pressure")
-
-tmap_arrange(zoning_map, mismatch)
+
filtered_zoning <- zoning %>%
+                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
+                            CODE != "I2",
+                            !str_detect(CODE, "SP")) %>%
+                     st_join(., rf_val_preds %>% select(rf_val_preds))
+
+
+zoning_map <- tmap_theme(tm_shape(filtered_zoning) +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey", title = "Zoning Code", palette = zoning_palette) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE,
+            legend.height = 0.4),
+  "Restrictive Zoning")
+  
+mismatch <- tmap_theme(tm_shape(filtered_zoning) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher", title = "Predicted New Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Development Pressure")
+
+tmap_arrange(zoning_map, mismatch)

@@ -10288,70 +10304,70 @@

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  filter(rf_val_preds > 10) %>%
-tm_shape() +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
-                    popup.vars = c('rf_val_preds', 'CODE'), palette = zoning_palette) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
tmap_mode('view')
+
+filtered_zoning %>%
+  filter(rf_val_preds > 10) %>%
+tm_shape() +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
+                    popup.vars = c('rf_val_preds', 'CODE'), palette = zoning_palette) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

Show the code -
nbs <- filtered_zoning %>% 
-  mutate(nb = st_contiguity(geometry))
-
-# Create edge list while handling cases with no neighbors
-edge_list <- tibble::tibble(id = 1:length(nbs$nb), nbs = nbs$nb) %>% 
-  unnest(nbs) %>% 
-  filter(nbs != 0)
-
-# Create a graph with a node for each row in filtered_zoning
-g <- make_empty_graph(n = nrow(filtered_zoning))
-V(g)$name <- as.character(1:nrow(filtered_zoning))
-
-# Add edges if they exist
-if (nrow(edge_list) > 0) {
-  edges <- as.matrix(edge_list)
-  g <- add_edges(g, c(t(edges)))
-}
-
-# Calculate the number of contiguous neighbors and sum of contiguous areas
-n_contiguous <- numeric(nrow(filtered_zoning))
-sum_contig_area <- numeric(nrow(filtered_zoning))
-
-for (i in 1:nrow(filtered_zoning)) {
-  neighbors <- neighborhood(g, order = 1, nodes = i)[[1]]
-  # Exclude the node itself from its list of neighbors
-  neighbors <- neighbors[neighbors != i]
-  n_contiguous[i] <- length(neighbors)
-  sum_contig_area[i] <- sum(filtered_zoning$Shape__Area[neighbors], na.rm = TRUE)
-}
-
-contig_info <- data.frame(n_contig = unlist(n_contiguous), sum_contig_area = unlist(sum_contig_area))
-filtered_zoning <- cbind(filtered_zoning, contig_info)
-
-
-filtered_zoning %>%
-  st_drop_geometry() %>%
-  select(rf_val_preds,
-         n_contig,
-         sum_contig_area,
-         CODE) %>%
-  filter(rf_val_preds > 10,
-         n_contig > 2) %>%
-  arrange(desc(rf_val_preds)) %>%
-  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
+
nbs <- filtered_zoning %>% 
+  mutate(nb = st_contiguity(geometry))
+
+# Create edge list while handling cases with no neighbors
+edge_list <- tibble::tibble(id = 1:length(nbs$nb), nbs = nbs$nb) %>% 
+  unnest(nbs) %>% 
+  filter(nbs != 0)
+
+# Create a graph with a node for each row in filtered_zoning
+g <- make_empty_graph(n = nrow(filtered_zoning))
+V(g)$name <- as.character(1:nrow(filtered_zoning))
+
+# Add edges if they exist
+if (nrow(edge_list) > 0) {
+  edges <- as.matrix(edge_list)
+  g <- add_edges(g, c(t(edges)))
+}
+
+# Calculate the number of contiguous neighbors and sum of contiguous areas
+n_contiguous <- numeric(nrow(filtered_zoning))
+sum_contig_area <- numeric(nrow(filtered_zoning))
+
+for (i in 1:nrow(filtered_zoning)) {
+  neighbors <- neighborhood(g, order = 1, nodes = i)[[1]]
+  # Exclude the node itself from its list of neighbors
+  neighbors <- neighbors[neighbors != i]
+  n_contiguous[i] <- length(neighbors)
+  sum_contig_area[i] <- sum(filtered_zoning$Shape__Area[neighbors], na.rm = TRUE)
+}
+
+contig_info <- data.frame(n_contig = unlist(n_contiguous), sum_contig_area = unlist(sum_contig_area))
+filtered_zoning <- cbind(filtered_zoning, contig_info)
+
+
+filtered_zoning %>%
+  st_drop_geometry() %>%
+  select(rf_val_preds,
+         n_contig,
+         sum_contig_area,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+  arrange(desc(rf_val_preds)) %>%
+  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
@@ -10464,16 +10480,16 @@

Show the code -
tmap_mode('plot')
-
-preds24 <- tmap_theme(tm_shape(rf_proj_preds) +
-        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Predicted New Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Projected New Development, 2024")
-
-tmap_arrange(preds24, med_inc)
+
tmap_mode('plot')
+
+preds24 <- tmap_theme(tm_shape(rf_proj_preds) +
+        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Predicted New Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Projected New Development, 2024")
+
+tmap_arrange(preds24, med_inc)

@@ -10482,7 +10498,7 @@

9 Web Application

-

Here is a preview of the SmartZoning web application. The UX offers key features that leverages this study’s modeling and mapping.

+

Below is a wireframe preview of the SmartZoning web application; a prototype built in Shiny is available here. The UX offers key features that leverages this study’s modeling and mapping.

Key Features:

  • Interactive parcel level map that makes key information about parcel easily accessible
  • diff --git a/index.html b/index.html index 4312099..d67defb 100644 --- a/index.html +++ b/index.html @@ -9336,28 +9336,38 @@

    suppressMessages( tmap_animation(tm, "assets/permits_animation.gif", delay = 50) -)

+) + +bar_graph <- ggplot(building_permits %>% filter(!year %in% c(2024)), aes(x = as.factor(year))) + + geom_bar(fill = palette[1], color = NA, alpha = 0.7) + + labs(title = "Permits per Year", + y = "Count") + + theme_minimal() + + theme(axis.title.x = element_blank(), + aspect.ratio = .75) + +# Ensure the 'assets' directory exists +if (!dir.exists("assets")) { + dir.create("assets") +} + +# Save the plot +ggsave(bar_graph, filename = "assets/permits_per_year.png")

+
+
+

Philadelphia Building Permits, 2013 - 2023
-
-
-Show the code -
ggplot(building_permits %>% filter(!year %in% c(2024)), aes(x = as.factor(year))) +
-  geom_bar(fill = palette[1], color = NA, alpha = 0.7) +
-  labs(title = "Permits per Year",
-       y = "Count") +
-  theme_minimal() +
-  theme(axis.title.x = element_blank(),
-        aspect.ratio = .75)
-
-
-

+
+
+

+

We note a significant uptick in new construction permits as we approach 2021, followed by a sharp decline. It is generally acknowledged that this trend was due to the expiration of a tax abatement program for developers.

@@ -9365,36 +9375,36 @@

Show the code -
perms_x_dist <- st_join(building_permits, council_dists)
-
-perms_x_dist_sum <- perms_x_dist %>%
-                  st_drop_geometry() %>%
-                  group_by(DISTRICT, year) %>%
-                  summarize(permits_count = n())
-
-perms_x_dist_mean = perms_x_dist_sum %>%
-                      group_by(year) %>%
-                      summarize(permits_count = mean(permits_count)) %>%
-                      mutate(DISTRICT = "Average")
-
-perms_x_dist_sum <- bind_rows(perms_x_dist_sum, perms_x_dist_mean) %>%
-                        mutate(color = ifelse(DISTRICT != "Average", 0, 1))
-
-ggplotly(
-ggplot(perms_x_dist_sum %>% filter(year > 2013, year < 2024), aes(x = year, y = permits_count, color = as.character(color), group = interaction(DISTRICT, color))) +
-  geom_line(lwd = 0.7) +
-  labs(title = "Permits per Year by Council District",
-       y = "Total Permits") +
-  # facet_wrap(~DISTRICT) +
-  theme_minimal() +
-  theme(axis.title.x = element_blank(),
-        legend.position = "none") +
-  scale_color_manual(values = c(palette[5], palette[1]))
-)
+
perms_x_dist <- st_join(building_permits, council_dists)
+
+perms_x_dist_sum <- perms_x_dist %>%
+                  st_drop_geometry() %>%
+                  group_by(DISTRICT, year) %>%
+                  summarize(permits_count = n())
+
+perms_x_dist_mean = perms_x_dist_sum %>%
+                      group_by(year) %>%
+                      summarize(permits_count = mean(permits_count)) %>%
+                      mutate(DISTRICT = "Average")
+
+perms_x_dist_sum <- bind_rows(perms_x_dist_sum, perms_x_dist_mean) %>%
+                        mutate(color = ifelse(DISTRICT != "Average", 0, 1))
+
+ggplotly(
+ggplot(perms_x_dist_sum %>% filter(year > 2013, year < 2024), aes(x = year, y = permits_count, color = as.character(color), group = interaction(DISTRICT, color))) +
+  geom_line(lwd = 0.7) +
+  labs(title = "Permits per Year by Council District",
+       y = "Total Permits") +
+  # facet_wrap(~DISTRICT) +
+  theme_minimal() +
+  theme(axis.title.x = element_blank(),
+        legend.position = "none") +
+  scale_color_manual(values = c(palette[5], palette[1]))
+)
-
- +
+

@@ -9404,26 +9414,26 @@

Show the code -
permits_bg_long <- permits_bg %>%
-                    filter(!year %in% c(2024)) %>%
-                    st_drop_geometry() %>%
-                    pivot_longer(
-                      cols = c(starts_with("lag")),
-                      names_to = "Variable",
-                      values_to = "Value"
-                    )
-
-
-ggscatter(permits_bg_long, x = "permits_count", y = "Value", facet.by = "Variable",
-   add = "reg.line",
-   add.params = list(color = palette[3]),
-   conf.int = TRUE, alpha = 0.2
-   ) + 
-  stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01, size = 3) +
-  labs(title = "Correlation of `permits_count` and Engineered Features",
-       x = "Value",
-       y = "Permits Count") +
-  theme_minimal()
+
permits_bg_long <- permits_bg %>%
+                    filter(!year %in% c(2024)) %>%
+                    st_drop_geometry() %>%
+                    pivot_longer(
+                      cols = c(starts_with("lag")),
+                      names_to = "Variable",
+                      values_to = "Value"
+                    )
+
+
+ggscatter(permits_bg_long, x = "permits_count", y = "Value", facet.by = "Variable",
+   add = "reg.line",
+   add.params = list(color = palette[3]),
+   conf.int = TRUE, alpha = 0.2
+   ) + 
+  stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01, size = 3) +
+  labs(title = "Correlation of `permits_count` and Engineered Features",
+       x = "Value",
+       y = "Permits Count") +
+  theme_minimal()

@@ -9437,28 +9447,28 @@

<
Show the code -
med_inc <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "med_inc", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Med. Inc. ($)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Median Income")
-  
-race <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "percent_nonwhite", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Nonwhite (%)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Race")
-  
-rent_burd <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
-        tm_polygons(col = "ext_rent_burden", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Rent Burden (%)") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "darkgrey") +
-  tm_layout(frame = FALSE),
-  "Extreme Rent Burden")
-  
-tmap_arrange(med_inc, race, rent_burd)
+
med_inc <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "med_inc", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Med. Inc. ($)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Median Income")
+  
+race <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "percent_nonwhite", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Nonwhite (%)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Race")
+  
+rent_burd <- tmap_theme(tm_shape(permits_bg %>% filter(year == 2022)) +
+        tm_polygons(col = "ext_rent_burden", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Rent Burden (%)") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey") +
+  tm_layout(frame = FALSE),
+  "Extreme Rent Burden")
+  
+tmap_arrange(med_inc, race, rent_burd)

@@ -9480,25 +9490,25 @@

Show the code -
corr_vars <- c("total_pop",
-               "med_inc",
-               "percent_nonwhite",
-               "percent_renters",
-               "rent_burden",
-               "ext_rent_burden")
-
-corr_dat <- permits_bg %>% select(all_of(corr_vars), permits_count) %>% select(where(is.numeric)) %>% st_drop_geometry() %>% unique() %>% na.omit()
-
-corr <- round(cor(corr_dat), 2)
-p.mat <- cor_pmat(corr_dat)
-
-ggcorrplot(corr, p.mat = p.mat, hc.order = FALSE,
-    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
-  annotate(
-  geom = "rect",
-  xmin = .5, xmax = 7.5, ymin = 6.5, ymax = 7.5,
-  fill = "transparent", color = "red", alpha = 0.5
-)
+
corr_vars <- c("total_pop",
+               "med_inc",
+               "percent_nonwhite",
+               "percent_renters",
+               "rent_burden",
+               "ext_rent_burden")
+
+corr_dat <- permits_bg %>% select(all_of(corr_vars), permits_count) %>% select(where(is.numeric)) %>% st_drop_geometry() %>% unique() %>% na.omit()
+
+corr <- round(cor(corr_dat), 2)
+p.mat <- cor_pmat(corr_dat)
+
+ggcorrplot(corr, p.mat = p.mat, hc.order = FALSE,
+    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
+  annotate(
+  geom = "rect",
+  xmin = .5, xmax = 7.5, ymin = 6.5, ymax = 7.5,
+  fill = "transparent", color = "red", alpha = 0.5
+)

@@ -9511,13 +9521,13 @@

Show the code -
ols <- lm(permits_count ~ ., data = permits_bg %>% filter(year < 2024) %>% select(-c(mapname, geoid10, year)) %>% st_drop_geometry())
-vif(ols) %>%
-  data.frame() %>%
-  clean_names() %>%
-  select(-df) %>%
-  arrange(desc(gvif)) %>%
-  kablerize()
+
ols <- lm(permits_count ~ ., data = permits_bg %>% filter(year < 2024) %>% select(-c(mapname, geoid10, year)) %>% st_drop_geometry())
+vif(ols) %>%
+  data.frame() %>%
+  clean_names() %>%
+  select(-df) %>%
+  arrange(desc(gvif)) %>%
+  kablerize()
@@ -9967,15 +9977,15 @@

Show the code -
ggplot(permits_bg %>% st_drop_geometry %>% filter(!year %in% c(2024)), aes(x = permits_count)) +
-  geom_histogram(fill = palette[1], color = NA, alpha = 0.7) +
-  labs(title = "Permits per Block Group per Year",
-       subtitle = "Log-Transformed",
-       y = "Count") +
-  scale_x_log10() +
-  facet_wrap(~year) +
-  theme_minimal() +
-  theme(axis.title.x = element_blank())
+
ggplot(permits_bg %>% st_drop_geometry %>% filter(!year %in% c(2024)), aes(x = permits_count)) +
+  geom_histogram(fill = palette[1], color = NA, alpha = 0.7) +
+  labs(title = "Permits per Block Group per Year",
+       subtitle = "Log-Transformed",
+       y = "Count") +
+  scale_x_log10() +
+  facet_wrap(~year) +
+  theme_minimal() +
+  theme(axis.title.x = element_blank())

@@ -9989,69 +9999,75 @@

Show the code -
lisa <- permits_bg %>% 
-  filter(year == 2023) %>%
-  mutate(nb = st_contiguity(geometry),
-                         wt = st_weights(nb),
-                         permits_lag = st_lag(permits_count, nb, wt),
-          moran = local_moran(permits_count, nb, wt)) %>% 
-  tidyr::unnest(moran) %>% 
-  mutate(pysal = ifelse(p_folded_sim <= 0.1, as.character(pysal), NA),
-         hotspot = case_when(
-           pysal == "High-High" ~ "Yes",
-           TRUE ~ "No"
-         ))
-
-# 
-# palette <- c("High-High" = "#B20016", 
-#              "Low-Low" = "#1C4769", 
-#              "Low-High" = "#24975E", 
-#              "High-Low" = "#EACA97")
-
-morans_i <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "Moran's I"),
-  "Local Moran's I (2023)")
-
-p_value <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "P-Value"),
-  "Moran's I P-Value (2023)")
-
-sig_hotspots <- tmap_theme(tm_shape(lisa) +
-  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(mono_5_green[1], mono_5_green[5]), textNA = "Not a Hotspot", title = "Hotspot?"),
-  "Construction Hotspots (2023)")
-
-tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
+
lisa <- permits_bg %>% 
+  filter(year == 2023) %>%
+  mutate(nb = st_contiguity(geometry),
+                         wt = st_weights(nb),
+                         permits_lag = st_lag(permits_count, nb, wt),
+          moran = local_moran(permits_count, nb, wt)) %>% 
+  tidyr::unnest(moran) %>% 
+  mutate(pysal = ifelse(p_folded_sim <= 0.1, as.character(pysal), NA),
+         hotspot = case_when(
+           pysal == "High-High" ~ "Yes",
+           TRUE ~ "No"
+         ))
+
+# 
+# palette <- c("High-High" = "#B20016", 
+#              "Low-Low" = "#1C4769", 
+#              "Low-High" = "#24975E", 
+#              "High-Low" = "#EACA97")
+
+morans_i <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "Moran's I") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Local Moran's I (2023)")
+
+p_value <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green, title = "P-Value") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Moran's I P-Value (2023)")
+
+sig_hotspots <- tmap_theme(tm_shape(lisa) +
+  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(mono_5_green[1], mono_5_green[5]), textNA = "Not a Hotspot", title = "Hotspot?") +
+      tm_shape(broad_and_market) +
+  tm_lines(col = "darkgrey"),
+  "Construction Hotspots (2023)")
+
+tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
-

+

Emergeging hotspots…? If I can get it to work.

Show the code -
# Prepare the data
-permits_data <- permits_bg %>%
-  filter(year < 2024,
-         year > 2012) %>%
-  select(permits_count, geoid10, year) %>%
-  na.omit()
-
-# Create spacetime object
-stc <- as_spacetime(permits_data,
-                    .loc_col = "geoid10",
-                    .time_col = "year")
-
-# Run emerging hotspot analysis
-ehsa <- emerging_hotspot_analysis(
-  x = stc,
-  .var = "permits_count",
-  k = 1,
-  nsim = 25
-)
-
-# Analyze the result
-count(ehsa, classification)
+
# Prepare the data
+permits_data <- permits_bg %>%
+  filter(year < 2024,
+         year > 2012) %>%
+  select(permits_count, geoid10, year) %>%
+  na.omit()
+
+# Create spacetime object
+stc <- as_spacetime(permits_data,
+                    .loc_col = "geoid10",
+                    .time_col = "year")
+
+# Run emerging hotspot analysis
+ehsa <- emerging_hotspot_analysis(
+  x = stc,
+  .var = "permits_count",
+  k = 1,
+  nsim = 25
+)
+
+# Analyze the result
+count(ehsa, classification)
       classification    n
@@ -10069,17 +10085,17 @@ 

Show the code -
suppressMessages(
-ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: OLS",
-       subtitle = "2022 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: OLS",
+       subtitle = "2022 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10088,21 +10104,21 @@

Show the code -
ols_preds_map <- tmap_theme(tm_shape(ols_preds) +
-        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: OLS")
-
-ols_error_map <- tmap_theme(tm_shape(ols_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: OLS")
-
-tmap_arrange(ols_preds_map, ols_error_map)
+
ols_preds_map <- tmap_theme(tm_shape(ols_preds) +
+        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: OLS")
+
+ols_error_map <- tmap_theme(tm_shape(ols_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: OLS")
+
+tmap_arrange(ols_preds_map, ols_error_map)

@@ -10115,17 +10131,17 @@

Show the code -
suppressMessages(
-ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: RF",
-       subtitle = "2022 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: RF",
+       subtitle = "2022 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10134,21 +10150,21 @@

Show the code -
test_preds_map <- tmap_theme(tm_shape(rf_test_preds) +
-        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: RF Test")
-
-test_error_map <- tmap_theme(tm_shape(rf_test_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: RF Test") 
-
-tmap_arrange(test_preds_map, test_error_map)
+
test_preds_map <- tmap_theme(tm_shape(rf_test_preds) +
+        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: RF Test")
+
+test_error_map <- tmap_theme(tm_shape(rf_test_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: RF Test") 
+
+tmap_arrange(test_preds_map, test_error_map)

@@ -10159,43 +10175,43 @@

5 Model Testing

-

Model training, validation, and testing involved three steps. First, we partitioned our data into training, validation, and testing sets. We used data from 2013 through 2021 for initial model training. Next, we evaluated our models’ ability to accurately predict 2022 construction permits using our validation set, which consisted of all permits in 2022. We carried out additional feature engineering and model tuning, iterating based on the results of these training and testing splits. We sought to minimize both the mean absolute error (MAE) of our best model and the distribution of absolute error. Finally, when we were satisfied with the results of our best model, we evaluated it again by training it on all data from 2013 through 2022 and validating it on data from 2023 (all but the last two weeks, which we consider negligible for our purposes), which the model had never “seen” before. As Kuhn and Johnson write in Applied Predictive Modeling (2013), “Ideally, the model should be evaluated on samples that were not used to build or fine-tune the model, so that they provide an unbiased sense of model effectiveness.”

+

Model training, validation, and testing involved three steps. First, we partitioned our data into training, validation, and testing sets. We used data from 2013 through 2021 for initial model training. Next, we evaluated our models’ ability to accurately predict 2022 construction permits using our validation set, which consisted of all permits in 2022. We carried out additional feature engineering and model tuning, iterating based on the results of these training and testing splits. We sought to minimize both the mean absolute error (MAE) of our best model and the distribution of absolute error. Finally, when we were satisfied with the results of our best model, we evaluated it again by training it on all data from 2013 through 2022 and validating it on data from 2023 (all but the last two weeks, which we consider negligible for our purposes), which the model had never “seen” before. As Kuhn and Johnson write in Applied Predictive Modeling (2013), “Ideally, the model should be evaluated on samples that were not used to build or fine-tune the model, so that they provide an unbiased sense of model effectiveness.” (Code for all of these steps is available on GitHub.)

Again, testing confirms the strength of our model; based on 2023 data, our random forest model produces a MAE of 2.19. We note again that the range of model error is relatively narrow. Generally, we see that where the model predicts there to be more permits, there is also higher error. This spatial trend is also seen in the distribution of absolute errors clustering in a handful of block groups with high permit counts.

Show the code -
val_preds_map <- tmap_theme(tm_shape(rf_val_preds) +
-        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Predicted Permits: RF Validate")
-
-val_error_map <- tmap_theme(tm_shape(rf_val_preds) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Absolute Error: RF Validate")
-
-tmap_arrange(val_preds_map, val_error_map)
+
val_preds_map <- tmap_theme(tm_shape(rf_val_preds) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Predicted Permits: RF Validate")
+
+val_error_map <- tmap_theme(tm_shape(rf_val_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey", title = "Absolute Error") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Absolute Error: RF Validate")
+
+tmap_arrange(val_preds_map, val_error_map)

Show the code -
suppressMessages(
-ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
-  geom_point(alpha = 0.2) +
-  labs(title = "Predicted vs. Actual Permits: RF",
-       subtitle = "2023 Data",
-       x = "Actual Permits",
-       y = "Predicted Permits") +
-  geom_abline() +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  theme_minimal()
-)
+
suppressMessages(
+ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
+  geom_point(alpha = 0.2) +
+  labs(title = "Predicted vs. Actual Permits: RF",
+       subtitle = "2023 Data",
+       x = "Actual Permits",
+       y = "Predicted Permits") +
+  geom_abline() +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  theme_minimal()
+)

@@ -10209,24 +10225,24 @@

Show the code -
rf_val_preds_long <- rf_val_preds %>%
-  pivot_longer(cols = c(rent_burden, percent_nonwhite, total_pop, med_inc),
-               names_to = "variable", values_to = "value") %>%
-  mutate(variable = case_when(
-    variable == "med_inc" ~ "Median Income ($)",
-    variable == "percent_nonwhite" ~ "Nonwhite (%)",
-    variable == "rent_burden" ~ "Rent Burden (%)",
-    TRUE ~ "Total Pop."
-  ))
-
-ggplot(rf_val_preds_long, aes(x = value, y = abs_error)) +
-  geom_point(alpha = 0.2) +
-  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
-  facet_wrap(~ variable, scales = "free_x") +
-  labs(title = "Generalizability of Absolute Error",
-       x = "Value",
-       y = "Absolute Error") +
-  theme_minimal()
+
rf_val_preds_long <- rf_val_preds %>%
+  pivot_longer(cols = c(rent_burden, percent_nonwhite, total_pop, med_inc),
+               names_to = "variable", values_to = "value") %>%
+  mutate(variable = case_when(
+    variable == "med_inc" ~ "Median Income ($)",
+    variable == "percent_nonwhite" ~ "Nonwhite (%)",
+    variable == "rent_burden" ~ "Rent Burden (%)",
+    TRUE ~ "Total Pop."
+  ))
+
+ggplot(rf_val_preds_long, aes(x = value, y = abs_error)) +
+  geom_point(alpha = 0.2) +
+  geom_smooth(method = "lm", se = FALSE, color = palette[3]) +
+  facet_wrap(~ variable, scales = "free_x") +
+  labs(title = "Generalizability of Absolute Error",
+       x = "Value",
+       y = "Absolute Error") +
+  theme_minimal()

@@ -10236,14 +10252,14 @@

Show the code -
suppressMessages(
-  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
-    geom_boxplot(fill = NA, color = palette[3], alpha = 0.7) +
-    labs(title = "MAE by Council District",
-         y = "Mean Absolute Error",
-         x = "Council District") +
-    theme_minimal()
-)
+
suppressMessages(
+  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
+    geom_boxplot(fill = NA, color = palette[3], alpha = 0.7) +
+    labs(title = "MAE by Council District",
+         y = "Mean Absolute Error",
+         x = "Council District") +
+    theme_minimal()
+)

@@ -10256,29 +10272,29 @@

Show the code -
filtered_zoning <- zoning %>%
-                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
-                            CODE != "I2",
-                            !str_detect(CODE, "SP")) %>%
-                     st_join(., rf_val_preds %>% select(rf_val_preds))
-
-
-zoning_map <- tmap_theme(tm_shape(filtered_zoning) +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey", title = "Zoning Code", palette = zoning_palette) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE,
-            legend.height = 0.4),
-  "Restrictive Zoning")
-  
-mismatch <- tmap_theme(tm_shape(filtered_zoning) +
-        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher", title = "Predicted New Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Development Pressure")
-
-tmap_arrange(zoning_map, mismatch)
+
filtered_zoning <- zoning %>%
+                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
+                            CODE != "I2",
+                            !str_detect(CODE, "SP")) %>%
+                     st_join(., rf_val_preds %>% select(rf_val_preds))
+
+
+zoning_map <- tmap_theme(tm_shape(filtered_zoning) +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey", title = "Zoning Code", palette = zoning_palette) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE,
+            legend.height = 0.4),
+  "Restrictive Zoning")
+  
+mismatch <- tmap_theme(tm_shape(filtered_zoning) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher", title = "Predicted New Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Development Pressure")
+
+tmap_arrange(zoning_map, mismatch)

@@ -10288,70 +10304,70 @@

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  filter(rf_val_preds > 10) %>%
-tm_shape() +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
-                    popup.vars = c('rf_val_preds', 'CODE'), palette = zoning_palette) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
tmap_mode('view')
+
+filtered_zoning %>%
+  filter(rf_val_preds > 10) %>%
+tm_shape() +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
+                    popup.vars = c('rf_val_preds', 'CODE'), palette = zoning_palette) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

Show the code -
nbs <- filtered_zoning %>% 
-  mutate(nb = st_contiguity(geometry))
-
-# Create edge list while handling cases with no neighbors
-edge_list <- tibble::tibble(id = 1:length(nbs$nb), nbs = nbs$nb) %>% 
-  unnest(nbs) %>% 
-  filter(nbs != 0)
-
-# Create a graph with a node for each row in filtered_zoning
-g <- make_empty_graph(n = nrow(filtered_zoning))
-V(g)$name <- as.character(1:nrow(filtered_zoning))
-
-# Add edges if they exist
-if (nrow(edge_list) > 0) {
-  edges <- as.matrix(edge_list)
-  g <- add_edges(g, c(t(edges)))
-}
-
-# Calculate the number of contiguous neighbors and sum of contiguous areas
-n_contiguous <- numeric(nrow(filtered_zoning))
-sum_contig_area <- numeric(nrow(filtered_zoning))
-
-for (i in 1:nrow(filtered_zoning)) {
-  neighbors <- neighborhood(g, order = 1, nodes = i)[[1]]
-  # Exclude the node itself from its list of neighbors
-  neighbors <- neighbors[neighbors != i]
-  n_contiguous[i] <- length(neighbors)
-  sum_contig_area[i] <- sum(filtered_zoning$Shape__Area[neighbors], na.rm = TRUE)
-}
-
-contig_info <- data.frame(n_contig = unlist(n_contiguous), sum_contig_area = unlist(sum_contig_area))
-filtered_zoning <- cbind(filtered_zoning, contig_info)
-
-
-filtered_zoning %>%
-  st_drop_geometry() %>%
-  select(rf_val_preds,
-         n_contig,
-         sum_contig_area,
-         CODE) %>%
-  filter(rf_val_preds > 10,
-         n_contig > 2) %>%
-  arrange(desc(rf_val_preds)) %>%
-  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
+
nbs <- filtered_zoning %>% 
+  mutate(nb = st_contiguity(geometry))
+
+# Create edge list while handling cases with no neighbors
+edge_list <- tibble::tibble(id = 1:length(nbs$nb), nbs = nbs$nb) %>% 
+  unnest(nbs) %>% 
+  filter(nbs != 0)
+
+# Create a graph with a node for each row in filtered_zoning
+g <- make_empty_graph(n = nrow(filtered_zoning))
+V(g)$name <- as.character(1:nrow(filtered_zoning))
+
+# Add edges if they exist
+if (nrow(edge_list) > 0) {
+  edges <- as.matrix(edge_list)
+  g <- add_edges(g, c(t(edges)))
+}
+
+# Calculate the number of contiguous neighbors and sum of contiguous areas
+n_contiguous <- numeric(nrow(filtered_zoning))
+sum_contig_area <- numeric(nrow(filtered_zoning))
+
+for (i in 1:nrow(filtered_zoning)) {
+  neighbors <- neighborhood(g, order = 1, nodes = i)[[1]]
+  # Exclude the node itself from its list of neighbors
+  neighbors <- neighbors[neighbors != i]
+  n_contiguous[i] <- length(neighbors)
+  sum_contig_area[i] <- sum(filtered_zoning$Shape__Area[neighbors], na.rm = TRUE)
+}
+
+contig_info <- data.frame(n_contig = unlist(n_contiguous), sum_contig_area = unlist(sum_contig_area))
+filtered_zoning <- cbind(filtered_zoning, contig_info)
+
+
+filtered_zoning %>%
+  st_drop_geometry() %>%
+  select(rf_val_preds,
+         n_contig,
+         sum_contig_area,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+  arrange(desc(rf_val_preds)) %>%
+  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
@@ -10464,16 +10480,16 @@

Show the code -
tmap_mode('plot')
-
-preds24 <- tmap_theme(tm_shape(rf_proj_preds) +
-        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Predicted New Permits") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE),
-  "Projected New Development, 2024")
-
-tmap_arrange(preds24, med_inc)
+
tmap_mode('plot')
+
+preds24 <- tmap_theme(tm_shape(rf_proj_preds) +
+        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey", title = "Predicted New Permits") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE),
+  "Projected New Development, 2024")
+
+tmap_arrange(preds24, med_inc)

@@ -10482,7 +10498,7 @@

9 Web Application

-

Here is a preview of the SmartZoning web application. The UX offers key features that leverages this study’s modeling and mapping.

+

Below is a wireframe preview of the SmartZoning web application; a prototype built in Shiny is available here. The UX offers key features that leverages this study’s modeling and mapping.

Key Features:

  • Interactive parcel level map that makes key information about parcel easily accessible