The Department of Defense publishes 14 years of Basic Allowance for Housing (BAH) by Military Housing Area — 349 installations across roughly 250 MHA codes. The question: across the 2021→2026 window, what local economic or geographic features predict an MHA's annualised BAH growth rate? We pre-registered five hypotheses, ran an OLS regression, and report the honest result — including the parts that did not work.
One row per Military Housing Area. Target = the 5-year compound annual growth rate of BAH (with-dependents, E5 tier) from 2021→2026, taken from DoD's annual rate releases. Features include the same MHA's Zillow rent index (ZORI) 5y CAGR, Zillow home value index (ZHVI) 5y CAGR, log troop count, years the installation has existed, latitude, longitude, and haversine distance to the nearest coastal city.
349 installations collapse to 177 unique CONUS MHA codes (many bases share an
MHA — e.g. the National Capital Region has multiple installations
under a single BAH rate). We drop overseas MHAs (codes starting with
ZZ) because OHA, not BAH, governs them.
dist_to_coast_km.log_troops = −0.0024 · p = 0.19 · bivariate r = −0.09.
Wrong sign. BAH is set by local market rent, not by garrison size — the marginal Marine does not move the Camp Lejeune rent index.
(Caveat: tested as level, not change-in-troops, because we only have a single snapshot.)
| Feature | β | 95% CI low | 95% CI high | t | p | bivariate r |
|---|---|---|---|---|---|---|
| const | +0.0641 | +0.0606 | +0.0676 | +36.30 | <0.001 | — |
| zhvi_5y_cagr p<.001 | +0.0090 | +0.0052 | +0.0127 | +4.73 | <0.001 | +0.37 |
| zillow_zori_5y_cagr p<.01 | +0.0054 | +0.0016 | +0.0091 | +2.85 | 0.005 | +0.26 |
| log_troops n.s. | −0.0024 | −0.0059 | +0.0012 | −1.33 | 0.186 | −0.09 |
| abs_lat n.s. | +0.0015 | −0.0021 | +0.0051 | +0.84 | 0.403 | +0.02 |
| lon n.s. | −0.0014 | −0.0052 | +0.0025 | −0.71 | 0.479 | +0.14 |
| dist_to_coast_km n.s. | −0.0003 | −0.0041 | +0.0035 | −0.14 | 0.887 | −0.07 |
| years_active n.s. | +0.0001 | −0.0036 | +0.0038 | +0.06 | 0.949 | −0.00 |
Read: a one-standard-deviation increase in ZHVI 5y CAGR
is associated with a +0.90 percentage-point increase in BAH 5y CAGR (an
MHA at mean+1σ of home-value growth grew BAH about
0.90 pp/yr faster than the average MHA). ZORI gives +0.54 pp/yr per
sigma. Everything else is statistical noise.
scripts/run_regression.py ·
regression_results.json
| R² (train, n=142) | 0.240 |
| Adjusted R² | 0.200 |
| F-test p-value | 3.8×10⁻⁶ |
| MAPE train | 0.513 |
The model is jointly significant in-sample. Rent and home-value indices carry the load.
| R² (test, n=35) | −0.272 |
| RMSE test | 0.0236 |
| MAPE test | 0.436 |
| Target std | 0.0230 |
Worse than predicting the unconditional mean. RMSE (0.024) is the same order as the target's std (0.023). The model has directional signal but cannot underwrite a specific MHA out-of-sample.
troop_count (replaced with log_troops due to heavy right tail); branch and state dummies (would have ballooned the feature count past safe levels for n=177).sklearn is not installed in the working Python environment, so the GBM result is reported as null rather than hand-rolled.market_rent_allhomes from area_econ for the 2021–2026 window. Those rows received the cohort-mean ZORI. A missingness dummy is in the panel but not in the published feature set — v2 should test whether imputed rows behave differently.
The regression is one Python script. It reads projects/milbase/web/data.json,
builds the MHA panel in parallel threads, fits the OLS, and writes the
JSON result file. No external network calls, no hidden state.
[data] kept 294 installations after CONUS+target filter (of 454 total) [panel] 177 MHA rows [fit] OLS on n_train=142 features=7 === OLS coefficient summary (standardized) === const beta=+0.0641 t=+36.30 p=0.000 zillow_zori_5y_cagr beta=+0.0054 t= +2.85 p=0.005 *** zhvi_5y_cagr beta=+0.0090 t= +4.73 p=0.000 *** log_troops beta=-0.0024 t= -1.33 p=0.186 years_active beta=+0.0001 t= +0.06 p=0.949 abs_lat beta=+0.0015 t= +0.84 p=0.403 lon beta=-0.0014 t= -0.71 p=0.479 dist_to_coast_km beta=-0.0003 t= -0.14 p=0.887 R^2 train = 0.2402 R^2 test = -0.2716
v1 flagged H2 (income growth) and H4 (population growth) as
untestable because no per-MHA ACS series were cached. The
projects/zipdata/data/zip_master.json spine (31,646 ZCTAs,
13 datasets) closes that gap. v2 keeps the v1 features and adds:
median HH income, total population, bachelor+ pct, ACS unemployment,
BLS LAUS unemployment, IRS net migration per capita, HUD FMR 2BR, and
median age — aggregated to the installation's feeder county.
Honest framing up front: zip_master holds latest
cross-sectional values, not time series, for the new features. So
H2/H4/H8 are tested as levels against BAH 5y CAGR, not as
growth-on-growth. That's an honest re-formulation, not a re-write
of the hypothesis — it is stated in the pre-registration in
RESEARCH.md.
| Metric | v1 | v2 (main) | v2 (joined-only) | v2 (parsimonious) |
|---|---|---|---|---|
| n (MHAs) | 177 | 177 | 126 | 177 |
| n_train / n_test | 142 / 35 | 142 / 35 | 101 / 25 | 142 / 35 |
| features | 7 | 15 | 15 | 3 |
| R² train | 0.240 | 0.289 | 0.318 | 0.222 |
| R² test | −0.272 | −0.510 | −0.265 | −0.133 |
Main v2 spec adds explanatory power in-sample (R² train 0.240 → 0.289) but loses ground out-of-sample — classic overfit with k=15 features on n_train=142. The parsimonious sensitivity (only the three features with |t|≥2 in the full v2) is the cleanest out-of-sample result and still negative.
| Feature | β | t | p | Source dataset |
|---|---|---|---|---|
| zhvi_5y_cagr p<.001 | +0.0076 | +3.82 | 0.000 | Zillow ZHVI time series |
| zillow_zori_5y_cagr p<.05 | +0.0049 | +2.50 | 0.014 | Zillow ZORI time series |
| log_income p<.05 (WRONG SIGN) | −0.0096 | −2.19 | 0.031 | ACS B19013 (level) |
| z_bls_unemployment_pct p<.10 (WRONG SIGN) | +0.0044 | +1.73 | 0.085 | BLS LAUS 2024 (level) |
| z_unemployment_pct_acs n.s. | −0.0037 | −1.37 | 0.173 | ACS B23025 (level) |
Two sensitivity models reported alongside the main spec, both pre-stated as robustness checks, not re-specifications: