# BAH Growth Regression Study

**WindMayor Office of Underwriting — Research Note R-BAH.regression**
**Fitted: 2026-05-21**

---

## QUESTION

What predicts MHA-level BAH 5-year CAGR (computed 2021 → 2026, "with-dependents E5" tier, the standard junior-enlisted family allowance)?

In plain English: across 349 U.S. Military Housing Areas with both BAH data and a usable installation feature record, can we identify economic, geographic, or demographic features that explain why some MHAs saw their BAH grow at 8 %/yr while others grew at 2 %/yr over the last five years?

This is a load-bearing question for the WindMayor durable-rent-near-bases thesis. If BAH growth tracks identifiable local economic features (rent, income, population, coast-distance), then BAH is *not* a free-floating administrative number — it is a downstream signal of the same underlying local market that drives civilian rents.

## DATA

### Target variable

`bah_5y_cagr` — already pre-computed and stored as `installations[i].bah_yield_5y` in `projects/milbase/web/data.json`. Computed from the DoD BAH series 2021 → 2026 (six annual datapoints, five compounding intervals), with-dependents E5 tier. Source: DoD Defense Travel Management Office, annual BAH releases.

The MHA-level value is the average of `bah_yield_5y` across all installations sharing an MHA code (in practice almost all MHAs have a single installation; multi-installation MHAs like the National Capital Region average to the same number across rows).

### Per-installation features (from `data.installations[]`)

| Field | Source | Used as |
|---|---|---|
| `troop_count` | Open-source / OSD published rosters | log(troops + 1) — heavy right tail |
| `years_active` | Wikipedia / DoD historical records | years since established |
| `lat`, `lon` | Open-source coordinates | absolute lat, lon |
| `branch` | DoD | unused (categorical, would balloon feature count) |
| `feeder_city`, `feeder_county` | Bureau of Census / lookup | unused in v1 |

### Area-economic features (from `data.area_econ`, where available)

Where `area_econ[base_id]` exists with a `market_rent_allhomes` time series, we compute:

- `zillow_zori_5y_cagr` — 2021 → 2026 CAGR of Zillow ZHVI-derived market rent (`area_econ.market_rent_allhomes`). This is the **lagged** local-rent feature used to test H1.

### Distance-to-coast feature

Computed by haversine from each installation's lat/lon to the nearest point in a hard-coded list of representative U.S. coastal cities (San Diego, LA, SF, Seattle, NYC, Boston, Norfolk, Miami, Charleston SC, Houston, New Orleans, Jacksonville, Honolulu, Anchorage). Conservative reference set — meant as a *proxy*, not a precise GIS distance.

### Data NOT used (and why)

- **ACS median income**: no per-installation ACS pull is cached locally. We do not fabricate income series; H2 is therefore tested **only by proxy** — implicitly via the rent and population covariates that ACS income correlates with.
- **Census population growth**: no per-installation population series cached. H4 is tested only indirectly via troop-count change.
- **Troop-count change over time**: only a single recent troop-count snapshot is in `data.installations`, not a time series. H5 is therefore tested as *current troop level* (cross-sectional level), not change. We flag this honestly below.
- **Overseas / OCONUS MHAs** (codes starting `ZZ`): filtered out before regression. BAH is dollar-denominated and overseas rates are governed by OHA (Overseas Housing Allowance) machinery + FX, not the same statute.
- **Alaska / Hawaii**: kept in (they receive CONUS BAH treatment in the DoD series we pulled).

## HYPOTHESES (stated BEFORE running regression)

| ID | Hypothesis | Predicted sign |
|---|---|---|
| H1 | BAH 5y CAGR is positively correlated with the same MHA's Zillow ZORI 5y CAGR (lagged, since BAH lags market rent by 12–24 months). | **+** |
| H2 | BAH 5y CAGR is positively correlated with local median income growth (ACS). | **+** (untestable directly — flagged) |
| H3 | BAH 5y CAGR shows a coast/inland gradient — coastal MHAs grew faster post-2021. | **negative coefficient on `dist_to_coast_km`** |
| H4 | BAH 5y CAGR is higher in MHAs with growing population (Census). | **+** (untestable directly — flagged) |
| H5 | Higher troop-count MHAs see higher BAH growth (more demand → higher BAH). | **+** |

