🏠 Hedonic Housing Price Models for the United States
UQO Working Paper No. 3 — A Multi-Method Comparison of Parametric, Quantile, and Machine Learning Approaches
TL;DR — Three hedonic frameworks are compared on 788,842 active Zillow listings covering all 50 U.S. states and DC. Semi-log OLS with 62 regressors gives R² = 0.634 (0.725 out of sample with 885 ZIP3 fixed effects; Moran's I = 0.27 on residuals); cluster-robust standard errors are 8–12× the HC3 errors and leave 33 of 63 coefficients significant. State-by-state estimation shows living-area elasticities from 0.06 to 1.15; a fully interacted model gains +6.1 pp over state fixed effects. Paired cluster-bootstrap Wald tests reject coefficient equality between τ = 0.10 and 0.90 for 10 of 13 key variables. XGBoost reaches R² = 0.833 under a random split but 0.425 when ten states are held out (median 0.653 across 51 leave-one-state-out models); once 0.6 % of corrupt records are removed, OLS is as accurate out of state (0.539) as the best tree ensemble. The gap is traced to memorised state price levels (within-state R² = 0.62), to "neighbourhood" variables that are state-median imputations for 93 % of listings, and to boosting capacity (held-out R² peaks at 75 rounds). A coding error (ln lot = 0 for 16.5 % of listings) is corrected: lot elasticity 0.095, not 0.018.
📌 Status
| Item | Value |
|---|---|
| Version | 1.2 (October 2026) — revised 2026-10-17 by the editorial-board review (docs/REVIEW-REPORT.md, docs/CHANGELOG-REVISION.md); v1.1 (May 2026) frozen as git tag pre-revision-20261017 and docs/main_pre-revision-20261017.pdf |
| SSRN | not yet submitted |
| Journal | not yet submitted |
| Code & data | figures and tables reproducible from stored artifacts; raw Zillow extract (3.2 GB) available on request |
🎯 What this paper does
The paper compares three frameworks for hedonic housing valuation on a single large dataset:
| Framework | Question it answers | Headline result |
|---|---|---|
| Semi-log OLS (62 regressors; HC3, ZIP3- and state-clustered SE; state-by-state) | Average conditional association between an attribute and log listing price, and how much it hides | R² = 0.634; area elasticity 0.63 (0.06–1.15 across states); lot elasticity 0.095 (recoded) |
| Quantile regression (τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90}; paired cluster bootstrap) | How attribute gradients vary across the price distribution | Equality rejected for 10 of 13 variables (11 under the independence test) |
| Gradient boosting + SHAP (XGBoost, LightGBM; random / 10-state / GroupKFold / LOSO) | How well prices can be predicted, in seen and unseen markets, and which features drive predictions | R² = 0.833 (random) vs. 0.425 (10 states) vs. 0.653 (LOSO median); within-state 0.62 |
ln P_i = α + x_i'β + μ_g(i) + ε_i g = Census region → state → ZIP3The core methodological contribution is a systematic study of spatial leakage in hedonic model evaluation: random train/test splits let geographically proximate listings appear on both sides of the split, inflating performance metrics. The revision quantifies the damage with a 10-state holdout, grouped 5-fold CV by state, leave-one-state-out, a size-matched random split and a within-state R², and identifies its mechanics: region dummies, state-median-imputed "neighbourhood" variables (93 % of school ratings) and boosting capacity (held-out R² peaks at 75 rounds, falls to 0.425 at 1,000). All estimates are listing-price gradients (asking prices), not causal willingness-to-pay parameters.
🏆 Headline results
| Model | Random R² | 10-state holdout R² | Within-state R² (10 states) | RMSE (random) |
|---|---|---|---|---|
| OLS (62 regressors) | 0.630 | 0.539 (implausible records removed; −2×10⁶ with them) | 0.560 | 0.491 |
| OLS + State FE | 0.678 | — | — | 0.459 |
| OLS + ZIP3 FE | 0.725 | — | — | 0.423 |
| OLS, state-by-state (fully interacted) | 0.751 | — | — | — |
| Random Forest | 0.784 | 0.542 | — | 0.375 |
| LightGBM | 0.809 | — | — | 0.353 |
| XGBoost, 1,000 rounds | 0.833 | 0.425 | 0.618 | 0.330 |
| XGBoost, 75–100 rounds | 0.75–0.77 | 0.544–0.550 | — | — |
| XGBoost, no region dummies | 0.830 | 0.519 | 0.621 | — |
| XGBoost, no school variables | 0.817 | 0.377 | 0.596 | — |
| XGBoost + 6 missingness indicators | 0.835 | 0.446 | 0.623 | — |
| XGBoost + lat/lon | 0.870 | 0.464 | — | — |
| XGBoost, grouped 5-fold by state (pooled) | — | 0.620 | 0.657 | — |
| XGBoost, leave-one-state-out (51 models) | — | median 0.653 | — | — |
- Geography does the heavy lifting in OLS: region dummies → state FE → 885 ZIP3 FE raises out-of-sample R² from 0.630 → 0.678 → 0.725; residual Moran's I = 0.2745 (SD 0.0076); tree-ensemble test residuals: 0.079 (XGBoost).
- Inference: ZIP3- and state-clustered SEs are 7.6× and 11.6× HC3 (medians); significant coefficients 57 → 35 → 33 of 63.
- One market or 51? State-by-state living-area elasticity 0.06–1.15; bedroom, pool, luxury and HOA coefficients change sign in 19–23 states; fully interacted OLS: +6.1 pp over state FE.
- Distributional heterogeneity: the garage gradient is 10× larger at τ = 0.10 than at τ = 0.90; the pool premium only emerges above the median; 10 of 13 inter-quantile differences survive a paired ZIP3-cluster bootstrap.
- Ablation cascade (XGBoost): structural 0.498 → +lot 0.546 → +amenities 0.638 → +neighbourhood 0.814 → +market 0.824 → full 0.833 — but the neighbourhood block is mostly a state encoding (school variables imputed for 93 %).
- Boosting rounds: held-out-state R² 0.550 at 75 rounds → 0.425 at 1,000 while within-training R² keeps rising.
- SHAP rankings are model-stable (Spearman ρ = 0.89–0.99) but not structural: the 3rd-ranked feature is a state identifier.
- Lot-size coding: ln(lot) was 0 for 16.5 % of listings; recoded elasticity 0.095 (not 0.018 / 0.059).
🗂 Repository layout
wp3_uqo/
├── README.md
├── requirements.txt # pinned dependencies (validated 2026-08-05)
├── docs/ # REVIEW-REPORT.md · CHANGELOG-REVISION.md (2026-10-17) · AUDIT.md · CHANGES.md · PAPER_REVIEW.md · UPGRADE_REPORT.md
├── data/ # NOT tracked (3.2 GB)
│ ├── raw/us_housing.duckdb # raw Zillow extract (839,313 × 116 + 4 aux tables)
│ └── processed/ # analytical_sample.pkl · model_data.pkl · shap_data.pkl
├── src/wp3/ # shared library
│ ├── config.py # paths (WP3_ROOT-overridable), constants, feature blocks, holdout states, seeds
│ ├── data.py # loaders + 62-regressor design-matrix builder
│ ├── models.py # XGBoost training helper (dual validation schemes)
│ └── plotting.py # journal figure style + variable labels
├── scripts/ # the pipeline, in order
│ ├── 01_spatial_robustness.py # ZIP3 FE OLS · Moran's I · XGB ± geography · ablation
│ ├── 02_qr_imputation.py # QR stability · inter-quantile Wald · imputation · winsorisation
│ ├── 03_make_figures.py # figures 1–11 + verification stats
│ ├── 04_export_tables.py # paper tables → results/tables/*.csv
│ ├── 05_revision_geo_validation.py # trees curve · size-matched split · variants · OLS geo · GroupKFold · LOSO (~25 min)
│ ├── 06_revision_inference.py # missingness audit · clustered SE · lot recoding · state-by-state · R² bootstrap · Moran (~6 min)
│ ├── 07_revision_qr_bootstrap.py # paired (obs. + ZIP3-cluster) bootstrap for inter-quantile Wald tests (~25 min)
│ └── 08_revision_tables_figures.py # paper/tables/*.tex fragments + figures 12–14
├── results/
│ ├── *.pkl # NOT tracked (up to 437 MB)
│ ├── tables/ # 13 CSVs mirroring the v1.1 tables
│ └── revision/ # 18 CSVs feeding the v1.2 tables (scripts 05–07)
├── figures/ # 14 paper figures (PNG, 300 dpi)
└── paper/ # main.tex + sections/ + appendix/ + tables/ (generated) + references.bib → main.pdf🚀 Quick start
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# place the artifacts (request them, see Data) in data/raw/, data/processed/, results/
export WP3_ROOT=/path/to/artifacts # optional, defaults to the repo root
python scripts/01_spatial_robustness.py # ~1–2 h (16 XGBoost models)
python scripts/02_qr_imputation.py # ~1–3 h (12 quantile regressions on 150k obs)
python scripts/03_make_figures.py # ~5 min, figures 1–11 (--figs fig2 fig4 for a subset)
python scripts/04_export_tables.py # ~2 min, 13 CSV tables
python scripts/05_revision_geo_validation.py # ~25 min (104 XGBoost fits)
python scripts/06_revision_inference.py # ~6 min
python scripts/07_revision_qr_bootstrap.py # ~25 min on 8 workers (--draws 200)
python scripts/08_revision_tables_figures.py # <1 min, paper/tables/*.tex + figures 12–14
cd paper && make # latexmk → main.pdf (82 pages)Apple Silicon, 32 GB RAM; scripts use all cores. Every script prints verification statistics comparing recomputed values with the numbers reported in the paper.
🗃 Data
- Source: Zillow active for-sale listings, 2025–2026 snapshot; 839,313 residential properties × 116 variables in a
DuckDB database (
properties,price_history,schools,tax_history,meta_columns). - Analytical sample: 788,842 listings after five filters (price $10K–$10M, living area 200–20,000 sqft, 1–10 beds/baths, valid coordinates, US states + DC; 6.0 % attrition); 68 engineered columns (logs, age and age², ratios, amenity dummies, neighbourhood scores, market-status flags, 6 interactions, simplified categoricals).
- Not in git:
us_housing.duckdb(1.6 GB),analytical_sample.pkl(457 MB),model_data.pkl(867 MB),ml_results.pkl(437 MB). Available on request (subject to Zillow Terms of Service): simon-pierre.boucher@uqo.ca. - The scripts that built the analytical sample and estimated the primary models were lost before the 2026-08-05
restructuring; the stored pickles are the canonical record of those stages and every downstream number is validated
against them (
docs/AUDIT.md).
🔬 Methodology
- OLS — 62 regressors, standardised and unstandardised variants, HC3 robust SE; fixed effects at region (4), state
(50) and ZIP3 (886) levels (
scripts/01). - Spatial diagnostics — Moran's I on OLS residuals, row-standardised KNN weights (k = 8), 3 × 5,000-obs subsamples, 999 permutations.
- Quantile regression — statsmodels
QuantReg, τ ∈ {0.10, …, 0.90}, 150,000-obs subsample, 10-seed stability check; inter-quantile Wald z-tests on β̂(0.10) − β̂(0.90) (scripts/02). - Machine learning — XGBoost (1,000 trees, depth 8, lr 0.05, early stopping 50), LightGBM, RF, Ridge, Lasso,
Elastic Net; random 80/20 split (seed 42) and 10-state geographic holdout (CA NY TX FL OH CO NC WA IL GA,
438,315 test obs) (
src/wp3/models.py). - Ablation — 6 nested feature sets: structural → +lot → +amenities → +neighbourhood → +market → full.
- Interpretation — TreeSHAP on 10,000 test obs; cross-model Spearman stability.
- Sensitivity — state-median-imputation drop-out, lot-size winsorisation (p99.5), Bike Score cap.
- Revision (v1.2) — cluster-robust SE (ZIP3, state); state-by-state OLS and fully interacted prediction; paired
cluster-bootstrap inter-quantile tests; size-matched split, within-state R², boosting-round curve, GroupKFold by state,
leave-one-state-out; feature variants (no region, no school, missingness indicators); OLS geographic holdout on trimmed
data; lot-size recoding; Moran's I on tree residuals; bootstrap CIs for R² (
scripts/05–08).
🖼 Figures & tables inventory
| Figure | Content |
|---|---|
fig1_price_distribution |
Price & log-price histograms with medians |
fig2_ols_diagnostics |
Residuals-vs-fitted, Q-Q, density, scale-location |
fig3_quantile_coefficients |
QR coefficient paths vs. OLS benchmark (8 panels) |
fig4_model_comparison |
Predicted vs. actual: OLS / XGBoost / LightGBM |
fig5_shap_summary |
SHAP beeswarm, top 20 features |
fig6_shap_importance |
Mean |SHAP| bar chart, top 20 |
fig7_geographic_prices |
50,000-listing national price map |
fig8_regional_prices |
Log-price boxplots by Census region |
fig9_marginal_effects |
Bivariate scatters + binned means (6 attributes) |
fig10_shap_dependence |
SHAP dependence, 8 key features |
fig11_shap_interactions |
SHAP for interaction terms (waterfront × sqft, pool × South) |
fig12_geo_trees_curve |
Out-of-sample R² vs boosting rounds (random / early-stopping / 10 held-out states) |
fig13_loso_by_state |
Leave-one-state-out R² by held-out state |
fig14_state_heterogeneity |
State-by-state living-area elasticity and garage coefficient |
Tables (results/tables/*.csv, 13 files): OLS coefficients, QR paths, inter-quantile Wald tests, stability CVs,
Moran's I, imputation/winsorisation sensitivity, SHAP importance and cross-model correlations, ZIP3 FE, ablation.
Revision tables (results/revision/*.csv, 18 files → paper/tables/*.tex): missingness audit, implausible values,
list/Zestimate ratio, clustered SE, lot recoding, state heterogeneity, pooled vs interacted, R² bootstrap, Moran by model,
designs, trees curve, variants, OLS geo holdout, grouped CV, LOSO, inter-quantile bootstrap.
⚠️ Limitations
- Listing (asking) prices, not transactions; gradients are conditional associations, not willingness to pay.
- A single 2025–2026 snapshot: no time dimension, no repeat-sales control.
- School variables are state medians for 93 % of listings; ln(lot) was coded 0 for 16.5 % (corrected for OLS in v1.2, not for QR/ablation/SHAP); 0.6 % of records carry corrupt secondary attributes.
- Inference is cluster-robust but rests on 51 unequal state clusters; no spatial econometric model is estimated.
- The upstream sample-construction and primary-estimation scripts are lost; those stages are reproducible only from the stored artifacts.
📝 Citation
@techreport{boucher2026hedonicus,
author = {Boucher, Simon-Pierre},
title = {Hedonic Housing Price Models for the United States: A Multi-Method
Comparison of Parametric, Quantile, and Machine Learning Approaches},
institution = {Universit\'e du Qu\'ebec en Outaouais, D\'epartement des sciences administratives},
type = {Working Paper},
number = {3},
year = {2026},
month = {October},
note = {Version 1.2}
}👤 Author & contact
Simon-Pierre Boucher — Département des sciences administratives, Université du Québec en Outaouais (UQO), Gatineau, QC. 📧 simon-pierre.boucher@uqo.ca
🗒 Notes
- Format follows
Recherche-UQO/HARMONISATION.md(2026-10-17): template title page,natbib+aerbibliography style,docs/for the restructuring reports. - The original flat May 2026 project (
immo-wp3-spb-20260519, 3.2 GB) is archived inRecherche-UQO/_archives/versions-mai-2026/hedonic-models-united-states--immo-wp3-20260519/;docs/CHANGES.mdlists the six manuscript inconsistencies corrected on 2026-08-05 (no scientific result altered). - GitHub mirror:
spboucher-ai/wp3-hedonic-housing-us(remotegithub); canonical remote is spbgitwp3_uqo.git. - Scientific revision 2026-10-17 (v1.2): see
docs/REVIEW-REPORT.md(editorial-board report) anddocs/CHANGELOG-REVISION.md(audit trail of every correction, references added with DOIs, reanalyses run). The pre-revision state is tagpre-revision-20261017anddocs/main_pre-revision-20261017.pdf. - Build verified 2026-10-17: 0 errors, 0 undefined references, 92 references rendered, 82 pages.
- Table fragments in
paper/tables/are generated byscripts/08_revision_tables_figures.py; their last row carries no\\(supplied after\inputin the section files) to avoid aMisplaced \noalignerror.