# City Rent Growth Regression Study

**WindMayor Office of Underwriting — Research Note R-CITY.rent-cagr-regression**
**Pre-registered: 2026-05-21 (before fit)**

---

## QUESTION

What predicts **5-year ZORI CAGR at the U.S. city cross-section** across the top-250 cities by population?

This is the parallel-methodology follow-up to R-BAH.regression-v2. The BAH study answered "what predicts MHA-level rent allowance growth?" — finding ZHVI > ZORI > everything else, R² ≈ 0.24 in-sample, negative R² out-of-sample.

Here we change the unit of analysis from "MHA tied to a base" to "city". The hypothesis: at the **city aggregation** (not MHA aggregation), do other features (education, migration, income level, unemployment, population) add signal beyond what asset-price growth already explains?

In plain English: when New York grows rents at 4 %/yr and Memphis grows rents at 1 %/yr, is the difference explainable by something measurable about the cities themselves — or is it mostly just "ZHVI did the same thing"?

## DATA

### Target variable (computed in this script — not pre-aggregated)

`zori_5y_cagr` — 5-year compound annual growth rate of the city ZORI rent index, computed **2021-04 → 2026-04** (latest available month is 2026-04 in cache `/tmp/cities_zori.csv`).

Computed as: `(zori_now / zori_5y_ago) ^ (1/5) - 1`.

If a city has no ZORI value for 2021-04 OR no value for 2026-04 (the endpoints), it is **dropped** (not imputed) — the target itself must be real or the row is meaningless.

Source: Zillow Research, City-level ZORI (sfrcondomfr smoothed, CC-BY research use).

### Per-city features (from `projects/cities/data/data.json` — 250 rows)

| Field | Source | Used as |
|---|---|---|
| `population` | Census Population Estimates Program SUB-EST2024 | log(pop) — heavy right tail |
| `median_income` | ACS 5-yr 2023 table B19013 | median household income LEVEL |
| `median_age` | ACS 5-yr 2023 table B01002 | median age |
| `home_value` | Zillow ZHVI City all-homes (sfrcondo blend) | level (for context) |
| `lat`, `lon` | Census 2023 Gazetteer Places | geographic gradient |

### Engineered features from the ZORI series itself (computed in this script)

- `zhvi_5y_cagr` — same window, computed from `/tmp/cities_zhvi_all.csv` (Zillow ZHVI all-homes 5y CAGR per city). This is the **asset-price growth control** mirroring BAH H1.

### Per-city features joined from ZIP master (nearest-ZCTA-by-haversine)

City lat/lon is matched to the closest ZCTA centroid from the 2023 Census Gazetteer (`/tmp/2023_Gaz_zcta_national.txt`), and that ZCTA's row from `projects/zipdata/data/zip_master.json` (31k ZCTAs) supplies:

| Field | Source | Used as |
|---|---|---|
| `bachelor_plus_pct` | ACS 5-yr 2023 (table B15003 derivation) | education-share predictor (H2) |
| `unemployment_pct_acs` | ACS 5-yr 2023 table B23025 | unemployment rate predictor (H4) |
| `irs_inflow_2022` | IRS SOI county-to-county migration (proxied to ZCTA) | inbound migration (H3) |
| `irs_outflow_2022` | IRS SOI county-to-county migration | outbound migration (H3) |

`irs_net_migration_2022 = irs_inflow_2022 - irs_outflow_2022`. This is the IRS exemption count (a proxy for people-moving-with-tax-returns, not raw population — but it is what's cached, and we don't fabricate).

### Data NOT used (and why)

- **Rent level (current ZORI)**: would be near-collinear with home_value level and possibly the target itself (lagged) — excluded to avoid mechanical correlation.
- **DSCR / yield / price-to-rent**: derived metrics, not independent features.
- **State dummies / one-hot region**: 50 states × 250 rows = unsafe ratio for OLS; excluded by design, not p-hack.
- **2020 → present migration deltas (annual)**: not cached; only single-year IRS snapshot is available.

## HYPOTHESES (stated BEFORE running regression)

