From ab504a3e9e065334ae39101f6ffb176e2ff020f3 Mon Sep 17 00:00:00 2001 From: Nissim Lebovits <111617674+nlebovits@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:26:13 -0500 Subject: [PATCH] Built site for gh-pages --- .nojekyll | 2 +- assets/permits_animation.gif | Bin 59927 -> 67838 bytes final.html | 1135 ++++++++++++++++------------------ index.html | 1135 ++++++++++++++++------------------ 4 files changed, 1079 insertions(+), 1193 deletions(-) diff --git a/.nojekyll b/.nojekyll index 7565821..b083c9a 100644 --- a/.nojekyll +++ b/.nojekyll @@ -1 +1 @@ -8cb23537 \ No newline at end of file +3293c4dc \ No newline at end of file diff --git a/assets/permits_animation.gif b/assets/permits_animation.gif index 0f53bd787c7f7c4db6276175843930f537ba2628..b2f6a462b27b864c13f3e83578b87d12497b9099 100644 GIT binary patch delta 16200 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 diff --git a/final.html b/final.html index 99f12e1..3e4c12a 100644 --- a/final.html +++ b/final.html @@ -3302,20 +3302,18 @@ document.body.style.height = "100%"; document.documentElement.style.width = "100%"; document.documentElement.style.height = "100%"; - if (cel) { - cel.style.position = "absolute"; - var pad = unpackPadding(sizing.padding); - cel.style.top = pad.top + "px"; - cel.style.right = pad.right + "px"; - cel.style.bottom = pad.bottom + "px"; - cel.style.left = pad.left + "px"; - el.style.width = "100%"; - el.style.height = "100%"; - } + cel.style.position = "absolute"; + var pad = unpackPadding(sizing.padding); + cel.style.top = pad.top + "px"; + cel.style.right = pad.right + "px"; + cel.style.bottom = pad.bottom + "px"; + cel.style.left = pad.left + "px"; + el.style.width = "100%"; + el.style.height = "100%"; return { - getWidth: function() { return cel.offsetWidth; }, - getHeight: function() { return cel.offsetHeight; } + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } }; } else { @@ -3323,8 +3321,8 @@ el.style.height = px(sizing.height); return { - getWidth: function() { return el.offsetWidth; }, - getHeight: function() { return el.offsetHeight; } + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } }; } } @@ -3548,8 +3546,8 @@ elementData(el, "initialized", true); if (bindingDef.initialize) { - var result = bindingDef.initialize(el, el.offsetWidth, - el.offsetHeight); + var rect = el.getBoundingClientRect(); + var result = bindingDef.initialize(el, rect.width, rect.height); elementData(el, "init_result", result); } } @@ -3591,29 +3589,30 @@ forEach(matches, function(el) { var sizeObj = initSizing(el, binding); + var getSize = function(el) { + if (sizeObj) { + return {w: sizeObj.getWidth(), h: sizeObj.getHeight()} + } else { + var rect = el.getBoundingClientRect(); + return {w: rect.width, h: rect.height} + } + }; + if (hasClass(el, "html-widget-static-bound")) return; el.className = el.className + " html-widget-static-bound"; var initResult; if (binding.initialize) { - initResult = binding.initialize(el, - sizeObj ? sizeObj.getWidth() : el.offsetWidth, - sizeObj ? sizeObj.getHeight() : el.offsetHeight - ); + var size = getSize(el); + initResult = binding.initialize(el, size.w, size.h); elementData(el, "init_result", initResult); } if (binding.resize) { - var lastSize = { - w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, - h: sizeObj ? sizeObj.getHeight() : el.offsetHeight - }; + var lastSize = getSize(el); var resizeHandler = function(e) { - var size = { - w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, - h: sizeObj ? sizeObj.getHeight() : el.offsetHeight - }; + var size = getSize(el); if (size.w === 0 && size.h === 0) return; if (size.w === lastSize.w && size.h === lastSize.h) @@ -3915,7 +3914,6 @@ return result; } })(); - +
+
@@ -9415,23 +9409,28 @@
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)) %>% 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 = TRUE,
-    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3]))
+
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 = TRUE,
+    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
+  annotate(
+  geom = "rect",
+  xmin = .5, xmax = 7.5, ymin = 4.5, ymax = 5.5,
+  fill = "transparent", color = "red", alpha = 0.5
+)
-

+

@@ -9441,57 +9440,57 @@
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" ~ "Signficant",
-           TRUE ~ "Not Signficant"
-         ))
-
-# 
-# palette <- c("High-High" = "#B20016", 
-#              "Low-Low" = "#1C4769", 
-#              "Low-High" = "#24975E", 
-#              "High-Low" = "#EACA97")
-
-morans_i <- tm_shape(lisa) +
-  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = 'viridis')
-
-p_value <- tm_shape(lisa) +
-  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = '-viridis')
-
-sig_hotspots <- tm_shape(lisa) +
-  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = 'viridis', textNA = "Not a Hotspot")
-
-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" ~ "Signficant",
+           TRUE ~ "Not Signficant"
+         ))
+
+# 
+# palette <- c("High-High" = "#B20016", 
+#              "Low-Low" = "#1C4769", 
+#              "Low-High" = "#24975E", 
+#              "High-Low" = "#EACA97")
+
+morans_i <- tm_shape(lisa) +
+  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green)
+
+p_value <- tm_shape(lisa) +
+  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green)
+
+sig_hotspots <- tm_shape(lisa) +
+  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(palette[2], palette[3]), textNA = "Not a Hotspot")
+
+tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
-

+

