The warehouse
Where every number comes from, and how it was checked.
Prices from 2000 to 2026Q2, every census one year release from 2005 to 2024 (none in 2020), rents and income limits from HUD, weekly rates from Freddie Mac through 2026-10-22, permits and population by county from 2000, and 55,817,457 loans from the HMDA extract, 2018 to 2025, went through named quarantine rules (flagged and counted, nothing deleted) and 7 delineation vintages into a dbt warehouse of 35 models and 164 tests. The metric layer defines each of 21 metrics once over the finest grain and once over the shipped mart and compares them at 4 grains: 0 disagreements.
The quarantine report
| Source | Rule | Flagged | Rows |
|---|---|---|---|
| acs | jam_value | 7 | 2,415,786 |
| acs | moe_exceeds_estimate | 108,627 | 2,415,786 |
| acs | negative_moe | 0 | 2,415,786 |
| bps | county_listed_twice | 10 | 81,768 |
| bps | imputed_share_high | 8,045 | 81,768 |
| bps | malformed_fips | 0 | 81,768 |
| bps | negative_units | 0 | 81,768 |
| fhfa | implausible_jump | 15 | 85,272 |
| fhfa | missing_quarter | 61 | 85,272 |
| fhfa | non_positive_index | 0 | 85,272 |
| hmda | income_exempt_or_missing | 4,893,629 | 64,743,292 |
| hmda | metro_unresolved | 2,310,292 | 64,743,292 |
| hmda | not_principal_residence | 6,717,971 | 64,743,292 |
| hmda | rate_missing | 2,083,671 | 64,743,292 |
| hmda | reverse_open_or_business | 3,275,856 | 64,743,292 |
| hmda | term_not_30_years | 13,859,990 | 64,743,292 |
| hmda | value_below_loan | 984,896 | 64,743,292 |
| hud_fmr | bedroom_order | 0 | 4,764 |
| hud_fmr | non_positive_rent | 0 | 4,764 |
| hud_il | limit_order | 0 | 84,224 |
| hud_il | non_positive_income | 0 | 84,224 |
| omb | county_in_two_areas | 0 | 13,288 |
| omb | malformed_fips | 0 | 13,288 |
| pmms | implausible_rate | 0 | 2,900 |
| pmms | missing_week | 0 | 2,900 |
| popest | non_positive_population | 0 | 81,714 |
| popest | vintage_overlap | 0 | 81,714 |
Source: several sources; population: every row of every source; as of 2026-10-23.
The quarantine report by source and year
Rows flagged by each rule, by source and year; a row can trip more than one rule
Source: every source through its contract (real), the warehouse's mart_quarantine.
The delineation crosswalk
Every source is mapped to its area through the delineation vintage in force when it was published; the warehouse carries each area under its latest code with the earlier codes as aliases. 7 vintages, the latest 2,023; the crosswalk tests prove every county maps to one area per vintage.
The crosswalk counts are in the geography document.
The metric layer, reconciled
1,764 cells compared across 4 grains, 0 disagreements.
| Metric | Over the grain | Over the mart |
|---|---|---|
| households | sum(estimate) filter (where variable = 'B11001_001') | sum(households) |
| owner households | sum(estimate) filter (where variable = 'B25003_002') | sum(owner_households) |
| renter households | sum(estimate) filter (where variable = 'B25003_003') | sum(renter_households) |
| renters burdened | sum(estimate) filter (where variable in ('B25070_007', 'B25070_008', 'B25070_009', 'B25070_010')) | sum(renters_burdened) |
| renters severe | sum(estimate) filter (where variable = 'B25070_010') | sum(renters_severe) |
| renter burdened share | sum(estimate) filter (where variable in ('B25070_007', 'B25070_008', 'B25070_009', 'B25070_010')) / (sum(estimate) filter (where variable = 'B25070_001') - coalesce(sum(estimate) filter (where variable = 'B25070_011'), 0)) | sum(renters_burdened) / sum(renters_with_share) |
| housing units | sum(estimate) filter (where variable = 'B25001_001') | sum(housing_units) |
| permits | sum(permits) | sum(permits) |
| population | sum(population) | sum(population) |
| domestic migration | sum(domestic_migration) | sum(domestic_migration) |
| purchase loans | count(*) filter (where loan_purpose = 1) | sum(purchase_loans) |
| refinance loans | count(*) filter (where loan_purpose in (31, 32)) | sum(refinance_loans) |
| low rate originations | count(*) filter (where interest_rate < 4.0 and not q_rate_missing and not q_term_not_30_years) | sum(low_rate_originations) |
| turnover per 100 owners | 100.0 * sum(purchase_loans) / sum(owner_households) | 100.0 * sum(purchase_loans) / sum(owner_households) |
| lock in per 100 owners | 100.0 * sum(low_rate_originations) / sum(owner_households) | 100.0 * sum(low_rate_originations) / sum(owner_households) |
| permits per 100 households | 100.0 * sum(permits) / sum(households) | 100.0 * sum(permits) / sum(households) |
| domestic migration per 100 | 100.0 * sum(domestic_migration) / sum(population) | 100.0 * sum(domestic_migration) / sum(population) |
| purchase under 35 share | sum(purchase_under_35) / sum(purchase_with_age) | sum(purchase_under_35) / sum(purchase_with_age) |
| owner mortgage burdened share | sum(owners_mortgage_burdened) / sum(owners_mortgage_with_share) | sum(owners_mortgage_burdened) / sum(owners_mortgage_with_share) |
| payment share ratio of sums | 12.0 * sum(payment) / sum(median_income) | 12.0 * sum(payment) / sum(median_income) |
| price to income ratio of sums | sum(value_anchored) / sum(median_income) | sum(value_anchored) / sum(median_income) |
Source: several sources; population: every metro and year in the window, through the metric layer; as of 2026-10-23.
The anchor sensitivity
Every headline of chapter one re-anchored in each neighbour of the chosen year; the word degenerate marks a neighbour where the headline turns.
| Anchor year | Latest payment share | 2006 payment share | Difference | Metros above 2006 | Note |
|---|---|---|---|---|---|
| 2,015 | 35.0% | 33.8% | +1.1 pts | 283 | |
| 2,017 | 34.8% | 33.7% | +1.1 pts | 283 | |
| 2,019 | 34.9% | 33.7% | +1.1 pts | 283 | chosen |
| 2,023 | 33.5% | 32.3% | +1.2 pts | 283 |
Source: FHFA index anchored to the census, with Freddie Mac rates (real); model: anchor; population: every metropolitan area with a census income and an anchored value, 2005 to 2024; as of 2026-10-23.
The recovery study
Every estimator, run on simulated metros where the truth is known, 24 conditions with 20 seeds each, before it touched a real metro. The condition with strong reverse causality is drawn in clay; it is where the migration chapter admits its limit.
| Estimator | Mean bias | Mean RMSE | Mean coverage | Mean flag rate | Worst bias under | Lowest coverage | Lowest coverage under |
|---|---|---|---|---|---|---|---|
| anchored_level | -0.0014 | 0.0135 | not applicable | not applicable | weak_demand/narrow_supply/none_reverse/high_noise | not applicable | not available |
| anchored_payment_share | -0.0002 | 0.0038 | 43% | not applicable | weak_demand/narrow_supply/none_reverse/low_noise | 35% | weak_demand/narrow_supply/mild_reverse/high_noise |
| borrower_ratio_shift | -0.0011 | 0.0058 | 76% | not applicable | strong_demand/narrow_supply/strong_reverse/high_noise | 55% | weak_demand/wide_supply/mild_reverse/low_noise |
| bracket_interpolation | +0.0037 | 0.0039 | not applicable | not applicable | weak_demand/narrow_supply/strong_reverse/high_noise | not applicable | not available |
| breakeven_horizon | +4.4168 | 4.4415 | not applicable | 70% | weak_demand/narrow_supply/strong_reverse/low_noise | not applicable | not available |
| burden_changepoint | +0.0042 | 0.0186 | not applicable | 100% | strong_demand/narrow_supply/strong_reverse/low_noise | not applicable | not available |
| lock_in_did | +0.0015 | 0.0055 | 94% | 85% | weak_demand/narrow_supply/none_reverse/high_noise | 85% | weak_demand/narrow_supply/strong_reverse/high_noise |
| lock_in_placebo | -0.0001 | 0.0087 | 85% | not applicable | strong_demand/narrow_supply/none_reverse/high_noise | 80% | strong_demand/narrow_supply/mild_reverse/high_noise |
| migration_effect | -0.0002 | 0.0027 | 98% | 18% | strong_demand/wide_supply/strong_reverse/low_noise | 95% | strong_demand/narrow_supply/none_reverse/low_noise |
| migration_naive | -0.0036 | 0.0043 | 61% | not applicable | strong_demand/wide_supply/strong_reverse/low_noise | 5% | strong_demand/narrow_supply/strong_reverse/low_noise |
| migration_placebo | -0.0027 | 0.0043 | 82% | not applicable | strong_demand/wide_supply/strong_reverse/high_noise | 15% | strong_demand/narrow_supply/strong_reverse/high_noise |
| ranking_adjusted | -0.0437 | 0.0475 | not applicable | 89% | strong_demand/narrow_supply/strong_reverse/low_noise | not applicable | not available |
| self_report_correction | -0.0015 | 0.0034 | not applicable | 2% | weak_demand/wide_supply/mild_reverse/low_noise | not applicable | not available |
| severe_share | 0.0000 | 0.0000 | 100% | not applicable | strong_demand/narrow_supply/none_reverse/low_noise | 100% | strong_demand/narrow_supply/mild_reverse/high_noise |
| supply_controlled | +0.0026 | 0.0043 | 85% | not applicable | strong_demand/narrow_supply/strong_reverse/high_noise | 70% | strong_demand/wide_supply/strong_reverse/high_noise |
| supply_uncontrolled | +0.0058 | 0.0070 | 70% | not applicable | strong_demand/narrow_supply/strong_reverse/high_noise | 30% | strong_demand/narrow_supply/strong_reverse/high_noise |
| under_35_shift | -0.0002 | 0.0018 | 85% | not applicable | strong_demand/wide_supply/mild_reverse/high_noise | 60% | strong_demand/wide_supply/mild_reverse/high_noise |
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
anchored level: largest bias under weak_demand/narrow_supply/none_reverse/high_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
anchored payment share: largest bias under weak_demand/narrow_supply/none_reverse/low_noise
Absolute bias by condition; interval coverage averages 43%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
borrower ratio shift: largest bias under strong_demand/narrow_supply/strong_reverse/high_noise
Absolute bias by condition; interval coverage averages 76%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
bracket interpolation: largest bias under weak_demand/narrow_supply/strong_reverse/high_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
breakeven horizon: largest bias under weak_demand/narrow_supply/strong_reverse/low_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
burden changepoint: largest bias under strong_demand/narrow_supply/strong_reverse/low_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
lock in did: largest bias under weak_demand/narrow_supply/none_reverse/high_noise
Absolute bias by condition; interval coverage averages 94%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
lock in placebo: largest bias under strong_demand/narrow_supply/none_reverse/high_noise
Absolute bias by condition; interval coverage averages 85%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
migration effect: largest bias under strong_demand/wide_supply/strong_reverse/low_noise
Absolute bias by condition; interval coverage averages 97%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
migration naive: largest bias under strong_demand/wide_supply/strong_reverse/low_noise
Absolute bias by condition; interval coverage averages 61%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
migration placebo: largest bias under strong_demand/wide_supply/strong_reverse/high_noise
Absolute bias by condition; interval coverage averages 82%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
ranking adjusted: largest bias under strong_demand/narrow_supply/strong_reverse/low_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
self report correction: largest bias under weak_demand/narrow_supply/mild_reverse/high_noise
Absolute bias by condition
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
severe share: largest bias under strong_demand/narrow_supply/none_reverse/low_noise
Absolute bias by condition; interval coverage averages 100%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
supply controlled: largest bias under strong_demand/narrow_supply/strong_reverse/high_noise
Absolute bias by condition; interval coverage averages 85%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
supply uncontrolled: largest bias under strong_demand/narrow_supply/strong_reverse/high_noise
Absolute bias by condition; interval coverage averages 70%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
under 35 shift: largest bias under strong_demand/wide_supply/mild_reverse/high_noise
Absolute bias by condition; interval coverage averages 85%
Source: simulated metros with known truth; population: the recovery study, every condition and seed; seeds: 20; as of 2026-10-22.
The data dictionary
| Published file | Bytes |
|---|---|
| briefing.json | 50,056 |
| charts/burden.json | 6,871 |
| charts/buyers.json | 4,338 |
| charts/decision.json | 9,688 |
| charts/definition.json | 6,503 |
| charts/lock_in.json | 53,143 |
| charts/migration.json | 68,630 |
| charts/ranking.json | 12,912 |
| charts/rate_shock.json | 7,705 |
| charts/supply.json | 61,078 |
| live_check.json | 1,150 |
| manifest.json | 503,507 |
| marts/calc_metros.parquet | 157,642 |
| marts/calc_quarters.parquet | 137,286 |
| marts/ch1_national_series.parquet | 5,319 |
| marts/ch1_self_report.parquet | 12,128 |
| marts/ch2_decomposition.parquet | 4,345 |
| marts/ch2_lock_in_metros.parquet | 19,306 |
| marts/ch3_burden_by_income.parquet | 6,340 |
| marts/ch3_metro_shares.parquet | 317,476 |
| marts/ch3_national_bands.parquet | 3,104 |
| marts/ch4_cells.parquet | 64,388 |
| marts/ch5_ranking.parquet | 38,521 |
| marts/ch6_by_year.parquet | 5,002 |
| marts/ch6_largest_metros.parquet | 5,642 |
| marts/ch7_cross_section.parquet | 29,130 |
| marts/ch7_synthetic_controls.parquet | 7,580 |
| marts/ch8_curve.parquet | 2,940 |
| marts/ch8_metros.parquet | 94,319 |
| marts/mart_income_brackets.parquet | 732,677 |
| marts/mart_metro_quarter.parquet | 1,521,043 |
| marts/mart_metro_year.parquet | 3,561,523 |
| marts/mart_national_year.parquet | 4,474 |
| marts/mart_quarantine.parquet | 3,299 |
| marts/mart_rent_burden_by_income.parquet | 242,889 |
| recorded_session.json | 235,509 |
| story.json | 33,336 |
Every column of every exported table is named in the CSV bundle's dictionary, and every definition is in docs/definitions.md.
Exports and the BI companions
- The workbook with live formulas
Pick a metro on the Model sheet and type an income, a rate and a down payment; PMT and lookup formulas produce the payment, the payment share, the affordable price and a 360 row amortisation table. LibreOffice recalculates it in the tests against the calculator for three metros.
- The Tableau extracts and view specification
The metro by year and metro by quarter marts, the ranking, the burden bands and the supply cells as CSV, with the five views the companion workbook shows.
- The Power BI model specification
Tables, relationships and DAX measures (payment share, price to income, affordable share, cost burdened share, permits per hundred households, lock in share, turnover) as text.
- The CSV bundle and its dictionary
Every chapter table as CSV, with every column named.
What the housing economist would push back on
A warehouse can agree with itself and still be wrong about the world. The reconcile proves that two expressions over two grains give the same number; it cannot prove that owners reported their home values right or that a lender reported the income it used. That is why the quarantine rules flag a margin that exceeds its estimate, a jam value, a missing quarter and a loan with no rate rather than fixing them, and why every flagged row is counted here by source and year.