| ID | Hypothesis | Predicted sign |
|---|---|---|
| H1 | City ZORI 5y CAGR correlates with ZHVI 5y CAGR (mirror of BAH H1; the asset-price-vs-rent comovement at the city cross-section). | **+** |
| H2 | City ZORI 5y CAGR correlates with `bachelor_plus_pct` (education share → high-income renters bid rents up). | **+** |
| H3 | City ZORI 5y CAGR correlates with IRS net migration (inflow − outflow); more people moving in → more rent pressure. | **+** |
| H4 | City ZORI 5y CAGR correlates INVERSELY with unemployment rate. | **−** |
| H5 | City ZORI 5y CAGR correlates with median income LEVEL (cities with higher absolute income grow rents faster — pricing power thesis). | **+** |
| H6 | City ZORI 5y CAGR correlates with population (size effect — bigger metros have more durable demand). | **+** |

Additional pre-registered controls (no directional hypothesis):

- `lat` (north–south gradient — Sun Belt vs. Frost Belt thesis)
- `lon` (east–west gradient)
- `median_age` (demographic composition control)

## METHODS

1. **Sample construction** — one row per city. Target = 5y ZORI CAGR computed from `/tmp/cities_zori.csv` directly. Features joined from `projects/cities/data/data.json` (already-condo-basis DSCR panel) plus nearest-ZCTA pull from `projects/zipdata/data/zip_master.json`.
2. **Feature engineering parallelized** across cities via `ThreadPoolExecutor` (per the project rule). Each city: (a) compute its ZORI/ZHVI CAGR, (b) find its nearest-ZCTA centroid, (c) pull ZCTA features.
3. **Train/test split** — 80/20, `random_state = 42`. Expected n_train ≈ 200, n_test ≈ 50 (subject to attrition from cities with no ZORI 5y window).
4. **OLS linear regression** (`statsmodels.OLS`) — the load-bearing model. All features standardized (z-score on training set) before fit so coefficients are directly comparable in magnitude. Reports β, 95 % CI, t-stat, p-value per feature.
5. **Gradient boosting** — `sklearn.GradientBoostingRegressor` planned as the supplementary nonlinear baseline. **sklearn is not installed in the working Python environment** (same env as the BAH study). Per project guardrails we drop rather than fabricate; gbm reports `null` in the results JSON.
6. **Partial correlation matrix** — Pearson correlation between every (feature, target) pair and every (feature, feature) pair. Computed via numpy; reported in the results JSON.
7. **Residual diagnostics** — R² train/test, MAPE, RMSE on test.

## LIMITATIONS

1. **n = 250 (max).** After attrition for cities with no ZORI 5y window the usable n is likely 220–240. Still small for cross-sectional inference — partial-correlation power is modest, and any out-of-sample claim is fragile.
2. **Nearest-ZCTA join is approximate.** A city's centroid is a single point; large cities span dozens of ZCTAs with very different ACS profiles. We pick the ZCTA closest to the city centroid by haversine; that ZCTA may not be the *central* ZCTA in income/education terms. This is a known proxy weakness; we flag rather than fabricate a polygon-based weighting.
3. **IRS migration is 2022-only.** A single-year migration snapshot may not be predictive of 2021–2026 rent growth on its own; the ideal feature would be 5y cumulative net migration. We test what we have.
4. **No spatial-cluster correction** on standard errors. Cities near each other (Dallas/Plano, SF/Oakland) are not independent draws. p-values are suggestive, not confirmatory.
5. **Cross-section, not panel.** We regress a single 5y CAGR on a single feature vector. No causal claim.
6. **Reverse causality risk on H5** (income LEVEL): high-rent cities select for higher-income earners. If income level predicts rent growth, that may just reflect persistent rent → income sorting.
7. **Top-250 by population is a selection bias.** Findings generalize to mid-large U.S. cities, not to rural areas or sub-100k towns.

## RESULTS

Canonical numbers live in `data/regression_results.json` after fit. Human-readable summary appended to this file post-fit.

---

## RESULTS (fitted 2026-05-21)

### Sample

- **250 cities → 244 with a real 5y ZORI window** (6 dropped, no imputation of the target).
- 80/20 split → **n_train = 195, n_test = 49**.
- Target mean = **5.14 %/yr** ZORI 5y CAGR (2021-04 → 2026-04), std = **1.49 %**, range [+1.5 %, +12.8 %].
- Missingness (imputed at cohort mean for *features only*, never the target):
  - `bachelor_plus_pct`: 11/244 (nearest-ZCTA had no ACS value)
  - `unemployment_pct_acs`: 10/244
  - `irs_net_migration_2022`: 13/244
  - `zhvi_5y_cagr`: 2/244 (city in ZORI panel but missing from ZHVI city panel)