Emergeging hotspots

Show the code -
# stc <- as_spacetime(permits_bg,
-#                  .loc_col = "geoid10",
-#                  .time_col = "year")
-# 
-# # conduct EHSA
-# ehsa <- emerging_hotspot_analysis(
-#   x = stc, 
-#   .var = "permits_count", 
-#   k = 1, 
-#   nsim = 5
-# )
-# 
-# count(ehsa, classification)
+
# stc <- as_spacetime(permits_bg %>% select(permits_count, geoid10, year) %>% na.omit(),
+#                  .loc_col = "geoid10",
+#                  .time_col = "year")
+# 
+# # conduct EHSA
+# ehsa <- emerging_hotspot_analysis(
+#   x = stc,
+#   .var = "permits_count",
+#   k = 1,
+#   nsim = 5
+# )
+# 
+# count(ehsa, classification)
@@ -9500,21 +9499,21 @@

Show the code -
permits_bg_long <- permits_bg %>%
-                    filter(!year %in% c(2024)) %>%
-                    st_drop_geometry() %>%
-                    pivot_longer(
-                      cols = c(starts_with("lag"), dist_to_2022),
-                      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], fill = palette[5]),
-   conf.int = TRUE
-   ) + stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01)
+
permits_bg_long <- permits_bg %>%
+                    filter(!year %in% c(2024)) %>%
+                    st_drop_geometry() %>%
+                    pivot_longer(
+                      cols = c(starts_with("lag"), dist_to_2022),
+                      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], fill = palette[5]),
+   conf.int = TRUE
+   ) + stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01)

@@ -9536,94 +9535,147 @@

Show the code -
permits_train <- filter(permits_bg %>% select(-c(mapname, geoid10)), year < 2022)
-permits_test <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2022)
-permits_validate <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2023)
-permits_predict <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2024)
-
-reg <- lm(permits_count ~ ., data = st_drop_geometry(permits_train))
-
-predictions <- predict(reg, permits_test)
-predictions <- cbind(permits_test, predictions)
-
-predictions <- predictions %>%
-                  mutate(abs_error = abs(permits_count - predictions),
-                         pct_error = abs_error / permits_count)
-
-ggplot(predictions, aes(x = permits_count, y = predictions)) +
-  geom_point() +
-  labs(title = "Predicted vs. Actual Permits",
-       subtitle = "2022") +
-  geom_smooth(method = "lm", se = FALSE)
+
ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2022") +
+  geom_smooth(method = "lm", se = FALSE)
-

+

Show the code -
mae <- paste0("MAE: ", round(mean(predictions$abs_error, na.rm = TRUE), 2))
+
ggplot(ols_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "OLS, 2022")
+
+
+

+
+
+Show the code +
ols_mae <- paste0("MAE: ", round(mean(ols_preds$abs_error, na.rm = TRUE), 2))
 
-tm_shape(predictions) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = 'viridis', style = "fisher", colorNA = "lightgrey") +
+ols_preds_map <- tm_shape(ols_preds) +
+        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
   tm_shape(broad_and_market) +
   tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+ tm_layout(frame = FALSE) + +ols_error_map <- tm_shape(ols_preds) + + tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") + + tm_shape(broad_and_market) + + tm_lines(col = "lightgrey") + + tm_layout(frame = FALSE) + +tmap_arrange(ols_preds_map, ols_error_map)

-

+

We find that our OLS model has an MAE of only MAE: 2.66–not bad for such a simple model! Still, it struggles most in the areas where we most need it to succeed, so we will try to introduce better variables and apply a more complex model to improve our predictions.

-
-

4.2 Random Forest Regression

+
+

4.2 Random Forest Regression: Testing

+

We train and test up to 2022–we use this for model tuning and feature engineering.

