--- title: "Lab 6 Assignment | PAF 516" subtitle: "Spatio-Temporal Analysis — LISA Trajectories & Space-Time Moran's I" format: html: theme: readable highlight-style: tango toc: true self-contained: true number-sections: false execute: echo: true warning: false message: false fig-width: 10 --- ```{r} #| label: setup #| include: false knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE, fig.width = 10) options(tigris_use_cache = TRUE) options(tigris_progress_bar = FALSE) ``` ```{r} #| label: packages # ── One-time course environment setup ───────────────────────────────────── # Sentinel lives in user home (~/.paf516_ready) — persists across all lab # folders. renv restores ONCE for the entire course, not once per lab. # To force re-install: delete ~/.paf516_ready and re-render. if (!file.exists("~/.paf516_ready")) { if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv") tmp <- tempfile(fileext = ".lock") download.file( url = "https://raw.githubusercontent.com/AntJam-Howell/paf-516-labs/main/renv.lock", destfile = tmp, mode = "wb" ) renv::restore(lockfile = tmp, prompt = FALSE) file.create("~/.paf516_ready") } library(tidyverse) library(tidycensus) library(sf) library(spdep) library(viridis) library(patchwork) ``` ```{r} #| label: api-key # census_api_key("YOUR KEY GOES HERE") ``` ::: {.callout-important} ## Before you begin 1. **This file contains the same pre-filled code as the tutorial.** Steps 1–7 below produce the same output you saw in the tutorial. Knit immediately (Cmd+Shift+K / Ctrl+Shift+K) to see all results before answering. 2. **The assignment questions are at the top of this document.** Workflow: - **Q1** asks you to interpret the trajectory map and transition matrix. Knit first, review Steps 1–7 output, then write your answer. - **Q2** asks you to rerun the LISA classification at p < 0.01 and compare it to the p < 0.05 map. Fill in the scaffold chunks labeled `q2-...` at the end of this document, then re-knit. - **Q3** asks you to write a policy brief recommending which trajectory types to prioritize for intervention. 3. Submit both `Lab6_Assignment_YourLastName.qmd` and `Lab6_Assignment_YourLastName.html` to Canvas. ::: # Assignment Questions ## Q1 — Interpret: Trajectory Map and Transition Matrix {.question} **Interpret the trajectory map (Step 5) and transition matrix (Step 4). Address all of the following:** - How many tracts are **persistent hot spots** (HH→HH)? What does this suggest about structural hardship in Maricopa County? - How many 2013 hot spots **dissolved** by 2019? What might explain their recovery? - Are there **emerging hot spots** — tracts that became HH between 2013 and 2019? Where do they appear spatially? - What does the **space-time Moran's I result** (Step 6) tell you about whether change is a spatially spreading process? - Does the **three-point trend** (Step 7) give more or less confidence in the two-point change scores? What share of "improving" tracts show consistent improvement vs. non-monotone patterns? *Minimum 200 words. Reference specific numbers from the outputs above.* *YOUR ANSWER HERE* ## Q2 — Modify: Stricter Significance Threshold (p < 0.01) {.question} **Rerun the LISA classification using p < 0.01 instead of p < 0.05. The `classify_lisa()` function in Step 3 accepts a `p_thresh` argument — use it here. Reclassify trajectories, produce the trajectory map at p < 0.01, and write a paragraph comparing it to the p < 0.05 map.** Fill in the scaffold chunks labeled `q2-...` at the end of this document, then re-knit. *Comparison paragraph (min. 100 words): How does the p < 0.01 trajectory map differ from the p < 0.05 map? Which trajectory categories shrink the most? What does this tell you about multiple testing and the reliability of hot spot classifications?* *YOUR ANSWER HERE* ## Q3 — Policy Brief {.question} **You are advising the Maricopa County Office of Community Development. They have a fixed budget and can prioritize only two of the four trajectory types below. Using your trajectory map, space-time Moran's I result, and three-point trend as evidence, write a policy brief paragraph recommending which two to prioritize and why.** Trajectory types to consider: - **Persistent Hot Spots (HH→HH):** Deep, long-standing hardship clusters - **Emerging Hot Spots (→HH):** Deteriorating areas where clustering is forming - **Dissolving Hot Spots (HH→):** Recovering areas that may need displacement protection - **HL Outliers:** Isolated pockets of high hardship within low-hardship surroundings *Minimum 150 words. Ground your recommendation in specific numbers from your analysis. Cite at least one reading from Module 5 or Module 6.* *YOUR ANSWER HERE* --- # Overview **Rename this file** `Lab6_Assignment_YourLastName.qmd` before editing. Submit both `.qmd` and rendered `.html` to Canvas. **Index:** 3 variables — poverty rate (C17002), unemployment rate (B23025), median income (B19013, reversed). All pulls use 2010 TIGER/Line tract boundaries. **Time points:** 2013 (2009–2013 ACS), 2016 (2012–2016 ACS), 2019 (2015–2019 ACS) — all three share 2010 boundaries. **Checklist:** - [ ] Renamed with your last name - [ ] All pre-filled steps execute without error - [ ] Q1 answer — min. 200 words with specific numbers - [ ] Q2 code complete with output + comparison paragraph - [ ] Q3 policy brief — min. 150 words, cites at least one reading - [ ] Both `.qmd` and `.html` submitted to Canvas # Step 1: Rebuild Dataset ```{r} #| label: step.01-vars hardship_vars <- c( pov_below50 = "C17002_002", pov_50to99 = "C17002_003", pov_den = "C17002_001", unemp_num = "B23025_005", unemp_den = "B23025_002", median_income = "B19013_001" ) ``` ```{r} #| label: step.01-pull #| results: hide load_or_pull <- function(rds_path, pull_fn) { if (file.exists(rds_path)) { readRDS(rds_path) } else { result <- pull_fn() dir.create(dirname(rds_path), showWarnings = FALSE, recursive = TRUE) saveRDS(result, rds_path) result } } tract_2013_raw <- load_or_pull( "../data/maricopa_tracts_2013.rds", function() get_acs( geography = "tract", variables = hardship_vars, state = "AZ", county = "Maricopa", year = 2013, survey = "acs5", geometry = TRUE, progress_bar = FALSE ) ) tract_2019_raw <- load_or_pull( "../data/maricopa_tracts_2019.rds", function() get_acs( geography = "tract", variables = hardship_vars, state = "AZ", county = "Maricopa", year = 2019, survey = "acs5", geometry = TRUE, progress_bar = FALSE ) ) ``` ```{r} #| label: step.01-pull-offline #| eval: false # ── Offline fallback — run this block instead if the Census API is down ── # Replace the get_acs() chunk above with these readRDS() calls. # All datasets are pre-built and stored in data/ for offline use. # # tract_2013_raw <- readRDS("../data/maricopa_tracts_2013.rds") # tract_2019_raw <- readRDS("../data/maricopa_tracts_2019.rds") ``` ```{r} #| label: step.01-wrangle widen_and_rate <- function(df) { df %>% select(-moe) %>% pivot_wider(names_from = variable, values_from = estimate) %>% mutate(poverty_rate = (pov_below50 + pov_50to99) / pov_den, unemp_rate = unemp_num / unemp_den) %>% filter(!is.na(poverty_rate), !is.na(unemp_rate), !is.na(median_income)) } tract_2013_wide <- widen_and_rate(tract_2013_raw) tract_2019_wide <- widen_and_rate(tract_2019_raw) vars_to_pool <- c("poverty_rate", "unemp_rate", "median_income") r2013 <- tract_2013_wide %>% st_drop_geometry() %>% select(GEOID, all_of(vars_to_pool)) r2019 <- tract_2019_wide %>% st_drop_geometry() %>% select(GEOID, all_of(vars_to_pool)) pooled_stats <- map_dfr(vars_to_pool, function(v) { vals <- c(r2013[[v]], r2019[[v]]) tibble(variable = v, mean = mean(vals, na.rm = TRUE), sd = sd(vals, na.rm = TRUE)) }) apply_pooled_z <- function(df, stats) { for (v in stats$variable) { pm <- stats$mean[stats$variable == v]; ps <- stats$sd[stats$variable == v] df[[paste0("z_", v)]] <- (df[[v]] - pm) / ps } df$z_median_income <- -1 * df$z_median_income df$hardship_index <- rowMeans( df[, c("z_poverty_rate", "z_unemp_rate", "z_median_income")], na.rm = TRUE) return(df) } r2013_z <- apply_pooled_z(r2013, pooled_stats) r2019_z <- apply_pooled_z(r2019, pooled_stats) change_df <- tract_2019_wide %>% select(GEOID, NAME) %>% left_join(r2019_z %>% select(GEOID, hardship_2019 = hardship_index), by = "GEOID") %>% inner_join(r2013_z %>% select(GEOID, hardship_2013 = hardship_index), by = "GEOID") %>% mutate(hardship_change = hardship_2019 - hardship_2013) cat("Tracts:", nrow(change_df), "| Mean change:", round(mean(change_df$hardship_change), 3), "\n") ``` # Step 2: Spatial Weights ```{r} #| label: step.02-weights nb <- poly2nb(change_df, queen = TRUE) w <- nb2listw(nb, style = "W", zero.policy = TRUE) ``` # Step 3: LISA — 2013 and 2019 ```{r} #| label: step.03-lisa classify_lisa <- function(x, w, p_thresh = 0.05) { lisa <- localmoran(x, w, zero.policy = TRUE) z <- scale(x)[, 1] wz <- lag.listw(w, z, zero.policy = TRUE) p <- lisa[, "Pr(z != E(Ii))"] case_when( z > 0 & wz > 0 & p < p_thresh ~ "HH", z < 0 & wz < 0 & p < p_thresh ~ "LL", z > 0 & wz < 0 & p < p_thresh ~ "HL", z < 0 & wz > 0 & p < p_thresh ~ "LH", TRUE ~ "NS" ) } change_df <- change_df %>% mutate( lisa_2013 = classify_lisa(hardship_2013, w), lisa_2019 = classify_lisa(hardship_2019, w) ) cat("2013 LISA:\n"); print(table(change_df$lisa_2013)) cat("2019 LISA:\n"); print(table(change_df$lisa_2019)) ``` # Step 4: Transition Matrix ```{r} #| label: step.04-transition cat("── Transition matrix (rows = 2013, columns = 2019) ──\n") print(table(`2013` = change_df$lisa_2013, `2019` = change_df$lisa_2019)) cat("\n── Row proportions ──\n") print(round(prop.table( table(`2013` = change_df$lisa_2013, `2019` = change_df$lisa_2019), margin = 1) * 100, 1)) ``` # Step 5: Trajectory Classification and Map ```{r} #| label: step.05-trajectory change_df <- change_df %>% mutate(trajectory = case_when( lisa_2013 == "HH" & lisa_2019 == "HH" ~ "Persistent HH", lisa_2013 != "HH" & lisa_2019 == "HH" ~ "Emerging HH", lisa_2013 == "HH" & lisa_2019 != "HH" ~ "Dissolving HH", lisa_2013 == "LL" & lisa_2019 == "LL" ~ "Persistent LL", lisa_2013 != "LL" & lisa_2019 == "LL" ~ "Emerging LL", lisa_2013 == "HL" | lisa_2019 == "HL" ~ "HL Outlier", TRUE ~ "Stable NS" )) cat("── Trajectory distribution ──\n") change_df %>% st_drop_geometry() %>% count(trajectory, sort = TRUE) %>% mutate(pct = round(100 * n / sum(n), 1)) %>% print() ``` ```{r} #| label: step.05-map #| fig-height: 9 traj_colors <- c( "Persistent HH" = "#D7191C", "Emerging HH" = "#FDAE61", "Dissolving HH" = "#FEE08B", "Persistent LL" = "#2C7BB6", "Emerging LL" = "#ABD9E9", "HL Outlier" = "#9E5BB4", "Stable NS" = "#CCCCCC" ) ggplot(change_df) + geom_sf(aes(fill = trajectory), color = NA) + scale_fill_manual(values = traj_colors, name = "Trajectory\n(2013 → 2019)") + theme_void() + labs(title = "Neighborhood Hardship Trajectories, 2013–2019", subtitle = "Maricopa County Census Tracts", caption = "LISA run on 2013 and 2019 pooled-standardized economic hardship index (queen contiguity, p < 0.05)") ``` # Step 6: Space-Time Moran's I ```{r} #| label: step.06-moran set.seed(42) moran_chg <- moran.mc(change_df$hardship_change, w, nsim = 999, zero.policy = TRUE) cat("Space-Time Moran's I (on 2013→2019 change score):\n") cat(" I =", round(moran_chg$statistic, 4), "\n") cat(" p =", round(moran_chg$p.value, 4), "\n") ``` ```{r} #| label: step.06-moran-plot #| fig-height: 5 z_chg <- scale(change_df$hardship_change)[, 1] wz_chg <- lag.listw(w, z_chg, zero.policy = TRUE) ggplot(tibble(z = z_chg, wz = wz_chg), aes(x = z, y = wz)) + geom_hline(yintercept = 0, color = "gray70") + geom_vline(xintercept = 0, color = "gray70") + geom_point(alpha = 0.35, size = 1.2, color = "#8C1D40") + geom_smooth(method = "lm", se = FALSE, color = "#FFC627", linewidth = 1) + theme_minimal() + labs(title = "Moran Scatterplot: Change Scores (2013 → 2019)", subtitle = paste0("I = ", round(moran_chg$statistic, 3), " | p = ", round(moran_chg$p.value, 3)), x = "Standardized Change", y = "Spatial Lag of Change") ``` # Step 7: Three-Point Trend ```{r} #| label: step.07-pull-2016 #| results: hide # year = 2016: 2012–2016 ACS | also uses 2010 TIGER/Line boundaries tract_2016_raw <- load_or_pull( "../data/maricopa_tracts_2016.rds", function() get_acs( geography = "tract", variables = hardship_vars, state = "AZ", county = "Maricopa", year = 2016, survey = "acs5", geometry = FALSE, progress_bar = FALSE ) ) ``` ```{r} #| label: step.07-pull-2016-offline #| eval: false # ── Offline fallback — run this block instead if the Census API is down ── # Replace the get_acs() chunk above with these readRDS() calls. # All datasets are pre-built and stored in data/ for offline use. # # tract_2016_raw <- readRDS("../data/maricopa_tracts_2016.rds") ``` ```{r} #| label: step.07-trend tract_2016_wide <- widen_and_rate(tract_2016_raw %>% mutate(geometry = NULL)) r2016 <- tract_2016_wide %>% select(GEOID, all_of(vars_to_pool)) r2016_z <- apply_pooled_z(r2016, pooled_stats) trend_df <- change_df %>% st_drop_geometry() %>% select(GEOID, hardship_2013, hardship_2019) %>% inner_join(r2016_z %>% select(GEOID, hardship_2016 = hardship_index), by = "GEOID") %>% mutate( trend_type = case_when( hardship_2013 > hardship_2016 & hardship_2016 > hardship_2019 ~ "Consistently improving", hardship_2013 < hardship_2016 & hardship_2016 < hardship_2019 ~ "Consistently worsening", hardship_2013 > hardship_2016 & hardship_2016 < hardship_2019 ~ "V-shaped (improved mid)", hardship_2013 < hardship_2016 & hardship_2016 > hardship_2019 ~ "Inverted-V (worsened mid)", TRUE ~ "Flat" ) ) cat("── Three-point trend distribution (2013 | 2016 | 2019) ──\n") trend_df %>% count(trend_type, sort = TRUE) %>% mutate(pct = round(100 * n / sum(n), 1)) %>% print() change_df <- change_df %>% left_join(trend_df %>% select(GEOID, trend_type), by = "GEOID") ``` ```{r} #| label: step.07-trend-map #| fig-height: 9 trend_colors <- c( "Consistently improving" = "#2166AC", "Consistently worsening" = "#D7191C", "V-shaped (improved mid)" = "#74ADD1", "Inverted-V (worsened mid)" = "#F46D43", "Flat" = "#CCCCCC" ) ggplot(change_df) + geom_sf(aes(fill = trend_type), color = NA) + scale_fill_manual(values = trend_colors, na.value = "#EEEEEE", name = "Trend (2013|2016|2019)") + theme_void() + labs(title = "Three-Point Hardship Trend (2013 | 2016 | 2019)", subtitle = "Maricopa County Census Tracts") ``` --- # Question 2: Student Modify Code ```{r} #| label: q2-lisa-strict # YOUR CODE HERE — rerun classify_lisa() with p_thresh = 0.01 ``` ```{r} #| label: q2-trajectory-strict # YOUR CODE HERE — reclassify trajectories with strict LISA ``` ```{r} #| label: q2-map-strict #| fig-height: 9 # YOUR CODE HERE — trajectory map at p < 0.01 ```