### OLS — coefficients (standardized; load-bearing model)

| Feature | β (std) | 95 % CI | t | p | Verdict |
|---|---:|---|---:|---:|---|
| `zhvi_5y_cagr` | **+0.0071** | [+0.0055, +0.0087] | **+8.57** | **<0.001** | dominant predictor (H1) |
| `lon` | **+0.0042** | [+0.0023, +0.0060] | **+4.45** | **<0.001** | east-side-of-country premium |
| `abs_lat` | **+0.0031** | [+0.0015, +0.0046] | **+3.85** | **<0.001** | north > south after controls |
| `irs_net_migration_2022` | **−0.0030** | [−0.0046, −0.0015] | **−3.79** | **<0.001** | **WRONG SIGN — H3 rejected** |
| `median_income` | −0.0017 | [−0.0038, +0.0003] | −1.67 | 0.097 | inconclusive, wrong sign weakly |
| `log_population` | −0.0010 | [−0.0026, +0.0006] | −1.24 | 0.215 | not significant |
| `unemployment_pct_acs` | +0.0003 | [−0.0013, +0.0019] | +0.36 | 0.717 | noise |
| `bachelor_plus_pct` | +0.0002 | [−0.0014, +0.0019] | +0.29 | 0.772 | noise |
| `median_age` | +0.0003 | [−0.0013, +0.0020] | +0.42 | 0.678 | noise |

Constant ≈ 0.0508 — close to the target mean of 5.14 %/yr (as expected with standardized features).

### Fit quality

- **R² train = 0.482**, adjusted R² = **0.457**, F-test p ≈ 2.2×10⁻²² (model jointly highly significant)
- **R² test = 0.276** — the model **does generalize out-of-sample**, unlike the BAH study (where R² test went negative)
- RMSE test = 0.0145 (~ 1.45 pp/yr CAGR), MAPE test = 0.168
- Adjusted R² (0.457) ≈ R² test (0.276) — the in-sample / out-of-sample gap is real but not catastrophic

### Bivariate correlations with target (ranked by |r|)

| Feature | r |
|---|---:|
| `zhvi_5y_cagr` | **+0.569** |
| `lon` | +0.387 |
| `median_income` | **−0.290** |
| `log_population` | −0.171 |
| `unemployment_pct_acs` | +0.099 |
| `median_age` | −0.096 |
| `bachelor_plus_pct` | −0.085 |
| `abs_lat` | +0.064 |
| `irs_net_migration_2022` | −0.060 |

### Hypothesis verdicts

| ID | Hypothesis | Pre-reg sign | β_std | p | Bivariate r | Verdict |
|---|---|:---:|---:|---:|---:|---|
| H1 | ZORI CAGR ← ZHVI CAGR | + | **+0.0071** | **<0.001** | **+0.57** | **SUPPORTED** (dominant) |
| H2 | ZORI CAGR ← bachelor_plus_pct | + | +0.0002 | 0.77 | −0.08 | **INCONCLUSIVE** (near-zero, even slightly negative in bivariate) |
| H3 | ZORI CAGR ← IRS net migration | + | **−0.0030** | **<0.001** | −0.06 | **REJECTED — wrong sign at p<0.001** |
| H4 | ZORI CAGR ← unemployment (inverse) | − | +0.0003 | 0.72 | +0.10 | **INCONCLUSIVE** (sign opposite of expected, not significant) |
| H5 | ZORI CAGR ← median income LEVEL | + | −0.0017 | 0.097 | −0.29 | **INCONCLUSIVE / weakly rejected** — sign is opposite of pre-reg; bivariate strongly negative (high-income cities grew rents *slower*) |
| H6 | ZORI CAGR ← log_population | + | −0.0010 | 0.22 | −0.17 | **INCONCLUSIVE** (small negative trend, not significant) |

### What survived