Show the code -
rf <- randomForest(permits_count ~ ., 
-                   data = st_drop_geometry(permits_train),
-                   importance = TRUE, 
-                   na.action = na.omit)
-
-rf_predictions <- predict(rf, permits_test)
-rf_predictions <- cbind(permits_test, rf_predictions)
-rf_predictions <- rf_predictions %>%
-                  mutate(abs_error = abs(permits_count - rf_predictions),
-                         pct_error = abs_error / (permits_count + 0.0001))
-
-tm_shape(rf_predictions) +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
test_preds_map <- tm_shape(rf_test_preds) +
+        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+test_error_map <- tm_shape(rf_test_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+tmap_arrange(test_preds_map, test_error_map)
-

+

Show the code -
ggplot(rf_predictions, aes(x = permits_count, y = rf_predictions)) +
-  geom_point() +
-  labs(title = "Predicted vs. Actual Permits",
-       subtitle = "2022") +
-  geom_smooth(method = "lm", se = FALSE)
+
ggplot(rf_test_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "Random Forest, 2022")
-

+

Show the code -
rf_mae <- paste0("MAE: ", round(mean(rf_predictions$abs_error, na.rm = TRUE), 2))
-
-tm_shape(rf_predictions) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2022") +
+  geom_smooth(method = "lm", se = FALSE)
-

+

+
+Show the code +
rf_test_mae <- paste0("MAE: ", round(mean(rf_test_preds$abs_error, na.rm = TRUE), 2))
+
+
+
+
+

4.3 Random Forest Regression: Validation

+

Having settled on our model features and tuning, we now validate on 2023 data.

+
+
+Show the code +
val_preds_map <- tm_shape(rf_val_preds) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+val_error_map <- tm_shape(rf_val_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+tmap_arrange(val_preds_map, val_error_map)
+
+
+

+
+
+Show the code +
ggplot(rf_val_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "Random Forest, 2023")
+
+
+

+
+
+Show the code +
ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2023") +
+  geom_smooth(method = "lm", se = FALSE)
+
+
+

+
+
+Show the code +
rf_val_mae <- paste0("MAE: ", round(mean(rf_val_preds$abs_error, na.rm = TRUE), 2))
+
@@ -9635,23 +9687,16 @@

Show the code -
nbins <- as.integer(sqrt(nrow(rf_predictions)))
-vline <- mean(rf_predictions$abs_error, na.rm = TRUE)
-
-ggplot(rf_predictions, aes(x = abs_error)) +
-  geom_histogram(bins = nbins) +
-  geom_vline(aes(xintercept = vline))
+
nbins <- as.integer(sqrt(nrow(rf_val_preds)))
+vline <- mean(rf_val_preds$abs_error, na.rm = TRUE)
+
+ggplot(rf_val_preds, aes(x = abs_error)) +
+  geom_histogram(bins = nbins) +
+  geom_vline(aes(xintercept = vline))
-

+

-
-Show the code -
hmm <- permits_bg %>%
-  st_drop_geometry() %>%
-  group_by(year) %>%
-  summarize_all(.funs = list(~sum(is.na(.)))) # Check NA for all columns
-
@@ -9659,78 +9704,78 @@

Show the code -
rf_predictions <- rf_predictions %>%
-                      mutate(race_comp = case_when(
-                        percent_nonwhite >= .50 ~ "Majority Non-White",
-                        TRUE ~ "Majority White"
-                      ))
-
-ggplot(rf_predictions, aes(y = abs_error, color = race_comp)) +
-  geom_boxplot(fill = NA)
+
rf_val_preds <- rf_val_preds %>%
+                      mutate(race_comp = case_when(
+                        percent_nonwhite >= .50 ~ "Majority Non-White",
+                        TRUE ~ "Majority White"
+                      ))
+
+ggplot(rf_val_preds, aes(y = abs_error, color = race_comp)) +
+  geom_boxplot(fill = NA)
-

+

We find that error is not related to affordability and actually trends downward with percent nonwhite. (This is probably because there is less total development happening there in majority-minority neighborhoods to begin with, so the magnitude of error is less, even though proportionally it might be more.) Error increases slightly with total pop. This makes sense–more people –> more development.

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = rent_burden)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = rent_burden)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = percent_nonwhite)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = percent_nonwhite)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = total_pop)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = total_pop)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = med_inc)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = med_inc)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

How does this generalize across council districts? Don’t forget to refactor

Show the code -
suppressMessages(
-  ggplot(rf_predictions, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
-    geom_boxplot(fill = NA) +
-    labs(title = "MAE by Council District",
-         y = "Mean Absolute Error",
-         x = "Council District") +
-    theme_minimal() +
-    theme()
-)
+
suppressMessages(
+  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
+    geom_boxplot(fill = NA) +
+    labs(title = "MAE by Council District",
+         y = "Mean Absolute Error",
+         x = "Council District") +
+    theme_minimal() +
+    theme()
+)
-

+

@@ -9741,17 +9786,17 @@

Show the code -
filtered_zoning <- zoning %>%
-                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
-                            CODE != "I2",
-                            !str_detect(CODE, "SP"))
-
-
-tm_shape(filtered_zoning) +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
filtered_zoning <- zoning %>%
+                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
+                            CODE != "I2",
+                            !str_detect(CODE, "SP"))
+
+
+tm_shape(filtered_zoning) +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)

@@ -9761,85 +9806,85 @@

Show the code -
filtered_zoning <- st_join(
-  filtered_zoning,
-  rf_predictions %>% select(rf_predictions)
-)
-
-tm_shape(filtered_zoning) +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
filtered_zoning <- st_join(
+  filtered_zoning,
+  rf_val_preds %>% select(rf_val_preds)
+)
+
+tm_shape(filtered_zoning) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-

+

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  filter(rf_predictions > 10) %>%
-tm_shape() +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
-                    popup.vars = c('rf_predictions', 'CODE')) +
-  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')) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