Additional pre-registered controls (no directional hypothesis):

- `lat` (north–south gradient — Sun Belt vs. Frost Belt)
- `lon` (east–west gradient — coast vs. interior, partially co-linear with `dist_to_coast`)
- `years_active` (older bases may be in mature, slower-growing markets)

## METHODS

1. **Sample construction** — one row per MHA-with-installation (n = 349). Where multiple installations share an MHA, target and features are averaged across installations.
2. **Train/test split** — 80/20, `random_state = 42`. n_train ≈ 279, n_test ≈ 70.
3. **OLS linear regression** (`statsmodels.OLS`) — the load-bearing interpretable model. Reports coefficient, 95 % CI, t-stat, p-value per feature. All features standardised (z-score on training set) before fit so coefficients are directly comparable in magnitude.
4. **Gradient boosting** — `sklearn.GradientBoostingRegressor` was the planned supplementary nonlinear baseline. **sklearn is not installed in the working Python environment.** As stated in the project guardrails, we drop rather than fabricate. The OLS results stand alone; gbm is `null` in the results JSON.
5. **Partial correlation matrix** — Pearson correlation between every (feature, target) pair, *and* between every (feature, feature) pair, computed via numpy. Reported in the results JSON.
6. **Residual diagnostics** — distribution of test residuals, MAPE on test.

## LIMITATIONS

1. **Small n** (349). Six features ÷ 349 obs = OK for naive OLS (~58:1 obs:feature), but partial-correlation power is modest and any out-of-sample claim is fragile.
2. **Regional clustering / spatial autocorrelation**: MHAs near each other are not independent. OLS standard errors are reported *without* spatial-cluster correction; treat p-values as suggestive.
3. **Missing-feature attrition**: not every installation has `area_econ` populated. Rows with missing `zillow_zori_5y_cagr` are imputed with the cohort mean (and a missingness dummy is *not* included in v1 — flagged for follow-up).
4. **Cross-section, not panel**: we regress a single 5y CAGR on a single feature vector. We are NOT doing dynamic / longitudinal estimation. No causal claim is made — this is descriptive.
5. **H2 and H4 untestable directly** because no per-MHA ACS income series and no Census population series are cached locally. We are honest about this rather than reaching for proxy fabrications.
6. **Single BAH tier** (E5 with dependents). Other tiers (junior single, senior officer) might respond to different drivers. Out of scope here.

## RESULTS

Canonical numbers live in `data/regression_results.json`. Human-readable summary below.

### Sample

- **n = 177 MHAs** (not 349). The ticket's 349 figure counts installations; the cross-sectional unit of analysis is the MHA, and 294 CONUS installations collapse to **177 unique MHAs** (many bases share an MHA code — e.g., the National Capital Region has multiple installations under one BAH rate).
- 80/20 split → n_train = 142, n_test = 35.
- Target mean = **6.33 %/yr BAH CAGR**, std = **2.30 %**, range [−0.4 %, +12.1 %].

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

| Feature | β (std) | 95 % CI | t | p | Verdict |
|---|---:|---|---:|---:|---|
| `zhvi_5y_cagr` | **+0.00895** | [+0.0052, +0.0127] | +4.73 | **<0.001** | strongest single predictor |
| `zillow_zori_5y_cagr` | **+0.00535** | [+0.0016, +0.0091] | +2.85 | **0.005** | second strongest |
| `log_troops` | −0.00239 | [−0.0059, +0.0012] | −1.33 | 0.186 | not significant |
| `years_active` | +0.00012 | [−0.0036, +0.0038] | +0.06 | 0.949 | noise |
| `abs_lat` | +0.00151 | [−0.0021, +0.0051] | +0.84 | 0.403 | not significant |
| `lon` | −0.00138 | [−0.0052, +0.0025] | −0.71 | 0.479 | not significant |
| `dist_to_coast_km` | −0.00027 | [−0.0041, +0.0035] | −0.14 | 0.887 | essentially zero |