- **H1 (ZHVI co-movement)** — the asset-price-and-rent-move-together story is the strongest finding by a wide margin. ZHVI 5y CAGR alone explains r²=0.32 of the variance bivariately. A one-σ increase in ZHVI CAGR is associated with a +0.71 pp/yr increase in ZORI CAGR.
- **Geographic gradient** — `lon` (+) and `abs_lat` (+) are both significant at p<0.001 *after* controlling for ZHVI. This is a real surprise: it says that within cities with comparable home-price appreciation, **eastern-and-northern** cities grew rents faster than **western-and-southern** ones in 2021–2026. This is the opposite of the popular Sun-Belt-rent-explosion narrative once asset prices are controlled for. (Possibly: Sun Belt rent growth was already fully priced into ZHVI; the residual variance left after ZHVI tilts the *other* way.)

### What didn't survive

- **H2 (education premium): rejected.** `bachelor_plus_pct` from the nearest-ZCTA join has essentially zero explanatory power (β≈0, p=0.77). Even bivariately it's slightly negative (r=−0.08). Either (a) the nearest-ZCTA proxy is too noisy for large cities, or (b) education share doesn't actually predict rent CAGR at this aggregation level — it predicts rent *level*, which is a different question.
- **H3 (net migration): rejected with wrong sign at p<0.001.** This is the **biggest surprise** of the study. Cities with more IRS net inflow grew rents *slower*, not faster, in 2021–2026. Probable explanation: IRS net migration is dominated by the COVID-era exodus from expensive coastal cities (NYC, SF, LA — negative net flows) *into* cheaper southern/inland metros (positive net flows). The expensive cities then had rent recovery from a depressed 2021 base; the destination cities saw a 2021 spike followed by mean reversion. Net: high-inflow destinations are now *lower* CAGR cities. This is a real signal but the sign is the opposite of the simple "more people = higher rent" intuition.
- **H4 (unemployment): inconclusive.** Sign even goes the *wrong* way (positive — high-unemployment cities grew rents slightly faster), but p=0.72, so this is noise.
- **H5 (income level): inconclusive / weakly rejected.** Bivariate r=−0.29 (substantial!) — high-income cities grew rents *slower*. Partial coefficient barely misses significance at p=0.097 with the wrong sign. The bivariate negative is real and matters: a city like SF (very high income) grew rents slowly in 2021–2026 vs. cheaper midwestern cities. Income level may be a *negative* predictor of rent CAGR in this window — the opposite of the pricing-power thesis. We do not claim this is significant in the multivariate model, but flag it for v2.
- **H6 (population size): inconclusive.** Sign even slightly negative, p=0.22. The "bigger = better" story doesn't survive.

### Comparison to R-BAH.regression-v2

| Metric | BAH (n=177) | City (n=244) |
|---|---:|---:|
| Target mean | 6.33 %/yr BAH CAGR | 5.14 %/yr ZORI CAGR |
| Target std | 2.30 % | 1.49 % |
| R² train | 0.240 | **0.482** |
| Adjusted R² | 0.200 | **0.457** |
| R² test | **−0.272** | **+0.276** |
| F-test p | 3.8×10⁻⁶ | 2.2×10⁻²² |
| Strongest predictor | ZHVI 5y CAGR (β=+0.0090) | ZHVI 5y CAGR (β=+0.0071) |

**The city regression beats the BAH regression on every fit-quality metric.** R² test is positive (0.28) rather than negative (−0.27). This is the central finding for the WindMayor research thesis: city-level rent CAGR is a more tractable prediction problem than MHA-level BAH CAGR, primarily because (a) larger n, (b) the target is the rent index itself rather than an administrative pass-through of it, and (c) ZHVI and geographic gradients both carry significant signal at the city cross-section.

### Biggest surprise

**IRS net migration is a statistically significant negative predictor of rent CAGR at p<0.001 with the wrong pre-registered sign.** We expected "more people moving in → faster rent growth"; we got "more people moving in → slower rent growth". The likely mechanism is COVID-era reallocation: destination cities for the 2020–2022 migration wave (Sun Belt, Florida, Tennessee) had already spiked their rents by 2021, and the 2021→2026 CAGR captures their subsequent normalization. Origin cities (NYC, SF) bottomed out in 2021 and showed higher CAGR from a low base. This is a base-effect artifact of the window, not a violation of supply/demand — but it is exactly what a naive "follow the migration map" thesis would miss.