Furthermore, we can identify properties with high potential for assemblage, which suggests the ability to accomodate high-density, multi-unit housing.

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) %>% 
-  tidyr::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, handling nodes without neighbors
-n_contiguous <- sapply(V(g)$name, function(node) {
-  if (node %in% edges) {
-    length(neighborhood(g, order = 1, nodes = as.numeric(node))[[1]])
-  } else {
-    1  # Nodes without neighbors count as 1 (themselves)
-  }
-})
-
-filtered_zoning <- filtered_zoning %>%
-                    mutate(n_contig = n_contiguous)
-
-filtered_zoning %>%
-  st_drop_geometry() %>%
-  select(rf_predictions,
-         n_contig,
-         OBJECTID,
-         CODE) %>%
-  filter(rf_predictions > 10,
-         n_contig > 2) %>%
-  arrange(desc(rf_predictions)) %>%
-  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) %>% 
+  tidyr::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, handling nodes without neighbors
+n_contiguous <- sapply(V(g)$name, function(node) {
+  if (node %in% edges) {
+    length(neighborhood(g, order = 1, nodes = as.numeric(node))[[1]])
+  } else {
+    1  # Nodes without neighbors count as 1 (themselves)
+  }
+})
+
+filtered_zoning <- filtered_zoning %>%
+                    mutate(n_contig = n_contiguous)
+
+filtered_zoning %>%
+  st_drop_geometry() %>%
+  select(rf_val_preds,
+         n_contig,
+         OBJECTID,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+  arrange(desc(rf_val_preds)) %>%
+  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
@@ -9847,7 +9892,7 @@

- + @@ -9855,368 +9900,270 @@

- - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + + + + - + - + - + - - - + + + - + - + - + - + - + - - - - - - - - - + + - + - - - - - - - - - - - - - - - - + + - + - - + + - - - - - - - - + - - - - - - - - - - - - - - - - + + - + - - - - - - - - - + + - + - - + + - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + - + - + - - + + - - + + - - - - - - - - - + + - + - - - - - - - - - + + - + - - + + - - + + - - - - - + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - - + - + - + - + - + - + - + - + - + - - - - - - - - - + + - - + + - - - - - + + + + +
rf_predictionsrf_val_preds n_contig OBJECTID CODE
751744.2962086827.06613 316717RSA51615ICMX
495741.61663154827.06613 310410ICMX2736IRMX
495841.61663158727.06613 310411RSA52804IRMX
495941.61663342027.06613 310412ICMX6405RSA5
524541.61663466727.06613 3111609661 RSA5
916927.06613420073ICMX
176834.2469022.24860 3 3128 IRMX
364034.2469022.24860 3 6901 ICMX
446029.67577
751721.08143 3909316717 RSA5
393428.7265720.84390 3 7646 ICMX
1232628.7265720.84390 4 25776 RSA5
1357824.43513327869IRMX
86823.84580495720.67827 3161510410 ICMX
154823.8458032736IRMX
158723.8458032804IRMX
342023.84580495820.67827 3640510411 RSA5
466723.84580495920.67827 39661RSA5
916923.8458042007310412 ICMX
508819.26747310759IRMX
772618.15027317168ICMX
783318.11450524520.67827 31740811160 RSA5
664516.99250314648ICMX
728016.99250446017.31087 3161799093 RSA5
991216.99250772615.16207 32152717168 ICMX
395715.647831357815.12210 3770427869 IRMX
596414.50423312931ICMX
639614.50423313980RSA3
654014.50423314372RSA5
655014.50423508815.00973 314401RSA5
213813.645004374410759 IRMX
451213.2790314.95163 5 9243 IRMX
601413.2790314.95163 6 13057 ICMX
577613.26503304112.82333 312473RSA55568ICMX
825213.26503318254RSA3
1284013.26503984212.82333 32662721369 RSA5
620012.71553413532ICMX
669112.43360984312.82333 31474721370 ICMX
414612.33040984512.82333 38265IRMX21372RSA5
510812.33040410795IRMX783312.25843317408RSA5
431812.08010395711.49060 38705RSD37704IRMX
1327012.08010664511.22550 327311RSD114648ICMX
13750.112.08010728011.22550 328226RSA316179RSA5
1334010.30770991211.22550 327419RSD321527ICMX
1403310.30770328807RSD3213810.8825343744IRMX
1403410.30770328808RSD1
814310.2576010.71940 3 18031 RSD3
865610.2576010.71940 3 19076 RSA3
940910.2576010.71940 4 20534 RSA2
1017510.2576010.71940 3 22002 RSD1
1260510.2576010.71940 3 26247 RSD1
570610.09430312329ICMX
300710.07567414610.39053 35506RSA58265IRMX
455810.0756739370ICMX510810.39053410795IRMX
@@ -10225,49 +10172,45 @@

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  select(rf_predictions,
-         n_contig,
-         OBJECTID,
-         CODE) %>%
-  filter(rf_predictions > 10,
-         n_contig > 2) %>%
-tm_shape() +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, colorNA = "lightgrey", palette = "viridis", style = "fisher",
-                    popup.vars = c('rf_predictions', 'n_contig', 'CODE'), alpha = 0.5) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
tmap_mode('view')
+
+filtered_zoning %>%
+  select(rf_val_preds,
+         n_contig,
+         OBJECTID,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+tm_shape() +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = "viridis", style = "fisher",
+                    popup.vars = c('rf_val_preds', 'n_contig', 'CODE'), alpha = 0.5) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

-
-

5.4 2024 Predictions

-

Just for shits and giggles, throw in 2024 predictions. (Can use data from 2023.)

+
+

5.4 Random Forest Regression: Projection

+

Just for shits and giggles, we can now predict for 2024.

Show the code -
rf_predictions_2024 <- predict(rf, permits_predict)
-rf_predictions_2024 <- cbind(permits_predict, rf_predictions_2024)
-
-
-tm_shape(rf_predictions_2024) +
-        tm_polygons(col = "rf_predictions_2024", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
tmap_mode('plot')
+
+tm_shape(rf_proj_preds) +
+        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
- -
- +

diff --git a/index.html b/index.html index 99f12e1..3e4c12a 100644 --- a/index.html +++ b/index.html @@ -3302,20 +3302,18 @@ document.body.style.height = "100%"; document.documentElement.style.width = "100%"; document.documentElement.style.height = "100%"; - if (cel) { - cel.style.position = "absolute"; - var pad = unpackPadding(sizing.padding); - cel.style.top = pad.top + "px"; - cel.style.right = pad.right + "px"; - cel.style.bottom = pad.bottom + "px"; - cel.style.left = pad.left + "px"; - el.style.width = "100%"; - el.style.height = "100%"; - } + cel.style.position = "absolute"; + var pad = unpackPadding(sizing.padding); + cel.style.top = pad.top + "px"; + cel.style.right = pad.right + "px"; + cel.style.bottom = pad.bottom + "px"; + cel.style.left = pad.left + "px"; + el.style.width = "100%"; + el.style.height = "100%"; return { - getWidth: function() { return cel.offsetWidth; }, - getHeight: function() { return cel.offsetHeight; } + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } }; } else { @@ -3323,8 +3321,8 @@ el.style.height = px(sizing.height); return { - getWidth: function() { return el.offsetWidth; }, - getHeight: function() { return el.offsetHeight; } + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } }; } } @@ -3548,8 +3546,8 @@ elementData(el, "initialized", true); if (bindingDef.initialize) { - var result = bindingDef.initialize(el, el.offsetWidth, - el.offsetHeight); + var rect = el.getBoundingClientRect(); + var result = bindingDef.initialize(el, rect.width, rect.height); elementData(el, "init_result", result); } } @@ -3591,29 +3589,30 @@ forEach(matches, function(el) { var sizeObj = initSizing(el, binding); + var getSize = function(el) { + if (sizeObj) { + return {w: sizeObj.getWidth(), h: sizeObj.getHeight()} + } else { + var rect = el.getBoundingClientRect(); + return {w: rect.width, h: rect.height} + } + }; + if (hasClass(el, "html-widget-static-bound")) return; el.className = el.className + " html-widget-static-bound"; var initResult; if (binding.initialize) { - initResult = binding.initialize(el, - sizeObj ? sizeObj.getWidth() : el.offsetWidth, - sizeObj ? sizeObj.getHeight() : el.offsetHeight - ); + var size = getSize(el); + initResult = binding.initialize(el, size.w, size.h); elementData(el, "init_result", initResult); } if (binding.resize) { - var lastSize = { - w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, - h: sizeObj ? sizeObj.getHeight() : el.offsetHeight - }; + var lastSize = getSize(el); var resizeHandler = function(e) { - var size = { - w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, - h: sizeObj ? sizeObj.getHeight() : el.offsetHeight - }; + var size = getSize(el); if (size.w === 0 && size.h === 0) return; if (size.w === lastSize.w && size.h === lastSize.h) @@ -3915,7 +3914,6 @@ return result; } })(); - +
+
@@ -9415,23 +9409,28 @@
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)) %>% 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 = TRUE,
-    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3]))
+
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 = TRUE,
+    type = "full", insig = "blank", lab = TRUE, colors = c(palette[2], "white", palette[3])) +
+  annotate(
+  geom = "rect",
+  xmin = .5, xmax = 7.5, ymin = 4.5, ymax = 5.5,
+  fill = "transparent", color = "red", alpha = 0.5
+)
-

+

@@ -9441,57 +9440,57 @@
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" ~ "Signficant",
-           TRUE ~ "Not Signficant"
-         ))
-
-# 
-# palette <- c("High-High" = "#B20016", 
-#              "Low-Low" = "#1C4769", 
-#              "Low-High" = "#24975E", 
-#              "High-Low" = "#EACA97")
-
-morans_i <- tm_shape(lisa) +
-  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = 'viridis')
-
-p_value <- tm_shape(lisa) +
-  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = '-viridis')
-
-sig_hotspots <- tm_shape(lisa) +
-  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = 'viridis', textNA = "Not a Hotspot")
-
-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" ~ "Signficant",
+           TRUE ~ "Not Signficant"
+         ))
+
+# 
+# palette <- c("High-High" = "#B20016", 
+#              "Low-Low" = "#1C4769", 
+#              "Low-High" = "#24975E", 
+#              "High-Low" = "#EACA97")
+
+morans_i <- tm_shape(lisa) +
+  tm_polygons(col = "ii", border.alpha = 0, style = "jenks", palette = mono_5_green)
+
+p_value <- tm_shape(lisa) +
+  tm_polygons(col = "p_ii", border.alpha = 0, style = "jenks", palette = mono_5_green)
+
+sig_hotspots <- tm_shape(lisa) +
+  tm_polygons(col = "hotspot", border.alpha = 0, style = "cat", palette = c(palette[2], palette[3]), textNA = "Not a Hotspot")
+
+tmap_arrange(morans_i, p_value, sig_hotspots, ncol = 3)
-

+

Emergeging hotspots

Show the code -
# stc <- as_spacetime(permits_bg,
-#                  .loc_col = "geoid10",
-#                  .time_col = "year")
-# 
-# # conduct EHSA
-# ehsa <- emerging_hotspot_analysis(
-#   x = stc, 
-#   .var = "permits_count", 
-#   k = 1, 
-#   nsim = 5
-# )
-# 
-# count(ehsa, classification)
+
# stc <- as_spacetime(permits_bg %>% select(permits_count, geoid10, year) %>% na.omit(),
+#                  .loc_col = "geoid10",
+#                  .time_col = "year")
+# 
+# # conduct EHSA
+# ehsa <- emerging_hotspot_analysis(
+#   x = stc,
+#   .var = "permits_count",
+#   k = 1,
+#   nsim = 5
+# )
+# 
+# count(ehsa, classification)
@@ -9500,21 +9499,21 @@

Show the code -
permits_bg_long <- permits_bg %>%
-                    filter(!year %in% c(2024)) %>%
-                    st_drop_geometry() %>%
-                    pivot_longer(
-                      cols = c(starts_with("lag"), dist_to_2022),
-                      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], fill = palette[5]),
-   conf.int = TRUE
-   ) + stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01)
+
permits_bg_long <- permits_bg %>%
+                    filter(!year %in% c(2024)) %>%
+                    st_drop_geometry() %>%
+                    pivot_longer(
+                      cols = c(starts_with("lag"), dist_to_2022),
+                      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], fill = palette[5]),
+   conf.int = TRUE
+   ) + stat_cor(method = "pearson", p.accuracy = 0.001, r.accuracy = 0.01)