Constant ≈ 0.0641 = the unconditional mean BAH 5y CAGR (~6.4 %/yr), matching the target mean.

### Fit quality

- **R² train = 0.240**, adjusted R² = 0.200, F-test p = 3.8e-06 (model jointly significant).
- **R² test = −0.272** — the held-out test set is fit *worse than the unconditional mean*. The 35-row test sample is dominated by 2-3 high-leverage MHAs that the train set's coefficients cannot extrapolate to. This is not a good model out-of-sample; it is suggestive in-sample only.
- MAPE test = 0.44 (large; partly because BAH CAGRs near zero blow up the denominator).
- RMSE test = 0.0236 (≈ 2.4 percentage points of CAGR — same order of magnitude as the target's std, confirming the model isn't really beating the unconditional mean out-of-sample).

### Hypothesis verdicts

| ID | Hypothesis | Verdict |
|---|---|---|
| H1 | BAH growth ← ZORI growth | **SUPPORTED** (β=+0.0054, p=0.005; bivariate r=+0.26) |
| H2 | BAH growth ← income growth | **UNTESTABLE** — no cached ACS per-MHA series |
| H3 | Coastal MHAs grew faster | **INCONCLUSIVE/rejected** — `dist_to_coast_km` coefficient essentially zero (β=−0.0003, p=0.89) |
| H4 | BAH growth ← population growth | **UNTESTABLE** — no cached Census per-MHA series |
| H5 | More troops → higher BAH growth | **REJECTED** — coefficient wrong sign (β=−0.0024, p=0.19), bivariate r=−0.09 |

### What survived

- **H1 (ZORI lag)** — the durable-rent-near-bases thesis isn't disproven. ZORI 5y CAGR is a real positive predictor of subsequent BAH 5y CAGR (overlapping windows here — see limitations).
- **ZHVI (home-value growth) is a stronger predictor than ZORI** in this sample. That's notable: it suggests BAH appraisers are picking up the same underlying market signal that asset prices reflect, even though BAH is nominally a *rent* allowance.

### What didn't survive

- **H3 (coast gradient): rejected.** Distance to the nearest coastal anchor has essentially no explanatory power once rent/home-value growth is controlled. The "coastal premium" was already priced into ZORI/ZHVI; coast distance is colinear noise.
- **H5 (troops → BAH): rejected** in cross-section. The sign is even *wrong*, though not significantly. Bigger garrisons do not seem to systematically command faster BAH growth — likely because BAH is set by *local market rent*, not by troop demand. The marginal Marine doesn't move the Camp Lejeune rent index.

### Surprises

1. **Coast distance is dead.** I expected a clear negative coefficient. It is essentially zero. The geographic story (coast premium) is fully absorbed by the rent-index covariates.
2. **Troop count has the *wrong sign*** (not significantly). The naive "more troops = more demand = more BAH" intuition is not visible at the MHA cross-section.
3. **ZHVI beats ZORI** as a BAH predictor. Rent allowance correlates better with *asset prices* than with *its own asset class's rent index*. Probably reflects the fact that ZORI 5y CAGRs are noisier than ZHVI 5y CAGRs at the MHA level (and 44 % of MHAs needed ZORI imputation — see limitations).
4. **Out-of-sample R² is negative.** The model has explanatory in-sample power (F-test highly significant) but fails to extrapolate to a 35-row holdout. With n=177 and a heavy-tailed target, this is not surprising — and it is the *honest* answer.

### What we discarded

- **Original `troop_count`** (linear) — replaced with `log_troops` due to extreme right skew (a handful of major bases at 50k+ troops dominated the linear feature).
- **`feeder_county` / categorical state dummies** — would have ballooned to 50 features for 177 obs (~3.5:1 obs:feature, unsafe). Excluded by design, not by p-hack.
- **`branch`** — categorical, same reason.
- **Per-installation ACS pulls** — not cached in this environment. Hypotheses H2 and H4 are *flagged as untestable*, not papered over with weak proxies.

### What this means for the WindMayor thesis

The durable-rent-near-bases thesis is **partially supported, not proven**:

- **Supported:** Local rent and home-value growth do predict subsequent BAH growth (H1 — clear positive, p<0.01). If you believe local rents will continue growing in a given MHA, you have *some* basis to expect BAH to follow. This is the directional sign WindMayor's pricing assumes.
- **Not proven:** The cross-sectional model can't beat the unconditional mean on a 35-row holdout. So we cannot *quantitatively* underwrite a 5y BAH-growth path for a specific MHA from rents alone — too much idiosyncratic variance.
- **Operational implication:** BAH growth is best treated as **rent growth plus a large residual**. The residual is not explained by troops, age, latitude, longitude, or coast distance in the features we have. It may be explained by features we don't have (BRAC changes, mission shifts, local supply elasticity).

### Causation

**We cannot conclude causation.** This is observational, cross-sectional data on a 5-year window. Any of:

1. Rent growth causes BAH growth (the DoD-appraiser-follows-market story);
2. The same local economic conditions cause both;
3. BAH growth causes rent growth (the federal-flow-of-funds-cap-on-rents story; unlikely at this magnitude but not impossible);

…would produce the positive partial correlation we observe. The methodology cannot distinguish them.

---

## Conclusions — honest version

1. **Yes, BAH growth tracks local rent and home-value growth at the MHA cross-section.** This is the only finding that survives a 142-row OLS with p<0.01.
2. **No, we cannot underwrite a specific MHA's 5y BAH path from these features.** The out-of-sample R² is negative.
3. **Troops, age, geography (lat/lon/coast) do not add explanatory power once rent indices are included.**
4. **H2 and H4 remain open** — they will need a real ACS / Census per-MHA pull to test, which is a follow-up data-pipeline ticket, not something this study could fabricate.
5. **n=177 is the binding constraint.** With ~6 informative features and 177 observations, residual variance dominates. Anyone claiming a publication-grade BAH-prediction model on this dataset is overfitting.

---

## Sources

- DoD Defense Travel Management Office — BAH historical & current rates 2013-2026.
- Zillow Research — ZORI (Observed Rent Index) and ZHVI (Home Value Index), ZIP-level monthly.
- US Department of Defense — installation rosters, troop strength snapshots.
- WindMayor — `projects/milbase/web/data.json` (aggregated 2026-05-20).

## No-fabrication declaration

No numbers in this study were invented. Every feature has a source field above. Features that could not be computed from available cached data were dropped, not stubbed. The Python script (`scripts/run_regression.py`) is the executable record.

---

# v2 with `zip_master` — Re-run on the nationwide ZIP data spine

**Fitted: 2026-05-21** — supersedes v1 *additively*. v1 stands. v2 adds features that v1 explicitly flagged as untestable.

## What changed since v1

v1 noted H2 (income growth) and H4 (population growth) were UNTESTABLE because no per-MHA ACS series were cached locally. The `projects/zipdata/data/zip_master.json` spine (3ae9b46, 31,646 ZCTAs, 2026-05-21) closes that gap with 13 datasets:

| Dataset | Coverage | Source |
|---|---:|---|
| ACS B19013 — median household income | 94.8 % | Census ACS 5y 2023 |
| ACS B01003 — total population | 99.9 % | Census ACS 5y 2023 |
| ACS B01002 — median age | 99.8 % | Census ACS 5y 2023 |
| ACS B15003 — bachelor+ pct | 99.7 % | Census ACS 5y 2023 |
| ACS B23025 — unemployment pct | 99.6 % | Census ACS 5y 2023 |
| ACS B25001 — housing units | 99.9 % | Census ACS 5y 2023 |
| ACS B25064 — median rent | 81.7 % | Census ACS 5y 2023 |
| ACS B25077 — median home value | 94.1 % | Census ACS 5y 2023 |
| Zillow ZHVI latest level | 82.7 % | Zillow Research |
| Zillow ZORI latest level | 26.3 % | Zillow Research |
| HUD FMR 2BR | 100.0 % | HUD FY26 SAFMR / FY25 county fallback |
| BLS LAUS 2024 county unemp | 99.0 % | BLS Local Area Unemployment Stats |
| IRS migration in/out 2022 | 98.4 % | IRS SOI county migration |