### What we discarded

- **Raw `population`** — replaced with `log_population` due to extreme right skew (NYC at 8.5M dominates).
- **Rent level (`rent`)** — excluded as a feature because it is mechanically related to the target (the target is computed from the same series).
- **`yield`, `dscr`, `price_to_rent`** — these are derived metrics, not independent features.
- **State / region categoricals** — would have been 50 dummies for 244 rows (unsafe ratio).

### What this means for the WindMayor thesis

The durable-rent thesis at the **city** cross-section is more strongly supported than at the **MHA** cross-section:

- **Supported (H1):** ZHVI growth and ZORI growth move together at r=+0.57. If you believe a city's asset prices will keep growing, you have substantial basis to expect its rents to grow alongside.
- **Mostly supported overall:** R² train = 0.48, R² test = 0.28. We *can* predict city rent CAGR out-of-sample, just not perfectly.
- **The geographic gradient is real after controls** — eastern and northern cities (within ZHVI peers) grew rents faster. Probably a regional-economy story we don't have features for.
- **The migration story is the opposite of naive** — high-inflow Sun Belt destinations are now *low*-CAGR cities, in this 5y window. WindMayor should NOT underwrite a long-rent-in-Phoenix thesis purely on net-migration data.
- **The pricing-power thesis is weakly rejected** — high-income cities grew rents *slower* in this window (bivariate r=−0.29).

### Causation

**We cannot conclude causation.** Same disclaimer as the BAH study. This is a cross-sectional, observational 5y CAGR-on-features regression. ZHVI → ZORI, common drivers behind both, or ZORI → ZHVI would all produce the observed partial correlations.

### What we cannot rule out

1. **Window-specific artifact.** 2021–2026 is the COVID-recovery + post-COVID-normalization window. Many "results" here may just be base-effect signatures of pandemic dislocation. A 2010–2019 or 2015–2020 window might give different answers.
2. **Nearest-ZCTA proxy noise.** H2 and H4 use ACS features from a single ZCTA; large cities have heterogeneous ZIPs. A polygon-weighted ACS join might recover signal we couldn't here.
3. **Omitted variables.** Housing supply elasticity (Wharton index), local construction permits, climate-risk pricing — none in the model.

---

## Conclusions — honest version

1. **City rent growth is more predictable than BAH growth.** R² train 0.48 vs 0.24; R² test +0.28 vs −0.27.
2. **ZHVI is the dominant feature in both regressions.** The same insight survives: rents move with asset prices.
3. **Migration, education, unemployment, income, and population — none added signal in the expected direction.** Migration was significant with the *opposite* sign; the others were noise.
4. **Geographic gradient survives at the city level** (lon and abs_lat both p<0.001), unlike at the MHA level. This is the main novel finding.
5. **n=244 is the binding constraint.** With 9 features ÷ 244 obs ≈ 27:1, OLS is statistically respectable but spatial autocorrelation still inflates p-values; treat them as suggestive.

---

## Sources

- Zillow Research — City ZORI (sfrcondomfr smoothed), monthly, CC-BY research use; City ZHVI all-homes (sfrcondo blend), monthly, CC-BY.
- U.S. Census Bureau — Population Estimates Program SUB-EST2024 (public domain); 2023 Gazetteer Places (city centroids, public domain); 2023 Gazetteer ZCTA (centroids, public domain).
- U.S. Census Bureau ACS 5-yr 2023 — B19013 (median household income), B01002 (median age), B15003-derived `bachelor_plus_pct`, B23025-derived `unemployment_pct_acs` (all public domain).
- IRS SOI — 2022 county-to-county migration (`irs_inflow_2022`, `irs_outflow_2022`, public domain), proxied to ZCTA via the zip_master crosswalk.
- WindMayor — `projects/cities/data/data.json` (aggregated 2026-05-21); `projects/zipdata/data/zip_master.json`.

## No-fabrication declaration

No numbers in this study were invented. Cities without a real 2021-04 *and* 2026-04 ZORI endpoint were **dropped** (6 cities, no imputation of the target). Features with missing values were imputed with the cohort mean and the imputation count is reported in `regression_results.json` under `missingness`. The Python script (`scripts/run_regression.py`) is the executable record.