@@ -9536,94 +9535,147 @@

Show the code -
permits_train <- filter(permits_bg %>% select(-c(mapname, geoid10)), year < 2022)
-permits_test <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2022)
-permits_validate <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2023)
-permits_predict <- filter(permits_bg %>% select(-c(mapname, geoid10)), year == 2024)
-
-reg <- lm(permits_count ~ ., data = st_drop_geometry(permits_train))
-
-predictions <- predict(reg, permits_test)
-predictions <- cbind(permits_test, predictions)
-
-predictions <- predictions %>%
-                  mutate(abs_error = abs(permits_count - predictions),
-                         pct_error = abs_error / permits_count)
-
-ggplot(predictions, aes(x = permits_count, y = predictions)) +
-  geom_point() +
-  labs(title = "Predicted vs. Actual Permits",
-       subtitle = "2022") +
-  geom_smooth(method = "lm", se = FALSE)
+
ggplot(ols_preds, aes(x = permits_count, y = ols_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2022") +
+  geom_smooth(method = "lm", se = FALSE)
-

+

Show the code -
mae <- paste0("MAE: ", round(mean(predictions$abs_error, na.rm = TRUE), 2))
+
ggplot(ols_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "OLS, 2022")
+
+
+

+
+
+Show the code +
ols_mae <- paste0("MAE: ", round(mean(ols_preds$abs_error, na.rm = TRUE), 2))
 
-tm_shape(predictions) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = 'viridis', style = "fisher", colorNA = "lightgrey") +
+ols_preds_map <- tm_shape(ols_preds) +
+        tm_polygons(col = "ols_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
   tm_shape(broad_and_market) +
   tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+ tm_layout(frame = FALSE) + +ols_error_map <- tm_shape(ols_preds) + + tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") + + tm_shape(broad_and_market) + + tm_lines(col = "lightgrey") + + tm_layout(frame = FALSE) + +tmap_arrange(ols_preds_map, ols_error_map)

-

+

We find that our OLS model has an MAE of only MAE: 2.66–not bad for such a simple model! Still, it struggles most in the areas where we most need it to succeed, so we will try to introduce better variables and apply a more complex model to improve our predictions.

-
-

4.2 Random Forest Regression

+
+

4.2 Random Forest Regression: Testing

+

We train and test up to 2022–we use this for model tuning and feature engineering.

Show the code -
rf <- randomForest(permits_count ~ ., 
-                   data = st_drop_geometry(permits_train),
-                   importance = TRUE, 
-                   na.action = na.omit)
-
-rf_predictions <- predict(rf, permits_test)
-rf_predictions <- cbind(permits_test, rf_predictions)
-rf_predictions <- rf_predictions %>%
-                  mutate(abs_error = abs(permits_count - rf_predictions),
-                         pct_error = abs_error / (permits_count + 0.0001))
-
-tm_shape(rf_predictions) +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
test_preds_map <- tm_shape(rf_test_preds) +
+        tm_polygons(col = "rf_test_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+test_error_map <- tm_shape(rf_test_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+tmap_arrange(test_preds_map, test_error_map)
-

+

Show the code -
ggplot(rf_predictions, aes(x = permits_count, y = rf_predictions)) +
-  geom_point() +
-  labs(title = "Predicted vs. Actual Permits",
-       subtitle = "2022") +
-  geom_smooth(method = "lm", se = FALSE)
+
ggplot(rf_test_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "Random Forest, 2022")
-

+

Show the code -
rf_mae <- paste0("MAE: ", round(mean(rf_predictions$abs_error, na.rm = TRUE), 2))
-
-tm_shape(rf_predictions) +
-        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
ggplot(rf_test_preds, aes(x = permits_count, y = rf_test_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2022") +
+  geom_smooth(method = "lm", se = FALSE)
-

+

+
+Show the code +
rf_test_mae <- paste0("MAE: ", round(mean(rf_test_preds$abs_error, na.rm = TRUE), 2))
+
+
+
+
+

4.3 Random Forest Regression: Validation

+

Having settled on our model features and tuning, we now validate on 2023 data.

+
+
+Show the code +
val_preds_map <- tm_shape(rf_val_preds) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+val_error_map <- tm_shape(rf_val_preds) +
+        tm_polygons(col = "abs_error", border.alpha = 0, palette = mono_5_orange, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
+
+tmap_arrange(val_preds_map, val_error_map)
+
+
+

+
+
+Show the code +
ggplot(rf_val_preds, aes(x = abs_error)) +
+  geom_histogram() +
+  labs(title = "Distribution of Absolute Error per Block Group",
+       subtitle = "Random Forest, 2023")
+
+
+

+
+
+Show the code +
ggplot(rf_val_preds, aes(x = permits_count, y = rf_val_preds)) +
+  geom_point() +
+  labs(title = "Predicted vs. Actual Permits",
+       subtitle = "2023") +
+  geom_smooth(method = "lm", se = FALSE)
+
+
+

+
+
+Show the code +
rf_val_mae <- paste0("MAE: ", round(mean(rf_val_preds$abs_error, na.rm = TRUE), 2))
+
@@ -9635,23 +9687,16 @@

Show the code -
nbins <- as.integer(sqrt(nrow(rf_predictions)))
-vline <- mean(rf_predictions$abs_error, na.rm = TRUE)
-
-ggplot(rf_predictions, aes(x = abs_error)) +
-  geom_histogram(bins = nbins) +
-  geom_vline(aes(xintercept = vline))
+
nbins <- as.integer(sqrt(nrow(rf_val_preds)))
+vline <- mean(rf_val_preds$abs_error, na.rm = TRUE)
+
+ggplot(rf_val_preds, aes(x = abs_error)) +
+  geom_histogram(bins = nbins) +
+  geom_vline(aes(xintercept = vline))
-

+

-
-Show the code -
hmm <- permits_bg %>%
-  st_drop_geometry() %>%
-  group_by(year) %>%
-  summarize_all(.funs = list(~sum(is.na(.)))) # Check NA for all columns
-
@@ -9659,78 +9704,78 @@

Show the code -
rf_predictions <- rf_predictions %>%
-                      mutate(race_comp = case_when(
-                        percent_nonwhite >= .50 ~ "Majority Non-White",
-                        TRUE ~ "Majority White"
-                      ))
-
-ggplot(rf_predictions, aes(y = abs_error, color = race_comp)) +
-  geom_boxplot(fill = NA)
+
rf_val_preds <- rf_val_preds %>%
+                      mutate(race_comp = case_when(
+                        percent_nonwhite >= .50 ~ "Majority Non-White",
+                        TRUE ~ "Majority White"
+                      ))
+
+ggplot(rf_val_preds, aes(y = abs_error, color = race_comp)) +
+  geom_boxplot(fill = NA)
-

+

We find that error is not related to affordability and actually trends downward with percent nonwhite. (This is probably because there is less total development happening there in majority-minority neighborhoods to begin with, so the magnitude of error is less, even though proportionally it might be more.) Error increases slightly with total pop. This makes sense–more people –> more development.

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = rent_burden)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = rent_burden)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = percent_nonwhite)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = percent_nonwhite)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = total_pop)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = total_pop)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