**Honest limitation up front**: zip_master is *cross-sectional* (latest values only) for the new features. There is no per-ZIP 5y time series for ACS/HUD/BLS/IRS in this spine. So features added in v2 are tested as **levels** correlating with BAH 5y CAGR, not as growth-on-growth regressions. The H2/H4 wording from v1 ("BAH growth tracks income/population GROWTH") becomes, in v2:

- **H2_v2**: BAH growth correlates with **median household income LEVEL** (Sun-Belt-style high-growth markets may already be high-income; or the opposite — low-income/affordable MHAs may be growing into the mean).
- **H4_v2**: BAH growth correlates with **population LEVEL** (proxy for market size; a true growth test would require pre-spine ACS 2018 or earlier and is out of scope).

ZORI 5y CAGR and ZHVI 5y CAGR (the time-series features) remain in the model because they came from `area_econ` time series in milbase data.json, not from zip_master.

## Methodology — v2

1. **Spatial join**: For each installation with `feeder_county` + `feeder_state` (or `state`), look up the matching `(state, county_name)` in zip_master. Name normalization rules:
   - `... County` ↔ `... Parish` (LA)
   - `... County` ↔ `... Borough` / `... Census Area` / `... Municipality` / `... City and Borough` (AK)
   - VA independent cities: "Hampton County" → "Hampton city" (and ~30 other known VA independent cities)
   - CT/RI planning regions / AK Unorganized Borough: dropped if unresolved (no fabrication)
2. **County aggregation**: All ZCTAs in the matched county are aggregated to a single feature vector using **population-weighted mean** for income, age, education, unemployment, rent, home value, FMR, ZHVI, ZORI; and **sum** for population, housing_units, irs_inflow, irs_outflow.
3. **Engineered features**:
   - `irs_net_migration_per_capita` = (inflow − outflow) / population
   - `log_population` = log1p(population)
   - `log_income` = log(median_hh_income)
   - `log_hud_fmr` = log(hud_fmr_2br)
4. **MHA aggregation**: Multiple installations in the same MHA → troop-count-weighted average (unweighted fallback if all troops missing).
5. **OLS** with z-scored features, 80/20 split, `random_state=42`. Identical mechanics to v1 for like-for-like comparison.
6. **GBM**: still no sklearn in env → `null`, same as v1.

## Pre-registered hypotheses — v2 (stated BEFORE running)