Show the code -
ggplot(rf_predictions, aes(y = abs_error, x = med_inc)) + # or whatever the variable is
-  geom_point() +
-  geom_smooth(method = "lm", se= FALSE) +
-  theme_minimal()
+
ggplot(rf_val_preds, aes(y = abs_error, x = med_inc)) + # or whatever the variable is
+  geom_point() +
+  geom_smooth(method = "lm", se= FALSE) +
+  theme_minimal()
-

+

How does this generalize across council districts? Don’t forget to refactor

Show the code -
suppressMessages(
-  ggplot(rf_predictions, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
-    geom_boxplot(fill = NA) +
-    labs(title = "MAE by Council District",
-         y = "Mean Absolute Error",
-         x = "Council District") +
-    theme_minimal() +
-    theme()
-)
+
suppressMessages(
+  ggplot(rf_val_preds, aes(x = reorder(district, abs_error, FUN = mean), y = abs_error)) +
+    geom_boxplot(fill = NA) +
+    labs(title = "MAE by Council District",
+         y = "Mean Absolute Error",
+         x = "Council District") +
+    theme_minimal() +
+    theme()
+)
-

+

@@ -9741,17 +9786,17 @@

Show the code -
filtered_zoning <- zoning %>%
-                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
-                            CODE != "I2",
-                            !str_detect(CODE, "SP"))
-
-
-tm_shape(filtered_zoning) +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
filtered_zoning <- zoning %>%
+                     filter(str_detect(CODE, "RS") | str_detect(CODE, "I"),
+                            CODE != "I2",
+                            !str_detect(CODE, "SP"))
+
+
+tm_shape(filtered_zoning) +
+        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)

@@ -9761,85 +9806,85 @@

Show the code -
filtered_zoning <- st_join(
-  filtered_zoning,
-  rf_predictions %>% select(rf_predictions)
-)
-
-tm_shape(filtered_zoning) +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
filtered_zoning <- st_join(
+  filtered_zoning,
+  rf_val_preds %>% select(rf_val_preds)
+)
+
+tm_shape(filtered_zoning) +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = mono_5_orange, style = "fisher") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-

+

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  filter(rf_predictions > 10) %>%
-tm_shape() +
-        tm_polygons(col = "CODE", border.alpha = 0, colorNA = "lightgrey",
-                    popup.vars = c('rf_predictions', 'CODE')) +
-  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')) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

Furthermore, we can identify properties with high potential for assemblage, which suggests the ability to accomodate high-density, multi-unit housing.

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) %>% 
-  tidyr::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, handling nodes without neighbors
-n_contiguous <- sapply(V(g)$name, function(node) {
-  if (node %in% edges) {
-    length(neighborhood(g, order = 1, nodes = as.numeric(node))[[1]])
-  } else {
-    1  # Nodes without neighbors count as 1 (themselves)
-  }
-})
-
-filtered_zoning <- filtered_zoning %>%
-                    mutate(n_contig = n_contiguous)
-
-filtered_zoning %>%
-  st_drop_geometry() %>%
-  select(rf_predictions,
-         n_contig,
-         OBJECTID,
-         CODE) %>%
-  filter(rf_predictions > 10,
-         n_contig > 2) %>%
-  arrange(desc(rf_predictions)) %>%
-  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) %>% 
+  tidyr::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, handling nodes without neighbors
+n_contiguous <- sapply(V(g)$name, function(node) {
+  if (node %in% edges) {
+    length(neighborhood(g, order = 1, nodes = as.numeric(node))[[1]])
+  } else {
+    1  # Nodes without neighbors count as 1 (themselves)
+  }
+})
+
+filtered_zoning <- filtered_zoning %>%
+                    mutate(n_contig = n_contiguous)
+
+filtered_zoning %>%
+  st_drop_geometry() %>%
+  select(rf_val_preds,
+         n_contig,
+         OBJECTID,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+  arrange(desc(rf_val_preds)) %>%
+  kablerize(caption = "Poorly-Zoned Properties with High Development Risk")
@@ -9847,7 +9892,7 @@

- + @@ -9855,368 +9900,270 @@

- - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + + + + + - + - + - + - - - + + + - + - + - + - + - + - - - - - - - - - + + - + - - - - - - - - - - - - - - - - + + - + - - + + - - - - - - - - + - - - - - - - - - - - - - - - - + + - + - - - - - - - - - + + - + - - + + - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + - + - + - - + + - - + + - - - - - - - - - + + - + - - - - - - - - - + + - + - - + + - - + + - - - - - + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - - + - + - + - + - + - + - + - + - + - - - - - - - - - + + - - + + - - - - - + + + + +
rf_predictionsrf_val_preds n_contig OBJECTID CODE
751744.2962086827.06613 316717RSA51615ICMX
495741.61663154827.06613 310410ICMX2736IRMX
495841.61663158727.06613 310411RSA52804IRMX
495941.61663342027.06613 310412ICMX6405RSA5
524541.61663466727.06613 3111609661 RSA5
916927.06613420073ICMX
176834.2469022.24860 3 3128 IRMX
364034.2469022.24860 3 6901 ICMX
446029.67577
751721.08143 3909316717 RSA5
393428.7265720.84390 3 7646 ICMX
1232628.7265720.84390 4 25776 RSA5
1357824.43513327869IRMX
86823.84580495720.67827 3161510410 ICMX
154823.8458032736IRMX
158723.8458032804IRMX
342023.84580495820.67827 3640510411 RSA5
466723.84580495920.67827 39661RSA5
916923.8458042007310412 ICMX
508819.26747310759IRMX
772618.15027317168ICMX
783318.11450524520.67827 31740811160 RSA5
664516.99250314648ICMX
728016.99250446017.31087 3161799093 RSA5
991216.99250772615.16207 32152717168 ICMX
395715.647831357815.12210 3770427869 IRMX
596414.50423312931ICMX
639614.50423313980RSA3
654014.50423314372RSA5
655014.50423508815.00973 314401RSA5
213813.645004374410759 IRMX
451213.2790314.95163 5 9243 IRMX
601413.2790314.95163 6 13057 ICMX
577613.26503304112.82333 312473RSA55568ICMX
825213.26503318254RSA3
1284013.26503984212.82333 32662721369 RSA5
620012.71553413532ICMX
669112.43360984312.82333 31474721370 ICMX
414612.33040984512.82333 38265IRMX21372RSA5
510812.33040410795IRMX783312.25843317408RSA5
431812.08010395711.49060 38705RSD37704IRMX
1327012.08010664511.22550 327311RSD114648ICMX
13750.112.08010728011.22550 328226RSA316179RSA5
1334010.30770991211.22550 327419RSD321527ICMX
1403310.30770328807RSD3213810.8825343744IRMX
1403410.30770328808RSD1
814310.2576010.71940 3 18031 RSD3
865610.2576010.71940 3 19076 RSA3
940910.2576010.71940 4 20534 RSA2
1017510.2576010.71940 3 22002 RSD1
1260510.2576010.71940 3 26247 RSD1
570610.09430312329ICMX
300710.07567414610.39053 35506RSA58265IRMX
455810.0756739370ICMX510810.39053410795IRMX
@@ -10225,49 +10172,45 @@

Show the code -
tmap_mode('view')
-
-filtered_zoning %>%
-  select(rf_predictions,
-         n_contig,
-         OBJECTID,
-         CODE) %>%
-  filter(rf_predictions > 10,
-         n_contig > 2) %>%
-tm_shape() +
-        tm_polygons(col = "rf_predictions", border.alpha = 0, colorNA = "lightgrey", palette = "viridis", style = "fisher",
-                    popup.vars = c('rf_predictions', 'n_contig', 'CODE'), alpha = 0.5) +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE)
+
tmap_mode('view')
+
+filtered_zoning %>%
+  select(rf_val_preds,
+         n_contig,
+         OBJECTID,
+         CODE) %>%
+  filter(rf_val_preds > 10,
+         n_contig > 2) %>%
+tm_shape() +
+        tm_polygons(col = "rf_val_preds", border.alpha = 0, colorNA = "lightgrey", palette = "viridis", style = "fisher",
+                    popup.vars = c('rf_val_preds', 'n_contig', 'CODE'), alpha = 0.5) +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE)
-
- +
+

-
-

5.4 2024 Predictions

-

Just for shits and giggles, throw in 2024 predictions. (Can use data from 2023.)

+
+

5.4 Random Forest Regression: Projection

+

Just for shits and giggles, we can now predict for 2024.

Show the code -
rf_predictions_2024 <- predict(rf, permits_predict)
-rf_predictions_2024 <- cbind(permits_predict, rf_predictions_2024)
-
-
-tm_shape(rf_predictions_2024) +
-        tm_polygons(col = "rf_predictions_2024", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
-  tm_shape(broad_and_market) +
-  tm_lines(col = "lightgrey") +
-  tm_layout(frame = FALSE) 
+
tmap_mode('plot')
+
+tm_shape(rf_proj_preds) +
+        tm_polygons(col = "rf_proj_preds", border.alpha = 0, palette = mono_5_green, style = "fisher", colorNA = "lightgrey") +
+  tm_shape(broad_and_market) +
+  tm_lines(col = "lightgrey") +
+  tm_layout(frame = FALSE) 
- -
- +