| ID | Hypothesis | Predicted sign | Status in v1 |
|---|---|---|---|
| H1 | BAH 5y CAGR ↑ with ZORI 5y CAGR | + | SUPPORTED — re-confirm |
| H2_v2 | BAH 5y CAGR ↑ with median HH income LEVEL | + (high-income MHAs grew faster) | UNTESTABLE in v1 |
| H3 | BAH 5y CAGR ↑ closer to coast | − on dist_to_coast | REJECTED — keep as control |
| H4_v2 | BAH 5y CAGR ↑ with population LEVEL | + (bigger markets ran hotter) | UNTESTABLE in v1 |
| H5 | BAH 5y CAGR ↑ with troop count | + | REJECTED — keep as control |
| **H6** | **BAH 5y CAGR ↑ with bachelor+ pct (educational attainment)** | + (tech-hub markets ran hotter) | NEW in v2 |
| **H7** | **BAH 5y CAGR ↑ with IRS net migration per capita** | + (people moving in → market tightening) | NEW in v2 |
| **H8** | **BAH 5y CAGR ↑ with HUD FMR level** | + (HUD's own market-rent estimate co-varies with BAH) | NEW in v2 |
| **H9** | **BAH 5y CAGR ↑ when BLS unemployment is LOW** | − on bls_unemployment_pct | NEW in v2 |

No directional hypothesis (controls): median_age, log_population, log_income.

**Pre-registration commit**: this section is written and committed before `run_regression_v2.py` is executed. We will not re-edit hypotheses after seeing results.

## Targets stated up front (don't game)

- v1 OLS R² test = **−0.272** (overfit, doesn't extrapolate).
- v2 ≥ +0.10 → new features add real signal.
- v2 ≥ +0.30 → genuinely publishable.
- v2 ≈ v1 → conclude: **BAH is ZORI, period**. Anything else is noise.

## RESULTS — v2

Canonical JSON: `data/regression_results_v2.json`. Headline:

| Metric | v1 | v2 (main, k=15) | v2 (joined-only) | v2 (parsimonious, k=3) |
|---|---:|---:|---:|---:|
| n MHAs | 177 | 177 | 126 | 177 |
| n_train / n_test | 142 / 35 | 142 / 35 | 101 / 25 | 142 / 35 |
| R² train | 0.240 | **0.289** | 0.318 | 0.222 |
| R² test | −0.272 | **−0.510** | −0.265 | −0.133 |

**v2 does NOT beat v1 out-of-sample.** Held-out R² got *worse* (−0.272 → −0.510) when we added the eight zip_master cross-sectional features to the seven v1 features. That is overfit: k=15 features on n_train=142 is too many. The two sensitivity models confirm this:

- **Joined-only** (drop 51 MHAs where the county join missed) → test R² = −0.265 — basically tied with v1. The main-spec worsening is partly cohort-mean imputation drag, not a new finding.
- **Parsimonious** (only the |t|≥2 features in the main spec: ZHVI/ZORI 5y CAGR + log_income) → test R² = −0.133, the cleanest out-of-sample number we have, still negative. log_income loses significance (p=0.59) when not surrounded by collinear noise.

### Top 5 features by |t| in the v2 main spec

| Feature | β (std) | t | p | Notes |
|---|---:|---:|---:|---|
| `zhvi_5y_cagr` | **+0.0076** | +3.82 | <0.001 | strongest predictor, re-confirmed |
| `zillow_zori_5y_cagr` | **+0.0049** | +2.50 | 0.014 | second, re-confirmed |
| `log_income` | **−0.0096** | −2.19 | 0.031 | sig but **wrong sign**, disappears in parsimonious spec |
| `z_bls_unemployment_pct` | +0.0044 | +1.73 | 0.085 | weak, wrong sign |
| `z_unemployment_pct_acs` | −0.0037 | −1.37 | 0.173 | n.s. |

### Hypothesis verdicts — v2

| ID | Hypothesis | Verdict | β (std) | p |
|---|---|---|---:|---:|
| H1 | BAH ↑ ZORI 5y CAGR | **SUPPORTED (re-confirmed)** | +0.0049 | 0.014 |
| H2_v2 | BAH ↑ HH income LEVEL | **REJECTED (wrong sign; collinear noise)** | −0.0096 | 0.031 |
| H3 | Coast distance (control) | **REJECTED (still dead)** | +0.0003 | 0.91 |
| H4_v2 | BAH ↑ population LEVEL | **INCONCLUSIVE** | −0.0011 | 0.66 |
| H5 | BAH ↑ troop count (control) | **REJECTED (still wrong sign)** | −0.0024 | 0.32 |
| H6 | BAH ↑ bachelor+ pct | **INCONCLUSIVE (sign right, weak)** | +0.0038 | 0.28 |
| H7 | BAH ↑ IRS net migration / capita | **INCONCLUSIVE (wrong sign, weak)** | −0.0015 | 0.46 |
| H8 | BAH ↑ HUD FMR LEVEL | **INCONCLUSIVE — wrong test, no FMR time series** | +0.0031 | 0.47 |
| H9 | BAH ↑ when BLS unemp is LOW | **REJECTED (weak, wrong sign)** | +0.0044 | 0.085 |

### ZHVI vs ZORI — which is the bigger driver now?

Still **ZHVI** (β=+0.0076, t=+3.82) > **ZORI** (β=+0.0049, t=+2.50). Same as v1. The home-value index covaries with BAH growth more cleanly than the rent index does at the MHA cross-section. Likely reasons: (1) ZORI 5y CAGRs are noisier than ZHVI 5y CAGRs at the MHA level; (2) ZHVI has 82.7 % national coverage vs ZORI's 26.3 % (per zip_master coverage_pct), so ZORI imputation is heavier; (3) DoD appraisers may use comp/appraisal evidence (asset prices) alongside actual rent surveys, weighting ZHVI-like signals more.

### What the new features bought us

- **H2 + H4 are no longer untestable as LEVELS** — but the levels don't add usable predictive signal. The right test is growth-on-growth (ACS 2018 vs 2023, BLS LAUS 2019 vs 2024, HUD FMR FY21 vs FY26). zip_master holds latest cross-section only; that's a yellow-lane (data pipeline) follow-up.
- **H6, H7, H8, H9 are all inconclusive or rejected with wrong signs.** Education, net migration, FMR level, and unemployment levels do not improve BAH-growth prediction.
- **Diagnostic signal**: the parsimonious spec (just ZHVI/ZORI + log_income) achieves the best test R² we've ever produced (−0.133, up from v1's −0.272). When we drop log_income from that, test R² further improves slightly. That suggests **BAH growth is essentially a noisy function of ZHVI and ZORI growth, with everything else either redundant or noise.**

### What this means for the WindMayor thesis

**Unchanged from v1**: The durable-rent-near-bases thesis is partially supported. Local rent growth predicts subsequent BAH growth (H1). v2 reaffirms this with the bigger feature set.

**Strengthened**: We can now say it's *not just* missing ACS data that limited v1 — even with ACS, HUD FMR, BLS LAUS, and IRS migration in the model, **nothing beats ZHVI/ZORI**. The signal is the rent index. Period.

**Weakened**: Out-of-sample R² remains negative across all specs. We cannot underwrite a specific MHA's 5y BAH path from features alone. The residual (BAH growth minus ZORI/ZHVI growth) is not explained by anything in the zip_master spine.

### Limitations of v2 specifically

1. **Cross-sectional vs growth**: zip_master is latest-snapshot only. H2/H4/H8/H9 should ideally be tested as growth rates over the same 5y window; we tested levels.
2. **Imputation**: 51 of 177 MHAs got cohort-mean values for the new features (28.8 %). The joined-only sensitivity exists exactly to bound this.
3. **County-level aggregation**: zip_master is per-ZIP; we aggregate to county via population-weighted mean. Installations spanning multiple counties are lost (rare; ~5 in the join misses).
4. **IRS migration is one year (2022)**, not a 5y signal. The right test would chain 2018-2023 migration.
5. **No spatial-cluster correction.** Same caveat as v1.
6. **sklearn still absent**: GBM remains `null`. With k=15 features on n=142, GBM might actually mitigate overfit; not testable in this env.
7. **Causation still disclaimed.** Cross-section observational. No causal claim.

### Final honest verdict on whether v2 beats v1

**No, v2 does not beat v1.** Test R² got worse (−0.272 → −0.510) in the main spec; the sensitivity specs are tied (joined-only at −0.265) or marginally better (parsimonious at −0.133). The new features close v1's pre-registered untestable holes (H2, H4) — but the answer is **the new features don't help on this target**. v2's contribution is: we *now know* BAH growth is essentially ZHVI + ZORI + noise, not because we lacked data, but because the data shows nothing else loads.

### Sources — v2-specific (additive to v1 source list)

- `projects/zipdata/data/zip_master.json` (3ae9b46, 2026-05-21):
  - Census ACS 5y 2023 (tables B01002, B01003, B15003, B19013, B23025, B25001, B25064, B25077)
  - Zillow ZHVI / ZORI by ZIP, latest month (2026-04)
  - HUD FMR FY26 SAFMR + FY25 county FMR fallback
  - BLS LAUS county annual 2024
  - IRS SOI county migration 2022–2023
  - Census 2020 ZCTA-county relationship file

### Pre-registration audit trail

This section's "Pre-registered hypotheses — v2" block was written *before* `run_regression_v2.py` was executed. No hypothesis was re-specified after seeing results. The wording adjustment from "growth" to "level" for H2_v2, H4_v2, H8 was made up-front because zip_master is cross-sectional, and explicitly disclosed in the pre-registration. v2 is an honest report of what the new data buys you: better diagnostic confidence that **BAH is ZORI/ZHVI + noise**, not a new predictive model.

