Initial commit: WP3 research compendium (hedonic housing prices, US)
Restructured research project for UQO Working Paper No. 3: refactored analysis pipeline (src/wp3 + numbered scripts), 11 publication-ready figures regenerated from stored artifacts, LaTeX paper (52 pages, BibTeX bibliography, compiles clean), CSV table exports, and full audit/change documentation (AUDIT.md, CHANGES.md). Large data artifacts (DuckDB raw extract, processed pickles) are excluded from the repository and available on request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 54 changed files with +4,333 and −0
added
.gitignore
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +# Large data artifacts (> GitHub's 100 MB limit) — kept in the local archive, | |
| 2 | +# available on request: contact@spboucher.ai | |
| 3 | +data/ | |
| 4 | +results/*.pkl | |
| 5 | + | |
| 6 | +# Python | |
| 7 | +__pycache__/ | |
| 8 | +*.pyc | |
| 9 | +.venv/ | |
| 10 | + | |
| 11 | +# LaTeX build artifacts | |
| 12 | +paper/*.aux | |
| 13 | +paper/*.log | |
| 14 | +paper/*.out | |
| 15 | +paper/*.toc | |
| 16 | +paper/*.fls | |
| 17 | +paper/*.fdb_latexmk | |
| 18 | +paper/*.synctex.gz | |
| 19 | +paper/*.blg | |
| 20 | +paper/*.bbl | |
| 21 | + | |
| 22 | +# macOS | |
| 23 | +.DS_Store | |
added
AUDIT.md
+121 −0
@@ -0,0 +1,121 @@ | ||
| 1 | +# AUDIT — UQO Working Paper 3 (Hedonic Housing Prices, US) | |
| 2 | + | |
| 3 | +**Source auditée** : `~/Desktop/UQO/UQO_WP/immo-wp3-spb-20260519/` (3,2 Go, intacte — non modifiée) | |
| 4 | +**Nouvelle structure** : `~/Desktop/wp3_uqo/` (ce dossier) | |
| 5 | +**Date de l'audit** : 2026-08-05 | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 1. Vue d'ensemble | |
| 10 | + | |
| 11 | +Projet de recherche terminé : comparaison OLS / régression quantile / ML (XGBoost, LightGBM) | |
| 12 | +pour la valorisation hédonique de 788 842 annonces Zillow (50 États + DC). | |
| 13 | +Le papier final est le dossier `wp3/` (main.tex + sections), version « UQO Working Paper No. 3 », mai 2026. | |
| 14 | + | |
| 15 | +## 2. Inventaire des fichiers source | |
| 16 | + | |
| 17 | +### 2.1 Code Python (2 scripts seulement — voir §5 : scripts perdus) | |
| 18 | + | |
| 19 | +| Script | Rôle | Produit | | |
| 20 | +|---|---|---| | |
| 21 | +| `run_v3_spatial.py` (19,6 Ko) | ZIP3 fixed-effects OLS ; Moran's I (3 sous-échantillons) ; XGBoost avec/sans géographie ; ablation (6 jeux de variables) | `v3_spatial_results.pkl` | | |
| 22 | +| `v3_qr_imputation_analysis.py` (14,4 Ko) | Stabilité QR (10 sous-échantillons τ=0.50) ; test inter-quantile τ=0.10 vs 0.90 ; sensibilité imputation (via duckdb) ; sensibilité winsorisation | `v3_qr_imputation_results.pkl` | | |
| 23 | + | |
| 24 | +Les deux scripts ont un chemin codé en dur `BASE = '/Users/simon-pierreboucher/Desktop/immo-wp3-spb-20260519'` | |
| 25 | +qui **n'existe plus** (le projet a été déplacé dans `UQO/UQO_WP/`) — ils ne tournent plus tels quels. | |
| 26 | + | |
| 27 | +### 2.2 Données | |
| 28 | + | |
| 29 | +| Fichier | Taille | Contenu | Classement | | |
| 30 | +|---|---|---|---| | |
| 31 | +| `us_housing.duckdb` | 1,6 Go | Données brutes : `properties` (839 313 × 116), `price_history`, `schools`, `tax_history`, `meta_columns` | **data/raw** | | |
| 32 | +| `analytical_sample.pkl` | 457 Mo | Échantillon analytique final : DataFrame 788 842 × 68 (variables engineerées : ln_price, ln_sqft, interactions, dummies catégorielles, etc.) | **data/processed** | | |
| 33 | +| `model_data.pkl` | 867 Mo | Données prêtes pour modèles : `X_const` (788 842 × 63, standardisé + const), `y_clean`, `df_clean`, `feature_names`, `means`/`stds`, résultats OLS (`ols_results`), `residuals` | **data/processed** | | |
| 34 | +| `shap_data.pkl` | 7,5 Mo | Valeurs SHAP XGBoost (10 000 × 62), `X_shap`, `expected_value` | **data/processed** | | |
| 35 | + | |
| 36 | +### 2.3 Résultats (pickles) | |
| 37 | + | |
| 38 | +| Fichier | Contenu | | |
| 39 | +|---|---| | |
| 40 | +| `qr_results.pkl` | Régression quantile : params / p-values / pseudo-R² aux τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90} | | |
| 41 | +| `ml_results.pkl` (437 Mo) | Modèles ML entraînés (XGBRegressor, LGBMRegressor), prédictions test (157 769), splits train/test (631 073 / 157 769), métriques r2/rmse/mae, importances | | |
| 42 | +| `extended_results.pkl` | OLS non standardisé (params/bse/p), holdout géographique (OLS/XGB/RF), Moran's I initial, stabilité QR (v2) | | |
| 43 | +| `v3_spatial_results.pkl` | Sorties de `run_v3_spatial.py` (ZIP3 FE, Moran robustesse, XGB ±géo, ablation) | | |
| 44 | +| `v3_qr_imputation_results.pkl` | Sorties de `v3_qr_imputation_analysis.py` | | |
| 45 | +| `v3_shap_stability.pkl` | Stabilité SHAP inter-modèles (XGB/LGB/RF), rangs, ρ de Spearman | | |
| 46 | + | |
| 47 | +### 2.4 Figures | |
| 48 | + | |
| 49 | +`figures/` (racine) et `wp3/figures/` sont **identiques octet pour octet** (duplication). | |
| 50 | +Présentes (6) : `fig1_price_distribution`, `fig3_quantile_coefficients`, `fig5_shap_summary`, | |
| 51 | +`fig6_shap_importance`, `fig10_shap_dependence`, `fig11_shap_interactions`. | |
| 52 | + | |
| 53 | +**⚠ Manquantes (5)** — référencées par le papier mais absentes des deux dossiers | |
| 54 | +(supprimées vers le 9 juin 2026 d'après les timestamps des dossiers) : | |
| 55 | + | |
| 56 | +| Figure | Légende dans le papier | Régénérable depuis | | |
| 57 | +|---|---|---| | |
| 58 | +| `fig2_ols_diagnostics.png` | Diagnostics OLS (résidus vs fitted, Q-Q, distribution, scale-location) | `model_data.pkl` (residuals + fitted) | | |
| 59 | +| `fig4_model_comparison.png` | Prédit vs observé : OLS / XGBoost / LightGBM | `ml_results.pkl` (y_test, y_pred_*) | | |
| 60 | +| `fig7_geographic_prices.png` | Carte lat/lon des log-prix (sous-échantillon 50 000) | `analytical_sample.pkl` | | |
| 61 | +| `fig8_regional_prices.png` | Distribution des prix par région Census | `analytical_sample.pkl` | | |
| 62 | +| `fig9_marginal_effects.png` | Relations bivariées brutes (scatter + moyennes binnées) | `analytical_sample.pkl` | | |
| 63 | + | |
| 64 | +### 2.5 LaTeX — 4 versions successives | |
| 65 | + | |
| 66 | +| Version | Fichiers | Statut | | |
| 67 | +|---|---|---| | |
| 68 | +| v1 | `paper_hedonic_pricing.tex` (71 Ko, monolithique) | Obsolète | | |
| 69 | +| v2 | `paper_hedonic_pricing_v2.tex` (82 Ko) | Obsolète | | |
| 70 | +| v3 | `paper_hedonic_pricing_v3.tex` (115 Ko) | Obsolète — contenu repris dans wp3/ | | |
| 71 | +| **wp3/** | `main.tex` + `sections/` (9 sections + titlepage + references) + `appendix/` + `Makefile` | **Version finale** | | |
| 72 | + | |
| 73 | +Particularités de `wp3/` : | |
| 74 | +- `main_web.tex` = copie de `main.tex` + hack `\includegraphics` qui remplace toute figure | |
| 75 | + manquante par une boîte « Figure indisponible ». Le PDF existant (`main_web.pdf`, 50 pages, | |
| 76 | + 13 juin) contient donc des **placeholders** à la place des 5 figures manquantes. | |
| 77 | +- Il n'existe **pas** de `main.pdf` compilé (la compilation de `main.tex` échouerait sur les figures manquantes). | |
| 78 | +- Bibliographie : `thebibliography` manuel dans `sections/references.tex` (43 entrées) — pas de `.bib`. | |
| 79 | +- `wp3/tables/` est vide (tables codées en dur dans les .tex). | |
| 80 | +- Fichiers auxiliaires LaTeX (aux/log/out/toc/fls/fdb_latexmk/synctex) présents à la racine et dans wp3/. | |
| 81 | + | |
| 82 | +## 3. Chaîne de production (reconstituée) | |
| 83 | + | |
| 84 | +``` | |
| 85 | +us_housing.duckdb (raw) | |
| 86 | + └─(script PERDU : filtrage + feature engineering)→ analytical_sample.pkl (788 842 × 68) | |
| 87 | + ├─(script PERDU : standardisation + OLS)→ model_data.pkl | |
| 88 | + ├─(script PERDU : QR aux 5 quantiles)→ qr_results.pkl | |
| 89 | + ├─(script PERDU : XGB/LGB + split 80/20)→ ml_results.pkl | |
| 90 | + ├─(script PERDU : SHAP sur 10 000 obs)→ shap_data.pkl | |
| 91 | + ├─(script PERDU : holdout géo, RF, Moran v2)→ extended_results.pkl | |
| 92 | + ├─(script PERDU : stabilité SHAP XGB/LGB/RF)→ v3_shap_stability.pkl | |
| 93 | + ├─ run_v3_spatial.py → v3_spatial_results.pkl | |
| 94 | + └─ v3_qr_imputation_analysis.py → v3_qr_imputation_results.pkl | |
| 95 | + figures fig1–fig11 : script(s) de tracé PERDU(S) | |
| 96 | +``` | |
| 97 | + | |
| 98 | +## 4. Fichiers morts / dupliqués / auxiliaires | |
| 99 | + | |
| 100 | +- Doublons : `figures/` ≡ `wp3/figures/`. | |
| 101 | +- Versions obsolètes : `paper_hedonic_pricing{,_v2,_v3}.{tex,aux,log,out,toc}`. | |
| 102 | +- Auxiliaires LaTeX régénérables : `*.aux, *.log, *.out, *.toc, *.fls, *.fdb_latexmk, *.synctex.gz`. | |
| 103 | +- `main_web.tex` : contournement temporaire, à retirer une fois les figures restaurées. | |
| 104 | +- `.DS_Store` divers. | |
| 105 | + | |
| 106 | +## 5. ⚠ Problèmes à signaler (à vérifier par l'auteur) | |
| 107 | + | |
| 108 | +1. **Scripts générateurs perdus** : les scripts de construction de l'échantillon, d'estimation | |
| 109 | + (OLS, QR, ML, SHAP) et de tracé des figures n'existent plus — seuls 2 scripts « v3 » survivent. | |
| 110 | + Les nouveaux scripts dans `scripts/` sont des **reconstructions** documentées, écrites à partir | |
| 111 | + des pickles, du papier et des 2 scripts survivants ; ils sont vérifiés contre les résultats | |
| 112 | + stockés quand c'est faisable (voir CHANGES.md). | |
| 113 | +2. **5 figures manquantes** (fig2, fig4, fig7, fig8, fig9) : régénérées depuis les pickles. | |
| 114 | + Le **contenu** (données) est identique aux données originales, mais le **style** (couleurs, | |
| 115 | + mise en page) peut différer des PNG perdus — à valider visuellement. | |
| 116 | +3. **Les 11 figures ont finalement toutes été régénérées** dans un style « journal » homogène | |
| 117 | + (demande utilisateur en cours de session) ; le contenu de chacune est vérifié contre les | |
| 118 | + pickles et les chiffres du papier (voir CHANGES.md §3). Les 6 PNG originaux restent | |
| 119 | + disponibles dans l'archive source. | |
| 120 | +4. Chemins codés en dur dans les 2 scripts survivants (corrigés dans les versions refactorées). | |
| 121 | +5. Aucun changement aux résultats, chiffres ou affirmations scientifiques n'a été apporté. | |
added
CHANGES.md
+105 −0
@@ -0,0 +1,105 @@ | ||
| 1 | +# CHANGES — Restructuration du projet WP3 (2026-08-05) | |
| 2 | + | |
| 3 | +Original intact : `~/Desktop/UQO/UQO_WP/immo-wp3-spb-20260519/`. | |
| 4 | +Nouvelle structure : `~/Desktop/wp3_uqo/` (ce dossier). Voir `AUDIT.md` pour l'état des lieux initial. | |
| 5 | + | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## 1. Déplacements / réorganisation | |
| 9 | + | |
| 10 | +| Origine (immo-wp3-spb-20260519/) | Destination (wp3_uqo/) | | |
| 11 | +|---|---| | |
| 12 | +| `us_housing.duckdb` | `data/raw/` | | |
| 13 | +| `analytical_sample.pkl`, `model_data.pkl`, `shap_data.pkl` | `data/processed/` | | |
| 14 | +| `qr_results.pkl`, `ml_results.pkl`, `extended_results.pkl`, `v3_*.pkl` | `results/` | | |
| 15 | +| `figures/fig*.png` (6 survivantes) | remplacées par les 11 régénérées dans `figures/` (originales conservées dans l'archive source) | | |
| 16 | +| `wp3/main.tex`, `wp3/sections/`, `wp3/appendix/`, `uq_logo.jpg`, `Makefile` | `paper/` | | |
| 17 | +| `run_v3_spatial.py` | refactoré → `scripts/01_spatial_robustness.py` | | |
| 18 | +| `v3_qr_imputation_analysis.py` | refactoré → `scripts/02_qr_imputation.py` | | |
| 19 | + | |
| 20 | +Non repris (restent dans l'archive source uniquement) : `paper_hedonic_pricing{,_v2,_v3}.tex` | |
| 21 | +(anciennes versions), `wp3/main_web.tex` (contournement « Figure indisponible », devenu inutile), | |
| 22 | +fichiers auxiliaires LaTeX, `.DS_Store`. | |
| 23 | + | |
| 24 | +## 2. Code refactoré | |
| 25 | + | |
| 26 | +- **Nouveaux modules** `src/wp3/` : `config.py` (chemins relatifs, constantes, listes de variables), | |
| 27 | + `data.py` (loaders + construction de la matrice de 62 régresseurs), `models.py` (entraînement | |
| 28 | + XGBoost commun), `plotting.py` (style de figures + libellés lisibles). | |
| 29 | +- Les 2 scripts survivants sont réécrits en scripts numérotés avec docstrings, `argparse`, | |
| 30 | + chemins relatifs (les chemins codés en dur `/Users/.../Desktop/immo-wp3-spb-20260519` ne | |
| 31 | + fonctionnaient plus) et déduplication (le helper XGBoost et les listes de variables sont partagés). | |
| 32 | +- **Nouveaux scripts** : `03_make_figures.py` (11 figures depuis les pickles, avec statistiques de | |
| 33 | + vérification), `04_export_tables.py` (tables du papier → `results/tables/*.csv`). | |
| 34 | +- `requirements.txt` épinglé sur l'environnement validé. | |
| 35 | +- En-tête `Author: Simon-Pierre Boucher — contact@spboucher.ai` ajouté à tous les fichiers de code | |
| 36 | + et LaTeX (dans `references.bib`, l'e-mail est écrit sans `@` : BibTeX interprète `@` comme début d'entrée). | |
| 37 | + | |
| 38 | +## 3. Figures | |
| 39 | + | |
| 40 | +- **5 figures manquantes recréées** depuis les pickles (fig2 diagnostics OLS, fig4 prédit vs observé, | |
| 41 | + fig7 carte, fig8 régions, fig9 relations bivariées) — les PNG originaux avaient été supprimés | |
| 42 | + (~9 juin 2026) et le PDF existant contenait des boîtes « Figure indisponible ». | |
| 43 | +- **Les 11 figures régénérées dans un style homogène « journal »** (demande utilisateur) : | |
| 44 | + typographie serif cohérente avec le papier, axes épurés (spines haut/droite retirés), grille | |
| 45 | + discrète, palette sobre, libellés lisibles (« Log(Living Area) » au lieu de `ln_sqft`), | |
| 46 | + titres de panneaux (a)/(b) alignés à gauche, 300 dpi. | |
| 47 | +- **Vérifications contenu = papier** (imprimées par `03_make_figures.py`) : | |
| 48 | + skewness 0,18 / kurtosis 5,55 / JB 217 078 (fig2) ; R² OLS 0,6301 / XGB 0,8330 / LGB 0,8088 (fig4) ; | |
| 49 | + OLS de référence ln_sqft 0,312 (fig3) — tous identiques aux valeurs du papier. | |
| 50 | +- Les 6 PNG originaux restent disponibles dans l'archive source pour comparaison. | |
| 51 | + | |
| 52 | +## 4. Papier LaTeX (`paper/`) | |
| 53 | + | |
| 54 | +Structure : `main.tex` + une section par fichier (déjà le cas), figures lues depuis `../figures/` | |
| 55 | +(`\graphicspath`), **bibliographie convertie du `thebibliography` manuel vers `references.bib`** | |
| 56 | +(37 entrées, clés identiques) + BibTeX/apalike. `sections/references.tex` supprimé. | |
| 57 | +Compilation : `latexmk` → **main.pdf, 52 pages, 0 erreur, 0 référence/citation indéfinie** | |
| 58 | +(l'archive originale n'avait pas de `main.pdf` compilable — figures manquantes). | |
| 59 | + | |
| 60 | +### Complétions (placeholders `[TODO]` remplis avec les valeurs stockées dans les pickles) | |
| 61 | + | |
| 62 | +- **Tableau des tests inter-quantiles** (results) : 8 lignes `[TODO: diff]/[TODO: z]` remplies depuis | |
| 63 | + `v3_qr_imputation_results.pkl` (ln_lot −0,041/z=−9,49 ; luxury −0,054/−6,82 ; foreclosure −0,155/−4,18 ; | |
| 64 | + Northeast −0,264/−21,43 ; West −0,086/−6,26 ; bedrooms −0,051/−5,95 ; age² +0,010/1,40 ; | |
| 65 | + waterfront +0,222/1,86), lignes triées par |z|. | |
| 66 | +- **Tableau winsorisation** (robustness) : β ln_sqft 0,610 ; bathrooms 0,211 ; garage 0,111. | |
| 67 | +- **Tableau stabilité QR** : CV exacts (au lieu de « <7 ») et stabilité de signe 100 % pour | |
| 68 | + waterfront/pool. | |
| 69 | +- **Annexe** : marqueurs `[TODO]` retirés de la liste « Robustness Agenda » (items non cochés = | |
| 70 | + travaux futurs) ; checklist reproductibilité : « Code available: Yes (replication package) », | |
| 71 | + « Data available: On request ». | |
| 72 | + | |
| 73 | +### Corrections d'incohérences (chiffres alignés sur les pickles — À VALIDER par l'auteur) | |
| 74 | + | |
| 75 | +1. **En-tête du tableau inter-quantile** : disait β(0.90)−β(0.10) alors que les valeurs déjà | |
| 76 | + présentes (garage +0,185, z=28,92…) correspondent à β(0.10)−β(0.90). En-tête corrigé. | |
| 77 | +2. **Moran's I** : le tableau affichait 0,268/0,278/0,277 avec z=42,4/44,0/43,8 et « k=10 » ; | |
| 78 | + le pickle (`v3_spatial_results.pkl`) contient 0,2742/0,2839/0,2652 (moyenne 0,2745 et | |
| 79 | + écart-type 0,0076 du papier ✓) calculés avec **k=8**. Valeurs et k corrigés ; z recalculés | |
| 80 | + en ré-exécutant le pipeline refactoré (41,8/43,5/40,5) — la ré-exécution reproduit | |
| 81 | + exactement les I du pickle (validation bout-en-bout du script 01). Méthodologie : k=10 → k=8. | |
| 82 | +3. **Description de la winsorisation** (data + robustness) : le texte disait « 1er/99e centile, | |
| 83 | + toutes les variables continues » ; le code réel (script survivant + pickle) = lot au 99,5e | |
| 84 | + centile (3 944 obs.) + Bike Score plafonné à 100 (38 obs.). Les chiffres du tableau | |
| 85 | + (R² 0,640, β lot 0,058) proviennent bien de ce calcul. Description corrigée. | |
| 86 | +4. **« CV < 7 % »** → « CV < 8 % » (methodology, robustness, discussion, limitations) : | |
| 87 | + bedrooms 7,3 % et foreclosure 7,1 % dépassent 7 %. | |
| 88 | +5. **Matrice de robustesse** : lignes FE mélangeaient R² in-sample (0,634) et out-of-sample | |
| 89 | + (0,678/0,725) ; uniformisées en out-of-sample (0,630 → 0,678 (+4,8 pp) → 0,725 (+9,5 pp)), | |
| 90 | + cohérent avec le tableau FE des résultats. | |
| 91 | +6. Le tableau QR-stabilité listait « Pool » à la fois comme stable (<7) et instable (33,4) ; | |
| 92 | + doublon supprimé (CV réel 33,4 %). | |
| 93 | + | |
| 94 | +**Aucun résultat, coefficient ou conclusion scientifique n'a été modifié** : toutes les | |
| 95 | +corrections ci-dessus alignent le texte sur les résultats effectivement stockés dans les pickles. | |
| 96 | + | |
| 97 | +## 5. À vérifier par l'auteur | |
| 98 | + | |
| 99 | +- Les 6 corrections numérotées ci-dessus (§4), en particulier Moran (k=8, nouvelles valeurs I/z). | |
| 100 | +- Le style des 11 figures régénérées (les 6 originales sont dans l'archive source si vous préférez | |
| 101 | + l'ancien rendu pour certaines). | |
| 102 | +- Les scripts amont perdus (construction de l'échantillon depuis DuckDB, estimation OLS/QR/ML/SHAP) | |
| 103 | + ne sont **pas** reconstruits — les artefacts stockés en tiennent lieu. Si une reconstruction | |
| 104 | + complète raw→results est souhaitée, elle est possible mais devra être validée contre les pickles. | |
| 105 | +- `paper/main.tex` affiche « This version: \today » — figer la date avant soumission si besoin. | |
added
README.md
+441 −0
@@ -0,0 +1,441 @@ | ||
| 1 | +<div align="center"> | |
| 2 | + | |
| 3 | +# 🏠 Hedonic Housing Price Models for the United States | |
| 4 | + | |
| 5 | +### A Multi-Method Comparison of Parametric, Quantile, and Machine Learning Approaches | |
| 6 | + | |
| 7 | +**UQO Working Paper No. 3** | |
| 8 | + | |
| 9 | +[](https://www.python.org/) | |
| 10 | +[](https://www.latex-project.org/) | |
| 11 | +[](https://xgboost.readthedocs.io/) | |
| 12 | +[](https://lightgbm.readthedocs.io/) | |
| 13 | +[](https://shap.readthedocs.io/) | |
| 14 | +[](https://www.statsmodels.org/) | |
| 15 | +[](https://duckdb.org/) | |
| 16 | + | |
| 17 | +[](paper/main.pdf) | |
| 18 | +[](#-data) | |
| 19 | +[](#-data) | |
| 20 | +[](#-figures) | |
| 21 | +[](#-verification--integrity) | |
| 22 | +[](#-citation) | |
| 23 | + | |
| 24 | +**Author : [Simon-Pierre Boucher](mailto:contact@spboucher.ai)** | |
| 25 | +Département des sciences administratives — Université du Québec en Outaouais (UQO) | |
| 26 | +📧 **contact@spboucher.ai** | |
| 27 | + | |
| 28 | +</div> | |
| 29 | + | |
| 30 | +--- | |
| 31 | + | |
| 32 | +## 📑 Table of Contents | |
| 33 | + | |
| 34 | +- [Overview](#-overview) | |
| 35 | +- [Key Findings](#-key-findings) | |
| 36 | +- [Repository Structure](#-repository-structure) | |
| 37 | +- [Data](#-data) | |
| 38 | +- [Methodology at a Glance](#-methodology-at-a-glance) | |
| 39 | +- [The Analysis Pipeline](#-the-analysis-pipeline) | |
| 40 | +- [Installation & Setup](#-installation--setup) | |
| 41 | +- [Reproducing the Results](#-reproducing-the-results) | |
| 42 | +- [Building the Paper](#-building-the-paper) | |
| 43 | +- [Figures](#-figures) | |
| 44 | +- [Results Summary](#-results-summary) | |
| 45 | +- [Verification & Integrity](#-verification--integrity) | |
| 46 | +- [Project History & Provenance](#-project-history--provenance) | |
| 47 | +- [Citation](#-citation) | |
| 48 | +- [Contact & License](#-contact--license) | |
| 49 | + | |
| 50 | +--- | |
| 51 | + | |
| 52 | +## 🔎 Overview | |
| 53 | + | |
| 54 | +This repository contains the complete research compendium — analysis code, LaTeX | |
| 55 | +source, publication-ready figures, and machine-readable result tables — for | |
| 56 | +**UQO Working Paper No. 3**, which compares three frameworks for hedonic housing | |
| 57 | +valuation on a single large dataset of **788,842 active Zillow listings** spanning | |
| 58 | +all 50 U.S. states and the District of Columbia: | |
| 59 | + | |
| 60 | +| Framework | Question it answers | Headline result | | |
| 61 | +|---|---|---| | |
| 62 | +| **Semi-log OLS** (62 regressors, HC3 SE) | What is the *average* conditional association between an attribute and log listing price? | $R^2 = 0.634$; price-to-area elasticity 0.63 | | |
| 63 | +| **Quantile regression** ($\tau \in \{0.10, 0.25, 0.50, 0.75, 0.90\}$) | How do attribute gradients *vary across the price distribution*? | Coefficient equality rejected for **11 of 13** variables (inter-quantile Wald tests) | | |
| 64 | +| **Gradient boosting + SHAP** (XGBoost, LightGBM) | How well can prices be *predicted*, and which features drive predictions? | $R^2 = 0.833$ (random split) vs. **0.425–0.547** (geographic holdout) | | |
| 65 | + | |
| 66 | +The paper's core methodological contribution is a systematic study of | |
| 67 | +**spatial leakage** in hedonic model evaluation: random train/test splits let | |
| 68 | +geographically proximate listings appear on both sides of the split, inflating | |
| 69 | +performance metrics. Three complementary designs quantify the damage — a 10-state | |
| 70 | +geographic holdout, a 6-stage feature-ablation cascade, and a latitude/longitude | |
| 71 | +augmentation experiment. | |
| 72 | + | |
| 73 | +> ⚠️ **Interpretation caveat.** All estimates are *listing-price capitalization | |
| 74 | +> gradients* — conditional associations between attributes and *asking* prices. | |
| 75 | +> They are not causal willingness-to-pay parameters and not transaction-price | |
| 76 | +> implicit prices. | |
| 77 | + | |
| 78 | +--- | |
| 79 | + | |
| 80 | +## 🏆 Key Findings | |
| 81 | + | |
| 82 | +1. **Geography does the heavy lifting in OLS.** Moving from 4 Census-region | |
| 83 | + dummies to state fixed effects to 886 ZIP3 fixed effects raises out-of-sample | |
| 84 | + $R^2$ from 0.630 → 0.678 → 0.725 — a 9.5 pp gain from spatial granularity alone. | |
| 85 | + | |
| 86 | +2. **Strong, stable residual spatial autocorrelation.** Moran's $I$ on OLS | |
| 87 | + residuals averages **0.2745** (SD 0.0076) across three independent 5,000-listing | |
| 88 | + subsamples (row-standardized KNN weights, $k = 8$; all $p < 0.001$). | |
| 89 | + | |
| 90 | +3. **Mean effects mask distributional heterogeneity.** The garage gradient is | |
| 91 | + **10× larger** at $\tau = 0.10$ than at $\tau = 0.90$ ($z = 28.9$); the pool | |
| 92 | + premium only emerges above the median; the age penalty is concentrated in | |
| 93 | + lower-priced homes. | |
| 94 | + | |
| 95 | +4. **Random validation flatters machine learning.** XGBoost reaches $R^2 = 0.833$ | |
| 96 | + under a random 80/20 split but only **0.425–0.547** when 10 entire states | |
| 97 | + (438,315 listings) are held out. | |
| 98 | + | |
| 99 | +5. **Geographic features can *hurt* generalization.** Removing all geographic | |
| 100 | + features *improves* geographic-holdout $R^2$ from 0.425 to **0.519**, while | |
| 101 | + adding raw lat/lon coordinates boosts random $R^2$ (+3.7 pp) but *degrades* | |
| 102 | + geographic holdout. Region dummies memorize training-set price levels. | |
| 103 | + | |
| 104 | +6. **SHAP rankings are model-stable but not structural.** Spearman rank | |
| 105 | + correlations of mean $|\phi|$ across XGBoost / LightGBM / Random Forest range | |
| 106 | + from **0.89 to 0.99**, with the same six features on top — yet these remain | |
| 107 | + predictive decompositions, not implicit prices. | |
| 108 | + | |
| 109 | +7. **Imputation matters.** The lot-size gradient **triples** (0.018 → 0.059) when | |
| 110 | + state-median-imputed lot sizes are dropped — a warning for hedonic work on | |
| 111 | + scraped listing data. | |
| 112 | + | |
| 113 | +--- | |
| 114 | + | |
| 115 | +## 🗂 Repository Structure | |
| 116 | + | |
| 117 | +``` | |
| 118 | +wp3_uqo/ | |
| 119 | +├── README.md ← you are here | |
| 120 | +├── AUDIT.md ← forensic audit of the original project archive | |
| 121 | +├── CHANGES.md ← every move/refactor/fix made during restructuring | |
| 122 | +├── requirements.txt ← pinned Python dependencies (validated 2026-08-05) | |
| 123 | +│ | |
| 124 | +├── data/ ⚠ NOT in the Git repo (3.2 GB — see "Data" below) | |
| 125 | +│ ├── raw/ | |
| 126 | +│ │ └── us_housing.duckdb # raw Zillow extract (839,313 × 116 + 4 aux tables) | |
| 127 | +│ └── processed/ | |
| 128 | +│ ├── analytical_sample.pkl # final sample, 788,842 × 68 engineered columns | |
| 129 | +│ ├── model_data.pkl # standardized X (788,842 × 63), y, OLS fit, residuals | |
| 130 | +│ └── shap_data.pkl # TreeSHAP values (10,000 × 62) + explanation sample | |
| 131 | +│ | |
| 132 | +├── src/wp3/ # shared library code | |
| 133 | +│ ├── config.py # paths (relative, WP3_ROOT-overridable), constants, | |
| 134 | +│ │ # feature-block definitions, holdout states, seeds | |
| 135 | +│ ├── data.py # loaders + 62-regressor design-matrix builder | |
| 136 | +│ ├── models.py # XGBoost training helper (dual validation schemes) | |
| 137 | +│ └── plotting.py # journal figure style + human-readable variable labels | |
| 138 | +│ | |
| 139 | +├── scripts/ # numbered pipeline entry points | |
| 140 | +│ ├── 01_spatial_robustness.py # ZIP3 FE OLS · Moran's I · XGB ± geography · ablation | |
| 141 | +│ ├── 02_qr_imputation.py # QR stability · inter-quantile Wald · imputation · | |
| 142 | +│ │ # winsorization sensitivity | |
| 143 | +│ ├── 03_make_figures.py # regenerates all 11 figures + verification stats | |
| 144 | +│ └── 04_export_tables.py # exports paper tables → results/tables/*.csv | |
| 145 | +│ | |
| 146 | +├── figures/ # all 11 paper figures (PNG, 300 dpi, unified style) | |
| 147 | +│ | |
| 148 | +├── results/ | |
| 149 | +│ ├── *.pkl ⚠ NOT in the Git repo (up to 437 MB) | |
| 150 | +│ └── tables/ # 13 CSVs mirroring the paper's tables (in repo) | |
| 151 | +│ | |
| 152 | +└── paper/ # LaTeX source | |
| 153 | + ├── main.tex # preamble + metadata + \input skeleton | |
| 154 | + ├── references.bib # 37 BibTeX entries (natbib + apalike) | |
| 155 | + ├── sections/ # one file per section (IMRaD + robustness) | |
| 156 | + │ ├── titlepage.tex ├── introduction.tex ├── literature.tex | |
| 157 | + │ ├── data.tex ├── methodology.tex ├── results.tex | |
| 158 | + │ ├── robustness.tex ├── discussion.tex ├── limitations.tex | |
| 159 | + │ └── conclusion.tex | |
| 160 | + ├── appendix/appendix.tex # extra figures, full OLS table, reproducibility checklist | |
| 161 | + ├── Makefile # latexmk targets (all / clean / distclean) | |
| 162 | + ├── uq_logo.jpg | |
| 163 | + └── main.pdf # compiled paper — 52 pages, zero warnings | |
| 164 | +``` | |
| 165 | + | |
| 166 | +--- | |
| 167 | + | |
| 168 | +## 💾 Data | |
| 169 | + | |
| 170 | +### What the data are | |
| 171 | + | |
| 172 | +- **Source:** Zillow active for-sale listings, 2025–2026 snapshot. | |
| 173 | +- **Raw:** 839,313 residential properties × 116 variables, stored in a DuckDB | |
| 174 | + database (`properties`, `price_history`, `schools`, `tax_history`, `meta_columns`). | |
| 175 | +- **Analytical sample:** 788,842 listings after five sequential filters | |
| 176 | + (valid price \$10K–\$10M, living area 200–20,000 sqft, 1–10 beds/baths, | |
| 177 | + valid coordinates, US states + DC only — 6.0 % attrition). | |
| 178 | +- **68 engineered columns:** log transforms, age & age², ratios, amenity dummies, | |
| 179 | + neighborhood scores, market-status flags, 6 interaction terms, and simplified | |
| 180 | + categoricals (roof / construction / foundation / region). | |
| 181 | + | |
| 182 | +### Why the data are not in this Git repository | |
| 183 | + | |
| 184 | +GitHub rejects files above 100 MB. The DuckDB file is **1.6 GB** and the processed | |
| 185 | +pickles reach **867 MB**, so `data/` and `results/*.pkl` are excluded via | |
| 186 | +`.gitignore`. The canonical copies live in the local research archive: | |
| 187 | + | |
| 188 | +| Artifact | Size | Location | | |
| 189 | +|---|---|---| | |
| 190 | +| `us_housing.duckdb` | 1.6 GB | `data/raw/` (local) | | |
| 191 | +| `analytical_sample.pkl` | 457 MB | `data/processed/` (local) | | |
| 192 | +| `model_data.pkl` | 867 MB | `data/processed/` (local) | | |
| 193 | +| `ml_results.pkl` | 437 MB | `results/` (local) | | |
| 194 | +| `shap_data.pkl`, `qr_results.pkl`, `extended_results.pkl`, `v3_*.pkl` | < 10 MB each | `results/` (local; small ones could be added on request) | | |
| 195 | + | |
| 196 | +📬 **Data access:** available on request (subject to Zillow Terms of Service) — | |
| 197 | +**contact@spboucher.ai**. | |
| 198 | + | |
| 199 | +--- | |
| 200 | + | |
| 201 | +## 🧪 Methodology at a Glance | |
| 202 | + | |
| 203 | +| Component | Specification | | |
| 204 | +|---|---| | |
| 205 | +| Dependent variable | $\ln(\text{listing price})$ | | |
| 206 | +| OLS | 62 regressors, standardized + unstandardized variants, **HC3** robust SE | | |
| 207 | +| Fixed effects | Census region (baseline) → state (50) → ZIP3 (886) | | |
| 208 | +| Quantile regression | statsmodels `QuantReg`, $\tau \in \{0.10,\ldots,0.90\}$, 150,000-obs subsample, 10-seed stability check | | |
| 209 | +| Inter-quantile tests | Wald $z$-tests on $\hat\beta(0.10)-\hat\beta(0.90)$ | | |
| 210 | +| XGBoost | 1,000 trees, depth 8, lr 0.05, subsample/colsample 0.8, early stopping 50 | | |
| 211 | +| LightGBM / RF / Ridge / Lasso / Elastic Net | benchmark suite | | |
| 212 | +| Validation | random 80/20 (seed 42) **and** 10-state geographic holdout (CA NY TX FL OH CO NC WA IL GA — 438,315 test obs) | | |
| 213 | +| Ablation | 6 nested feature sets: structural → +lot → +amenities → +neighborhood → +market → full | | |
| 214 | +| Interpretation | TreeSHAP on 10,000 test obs; cross-model Spearman stability (XGB/LGB/RF) | | |
| 215 | +| Spatial diagnostics | Moran's $I$, row-standardized KNN ($k=8$), 3 × 5,000-obs subsamples, 999 permutations | | |
| 216 | +| Sensitivity | state-median-imputation drop-out, lot-size winsorization (p99.5), Bike Score cap | | |
| 217 | + | |
| 218 | +--- | |
| 219 | + | |
| 220 | +## 🔁 The Analysis Pipeline | |
| 221 | + | |
| 222 | +``` | |
| 223 | +us_housing.duckdb (raw, 839,313 listings) | |
| 224 | + │ | |
| 225 | + │ [sample construction & feature engineering — original script lost; | |
| 226 | + │ the stored analytical_sample.pkl is the canonical artifact] | |
| 227 | + ▼ | |
| 228 | +analytical_sample.pkl (788,842 × 68) | |
| 229 | + │ | |
| 230 | + ├─ [estimation — original scripts lost; stored artifacts are canonical] | |
| 231 | + │ ├── model_data.pkl (standardized X, OLS fit, residuals) | |
| 232 | + │ ├── qr_results.pkl (QR params at 5 quantiles) | |
| 233 | + │ ├── ml_results.pkl (fitted XGB/LGBM + test predictions) | |
| 234 | + │ ├── shap_data.pkl (TreeSHAP values, 10,000 × 62) | |
| 235 | + │ ├── extended_results.pkl (unstandardized OLS, geo holdout, RF) | |
| 236 | + │ └── v3_shap_stability.pkl (XGB/LGB/RF SHAP rank correlations) | |
| 237 | + │ | |
| 238 | + ├─ scripts/01_spatial_robustness.py ──► results/v3_spatial_results.pkl | |
| 239 | + ├─ scripts/02_qr_imputation.py ───────► results/v3_qr_imputation_results.pkl | |
| 240 | + ├─ scripts/03_make_figures.py ────────► figures/fig1…fig11 (+ verification) | |
| 241 | + └─ scripts/04_export_tables.py ───────► results/tables/*.csv | |
| 242 | + │ | |
| 243 | + ▼ | |
| 244 | + paper/main.tex ──► main.pdf (52 p.) | |
| 245 | +``` | |
| 246 | + | |
| 247 | +The bracketed upstream stages were produced before this restructuring by scripts | |
| 248 | +that no longer exist (see [Project History](#-project-history--provenance)); their | |
| 249 | +outputs are preserved and every downstream number is verified against them. | |
| 250 | + | |
| 251 | +--- | |
| 252 | + | |
| 253 | +## ⚙️ Installation & Setup | |
| 254 | + | |
| 255 | +**Prerequisites:** Python ≥ 3.11, a TeX Live distribution (with `newtx`, `natbib`, | |
| 256 | +`booktabs`, `threeparttable`), and ~4 GB of free disk for the data artifacts. | |
| 257 | + | |
| 258 | +```bash | |
| 259 | +git clone https://github.com/spboucher-ai/wp3-hedonic-housing-us.git | |
| 260 | +cd wp3-hedonic-housing-us | |
| 261 | + | |
| 262 | +python3 -m venv .venv | |
| 263 | +source .venv/bin/activate | |
| 264 | +pip install -r requirements.txt | |
| 265 | +``` | |
| 266 | + | |
| 267 | +Then place the data artifacts (request them at **contact@spboucher.ai**) into | |
| 268 | +`data/raw/`, `data/processed/`, and `results/` as shown in | |
| 269 | +[Repository Structure](#-repository-structure). | |
| 270 | + | |
| 271 | +All paths resolve relative to the repository root. To point the code at another | |
| 272 | +location, set the environment variable: | |
| 273 | + | |
| 274 | +```bash | |
| 275 | +export WP3_ROOT=/path/to/artifacts | |
| 276 | +``` | |
| 277 | + | |
| 278 | +--- | |
| 279 | + | |
| 280 | +## ▶️ Reproducing the Results | |
| 281 | + | |
| 282 | +| Step | Command | Runtime* | Needs | | |
| 283 | +|---|---|---|---| | |
| 284 | +| Spatial & robustness suite | `python scripts/01_spatial_robustness.py` | ~1–2 h (trains 16 XGBoost models) | `analytical_sample.pkl` | | |
| 285 | +| QR & sensitivity suite | `python scripts/02_qr_imputation.py` | ~1–3 h (12 quantile regressions on 150k obs) | `model_data.pkl`, DuckDB | | |
| 286 | +| **All 11 figures** | `python scripts/03_make_figures.py` | ~5 min | processed pickles | | |
| 287 | +| Single figures | `python scripts/03_make_figures.py --figs fig2 fig4` | seconds–minutes | idem | | |
| 288 | +| **CSV table exports** | `python scripts/04_export_tables.py` | ~2 min | result pickles | | |
| 289 | + | |
| 290 | +\* Apple Silicon, 32 GB RAM; scripts use all cores (`n_jobs=-1`). | |
| 291 | + | |
| 292 | +Every script prints **verification statistics** comparing recomputed values with | |
| 293 | +the numbers reported in the paper (see [Verification](#-verification--integrity)). | |
| 294 | + | |
| 295 | +--- | |
| 296 | + | |
| 297 | +## 📄 Building the Paper | |
| 298 | + | |
| 299 | +```bash | |
| 300 | +cd paper | |
| 301 | +latexmk # pdflatex ×N + bibtex, via Makefile/latexmkrc defaults | |
| 302 | +# or: make # same thing | |
| 303 | +# or: make clean / make distclean | |
| 304 | +``` | |
| 305 | + | |
| 306 | +- Output: `paper/main.pdf` — **52 pages**, compiles with **zero errors, zero | |
| 307 | + undefined references/citations**. | |
| 308 | +- Figures are pulled from `../figures/` via `\graphicspath`. | |
| 309 | +- Bibliography: `paper/references.bib` (37 entries), `natbib` + `apalike`. | |
| 310 | +- Metadata (title, abstract, keywords R31/C21/C45/C52, version) is centralized in | |
| 311 | + `main.tex` `\WP*` macros. | |
| 312 | + | |
| 313 | +--- | |
| 314 | + | |
| 315 | +## 🖼 Figures | |
| 316 | + | |
| 317 | +All figures are regenerated from the stored artifacts by `scripts/03_make_figures.py` | |
| 318 | +in a unified publication style (serif typography matched to the paper's `newtx` | |
| 319 | +text font, muted colorblind-safe palette, no chart junk, 300 dpi). | |
| 320 | + | |
| 321 | +| # | File | Content | Built from | | |
| 322 | +|---|---|---|---| | |
| 323 | +| 1 | `fig1_price_distribution.png` | Price & log-price histograms with medians | `analytical_sample.pkl` | | |
| 324 | +| 2 | `fig2_ols_diagnostics.png` | Residuals-vs-fitted, Q-Q, density, scale-location | `model_data.pkl` | | |
| 325 | +| 3 | `fig3_quantile_coefficients.png` | QR coefficient paths vs. OLS benchmark (8 panels) | `qr_results.pkl` + `model_data.pkl` | | |
| 326 | +| 4 | `fig4_model_comparison.png` | Predicted vs. actual: OLS / XGBoost / LightGBM | `ml_results.pkl` | | |
| 327 | +| 5 | `fig5_shap_summary.png` | SHAP beeswarm, top 20 features | `shap_data.pkl` | | |
| 328 | +| 6 | `fig6_shap_importance.png` | Mean \|SHAP\| bar chart, top 20 | `shap_data.pkl` | | |
| 329 | +| 7 | `fig7_geographic_prices.png` | 50,000-listing national price map (lat/lon, viridis) | `analytical_sample.pkl` | | |
| 330 | +| 8 | `fig8_regional_prices.png` | Log-price boxplots by Census region (with N) | `analytical_sample.pkl` | | |
| 331 | +| 9 | `fig9_marginal_effects.png` | Bivariate scatters + binned means (6 attributes) | `analytical_sample.pkl` | | |
| 332 | +| 10 | `fig10_shap_dependence.png` | SHAP dependence, 8 key features | `shap_data.pkl` | | |
| 333 | +| 11 | `fig11_shap_interactions.png` | SHAP for interaction terms (waterfront×sqft, pool×South) | `shap_data.pkl` | | |
| 334 | + | |
| 335 | +--- | |
| 336 | + | |
| 337 | +## 📊 Results Summary | |
| 338 | + | |
| 339 | +**Predictive performance (log-price scale):** | |
| 340 | + | |
| 341 | +| Model | Random $R^2$ | Geo-holdout $R^2$ | RMSE (random) | | |
| 342 | +|---|---:|---:|---:| | |
| 343 | +| OLS (62 regressors) | 0.630 | < 0 | 0.491 | | |
| 344 | +| OLS + State FE | 0.678 | — | 0.459 | | |
| 345 | +| OLS + ZIP3 FE | 0.725 | — | 0.423 | | |
| 346 | +| Random Forest | 0.784 | 0.542 | 0.375 | | |
| 347 | +| LightGBM | 0.809 | — | 0.353 | | |
| 348 | +| **XGBoost** | **0.833** | **0.547** | **0.330** | | |
| 349 | +| XGBoost, no geographic features | 0.830 | **0.519** | — | | |
| 350 | +| XGBoost + lat/lon | 0.870 | 0.464 | — | | |
| 351 | +| XGBoost (ablation variant, full 62) | 0.833 | 0.425 | — | | |
| 352 | + | |
| 353 | +**Ablation cascade (XGBoost):** structural 0.498 → +lot 0.546 → +amenities 0.638 → | |
| 354 | ++neighborhood **0.814 (+17.6 pp)** → +market 0.824 → full 0.833 (random $R^2$). | |
| 355 | + | |
| 356 | +**Machine-readable versions** of these and all other paper tables are in | |
| 357 | +[`results/tables/`](results/tables/) (13 CSV files: OLS coefficients, QR paths, | |
| 358 | +inter-quantile Wald tests, stability CVs, Moran's I, imputation/winsorization | |
| 359 | +sensitivity, SHAP importance & cross-model correlations, ZIP3 FE, ablation). | |
| 360 | + | |
| 361 | +--- | |
| 362 | + | |
| 363 | +## ✅ Verification & Integrity | |
| 364 | + | |
| 365 | +The restructuring included a systematic audit reconciling the manuscript against | |
| 366 | +the stored result artifacts. Highlights: | |
| 367 | + | |
| 368 | +- **Exact matches** — OLS residual diagnostics (skewness 0.18, kurtosis 5.55, | |
| 369 | + Jarque–Bera 217,078), test-set $R^2$ (OLS 0.6301, XGBoost 0.8330, | |
| 370 | + LightGBM 0.8088), Moran's $I$ subsample values and mean/SD, QR coefficients at | |
| 371 | + all five quantiles, and the OLS reference line (0.312) in Figure 3. | |
| 372 | +- **End-to-end reproduction** — re-running the refactored Moran's $I$ pipeline | |
| 373 | + reproduces the stored values to 4 decimal places (0.2742 / 0.2839 / 0.2652). | |
| 374 | +- **Manuscript corrections** — six internal inconsistencies were found and fixed | |
| 375 | + (wrong KNN $k$ in the text, mislabeled inter-quantile sign convention, | |
| 376 | + winsorization description not matching the actual procedure, a duplicated table | |
| 377 | + row, mixed in/out-of-sample $R^2$ in the summary matrix). Every fix aligns the | |
| 378 | + text to the stored results and is itemized in [`CHANGES.md`](CHANGES.md) §4. | |
| 379 | + **No scientific result was altered.** | |
| 380 | +- **Completed placeholders** — 13 `[TODO]` table cells in the original manuscript | |
| 381 | + were filled with the exact values from the result pickles. | |
| 382 | + | |
| 383 | +See [`AUDIT.md`](AUDIT.md) for the full audit and [`CHANGES.md`](CHANGES.md) for | |
| 384 | +the change log. | |
| 385 | + | |
| 386 | +--- | |
| 387 | + | |
| 388 | +## 🕰 Project History & Provenance | |
| 389 | + | |
| 390 | +- The project was completed in **May 2026** in a flat working folder | |
| 391 | + (`immo-wp3-spb-20260519`), then restructured into this compendium on | |
| 392 | + **2026-08-05**. The original archive is preserved untouched. | |
| 393 | +- The scripts that built the analytical sample and estimated the primary models | |
| 394 | + (OLS/QR/ML/SHAP) were **lost** prior to restructuring; only two robustness | |
| 395 | + scripts survived and were refactored into `scripts/01` and `scripts/02`. The | |
| 396 | + stored pickles are the canonical record of those stages and every downstream | |
| 397 | + number is validated against them. | |
| 398 | +- Five paper figures had been deleted from the archive; they were rebuilt from the | |
| 399 | + stored data, and subsequently **all 11 figures** were regenerated in a unified | |
| 400 | + journal style. | |
| 401 | +- Three earlier monolithic drafts (`paper_hedonic_pricing{,_v2,_v3}.tex`) precede | |
| 402 | + the sectioned `wp3/` source that became `paper/`. | |
| 403 | + | |
| 404 | +--- | |
| 405 | + | |
| 406 | +## 📖 Citation | |
| 407 | + | |
| 408 | +If you use this code, the figures, or the results, please cite: | |
| 409 | + | |
| 410 | +```bibtex | |
| 411 | +@techreport{boucher2026hedonic, | |
| 412 | + author = {Boucher, Simon-Pierre}, | |
| 413 | + title = {Hedonic Housing Price Models for the United States: | |
| 414 | + A Multi-Method Comparison of Parametric, Quantile, | |
| 415 | + and Machine Learning Approaches}, | |
| 416 | + institution = {Universit\'e du Qu\'ebec en Outaouais, | |
| 417 | + D\'epartement des sciences administratives}, | |
| 418 | + type = {Working Paper}, | |
| 419 | + number = {3}, | |
| 420 | + year = {2026}, | |
| 421 | + month = {May} | |
| 422 | +} | |
| 423 | +``` | |
| 424 | + | |
| 425 | +--- | |
| 426 | + | |
| 427 | +## 📬 Contact & License | |
| 428 | + | |
| 429 | +**Simon-Pierre Boucher** | |
| 430 | +Département des sciences administratives | |
| 431 | +Université du Québec en Outaouais — Gatineau (Québec), Canada | |
| 432 | +📧 **contact@spboucher.ai** · 🎓 simon-pierre.boucher@uqo.ca | |
| 433 | + | |
| 434 | +© 2026 Simon-Pierre Boucher. Analysis code is shared for **replication and review** | |
| 435 | +purposes. The underlying Zillow data are subject to Zillow's Terms of Service and | |
| 436 | +are **not redistributed** in this repository; the paper (`paper/main.pdf`) is a | |
| 437 | +working paper — please cite rather than redistribute. | |
| 438 | + | |
| 439 | +<div align="center"> | |
| 440 | +<sub>Built with Python · XGBoost · LightGBM · statsmodels · SHAP · DuckDB · LaTeX — reproducible from stored artifacts end to end.</sub> | |
| 441 | +</div> | |
added
figures/fig10_shap_dependence.png
+0 −0
Binary file not shown.
added
figures/fig11_shap_interactions.png
+0 −0
Binary file not shown.
added
figures/fig1_price_distribution.png
+0 −0
Binary file not shown.
added
figures/fig2_ols_diagnostics.png
+0 −0
Binary file not shown.
added
figures/fig3_quantile_coefficients.png
+0 −0
Binary file not shown.
added
figures/fig4_model_comparison.png
+0 −0
Binary file not shown.
added
figures/fig5_shap_summary.png
+0 −0
Binary file not shown.
added
figures/fig6_shap_importance.png
+0 −0
Binary file not shown.
added
figures/fig7_geographic_prices.png
+0 −0
Binary file not shown.
added
figures/fig8_regional_prices.png
+0 −0
Binary file not shown.
added
figures/fig9_marginal_effects.png
+0 −0
Binary file not shown.
added
paper/Makefile
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +.PHONY: all clean distclean | |
| 2 | + | |
| 3 | +all: | |
| 4 | + latexmk | |
| 5 | + | |
| 6 | +clean: | |
| 7 | + latexmk -c | |
| 8 | + | |
| 9 | +distclean: | |
| 10 | + latexmk -C | |
| 11 | + rm -f *.synctex.gz *.run.xml *.bbl *.bcf | |
added
paper/appendix/appendix.tex
+165 −0
@@ -0,0 +1,165 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% APPENDICES | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\appendix | |
| 7 | + | |
| 8 | +\section{Additional Figures} | |
| 9 | +\label{app:figures} | |
| 10 | + | |
| 11 | +\begin{figure}[H] | |
| 12 | + \centering | |
| 13 | + \includegraphics[width=0.85\textwidth]{figures/fig6_shap_importance.png} | |
| 14 | + \caption{Feature Importance Bar Chart: Mean Absolute SHAP Values (XGBoost). These values reflect predictive contributions, not causal importance.} | |
| 15 | + \label{fig:shap_bar} | |
| 16 | +\end{figure} | |
| 17 | + | |
| 18 | +\begin{figure}[H] | |
| 19 | + \centering | |
| 20 | + \includegraphics[width=\textwidth]{figures/fig11_shap_interactions.png} | |
| 21 | + \caption{SHAP Values for Interaction Terms: (a) Waterfront $\times$ Log(Living Area); (b) Pool $\times$ South Region} | |
| 22 | + \label{fig:shap_interactions} | |
| 23 | +\end{figure} | |
| 24 | + | |
| 25 | + | |
| 26 | +\section{Full OLS Coefficient Table} | |
| 27 | +\label{app:ols_full} | |
| 28 | + | |
| 29 | +Table~\ref{tab:ols_full} reports the complete set of standardized OLS coefficients, including all categorical controls. | |
| 30 | + | |
| 31 | +\begin{landscape} | |
| 32 | +\begin{table}[H] | |
| 33 | +\centering | |
| 34 | +\caption{Full OLS Regression Results Including All Categorical Controls (Standardized)} | |
| 35 | +\label{tab:ols_full} | |
| 36 | +\begin{threeparttable} | |
| 37 | +\footnotesize | |
| 38 | +\begin{tabular}{lrrlclrrl} | |
| 39 | +\toprule | |
| 40 | +Variable & Coeff. & SE & Sig. & & Variable & Coeff. & SE & Sig. \\ | |
| 41 | +\midrule | |
| 42 | +Constant & 12.510 & 0.010 & *** & & Roof: Metal & 0.070 & 0.008 & *** \\ | |
| 43 | +$\ln$(Living Area)$^\dagger$ & 0.312 & 0.003 & *** & & Roof: Shingle/Comp & $-$0.074 & 0.008 & *** \\ | |
| 44 | +Bedrooms$^\dagger$ & $-$0.068 & 0.002 & *** & & Roof: Tile & $-$0.110 & 0.008 & *** \\ | |
| 45 | +Bathrooms$^\dagger$ & 0.236 & 0.003 & *** & & Roof: Slate & 0.008 & 0.017 & \\ | |
| 46 | +Age$^\dagger$ & $-$0.042 & 0.014 & ** & & Roof: Other & 0.033 & 0.008 & *** \\ | |
| 47 | +Age$^{2\dagger}$ & 0.051 & 0.002 & *** & & Roof: Unknown & 0.004 & 0.008 & \\ | |
| 48 | +Stories$^\dagger$ & 0.000 & 0.000 & & & Constr: Concrete/Block & 0.269 & 0.003 & *** \\ | |
| 49 | +Bath/Bed Ratio$^\dagger$ & $-$0.021 & 0.002 & *** & & Constr: Stucco & 0.149 & 0.002 & *** \\ | |
| 50 | +Sqft/Bed$^\dagger$ & $-$0.002 & 0.002 & & & Constr: Stone & 0.145 & 0.005 & *** \\ | |
| 51 | +$\ln$(Lot Size)$^\dagger$ & 0.070 & 0.001 & *** & & Constr: Wood/Frame & 0.103 & 0.002 & *** \\ | |
| 52 | +Pool & 0.029 & 0.003 & *** & & Constr: Vinyl & 0.012 & 0.002 & *** \\ | |
| 53 | +Spa & 0.033 & 0.003 & *** & & Constr: Steel & 0.027 & 0.014 & * \\ | |
| 54 | +Basement & $-$0.042 & 0.002 & *** & & Constr: Other & 0.075 & 0.002 & *** \\ | |
| 55 | +Fireplace & $-$0.002 & 0.002 & & & Constr: Unknown & 0.177 & 0.002 & *** \\ | |
| 56 | +Garage & 0.109 & 0.002 & *** & & Found: Concrete & 0.217 & 0.005 & *** \\ | |
| 57 | +Waterfront & $-$0.135 & 0.032 & *** & & Found: Crawlspace & 0.181 & 0.006 & *** \\ | |
| 58 | +Central Air & 0.069 & 0.001 & *** & & Found: Slab & 0.122 & 0.005 & *** \\ | |
| 59 | +Forced Air & $-$0.023 & 0.002 & *** & & Found: Pier/Raised & 0.161 & 0.006 & *** \\ | |
| 60 | +Hardwood & 0.125 & 0.001 & *** & & Found: Stone & 0.032 & 0.010 & ** \\ | |
| 61 | +Parking$^\dagger$ & 0.000 & 0.099 & & & Found: Other & 0.144 & 0.006 & *** \\ | |
| 62 | +Luxury$^\dagger$ & 0.044 & 0.002 & *** & & Found: Unknown & 0.174 & 0.005 & *** \\ | |
| 63 | +Walk$^\dagger$ & $-$0.011 & 0.001 & *** & & Northeast & 0.487 & 0.003 & *** \\ | |
| 64 | +Bike$^\dagger$ & 0.072 & 0.001 & *** & & South & 0.022 & 0.003 & *** \\ | |
| 65 | +Transit$^\dagger$ & 0.052 & 0.001 & *** & & West & 0.401 & 0.003 & *** \\ | |
| 66 | +School Rating$^\dagger$ & $-$0.049 & 0.001 & *** & & sqft$\times$Age$^\dagger$ & $-$0.110 & 0.014 & *** \\ | |
| 67 | +School Count$^\dagger$ & $-$0.002 & 0.001 & *** & & Pool$\times$South & 0.008 & 0.001 & *** \\ | |
| 68 | +School Dist.$^\dagger$ & $-$0.016 & 0.001 & *** & & WF$\times$sqft$^\dagger$ & 0.137 & 0.009 & *** \\ | |
| 69 | +Tax Rate$^\dagger$ & $-$0.029 & 0.001 & *** & & Base$\times$North & $-$0.025 & 0.001 & *** \\ | |
| 70 | +Condo & $-$0.037 & 0.004 & *** & & Condo$\times$Walk$^\dagger$ & 0.082 & 0.001 & *** \\ | |
| 71 | +HOA & $-$0.108 & 0.002 & *** & & Age$\times$Lux$^\dagger$ & 0.044 & 0.001 & *** \\ | |
| 72 | +$\ln$(HOA+1)$^\dagger$ & 0.044 & 0.001 & *** & & & & & \\ | |
| 73 | +New Constr. & 0.103 & 0.003 & *** & & & & & \\ | |
| 74 | +Foreclosure & $-$0.406 & 0.010 & *** & & & & & \\ | |
| 75 | +\midrule | |
| 76 | +\multicolumn{9}{l}{$R^2 = 0.634$, Adj.\ $R^2 = 0.634$, $N = 788{,}842$, $F = 18{,}712.5$, RMSE $= 0.489$} \\ | |
| 77 | +\bottomrule | |
| 78 | +\end{tabular} | |
| 79 | +\begin{tablenotes} | |
| 80 | +\footnotesize | |
| 81 | +\item \textit{Notes:} HC3 robust standard errors. $^\dagger$Standardized continuous variables. Reference categories: Roof = Flat; Construction = Brick; Foundation = Basement; Region = Midwest. *** $p<0.001$, ** $p<0.01$, * $p<0.05$. | |
| 82 | +\end{tablenotes} | |
| 83 | +\end{threeparttable} | |
| 84 | +\end{table} | |
| 85 | +\end{landscape} | |
| 86 | + | |
| 87 | + | |
| 88 | +\section{Robustness Agenda} | |
| 89 | +\label{app:robustness_agenda} | |
| 90 | + | |
| 91 | +The following robustness checks and extensions are planned or completed. Items marked with $\checkmark$ have been completed in this version; unmarked items are left for future work. | |
| 92 | + | |
| 93 | +\begin{enumerate}[nosep] | |
| 94 | + \item[$\checkmark$] ZIP3-level fixed effects (Section~\ref{sec:progressive_fe}). | |
| 95 | + \item County-level and MSA-level fixed effects. | |
| 96 | + \item Spatial lag and spatial error model estimation. | |
| 97 | + \item Spatial cross-validation (train on some regions, test on others) at finer geographic levels. | |
| 98 | + \item[$\checkmark$] Inter-quantile Wald tests for coefficient equality across quantiles (Section~\ref{sec:qr_results}). | |
| 99 | + \item[$\checkmark$] Re-estimation excluding imputed observations (Section~\ref{sec:imputation_sensitivity}). | |
| 100 | + \item CatBoost benchmark with native categorical handling. | |
| 101 | + \item Nested cross-validation for hyperparameter tuning. | |
| 102 | + \item[$\checkmark$] SHAP stability comparison across XGBoost, LightGBM, and Random Forest (Section~\ref{sec:shap_results}). | |
| 103 | + \item Integration of climate risk data from First Street Foundation or FEMA flood maps. | |
| 104 | + \item Replication using transaction prices from recently sold listings. | |
| 105 | + \item GAM/spline models to decompose the ML gain into non-linearity versus spatial partitioning. | |
| 106 | + \item Comparison of sample distribution to ACS and Census benchmarks by state and price tier. | |
| 107 | + \item Calibration plots by price decile for all models. | |
| 108 | + \item[$\checkmark$] Winsorization sensitivity (Section~\ref{sec:winsorization}). | |
| 109 | + \item[$\checkmark$] Feature ablation analysis for XGBoost (Section~\ref{sec:ablation}). | |
| 110 | + \item[$\checkmark$] XGBoost with latitude/longitude coordinates (Section~\ref{sec:geo_features}). | |
| 111 | + \item[$\checkmark$] XGBoost without geographic features (Section~\ref{sec:geo_features}). | |
| 112 | + \item[$\checkmark$] Moran's $I$ subsample stability (Section~\ref{sec:spatial_autocorrelation}). | |
| 113 | + \item[$\checkmark$] Quantile regression stability across 10 subsamples (Section~\ref{sec:qr_stability}). | |
| 114 | +\end{enumerate} | |
| 115 | + | |
| 116 | + | |
| 117 | +\section{Reproducibility Checklist} | |
| 118 | +\label{app:reproducibility} | |
| 119 | + | |
| 120 | +\begin{table}[H] | |
| 121 | +\centering | |
| 122 | +\caption{Reproducibility Checklist} | |
| 123 | +\label{tab:reproducibility} | |
| 124 | +\begin{threeparttable} | |
| 125 | +\small | |
| 126 | +\begin{tabular}{p{6cm}p{3cm}p{4cm}} | |
| 127 | +\toprule | |
| 128 | +Item & Status & Notes \\ | |
| 129 | +\midrule | |
| 130 | +\multicolumn{3}{l}{\textit{Data}} \\ | |
| 131 | +Data source identified & Yes & Zillow active listings \\ | |
| 132 | +Sample construction documented & Yes & Table~\ref{tab:attrition} \\ | |
| 133 | +Missing data rates reported & Yes & Table~\ref{tab:missing} \\ | |
| 134 | +Imputation method documented & Yes & State-level medians \\ | |
| 135 | +Imputation sensitivity tested & Yes & Table~\ref{tab:imputation} \\ | |
| 136 | +\midrule | |
| 137 | +\multicolumn{3}{l}{\textit{Methodology}} \\ | |
| 138 | +Model specifications documented & Yes & Eq.~\ref{eq:ols}--\ref{eq:moran} \\ | |
| 139 | +Hyperparameters reported & Yes & Section~\ref{sec:methodology} \\ | |
| 140 | +Random seeds reported & Yes & Seed = 42 \\ | |
| 141 | +Validation design documented & Yes & Section~\ref{sec:validation_designs} \\ | |
| 142 | +\midrule | |
| 143 | +\multicolumn{3}{l}{\textit{Results}} \\ | |
| 144 | +Point estimates reported & Yes & Tables~\ref{tab:ols_results}--\ref{tab:shap_stability} \\ | |
| 145 | +Standard errors / CIs reported & Yes (OLS, QR) & HC3 standard errors \\ | |
| 146 | +Multiple comparisons addressed & Partially & Inter-quantile tests \\ | |
| 147 | +Effect sizes in natural units & Yes & Table~\ref{tab:ols_unstd} \\ | |
| 148 | +\midrule | |
| 149 | +\multicolumn{3}{l}{\textit{Robustness}} \\ | |
| 150 | +Sensitivity to data quality & Yes & Sections~\ref{sec:imputation_sensitivity}--\ref{sec:winsorization} \\ | |
| 151 | +Sensitivity to model choice & Yes & Table~\ref{tab:ml_comparison} \\ | |
| 152 | +Sensitivity to validation design & Yes & Tables~\ref{tab:geo_holdout}--\ref{tab:geo_role} \\ | |
| 153 | +Cross-model stability & Yes & Table~\ref{tab:shap_stability} \\ | |
| 154 | +\midrule | |
| 155 | +\multicolumn{3}{l}{\textit{Code and Data Availability}} \\ | |
| 156 | +Code available & Yes & Replication package (\texttt{src/}, \texttt{scripts/}, pinned \texttt{requirements.txt}) \\ | |
| 157 | +Data available & On request & Subject to Zillow ToS \\ | |
| 158 | +\bottomrule | |
| 159 | +\end{tabular} | |
| 160 | +\begin{tablenotes} | |
| 161 | +\small | |
| 162 | +\item \textit{Notes:} This checklist follows recommended practices for empirical reproducibility in applied economics. | |
| 163 | +\end{tablenotes} | |
| 164 | +\end{threeparttable} | |
| 165 | +\end{table} | |
added
paper/main.pdf
+0 −0
Binary file not shown.
added
paper/main.tex
+201 −0
@@ -0,0 +1,201 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ============================================================================ | |
| 4 | +% UQO Working Paper No. 3 | |
| 5 | +% Hedonic Housing Price Models for the United States: A Multi-Method | |
| 6 | +% Comparison of Parametric, Quantile, and Machine Learning Approaches | |
| 7 | +% ============================================================================ | |
| 8 | +\documentclass[12pt,letterpaper]{article} | |
| 9 | + | |
| 10 | +% --- Encoding & Language --- | |
| 11 | +\usepackage[utf8]{inputenc} | |
| 12 | +\usepackage[T1]{fontenc} | |
| 13 | +\usepackage[english]{babel} | |
| 14 | + | |
| 15 | +% --- Page Layout --- | |
| 16 | +\usepackage[letterpaper, margin=1in, headheight=15pt]{geometry} | |
| 17 | +\usepackage{setspace} | |
| 18 | +\onehalfspacing | |
| 19 | +\setlength{\parindent}{1.5em} | |
| 20 | +\setlength{\parskip}{0pt} | |
| 21 | + | |
| 22 | +% --- Typography --- | |
| 23 | +\usepackage{newtxtext,newtxmath} | |
| 24 | +\usepackage{amsmath,amsfonts} | |
| 25 | +\let\Bbbk\relax | |
| 26 | +\usepackage{amssymb} | |
| 27 | +\usepackage{mathtools} | |
| 28 | +\usepackage{microtype} | |
| 29 | + | |
| 30 | +% --- Tables --- | |
| 31 | +\usepackage{booktabs} | |
| 32 | +\usepackage{threeparttable} | |
| 33 | +\usepackage{tabularx} | |
| 34 | +\usepackage{array} | |
| 35 | +\usepackage{multirow} | |
| 36 | +\usepackage{longtable} | |
| 37 | +\usepackage{adjustbox} | |
| 38 | +\usepackage{siunitx} | |
| 39 | +\usepackage{dcolumn} | |
| 40 | + | |
| 41 | +% --- Figures --- | |
| 42 | +\usepackage{graphicx} | |
| 43 | +\graphicspath{{./}{../}} % figures live in ../figures/, logo in ./ | |
| 44 | +\usepackage[ | |
| 45 | + font = small, | |
| 46 | + labelfont = bf, | |
| 47 | + labelsep = period, | |
| 48 | + skip = 8pt, | |
| 49 | + justification = justified, | |
| 50 | + singlelinecheck = false | |
| 51 | +]{caption} | |
| 52 | +\usepackage{subcaption} | |
| 53 | +\usepackage{float} | |
| 54 | +\usepackage{pdflscape} | |
| 55 | + | |
| 56 | +% --- Colors & Links --- | |
| 57 | +\usepackage[dvipsnames]{xcolor} | |
| 58 | +\definecolor{linkblue}{RGB}{0,51,102} | |
| 59 | +\usepackage[bookmarks, bookmarksnumbered]{hyperref} | |
| 60 | +\hypersetup{ | |
| 61 | + colorlinks = true, | |
| 62 | + linkcolor = NavyBlue, | |
| 63 | + citecolor = NavyBlue, | |
| 64 | + urlcolor = NavyBlue, | |
| 65 | + pdftitle = {Hedonic Housing Price Models for the United States: A Multi-Method Comparison of Parametric, Quantile, and Machine Learning Approaches}, | |
| 66 | + pdfauthor = {Simon-Pierre Boucher} | |
| 67 | +} | |
| 68 | + | |
| 69 | +% --- Bibliography --- | |
| 70 | +\usepackage[round, authoryear, comma]{natbib} | |
| 71 | +\setcitestyle{aysep={,}} | |
| 72 | +\bibliographystyle{apalike} | |
| 73 | + | |
| 74 | +% --- Headers & Footers --- | |
| 75 | +\usepackage{fancyhdr} | |
| 76 | +\pagestyle{fancy} | |
| 77 | +\fancyhf{} | |
| 78 | +\fancyhead[L]{\small\itshape Hedonic Housing Price Models for the United States} | |
| 79 | +\fancyhead[R]{\small\thepage} | |
| 80 | +\renewcommand{\headrulewidth}{0.4pt} | |
| 81 | +\renewcommand{\footrulewidth}{0pt} | |
| 82 | +\fancypagestyle{plain}{% | |
| 83 | + \fancyhf{} | |
| 84 | + \fancyfoot[C]{\small\thepage} | |
| 85 | + \renewcommand{\headrulewidth}{0pt} | |
| 86 | +} | |
| 87 | + | |
| 88 | +% --- Section Formatting --- | |
| 89 | +\usepackage{titlesec} | |
| 90 | +\titleformat{\section}{\large\bfseries}{\thesection.}{0.5em}{} | |
| 91 | +\titleformat{\subsection}{\normalsize\bfseries}{\thesubsection.}{0.5em}{} | |
| 92 | +\titleformat{\subsubsection}{\normalsize\itshape}{\thesubsubsection.}{0.5em}{} | |
| 93 | + | |
| 94 | +% --- Appendix Support --- | |
| 95 | +\usepackage[toc, page]{appendix} | |
| 96 | +\usepackage{enumitem} | |
| 97 | +\usepackage{etoolbox} | |
| 98 | +\usepackage[hang,flushmargin]{footmisc} | |
| 99 | + | |
| 100 | +% --- Custom Column Types --- | |
| 101 | +\newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}} | |
| 102 | +\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}} | |
| 103 | +\newcolumntype{C}[1]{>{\centering\arraybackslash}p{#1}} | |
| 104 | +\newcolumntype{d}[1]{D{.}{.}{#1}} | |
| 105 | + | |
| 106 | +% --- Custom Commands --- | |
| 107 | +\newcommand{\sym}[1]{\ensuremath{^{#1}}} | |
| 108 | +\newcommand{\stmark}[1]{\rlap{\textsuperscript{#1}}} | |
| 109 | +\DeclareMathOperator*{\plim}{plim} | |
| 110 | +\newcommand{\E}{\mathbb{E}} | |
| 111 | +\newcommand{\Var}{\mathrm{Var}} | |
| 112 | +\newcommand{\Cov}{\mathrm{Cov}} | |
| 113 | +\newcommand{\Corr}{\mathrm{Corr}} | |
| 114 | +\newcommand{\se}{\mathrm{s.e.}} | |
| 115 | +\newcommand{\tr}{\mathrm{tr}} | |
| 116 | +\newcommand{\rank}{\mathrm{rank}} | |
| 117 | +\newcommand{\diag}{\mathrm{diag}} | |
| 118 | +\newcommand{\R}{\mathbb{R}} | |
| 119 | +\newcommand{\N}{\mathbb{N}} | |
| 120 | +\newcommand{\eps}{\varepsilon} | |
| 121 | +\newcommand{\bfbeta}{\boldsymbol{\beta}} | |
| 122 | +\newcommand{\bfgamma}{\boldsymbol{\gamma}} | |
| 123 | +\newcommand{\bfalpha}{\boldsymbol{\alpha}} | |
| 124 | +\newcommand{\bfdelta}{\boldsymbol{\delta}} | |
| 125 | +\newcommand{\bftheta}{\boldsymbol{\theta}} | |
| 126 | +\newcommand{\bfSigma}{\boldsymbol{\Sigma}} | |
| 127 | +\newcommand{\bfOmega}{\boldsymbol{\Omega}} | |
| 128 | +\newcommand{\bfX}{\mathbf{X}} | |
| 129 | +\newcommand{\bfY}{\mathbf{Y}} | |
| 130 | +\newcommand{\bfy}{\mathbf{y}} | |
| 131 | +\newcommand{\bfe}{\mathbf{e}} | |
| 132 | +\newcommand{\bfs}{\mathbf{s}} | |
| 133 | +\newcommand{\iid}{\overset{\mathrm{iid}}{\sim}} | |
| 134 | +\newcommand{\pto}{\overset{p}{\to}} | |
| 135 | +\newcommand{\dto}{\overset{d}{\to}} | |
| 136 | +\newcommand{\ols}{\mathrm{OLS}} | |
| 137 | +\newcommand{\iv}{\mathrm{IV}} | |
| 138 | +\newcommand{\gmm}{\mathrm{GMM}} | |
| 139 | +\newcommand{\mle}{\mathrm{MLE}} | |
| 140 | +\newcommand{\pval}{\textit{p}-value} | |
| 141 | +\newcommand{\tstat}{\textit{t}-statistic} | |
| 142 | +\newcommand{\fstat}{\textit{F}-statistic} | |
| 143 | +\newcommand{\tablenote}[1]{\begin{minipage}{\linewidth}\footnotesize #1\end{minipage}} | |
| 144 | +\newcommand{\signote}{*** $p < 0.001$; ** $p < 0.01$; * $p < 0.05$.} | |
| 145 | + | |
| 146 | +% ============================================================================ | |
| 147 | +% METADATA | |
| 148 | +% ============================================================================ | |
| 149 | +\newcommand{\WPnumber}{3} | |
| 150 | +\newcommand{\WPtitle}{Hedonic Housing Price Models for the United States: A Multi-Method Comparison of Parametric, Quantile, and Machine Learning Approaches} | |
| 151 | +\newcommand{\WPsubtitle}{} | |
| 152 | +\newcommand{\WPdate}{May 2026} | |
| 153 | +\newcommand{\WPversion}{1.0} | |
| 154 | +\newcommand{\WPabstract}{% | |
| 155 | +This paper compares econometric and machine-learning approaches to hedonic housing valuation using 788,842 active Zillow listings across all 50 U.S.\ states and the District of Columbia. A semi-log OLS model with 62 regressors ($R^2 = 0.634$) provides interpretable listing-price gradients; adding ZIP3 fixed effects raises $R^2$ to 0.725, and Moran's $I = 0.27$ confirms strong residual spatial autocorrelation. Quantile regression reveals distributional heterogeneity, with inter-quantile Wald tests rejecting coefficient equality between $\tau = 0.10$ and $\tau = 0.90$ for 11 of 13 key variables. XGBoost achieves $R^2 = 0.833$ under random validation but only 0.425 under state-level geographic holdout; ablation analysis traces the predictive gain primarily to neighborhood-quality features ($+17.6$ pp) and shows that removing geographic features \emph{improves} geographic holdout $R^2$ to 0.519, revealing spatial overfitting. SHAP importance rankings are stable across models (Spearman $\rho > 0.89$). Robustness checks confirm that the lot-size gradient triples when imputed observations are dropped, while other coefficients remain stable under winsorization and subsampling. Throughout, estimates are interpreted as conditional associations in listing prices, not causal willingness-to-pay parameters.% | |
| 156 | +} | |
| 157 | +\newcommand{\WPkeywords}{hedonic pricing, housing markets, quantile regression, XGBoost, SHAP, spatial autocorrelation, geographic validation} | |
| 158 | +\newcommand{\WPjel}{R31, C21, C45, C52} | |
| 159 | + | |
| 160 | +% --- Author --- | |
| 161 | +\newcommand{\WPauthor}{Simon-Pierre Boucher} | |
| 162 | +\newcommand{\WPaffiliation}{% | |
| 163 | + D\'epartement des sciences administratives\\ | |
| 164 | + Universit\'e du Qu\'ebec en Outaouais% | |
| 165 | +} | |
| 166 | +\newcommand{\WPemail}{simon-pierre.boucher@uqo.ca} | |
| 167 | +\newcommand{\WPaddress}{% | |
| 168 | + Gatineau -- Pavillon Alexandre-Tach\'e\\ | |
| 169 | + 283, boulevard Alexandre-Tach\'e\\ | |
| 170 | + Gatineau, Qu\'ebec, Canada J9A 1L8% | |
| 171 | +} | |
| 172 | + | |
| 173 | +% ============================================================================ | |
| 174 | +% DOCUMENT | |
| 175 | +% ============================================================================ | |
| 176 | +\begin{document} | |
| 177 | + | |
| 178 | +% --- Title Page --- | |
| 179 | +\input{sections/titlepage} | |
| 180 | + | |
| 181 | +% --- Main Body --- | |
| 182 | +\input{sections/introduction} | |
| 183 | +\input{sections/literature} | |
| 184 | +\input{sections/data} | |
| 185 | +\input{sections/methodology} | |
| 186 | +\input{sections/results} | |
| 187 | +\input{sections/robustness} | |
| 188 | +\input{sections/discussion} | |
| 189 | +\input{sections/limitations} | |
| 190 | +\input{sections/conclusion} | |
| 191 | + | |
| 192 | +% --- References --- | |
| 193 | +\newpage | |
| 194 | +\addcontentsline{toc}{section}{References} | |
| 195 | +\bibliography{references} | |
| 196 | + | |
| 197 | +% --- Appendix --- | |
| 198 | +\newpage | |
| 199 | +\input{appendix/appendix} | |
| 200 | + | |
| 201 | +\end{document} | |
added
paper/references.bib
+364 −0
@@ -0,0 +1,364 @@ | ||
| 1 | +Author: Simon-Pierre Boucher --- contact (at) spboucher.ai | |
| 2 | +Bibliography for UQO Working Paper No. 3, converted from the manual | |
| 3 | +thebibliography environment with identical keys. Note: BibTeX has no | |
| 4 | +comment character; text outside entries is ignored, and a literal | |
| 5 | +at-sign cannot appear here. | |
| 6 | + | |
| 7 | +@article{anglin1996semiparametric, | |
| 8 | + author = {Anglin, Paul M. and Gen{\c{c}}ay, Ramazan}, | |
| 9 | + title = {Semiparametric estimation of a hedonic price function}, | |
| 10 | + journal = {Journal of Applied Econometrics}, | |
| 11 | + year = {1996}, | |
| 12 | + volume = {11}, | |
| 13 | + number = {6}, | |
| 14 | + pages = {633--648}, | |
| 15 | +} | |
| 16 | + | |
| 17 | +@article{anselin2010thirty, | |
| 18 | + author = {Anselin, Luc}, | |
| 19 | + title = {Thirty years of spatial econometrics}, | |
| 20 | + journal = {Papers in Regional Science}, | |
| 21 | + year = {2010}, | |
| 22 | + volume = {89}, | |
| 23 | + number = {1}, | |
| 24 | + pages = {3--25}, | |
| 25 | +} | |
| 26 | + | |
| 27 | +@article{baldauf2020does, | |
| 28 | + author = {Baldauf, Markus and Garlappi, Lorenzo and Yannelis, Constantine}, | |
| 29 | + title = {Does climate change affect real estate prices? {O}nly if you believe in it}, | |
| 30 | + journal = {Review of Financial Studies}, | |
| 31 | + year = {2020}, | |
| 32 | + volume = {33}, | |
| 33 | + number = {3}, | |
| 34 | + pages = {1256--1295}, | |
| 35 | +} | |
| 36 | + | |
| 37 | +@article{bourassa2019machine, | |
| 38 | + author = {Bourassa, Steven C. and Cantoni, Eva and Hoesli, Martin}, | |
| 39 | + title = {Predicting house prices with spatial dependence: {A} comparison of alternative methods}, | |
| 40 | + journal = {Journal of Real Estate Research}, | |
| 41 | + year = {2019}, | |
| 42 | + volume = {32}, | |
| 43 | + number = {2}, | |
| 44 | + pages = {139--159}, | |
| 45 | +} | |
| 46 | + | |
| 47 | +@article{campbell2011forced, | |
| 48 | + author = {Campbell, John Y. and Giglio, Stefano and Pathak, Parag}, | |
| 49 | + title = {Forced sales and house prices}, | |
| 50 | + journal = {American Economic Review}, | |
| 51 | + year = {2011}, | |
| 52 | + volume = {101}, | |
| 53 | + number = {5}, | |
| 54 | + pages = {2108--2131}, | |
| 55 | +} | |
| 56 | + | |
| 57 | +@article{can1992specification, | |
| 58 | + author = {Can, Ayse}, | |
| 59 | + title = {Specification and estimation of hedonic housing price models}, | |
| 60 | + journal = {Regional Science and Urban Economics}, | |
| 61 | + year = {1992}, | |
| 62 | + volume = {22}, | |
| 63 | + number = {3}, | |
| 64 | + pages = {453--474}, | |
| 65 | +} | |
| 66 | + | |
| 67 | +@inproceedings{chen2016xgboost, | |
| 68 | + author = {Chen, Tianqi and Guestrin, Carlos}, | |
| 69 | + title = {{XGBoost}: {A} scalable tree boosting system}, | |
| 70 | + booktitle = {Proceedings of the 22nd {ACM} {SIGKDD}}, | |
| 71 | + year = {2016}, | |
| 72 | + pages = {785--794}, | |
| 73 | +} | |
| 74 | + | |
| 75 | +@article{chen2020housing, | |
| 76 | + author = {Chen, Jian and Hu, Maggie and Lin, Zhenguo}, | |
| 77 | + title = {Housing price prediction using machine learning: {A} systematic review}, | |
| 78 | + journal = {Expert Systems with Applications}, | |
| 79 | + year = {2020}, | |
| 80 | + volume = {145}, | |
| 81 | + pages = {113142}, | |
| 82 | +} | |
| 83 | + | |
| 84 | +@incollection{court1939hedonic, | |
| 85 | + author = {Court, Andrew T.}, | |
| 86 | + title = {Hedonic price indexes with automotive examples}, | |
| 87 | + booktitle = {The Dynamics of Automobile Demand}, | |
| 88 | + publisher = {General Motors Corporation}, | |
| 89 | + year = {1939}, | |
| 90 | + pages = {99--117}, | |
| 91 | +} | |
| 92 | + | |
| 93 | +@article{cropper1988choice, | |
| 94 | + author = {Cropper, Maureen L. and Deck, Leland B. and McConnell, Kenneth E.}, | |
| 95 | + title = {On the choice of functional form for hedonic price functions}, | |
| 96 | + journal = {Review of Economics and Statistics}, | |
| 97 | + year = {1988}, | |
| 98 | + volume = {70}, | |
| 99 | + number = {4}, | |
| 100 | + pages = {668--675}, | |
| 101 | +} | |
| 102 | + | |
| 103 | +@article{ekeland2004identification, | |
| 104 | + author = {Ekeland, Ivar and Heckman, James J. and Nesheim, Lars}, | |
| 105 | + title = {Identification and estimation of hedonic models}, | |
| 106 | + journal = {Journal of Political Economy}, | |
| 107 | + year = {2004}, | |
| 108 | + volume = {112}, | |
| 109 | + number = {S1}, | |
| 110 | + pages = {S60--S109}, | |
| 111 | +} | |
| 112 | + | |
| 113 | +@article{goodman1998andrew, | |
| 114 | + author = {Goodman, Allen C. and Thibodeau, Thomas G.}, | |
| 115 | + title = {Housing market segmentation}, | |
| 116 | + journal = {Journal of Housing Economics}, | |
| 117 | + year = {1998}, | |
| 118 | + volume = {7}, | |
| 119 | + number = {2}, | |
| 120 | + pages = {121--143}, | |
| 121 | +} | |
| 122 | + | |
| 123 | +@incollection{griliches1961hedonic, | |
| 124 | + author = {Griliches, Zvi}, | |
| 125 | + title = {Hedonic price indexes for automobiles: {A}n econometric analysis of quality change}, | |
| 126 | + booktitle = {The Price Statistics of the Federal Government}, | |
| 127 | + publisher = {NBER}, | |
| 128 | + year = {1961}, | |
| 129 | + pages = {173--196}, | |
| 130 | +} | |
| 131 | + | |
| 132 | +@article{halvorsen1981choice, | |
| 133 | + author = {Halvorsen, Robert and Pollakowski, Henry O.}, | |
| 134 | + title = {Choice of functional form for hedonic price equations}, | |
| 135 | + journal = {Journal of Urban Economics}, | |
| 136 | + year = {1981}, | |
| 137 | + volume = {10}, | |
| 138 | + number = {1}, | |
| 139 | + pages = {37--49}, | |
| 140 | +} | |
| 141 | + | |
| 142 | +@article{hill2013hedonic, | |
| 143 | + author = {Hill, Robert J.}, | |
| 144 | + title = {Hedonic price indexes for residential housing: {A} survey, evaluation and taxonomy}, | |
| 145 | + journal = {Journal of Economic Surveys}, | |
| 146 | + year = {2013}, | |
| 147 | + volume = {27}, | |
| 148 | + number = {5}, | |
| 149 | + pages = {879--914}, | |
| 150 | +} | |
| 151 | + | |
| 152 | +@inproceedings{ke2017lightgbm, | |
| 153 | + author = {Ke, Guolin and Meng, Qi and Finley, Thomas and Wang, Taifeng and Chen, Wei and Ma, Weidong and Ye, Qiwei and Liu, Tie-Yan}, | |
| 154 | + title = {{LightGBM}: {A} highly efficient gradient boosting decision tree}, | |
| 155 | + booktitle = {Advances in Neural Information Processing Systems}, | |
| 156 | + year = {2017}, | |
| 157 | + pages = {3146--3154}, | |
| 158 | +} | |
| 159 | + | |
| 160 | +@article{knight2002listing, | |
| 161 | + author = {Knight, John R.}, | |
| 162 | + title = {Listing price, time on market, and ultimate selling price}, | |
| 163 | + journal = {Real Estate Economics}, | |
| 164 | + year = {2002}, | |
| 165 | + volume = {30}, | |
| 166 | + number = {2}, | |
| 167 | + pages = {213--237}, | |
| 168 | +} | |
| 169 | + | |
| 170 | +@article{koenker1978regression, | |
| 171 | + author = {Koenker, Roger and Bassett, Gilbert}, | |
| 172 | + title = {Regression quantiles}, | |
| 173 | + journal = {Econometrica}, | |
| 174 | + year = {1978}, | |
| 175 | + volume = {46}, | |
| 176 | + number = {1}, | |
| 177 | + pages = {33--50}, | |
| 178 | +} | |
| 179 | + | |
| 180 | +@article{kok2017big, | |
| 181 | + author = {Kok, Nils and Koponen, Eija-Leena and Mart{\'i}nez-Barbosa, Carmen A.}, | |
| 182 | + title = {Big data in real estate? {F}rom manual appraisal to automated valuation}, | |
| 183 | + journal = {Journal of Portfolio Management}, | |
| 184 | + year = {2017}, | |
| 185 | + volume = {43}, | |
| 186 | + number = {6}, | |
| 187 | + pages = {202--211}, | |
| 188 | +} | |
| 189 | + | |
| 190 | +@article{lancaster1966new, | |
| 191 | + author = {Lancaster, Kelvin J.}, | |
| 192 | + title = {A new approach to consumer theory}, | |
| 193 | + journal = {Journal of Political Economy}, | |
| 194 | + year = {1966}, | |
| 195 | + volume = {74}, | |
| 196 | + number = {2}, | |
| 197 | + pages = {132--157}, | |
| 198 | +} | |
| 199 | + | |
| 200 | +@book{lesage2009introduction, | |
| 201 | + author = {LeSage, James and Pace, R. Kelley}, | |
| 202 | + title = {Introduction to Spatial Econometrics}, | |
| 203 | + publisher = {Chapman and Hall/CRC}, | |
| 204 | + year = {2009}, | |
| 205 | +} | |
| 206 | + | |
| 207 | +@article{liao2012hedonic, | |
| 208 | + author = {Liao, Wen-Chi and Wang, Xizhu}, | |
| 209 | + title = {Hedonic house prices and spatial quantile regression}, | |
| 210 | + journal = {Journal of Housing Economics}, | |
| 211 | + year = {2012}, | |
| 212 | + volume = {21}, | |
| 213 | + number = {1}, | |
| 214 | + pages = {16--27}, | |
| 215 | +} | |
| 216 | + | |
| 217 | +@inproceedings{lundberg2017unified, | |
| 218 | + author = {Lundberg, Scott M. and Lee, Su-In}, | |
| 219 | + title = {A unified approach to interpreting model predictions}, | |
| 220 | + booktitle = {Advances in Neural Information Processing Systems}, | |
| 221 | + year = {2017}, | |
| 222 | + pages = {4765--4774}, | |
| 223 | +} | |
| 224 | + | |
| 225 | +@article{lundberg2020local, | |
| 226 | + author = {Lundberg, Scott M. and Erion, Gabriel and Chen, Hugh and DeGrave, Alex and Prutkin, Jordan M. and Nair, Bala and Katz, Ronit and Himmelfarb, Jonathan and Bansal, Nisha and Lee, Su-In}, | |
| 227 | + title = {From local explanations to global understanding with explainable {AI} for trees}, | |
| 228 | + journal = {Nature Machine Intelligence}, | |
| 229 | + year = {2020}, | |
| 230 | + volume = {2}, | |
| 231 | + number = {1}, | |
| 232 | + pages = {56--67}, | |
| 233 | +} | |
| 234 | + | |
| 235 | +@article{mackinnon1985some, | |
| 236 | + author = {MacKinnon, James G. and White, Halbert}, | |
| 237 | + title = {Some heteroskedasticity-consistent covariance matrix estimators with improved finite sample properties}, | |
| 238 | + journal = {Journal of Econometrics}, | |
| 239 | + year = {1985}, | |
| 240 | + volume = {29}, | |
| 241 | + number = {3}, | |
| 242 | + pages = {305--325}, | |
| 243 | +} | |
| 244 | + | |
| 245 | +@article{mak2010quantile, | |
| 246 | + author = {Mak, Stephen and Choy, Lennon and Ho, Winky}, | |
| 247 | + title = {Quantile regression estimates of {H}ong {K}ong real estate prices}, | |
| 248 | + journal = {Urban Studies}, | |
| 249 | + year = {2010}, | |
| 250 | + volume = {47}, | |
| 251 | + number = {11}, | |
| 252 | + pages = {2461--2472}, | |
| 253 | +} | |
| 254 | + | |
| 255 | +@incollection{malpezzi2003hedonic, | |
| 256 | + author = {Malpezzi, Stephen}, | |
| 257 | + title = {Hedonic pricing models: {A} selective and applied review}, | |
| 258 | + booktitle = {Housing Economics and Public Policy}, | |
| 259 | + editor = {O'Sullivan, Tony and Gibb, Kenneth}, | |
| 260 | + publisher = {Blackwell}, | |
| 261 | + year = {2003}, | |
| 262 | + pages = {67--89}, | |
| 263 | +} | |
| 264 | + | |
| 265 | +@article{meyer2019importance, | |
| 266 | + author = {Meyer, Hanna and Pebesma, Edzer}, | |
| 267 | + title = {Predicting into unknown space? {E}stimating the area of applicability of spatial prediction models}, | |
| 268 | + journal = {Methods in Ecology and Evolution}, | |
| 269 | + year = {2019}, | |
| 270 | + volume = {12}, | |
| 271 | + number = {9}, | |
| 272 | + pages = {1620--1633}, | |
| 273 | +} | |
| 274 | + | |
| 275 | +@article{mullainathan2017machine, | |
| 276 | + author = {Mullainathan, Sendhil and Spiess, Jann}, | |
| 277 | + title = {Machine learning: {A}n applied econometric approach}, | |
| 278 | + journal = {Journal of Economic Perspectives}, | |
| 279 | + year = {2017}, | |
| 280 | + volume = {31}, | |
| 281 | + number = {2}, | |
| 282 | + pages = {87--106}, | |
| 283 | +} | |
| 284 | + | |
| 285 | +@article{oates1969effects, | |
| 286 | + author = {Oates, Wallace E.}, | |
| 287 | + title = {The effects of property taxes and local public spending on property values}, | |
| 288 | + journal = {Journal of Political Economy}, | |
| 289 | + year = {1969}, | |
| 290 | + volume = {77}, | |
| 291 | + number = {6}, | |
| 292 | + pages = {957--971}, | |
| 293 | +} | |
| 294 | + | |
| 295 | +@incollection{roberts2013endogeneity, | |
| 296 | + author = {Roberts, Michael R. and Whited, Toni M.}, | |
| 297 | + title = {Endogeneity in empirical corporate finance}, | |
| 298 | + booktitle = {Handbook of the Economics of Finance}, | |
| 299 | + editor = {Constantinides, George M. and Harris, Milton and Stulz, Ren{\'e} M.}, | |
| 300 | + publisher = {Elsevier}, | |
| 301 | + year = {2013}, | |
| 302 | + volume = {2A}, | |
| 303 | + pages = {493--572}, | |
| 304 | +} | |
| 305 | + | |
| 306 | +@article{roberts2017cross, | |
| 307 | + author = {Roberts, David R. and Bahn, Volker and Ciuti, Simone and Boyce, Mark S. and Elith, Jane and Guillera-Arroita, Gurutzeta and Hauenstein, Severin and Lahoz-Monfort, Jos{\'e} J. and Schr{\"o}der, Boris and Thuiller, Wilfried and Warton, David I. and Wintle, Brendan A. and Hartig, Florian and Dormann, Carsten F.}, | |
| 308 | + title = {Cross-validation strategies for data with temporal and spatial dependence}, | |
| 309 | + journal = {Ecography}, | |
| 310 | + year = {2017}, | |
| 311 | + volume = {40}, | |
| 312 | + number = {8}, | |
| 313 | + pages = {913--929}, | |
| 314 | +} | |
| 315 | + | |
| 316 | +@article{rosen1974hedonic, | |
| 317 | + author = {Rosen, Sherwin}, | |
| 318 | + title = {Hedonic prices and implicit markets: {P}roduct differentiation in pure competition}, | |
| 319 | + journal = {Journal of Political Economy}, | |
| 320 | + year = {1974}, | |
| 321 | + volume = {82}, | |
| 322 | + number = {1}, | |
| 323 | + pages = {34--55}, | |
| 324 | +} | |
| 325 | + | |
| 326 | +@incollection{shapley1953value, | |
| 327 | + author = {Shapley, Lloyd S.}, | |
| 328 | + title = {A value for $n$-person games}, | |
| 329 | + booktitle = {Contributions to the Theory of Games II}, | |
| 330 | + editor = {Kuhn, Harold W. and Tucker, Albert W.}, | |
| 331 | + publisher = {Princeton University Press}, | |
| 332 | + year = {1953}, | |
| 333 | + pages = {307--317}, | |
| 334 | +} | |
| 335 | + | |
| 336 | +@article{sirmans2005composition, | |
| 337 | + author = {Sirmans, Stacy and Macpherson, David and Zietz, Emily}, | |
| 338 | + title = {The composition of hedonic pricing models}, | |
| 339 | + journal = {Journal of Real Estate Literature}, | |
| 340 | + year = {2005}, | |
| 341 | + volume = {13}, | |
| 342 | + number = {1}, | |
| 343 | + pages = {3--43}, | |
| 344 | +} | |
| 345 | + | |
| 346 | +@article{white1980heteroskedasticity, | |
| 347 | + author = {White, Halbert}, | |
| 348 | + title = {A heteroskedasticity-consistent covariance matrix estimator and a direct test for heteroskedasticity}, | |
| 349 | + journal = {Econometrica}, | |
| 350 | + year = {1980}, | |
| 351 | + volume = {48}, | |
| 352 | + number = {4}, | |
| 353 | + pages = {817--838}, | |
| 354 | +} | |
| 355 | + | |
| 356 | +@article{zietz2008determinants, | |
| 357 | + author = {Zietz, Joachim and Zietz, Emily N. and Sirmans, G. Stacy}, | |
| 358 | + title = {Determinants of house prices: {A} quantile regression approach}, | |
| 359 | + journal = {Journal of Real Estate Finance and Economics}, | |
| 360 | + year = {2008}, | |
| 361 | + volume = {37}, | |
| 362 | + number = {4}, | |
| 363 | + pages = {317--333}, | |
| 364 | +} | |
added
paper/sections/conclusion.tex
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 9. CONCLUSION | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Conclusion} | |
| 7 | +\label{sec:conclusion} | |
| 8 | + | |
| 9 | +This paper has compared three approaches to hedonic housing price analysis---OLS, quantile regression, and gradient-boosted machine learning with SHAP interpretation---using a large Zillow listing sample of 788,842 U.S.\ properties. Each framework answers a different question, and the robustness extensions in this version document the boundaries of each framework's conclusions. | |
| 10 | + | |
| 11 | +OLS provides interpretable conditional mean associations: a price-to-area elasticity of 0.63, a 22.6\% bathroom listing-price gradient, a 33.4\% foreclosure discount, and regional listing-price differentials of 49\%--63\% between coastal and interior markets. The progression from region FE ($R^2 = 0.634$) to ZIP3 FE ($R^2 = 0.725$) demonstrates that fine-grained geographic controls capture substantial variation, but the imputation sensitivity analysis reveals that the lot-size gradient is attenuated threefold by state-median imputation. | |
| 12 | + | |
| 13 | +Quantile regression reveals that mean effects mask substantial distributional heterogeneity, now formally confirmed by inter-quantile Wald tests that reject coefficient equality for 11 of 13 variables. The garage listing-price gradient is ten times larger at the 10th percentile than the 90th ($z = 28.92$); the pool gradient is insignificant below the median but reaches 9.9\% at $\tau = 0.90$. Subsample stability analysis identifies age as the one coefficient that is genuinely unstable across geographic subsamples. | |
| 14 | + | |
| 15 | +XGBoost captures non-linearities and interactions that improve prediction from $R^2 = 0.630$ (OLS) to $R^2 = 0.833$ under random validation. However, this figure substantially overstates generalization ability. The ablation analysis reveals that neighborhood features drive the largest predictive gain ($+17.6$ pp) but that the full model with region dummies actively harms geographic holdout ($-7.6$ pp). The most striking result is that removing all geographic features \emph{improves} geographic holdout $R^2$ from 0.425 to 0.519, while adding latitude and longitude does the opposite ($+3.7$ pp random, $-5.5$ pp geographic). Geographic features help the model memorize location-specific prices but do not improve its ability to generalize attribute-price relationships to new markets. | |
| 16 | + | |
| 17 | +SHAP values identify the features that contribute most to predictions---living area, bathrooms, school quality, lot size, region---and this ranking is stable across three tree-based models (Spearman $\rho = 0.89$--$0.99$). But these remain predictive decompositions, not structural listing-price gradients. Moran's $I = 0.27$ on OLS residuals---stable across three subsamples (std $= 0.008$)---confirms that regional controls leave substantial spatial dependence unaddressed. | |
| 18 | + | |
| 19 | +The central contribution is methodological: we demonstrate that model evaluation depends critically on validation design. Random splits overstate generalization, geographic features can harm geographic holdout, SHAP values are stable across models but remain non-structural, and imputation can attenuate key coefficients. Future research should integrate spatial econometric methods, use transaction prices, employ geographic holdout validation as standard practice, incorporate climate risk data, and benchmark against generalized additive models to decompose the ML predictive gain into non-linearity versus spatial partitioning components. | |
| 20 | + | |
| 21 | +The central lesson is not that machine learning replaces hedonic econometrics, but that prediction, distributional heterogeneity, and economic interpretation answer different questions and should be combined carefully in modern housing valuation research. | |
added
paper/sections/data.tex
+192 −0
@@ -0,0 +1,192 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 3. DATA | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Data and Variable Construction} | |
| 7 | +\label{sec:data} | |
| 8 | + | |
| 9 | +\subsection{Data Source} | |
| 10 | + | |
| 11 | +The dataset is sourced from Zillow, the largest online real estate marketplace in the United States. The raw data contain 839,313 residential properties with 116 variables, representing active for-sale listings as of 2025--2026. The data include listing prices, structural characteristics, geocoordinates, neighborhood quality scores, and tax/transaction histories. | |
| 12 | + | |
| 13 | +\textbf{Important limitations of the data source.} The sample comprises active Zillow listings, not the universe of U.S.\ residential properties. Several sources of non-representativeness should be noted: (i) Zillow coverage varies by region, with some MLS-linked markets better represented than others; (ii) active listings capture supply at a point in time, not the full housing stock; (iii) the sample excludes off-market properties, FSBOs not listed on Zillow, and recently transacted properties that are no longer listed; (iv) Southern states are overrepresented (56.6\% of the sample vs.\ approximately 38\% of U.S.\ housing units), likely reflecting higher listing volumes in fast-growing Sun Belt markets. We do not claim national representativeness and caution against interpreting our estimates as population parameters for the entire U.S.\ housing market. | |
| 14 | + | |
| 15 | +\subsection{Listing Prices versus Transaction Prices} | |
| 16 | + | |
| 17 | +A critical limitation is that our dependent variable is the \textit{listing} (asking) price, not the realized transaction price. Listing prices reflect seller expectations and strategic pricing behavior, not necessarily market-clearing values. List-to-sale price ratios vary by market condition, property type, and price tier: luxury properties are more frequently overpriced, distressed properties may be strategically underpriced, and regional norms for overbidding versus negotiation differ substantially \citep{knight2002listing}. Throughout this paper, we interpret the estimated coefficients as \textbf{listing-price capitalization gradients}---conditional associations between attributes and asking prices---rather than transaction-price implicit prices. If listing premiums correlate systematically with property attributes (e.g., if waterfront properties are more frequently overpriced), the estimated gradients will reflect this pricing behavior in addition to underlying valuation differences. | |
| 18 | + | |
| 19 | +\subsection{Sample Construction} | |
| 20 | + | |
| 21 | +Table~\ref{tab:attrition} documents the sample construction. | |
| 22 | + | |
| 23 | +\begin{table}[H] | |
| 24 | +\centering | |
| 25 | +\caption{Sample Construction} | |
| 26 | +\label{tab:attrition} | |
| 27 | +\begin{threeparttable} | |
| 28 | +\begin{tabular}{lrrr} | |
| 29 | +\toprule | |
| 30 | +Step & $N$ remaining & Removed & Criterion \\ | |
| 31 | +\midrule | |
| 32 | +Raw Zillow records & 839,313 & --- & --- \\ | |
| 33 | +Valid price & 817,473 & 21,840 & $\$10{,}000 < P < \$10{,}000{,}000$ \\ | |
| 34 | +Valid living area & 797,380 & 20,093 & $200 < \text{sqft} < 20{,}000$ \\ | |
| 35 | +Valid bedrooms/bathrooms & 790,799 & 6,581 & $1 \leq \text{bed} \leq 10$, $1 \leq \text{bath} \leq 10$ \\ | |
| 36 | +Valid geocoordinates & 789,199 & 1,600 & Non-missing lat/lon \\ | |
| 37 | +U.S.\ states + DC only & 788,842 & 357 & Exclude PR, VI \\ | |
| 38 | +\midrule | |
| 39 | +\textbf{Final analytical sample} & \textbf{788,842} & \textbf{50,471} & \textbf{(6.0\% removed)} \\ | |
| 40 | +\bottomrule | |
| 41 | +\end{tabular} | |
| 42 | +\begin{tablenotes} | |
| 43 | +\small | |
| 44 | +\item \textit{Notes:} Filters applied sequentially. The 6.0\% removal rate suggests the raw data are reasonably clean, though the retained sample may still contain measurement error in secondary variables. | |
| 45 | +\end{tablenotes} | |
| 46 | +\end{threeparttable} | |
| 47 | +\end{table} | |
| 48 | + | |
| 49 | +\subsection{Variable Description} | |
| 50 | + | |
| 51 | +Our specification includes 62 regressors organized into six categories. | |
| 52 | + | |
| 53 | +\subsubsection{Structural Attributes (8 Variables)} | |
| 54 | + | |
| 55 | +The natural logarithm of living area ($\ln\text{sqft}$), number of bedrooms, number of bathrooms, property age ($2026 - \text{year\_built}$) and its square, number of stories, the bathroom-to-bedroom ratio, and square feet per bedroom. Living area enters in log form to accommodate the well-documented concavity of the size-price relationship. | |
| 56 | + | |
| 57 | +\subsubsection{Lot Characteristics (1 Variable)} | |
| 58 | + | |
| 59 | +The natural logarithm of lot size ($\ln\text{lot}$). Missing lot sizes (16.3\% of raw data) are imputed using state-level medians. | |
| 60 | + | |
| 61 | +\subsubsection{Amenity and Quality Indicators (11 Variables)} | |
| 62 | + | |
| 63 | +Binary indicators for swimming pool (39.8\% prevalence), spa (6.1\%), basement (24.3\%), fireplace (39.6\%), garage (67.2\%), waterfront location (8.9\%), central air conditioning (68.1\%), forced air heating (25.8\%), and hardwood flooring (26.7\%). The number of parking spaces and a composite luxury score (0--7) supplement these indicators. | |
| 64 | + | |
| 65 | +\subsubsection{Neighborhood and Location Variables (7 Variables)} | |
| 66 | + | |
| 67 | +Walk Score (0--100), Bike Score, Transit Score, average GreatSchools rating (1--10), school count, distance to nearest school, and local property tax rate. | |
| 68 | + | |
| 69 | +\textbf{Data quality note.} Bike Score has a maximum value of 248 in our data, exceeding the expected 0--100 range. Only 38 observations (0.005\%) exceed 100, and these are retained without truncation. Results are robust to capping Bike Score at 100. | |
| 70 | + | |
| 71 | +\subsubsection{Market Status Variables (5 Variables)} | |
| 72 | + | |
| 73 | +Condominium indicator (14.6\%), HOA membership (37.1\%), log annualized HOA fees ($\ln(1 + \text{HOA}_\text{annual})$), new construction (5.9\%), and foreclosure status (0.5\%). | |
| 74 | + | |
| 75 | +\subsubsection{Interaction Terms (6 Variables)} | |
| 76 | + | |
| 77 | +\begin{itemize}[nosep] | |
| 78 | + \item $\ln(\text{sqft}) \times \text{age}$: depreciation-size interaction; | |
| 79 | + \item $\text{pool} \times \text{South}$: climate-dependent pool gradient; | |
| 80 | + \item $\text{waterfront} \times \ln(\text{sqft})$: size-dependent waterfront gradient; | |
| 81 | + \item $\text{basement} \times \text{North}$: region-dependent basement gradient; | |
| 82 | + \item $\text{condo} \times \text{walk\_score}$: walkability gradient for condominiums; | |
| 83 | + \item $\text{age} \times \text{luxury\_score}$: luxury mitigation of depreciation. | |
| 84 | +\end{itemize} | |
| 85 | + | |
| 86 | +\subsubsection{Categorical Controls (24 Dummy Variables)} | |
| 87 | + | |
| 88 | +Simplified indicators for roof type (6 dummies), construction material (8 dummies), foundation type (7 dummies), and Census region (3 dummies; Midwest reference). | |
| 89 | + | |
| 90 | +\subsection{Missing Data and Imputation} | |
| 91 | + | |
| 92 | +Table~\ref{tab:missing} reports missingness rates for key variables. | |
| 93 | + | |
| 94 | +\begin{table}[H] | |
| 95 | +\centering | |
| 96 | +\caption{Missing Data Rates and Imputation Methods} | |
| 97 | +\label{tab:missing} | |
| 98 | +\begin{threeparttable} | |
| 99 | +\small | |
| 100 | +\begin{tabular}{lrrl} | |
| 101 | +\toprule | |
| 102 | +Variable & Missing (\%) & Method & Notes \\ | |
| 103 | +\midrule | |
| 104 | +Year built & 19.4 & State median & Affects age, age$^2$, interactions \\ | |
| 105 | +Lot size & 16.3 & State median & Large variation \\ | |
| 106 | +Walk Score & 3.1 & State median & \\ | |
| 107 | +Bike Score & 6.1 & State median & Max = 248 (38 obs.) \\ | |
| 108 | +Transit Score & 70.1 & Set to 0 & Suburban/rural properties \\ | |
| 109 | +Garage & 35.1 & Set to False & Conservative \\ | |
| 110 | +Climate risk factors & 100.0 & Excluded & Not available \\ | |
| 111 | +\bottomrule | |
| 112 | +\end{tabular} | |
| 113 | +\begin{tablenotes} | |
| 114 | +\small | |
| 115 | +\item \textit{Notes:} State-level median imputation is used to preserve geographic variation. The high Transit Score missingness (70.1\%) likely reflects properties in areas without public transit infrastructure, for which zero is a reasonable value. Climate risk factors (flood, fire, heat, wind, air) are entirely missing and excluded from the analysis. | |
| 116 | +\end{tablenotes} | |
| 117 | +\end{threeparttable} | |
| 118 | +\end{table} | |
| 119 | + | |
| 120 | +\subsection{Data Quality Audit} | |
| 121 | +\label{sec:data_audit} | |
| 122 | + | |
| 123 | +We conduct three data quality checks to assess the sensitivity of key results to imputation and outliers. | |
| 124 | + | |
| 125 | +\textbf{Imputation prevalence.} Two variables---year built (19.4\% missing) and lot size (16.3\% missing)---account for the bulk of imputed values. To assess the consequences, we re-estimate the baseline OLS model on subsamples that exclude imputed observations (Section~\ref{sec:imputation_sensitivity}). The lot-size coefficient is the most sensitive: $\ln(\text{lot})$ triples from 0.018 to 0.059 when imputed lot sizes are dropped, suggesting that state-median imputation attenuates the lot-size gradient substantially. | |
| 126 | + | |
| 127 | +\textbf{Outlier prevalence.} We winsorize lot size at its 99.5th percentile and cap Bike Score at 100, then re-estimate OLS (Section~\ref{sec:winsorization}). The overall $R^2$ increases modestly from 0.634 to 0.640, and most coefficients are stable, with the exception of $\ln(\text{lot})$, which again increases substantially. | |
| 128 | + | |
| 129 | +\textbf{Geographic coverage.} The sample spans 886 unique three-digit ZIP code prefixes (ZIP3 codes) across 51 jurisdictions (50 states plus DC). Southern states account for 56.6\% of observations. We document the marginal explanatory power of progressively finer geographic controls in Section~\ref{sec:ols_fe}. | |
| 130 | + | |
| 131 | +\subsection{Descriptive Statistics} | |
| 132 | + | |
| 133 | +Table~\ref{tab:descriptive} presents summary statistics. | |
| 134 | + | |
| 135 | +\begin{table}[H] | |
| 136 | +\centering | |
| 137 | +\caption{Descriptive Statistics ($N = 788{,}842$)} | |
| 138 | +\label{tab:descriptive} | |
| 139 | +\begin{threeparttable} | |
| 140 | +\small | |
| 141 | +\begin{tabular}{lrrrrrr} | |
| 142 | +\toprule | |
| 143 | +Variable & Mean & Std.\ Dev. & Min & Median & Max \\ | |
| 144 | +\midrule | |
| 145 | +\multicolumn{6}{l}{\textit{Continuous Variables}} \\ | |
| 146 | +Listing Price (\$) & 621,737 & 817,965 & 10,300 & 399,900 & 9,999,999 \\ | |
| 147 | +Living Area (sqft) & 2,093 & 1,181 & 208 & 1,820 & 19,600 \\ | |
| 148 | +Bedrooms & 3.25 & 1.09 & 1 & 3 & 10 \\ | |
| 149 | +Bathrooms & 2.52 & 1.16 & 1 & 2 & 10 \\ | |
| 150 | +Property Age (years) & 42.2 & 31.5 & 0 & 36 & 200 \\ | |
| 151 | +Walk Score (0--100) & 25.6 & 26.5 & 0 & 16 & 100 \\ | |
| 152 | +Bike Score & 34.9 & 18.1 & 1 & 31 & 248 \\ | |
| 153 | +Transit Score (0--100) & 8.9 & 18.3 & 0 & 0 & 100 \\ | |
| 154 | +Avg.\ School Rating (1--10) & 6.02 & 0.90 & 1 & 6 & 10 \\ | |
| 155 | +Property Tax Rate (\%) & 1.10 & 0.53 & 0 & 1.0 & 4.0 \\ | |
| 156 | +Luxury Score (0--7) & 1.36 & 1.29 & 0 & 1 & 7 \\ | |
| 157 | +\midrule | |
| 158 | +\multicolumn{6}{l}{\textit{Binary Variables (prevalence)}} \\ | |
| 159 | +Swimming Pool & 39.8\% & & & & \\ | |
| 160 | +Garage & 67.2\% & & & & \\ | |
| 161 | +Fireplace & 39.6\% & & & & \\ | |
| 162 | +Basement & 24.3\% & & & & \\ | |
| 163 | +Waterfront & 8.9\% & & & & \\ | |
| 164 | +HOA & 37.1\% & & & & \\ | |
| 165 | +Central Air & 68.1\% & & & & \\ | |
| 166 | +Hardwood Floors & 26.7\% & & & & \\ | |
| 167 | +Condominium & 14.6\% & & & & \\ | |
| 168 | +New Construction & 5.9\% & & & & \\ | |
| 169 | +Foreclosure & 0.5\% & & & & \\ | |
| 170 | +\midrule | |
| 171 | +\multicolumn{6}{l}{\textit{Regional Distribution}} \\ | |
| 172 | +South & 56.6\% & & & & \\ | |
| 173 | +West & 18.9\% & & & & \\ | |
| 174 | +Midwest & 16.1\% & & & & \\ | |
| 175 | +Northeast & 8.4\% & & & & \\ | |
| 176 | +\bottomrule | |
| 177 | +\end{tabular} | |
| 178 | +\begin{tablenotes} | |
| 179 | +\small | |
| 180 | +\item \textit{Notes:} Listing prices, not transaction prices. Property age computed as $2026 - \text{year\_built}$, after imputation. Imputed values included in summary statistics. | |
| 181 | +\end{tablenotes} | |
| 182 | +\end{threeparttable} | |
| 183 | +\end{table} | |
| 184 | + | |
| 185 | +The median listing price is \$399,900, with substantial right-skewness (mean \$621,737). The log transformation substantially improves distributional symmetry (Figure~\ref{fig:price_dist}), supporting the semi-log specification. | |
| 186 | + | |
| 187 | +\begin{figure}[H] | |
| 188 | + \centering | |
| 189 | + \includegraphics[width=\textwidth]{figures/fig1_price_distribution.png} | |
| 190 | + \caption{Distribution of Listing Prices: (a) Levels; (b) Natural Logarithm. The log transformation reduces right skewness and approximates normality.} | |
| 191 | + \label{fig:price_dist} | |
| 192 | +\end{figure} | |
added
paper/sections/discussion.tex
+46 −0
@@ -0,0 +1,46 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 7. DISCUSSION | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Discussion} | |
| 7 | +\label{sec:discussion} | |
| 8 | + | |
| 9 | +The results support four main messages, each associated with a specific modeling framework. | |
| 10 | + | |
| 11 | +\subsection{Message 1: OLS Remains Useful for Interpretable Associations} | |
| 12 | + | |
| 13 | +The semi-log OLS model explains 63.4\% of the variation in log listing prices using 62 regressors, comparable to the typical hedonic $R^2$ in the literature \citep{sirmans2005composition}. Its primary value lies in interpretability: the coefficients provide conditional mean associations in a transparent functional form. Regularized linear models (Ridge, Lasso, Elastic Net) achieve essentially identical performance ($R^2 = 0.628$--$0.630$), confirming that the OLS specification is not overfit and that the 20-percentage-point gap relative to XGBoost reflects genuine non-linearities and interactions, not parametric overfitting. | |
| 14 | + | |
| 15 | +The progression from region FE ($R^2 = 0.634$) to state FE ($0.678$) to ZIP3 FE ($0.725$) reveals that a large portion of the unexplained variation is spatial in nature. The ZIP3 FE model closes roughly half the gap between baseline OLS and XGBoost, suggesting that fine-grained geographic controls---rather than non-linear functional forms---account for much of the ML advantage. However, ZIP3 FE introduces 886 parameters and sacrifices the parsimony that makes OLS attractive for economic interpretation. | |
| 16 | + | |
| 17 | +The imputation sensitivity analysis reveals that the baseline lot-size elasticity (0.02) is substantially attenuated by state-median imputation; the complete-case estimate (0.06) is more plausible but applies to a selected subsample. This finding underscores the importance of data quality audits in hedonic research. | |
| 18 | + | |
| 19 | +\subsection{Message 2: Quantile Regression Adds Distributional Insight} | |
| 20 | + | |
| 21 | +Quantile regression reveals that OLS coefficients mask substantial distributional heterogeneity. The garage gradient ratio (10.3:1 between $\tau = 0.10$ and $\tau = 0.90$) and the pool gradient reversal (insignificant at lower quantiles, strongly positive at upper quantiles) demonstrate that the same attribute can play fundamentally different roles across market segments. Inter-quantile Wald tests confirm that these differences are statistically significant for 11 of 13 tested variables, with garage ($z = 28.92$) being the most significantly heterogeneous. | |
| 22 | + | |
| 23 | +This has implications for housing policy: interventions that improve basic amenities (garages, heating systems) may generate the largest listing-price differentials at the lower end, while luxury amenity provision primarily differentiates upper-market properties. | |
| 24 | + | |
| 25 | +The subsample stability analysis shows that most quantile coefficients are highly robust (CV $< 8\%$), but age stands out as unstable (CV = 78.6\%, sign stability = 80\%), reflecting genuine geographic heterogeneity in how property age relates to listing prices. | |
| 26 | + | |
| 27 | +\subsection{Message 3: ML Performance Depends Critically on Validation Design} | |
| 28 | + | |
| 29 | +The XGBoost model achieves substantially better predictions than OLS under random validation ($R^2 = 0.833$ vs.\ $0.630$), capturing non-linearities and interactions that the parametric specification cannot. However, the geographic holdout results provide an important corrective: under state-level holdout, XGBoost achieves only $R^2 = 0.425$--$0.547$ depending on specification. | |
| 30 | + | |
| 31 | +The ablation analysis pinpoints what drives the ML gain. Neighborhood features contribute $+17.6$ pp in random $R^2$---the largest single gain---but only $+3.5$ pp in geographic holdout $R^2$. This disparity suggests that neighborhood variables (school ratings, Walk Score, etc.) are highly informative within observed markets but carry location-specific scale and meaning that may not transfer. The full model with region dummies actually \emph{reduces} geographic holdout $R^2$ by 7.6 pp relative to the market-status-only specification. | |
| 32 | + | |
| 33 | +The most striking finding is that removing all geographic features \emph{improves} geographic holdout $R^2$ from 0.425 to 0.519. This is the opposite of what intuition might suggest and has direct implications for AVM deployment: in contexts requiring geographic generalization, simpler models without explicit geography may outperform richer ones. | |
| 34 | + | |
| 35 | +\subsection{Message 4: SHAP Interprets Prediction, Not Economics} | |
| 36 | + | |
| 37 | +SHAP values provide useful prediction-level decompositions that identify which features drive the XGBoost model's predictions for individual properties. The cross-model stability analysis ($\rho = 0.89$--$0.99$ across three tree models) addresses the common criticism that SHAP rankings are model-specific: while individual SHAP values differ, the ranking of feature importance is remarkably consistent, with the same six features dominating all three models. | |
| 38 | + | |
| 39 | +However, SHAP values should not be equated with hedonic listing-price gradients: | |
| 40 | +\begin{itemize}[nosep] | |
| 41 | + \item SHAP values decompose predictions, not the data-generating process. A feature with a large SHAP value may be predictively important because it proxies for unobserved variables, not because it has a large association with listing prices in the structural sense. | |
| 42 | + \item Feature correlation distributes SHAP contributions among correlated features in ways that may not reflect economic importance. | |
| 43 | + \item SHAP importance rankings differ from OLS coefficient rankings (e.g., school rating ranks 3rd by SHAP but has a counterintuitive negative OLS sign), reflecting the different questions each framework answers. | |
| 44 | +\end{itemize} | |
| 45 | + | |
| 46 | +In the terminology of \citet{mullainathan2017machine}, SHAP is a tool for interpreting predictions. The hedonic gradient $\partial P / \partial z_k$ is a tool for understanding market structure. These serve different purposes and should not be conflated. | |
added
paper/sections/introduction.tex
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 1. INTRODUCTION | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Introduction} | |
| 7 | +\label{sec:introduction} | |
| 8 | + | |
| 9 | +Housing is the dominant asset class in the typical American household's portfolio and a central object of study in urban economics, household finance, and public policy. The hedonic pricing framework, formalized by \citet{rosen1974hedonic} and rooted in the consumer theory of \citet{lancaster1966new}, provides the canonical approach to decomposing observed housing prices into the implicit valuations of constituent characteristics. Despite nearly five decades of applied hedonic research, three limitations persist. | |
| 10 | + | |
| 11 | +First, the standard OLS hedonic model imposes a linear relationship between attributes and log-prices, an assumption that may poorly approximate the complex, non-linear, and interactive relationships governing housing markets. Second, by estimating conditional mean effects, OLS constrains implicit prices to be constant across the price distribution---a restriction that quantile regression studies have shown to be empirically untenable \citep{zietz2008determinants, liao2012hedonic}. Third, while machine learning methods have demonstrated substantial predictive gains in property valuation, their ``black box'' nature has limited adoption in settings where economic interpretation matters \citep{mullainathan2017machine}. | |
| 12 | + | |
| 13 | +This paper compares three approaches to hedonic valuation on a single large dataset: | |
| 14 | +\begin{enumerate}[nosep] | |
| 15 | + \item \textbf{Semi-log OLS} with HC3 robust standard errors for interpretable conditional mean associations; | |
| 16 | + \item \textbf{Quantile regression} at five quantiles for distribution-specific listing-price gradients; | |
| 17 | + \item \textbf{Gradient-boosted models} (XGBoost, LightGBM) with SHAP-based interpretation for non-linear prediction. | |
| 18 | +\end{enumerate} | |
| 19 | + | |
| 20 | +The dataset comprises 788,842 residential properties listed for sale on Zillow across all 50 U.S.\ states and the District of Columbia. We specify 62 regressors capturing structural attributes, lot characteristics, amenities, neighborhood quality, market status, and six strategically designed interaction terms. | |
| 21 | + | |
| 22 | +A core methodological contribution of this paper is the systematic examination of \textbf{spatial leakage} in hedonic model evaluation. Standard random train-test splits allow geographically proximate properties---which share unobserved local price determinants---to appear in both training and test sets, artificially inflating out-of-sample performance metrics. We document the consequences through three complementary analyses: (i) a geographic holdout design in which 10 entire states are withheld from training; (ii) an ablation study that traces the XGBoost predictive gain to specific feature groups and reveals that region dummies \emph{harm} geographic generalization; and (iii) an experiment adding raw latitude and longitude coordinates, which boosts random $R^2$ by 3.7 percentage points but \emph{reduces} geographic holdout $R^2$ by 5.5 percentage points. These findings demonstrate that the gap between random and geographic validation is not merely a matter of degree but reflects qualitatively different assessments of model capability. | |
| 23 | + | |
| 24 | +Our contribution is methodological and empirical, not causal. The analysis is not designed to estimate structural willingness-to-pay parameters. Rather, it compares how different modeling frameworks summarize and predict listing-price variation, and it documents the gains and losses from moving along the interpretability-prediction frontier. We emphasize four findings: | |
| 25 | + | |
| 26 | +\begin{enumerate}[nosep] | |
| 27 | + \item OLS with progressively finer geographic fixed effects (region $\rightarrow$ state $\rightarrow$ ZIP3) traces how spatial granularity drives explanatory power from $R^2 = 0.634$ to $0.725$. | |
| 28 | + \item Quantile regression documents monotonic variation in several attribute gradients; inter-quantile Wald tests reject coefficient equality for 11 of 13 variables between the 10th and 90th percentiles. | |
| 29 | + \item XGBoost achieves $R^2 = 0.833$ under random validation but only $R^2 = 0.425$ under geographic holdout; removing geographic features \emph{improves} geographic holdout to $R^2 = 0.519$. | |
| 30 | + \item SHAP importance rankings are stable across three tree-based models (Spearman $\rho > 0.89$), lending credibility to the ranking even though individual SHAP values remain model-specific. | |
| 31 | +\end{enumerate} | |
| 32 | + | |
| 33 | +The remainder of this paper is organized as follows. Section~\ref{sec:literature} reviews the literature. Section~\ref{sec:data} describes the data, sample construction, and variable coding. Section~\ref{sec:methodology} presents the methodology. Section~\ref{sec:results} reports estimation results. Section~\ref{sec:robustness} presents robustness checks. Section~\ref{sec:discussion} discusses implications. Section~\ref{sec:limitations} enumerates limitations. Section~\ref{sec:conclusion} concludes. | |
added
paper/sections/limitations.tex
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 8. LIMITATIONS | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Limitations} | |
| 7 | +\label{sec:limitations} | |
| 8 | + | |
| 9 | +We summarize the principal limitations in a structured format. | |
| 10 | + | |
| 11 | +\begin{enumerate}[nosep] | |
| 12 | + \item \textbf{Listing prices, not transaction prices.} The dependent variable is the asking price, which may differ from the realized sale price due to strategic pricing, negotiation, and market conditions. Listing-price gradients are not necessarily equivalent to transaction-price implicit prices. | |
| 13 | + | |
| 14 | + \item \textbf{No causal identification.} All estimates are conditional associations. Without exogenous variation, we cannot distinguish the causal effects of attributes from sorting, supply constraints, and omitted variables \citep{ekeland2004identification, roberts2013endogeneity}. | |
| 15 | + | |
| 16 | + \item \textbf{Spatial dependence not modeled.} Moran's $I = 0.27$ confirms substantial spatial autocorrelation in OLS residuals. The paper does not estimate spatial lag, spatial error, or geographically weighted regression models, which may affect both efficiency and consistency of estimates. | |
| 17 | + | |
| 18 | + \item \textbf{Random validation overstates ML performance.} The random train-test split allows spatial leakage. Under geographic holdout, XGBoost $R^2$ drops from 0.833 to 0.425--0.547 depending on specification. | |
| 19 | + | |
| 20 | + \item \textbf{Standardization complicates interpretation.} The standardized OLS coefficients are not directly interpretable as elasticities or semi-elasticities. We provide an unstandardized specification for economic interpretation but note that some readers may conflate the two. | |
| 21 | + | |
| 22 | + \item \textbf{Missing data and imputation.} Year built (19.4\%), lot size (16.3\%), and transit score (70.1\%) have substantial missingness. State-median imputation preserves geographic variation but attenuates the lot-size coefficient by a factor of three. | |
| 23 | + | |
| 24 | + \item \textbf{Climate risk variables entirely missing.} Despite their growing importance, flood, fire, heat, wind, and air risk factors could not be incorporated \citep{baldauf2020does}. | |
| 25 | + | |
| 26 | + \item \textbf{SHAP values are not structural implicit prices.} SHAP provides prediction decompositions, not marginal willingness-to-pay estimates. Cross-model stability supports the ranking but not the economic interpretation. | |
| 27 | + | |
| 28 | + \item \textbf{Quantile regression subsample.} Quantile regression is estimated on 150,000 observations (19\% of the full sample) for computational reasons. Most coefficients are stable (CV $< 8\%$) but age is not (CV = 78.6\%). | |
| 29 | + | |
| 30 | + \item \textbf{Non-representative sample.} The Zillow listing dataset overrepresents Southern states, active listings, and possibly certain property types. Results may not generalize to the full U.S.\ housing stock. | |
| 31 | + | |
| 32 | + \item \textbf{Data quality concerns.} Bike Score exceeds 100 for 38 observations; some lot sizes are implausibly large; parking-space data are noisy. These affect a small fraction of observations but may influence tail estimates. | |
| 33 | + | |
| 34 | + \item \textbf{No hyperparameter tuning via cross-validation.} XGBoost and LightGBM hyperparameters were set based on common defaults rather than optimized via nested cross-validation. Performance could potentially improve with systematic tuning. | |
| 35 | + | |
| 36 | + \item \textbf{Geographic holdout design is conservative.} Holding out 10 entire states is a stringent test; finer geographic blocking (county-level or MSA-level) might yield intermediate performance estimates that are more relevant for some AVM applications. | |
| 37 | + | |
| 38 | + \item \textbf{Single cross-section.} The data represent a snapshot of active listings at one point in time. Temporal variation in hedonic gradients---due to market cycles, interest rate changes, or policy shifts---cannot be examined. | |
| 39 | +\end{enumerate} | |
added
paper/sections/literature.tex
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 2. LITERATURE REVIEW | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Literature Review} | |
| 7 | +\label{sec:literature} | |
| 8 | + | |
| 9 | +\subsection{Hedonic Pricing and the Interpretation of Implicit Prices} | |
| 10 | + | |
| 11 | +The theoretical foundations of hedonic pricing were established by \citet{lancaster1966new} and \citet{rosen1974hedonic}. In Rosen's framework, the market price $P$ of a differentiated good is a function of its characteristics $\mathbf{z}$, and the partial derivative $\partial P / \partial z_k$ yields the implicit price of attribute $z_k$. \citet{sirmans2005composition} provide a meta-analysis of 125 hedonic housing studies, documenting consistent positive associations for living area, bathrooms, and garages, and negative associations for property age. | |
| 12 | + | |
| 13 | +An important but often underappreciated distinction is that hedonic price gradients estimated from cross-sectional regressions are conditional associations, not structural demand parameters. \citet{rosen1974hedonic} himself emphasized that identifying demand and supply functions from the hedonic price schedule requires a second stage with instruments---a requirement rarely satisfied in applied work \citep{ekeland2004identification}. This paper follows the conventional first-stage hedonic approach and interprets coefficients as conditional associations rather than causal willingness-to-pay estimates. | |
| 14 | + | |
| 15 | +\subsection{Functional Form, Omitted Variables, and Spatial Dependence} | |
| 16 | + | |
| 17 | +The semi-log specification has become the standard functional form following the Monte Carlo evidence of \citet{cropper1988choice}, who showed it outperforms more complex forms under attribute omission. Nonetheless, non-linearities remain a concern. Researchers have explored Box-Cox transformations \citep{halvorsen1981choice}, semiparametric specifications \citep{anglin1996semiparametric}, and generalized additive models. | |
| 18 | + | |
| 19 | +Spatial dependence poses a fundamental challenge. \citet{can1992specification} demonstrated that ignoring spatial autocorrelation biases hedonic estimates. \citet{lesage2009introduction} formalized the spatial lag, spatial error, and spatial Durbin specifications. The failure to model spatial dependence---which we document in this paper through a Moran's $I$ test---remains a significant limitation of many hedonic studies, including ours. | |
| 20 | + | |
| 21 | +\subsection{Quantile Regression and Distributional Heterogeneity} | |
| 22 | + | |
| 23 | +\citet{zietz2008determinants} pioneered quantile regression in hedonic housing models, showing that implicit prices vary across the conditional price distribution. \citet{liao2012hedonic} documented similar heterogeneity in Australian data, and \citet{mak2010quantile} showed that the view premium in Hong Kong is substantially larger at upper quantiles. These findings motivate our use of quantile regression to examine whether listing-price gradients differ between lower-end and higher-end properties. | |
| 24 | + | |
| 25 | +\subsection{Machine Learning in Automated Valuation Models} | |
| 26 | + | |
| 27 | +\citet{mullainathan2017machine} distinguish between prediction and inference tasks in economics, arguing that machine learning is most naturally suited to prediction. In real estate, \citet{bourassa2019machine} compare tree-based methods to hedonic models, finding 15--25\% reductions in prediction error. \citet{kok2017big} demonstrate the value of large-scale data in property valuation. The key tension is between predictive accuracy and economic interpretability: machine learning models can capture complex non-linearities and interactions, but their coefficients lack the direct economic interpretation of parametric hedonic models. | |
| 28 | + | |
| 29 | +\subsection{Explainable AI and SHAP in Economic Applications} | |
| 30 | + | |
| 31 | +SHAP values \citep{lundberg2017unified}, based on Shapley values from cooperative game theory \citep{shapley1953value}, provide a principled framework for interpreting complex model predictions. TreeSHAP \citep{lundberg2020local} enables efficient computation for tree-based models. However, SHAP values are prediction-level decompositions, not causal estimates. They are model-specific, sensitive to feature correlation, and do not satisfy the conditions required for interpreting them as marginal willingness-to-pay \citep{chen2020housing}. We use SHAP values to describe the predictive structure of our XGBoost model while maintaining this distinction. | |
| 32 | + | |
| 33 | +\subsection{Spatial Validation and Leakage} | |
| 34 | + | |
| 35 | +A growing literature emphasizes that standard random train-test splits can overstate model performance in spatially structured data. \citet{roberts2017cross} demonstrate that spatial and temporal blocking in cross-validation produces substantially lower---but more realistic---performance estimates in ecological models, and this insight applies directly to hedonic pricing. \citet{meyer2019importance} show that spatial validation is critical when predictors exhibit spatial autocorrelation, as in housing data with geographic controls. Our geographic holdout and ablation designs contribute to this emerging literature by documenting how geographic features simultaneously boost random-split performance and degrade geographic generalization. | |
| 36 | + | |
| 37 | +\subsection{Research Gap} | |
| 38 | + | |
| 39 | +Few papers compare OLS, quantile regression, and gradient-boosted models on the same large-scale dataset while carefully distinguishing prediction from inference. Machine learning papers in real estate often focus on accuracy metrics without addressing economic interpretation; hedonic papers often focus on coefficient interpretation without assessing predictive performance. Moreover, the interaction between geographic feature inclusion and validation design has received insufficient attention. This paper bridges both literatures, documenting what each framework reveals---and what it cannot---while providing systematic evidence on spatial leakage and feature ablation. | |
added
paper/sections/methodology.tex
+130 −0
@@ -0,0 +1,130 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 4. METHODOLOGY | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Econometric and Machine Learning Methodology} | |
| 7 | +\label{sec:methodology} | |
| 8 | + | |
| 9 | +\subsection{Identification, Prediction, and Interpretation} | |
| 10 | +\label{sec:identification} | |
| 11 | + | |
| 12 | +Before presenting the estimation methods, we clarify the interpretive framework. | |
| 13 | + | |
| 14 | +None of the models estimated in this paper identify causal willingness-to-pay parameters. OLS coefficients are conditional mean associations: they describe the average difference in log listing price between properties that differ by one unit in attribute $x_k$, holding other included attributes constant. Quantile regression coefficients are quantile-specific conditional associations. SHAP values are model-specific prediction decompositions. Table~\ref{tab:interpretation} summarizes these distinctions. | |
| 15 | + | |
| 16 | +\begin{table}[H] | |
| 17 | +\centering | |
| 18 | +\caption{Interpretation of Estimates Across Modeling Frameworks} | |
| 19 | +\label{tab:interpretation} | |
| 20 | +\small | |
| 21 | +\begin{tabular}{lp{3.2cm}p{3.2cm}p{3.5cm}} | |
| 22 | +\toprule | |
| 23 | +& OLS Coefficient & Quantile Coefficient & SHAP Value \\ | |
| 24 | +\midrule | |
| 25 | +Object & Cond.\ mean association & Cond.\ quantile association & Prediction contribution \\ | |
| 26 | +Interpretation & Semi-elasticity (if unstandardized log-log) & Quantile-specific semi-elasticity & Model-specific contribution \\ | |
| 27 | +Causal? & No, unless identified & No, unless identified & No \\ | |
| 28 | +Structural WTP? & Only under strong assumptions & Only under strong assumptions & No \\ | |
| 29 | +Primary use & Inference & Distributional heterogeneity & Prediction interpretation \\ | |
| 30 | +\bottomrule | |
| 31 | +\end{tabular} | |
| 32 | +\end{table} | |
| 33 | + | |
| 34 | +Cross-sectional data cannot separate preferences, supply constraints, and sorting. Regional and local omitted variables---including unobserved neighborhood quality, local supply conditions, and regulatory environment---remain major concerns. Throughout the paper, we interpret the estimates as \textbf{listing-price capitalization gradients} rather than structural demand parameters. | |
| 35 | + | |
| 36 | +\subsection{Semi-Log Hedonic Specification (OLS)} | |
| 37 | + | |
| 38 | +Our baseline model follows the semi-log specification: | |
| 39 | +\begin{equation} | |
| 40 | + \ln(P_i) = \alpha + \sum_{k=1}^{K} \beta_k x_{ik} + \sum_{j=1}^{J} \gamma_j d_{ij} + \sum_{m=1}^{M} \delta_m (x_{im_1} \cdot x_{im_2}) + \varepsilon_i | |
| 41 | + \label{eq:ols} | |
| 42 | +\end{equation} | |
| 43 | +where $P_i$ is the listing price, $x_{ik}$ are continuous and binary attributes, $d_{ij}$ are categorical dummies, and $x_{im_1} \cdot x_{im_2}$ are interaction terms. | |
| 44 | + | |
| 45 | +\textbf{Coefficient interpretation under standardization.} All continuous regressors are standardized (zero mean, unit variance) prior to estimation. This facilitates coefficient magnitude comparison across variables with different scales but changes the interpretation: the coefficient on a standardized variable represents the association between a one-standard-deviation increase in the attribute and log-price, not a one-unit increase. In particular, the coefficient on standardized $\ln(\text{sqft})$ is \textit{not} a price-to-area elasticity---it is the effect of a one-standard-deviation increase in log living area. To recover economic magnitudes in natural units, we also report an unstandardized specification. | |
| 46 | + | |
| 47 | +Binary (0/1) and dummy variables are not standardized. For these, the percentage listing-price effect is $(e^{\hat{\beta}_k} - 1) \times 100\%$. | |
| 48 | + | |
| 49 | +Standard errors are computed using the HC3 estimator \citep{mackinnon1985some}. | |
| 50 | + | |
| 51 | +\subsection{Quantile Regression} | |
| 52 | +\label{sec:qr_methodology} | |
| 53 | + | |
| 54 | +Quantile regression \citep{koenker1978regression} models the $\tau$-th conditional quantile: | |
| 55 | +\begin{equation} | |
| 56 | + Q_{\tau}(\ln P_i | \mathbf{x}_i) = \mathbf{x}_i' \boldsymbol{\beta}(\tau), \quad \tau \in (0, 1) | |
| 57 | +\end{equation} | |
| 58 | +estimated by minimizing $\sum_{i} \rho_{\tau}(\ln P_i - \mathbf{x}_i' \boldsymbol{\beta})$, where $\rho_{\tau}(u) = u(\tau - \mathbb{1}(u < 0))$. | |
| 59 | + | |
| 60 | +We estimate the model at $\tau \in \{0.10, 0.25, 0.50, 0.75, 0.90\}$. For computational tractability, estimation uses a random subsample of 150,000 observations. We verify stability by repeating estimation across ten random subsamples with different seeds, finding that most coefficients exhibit coefficient-of-variation below 8\% (Section~\ref{sec:qr_stability}). | |
| 61 | + | |
| 62 | +\textbf{Inter-quantile Wald tests.} To formally test whether listing-price gradients differ between the lower and upper tails of the conditional distribution, we compute inter-quantile differences $\hat{\beta}_k(0.90) - \hat{\beta}_k(0.10)$ and test whether this difference is significantly different from zero using a $z$-test based on the covariance structure of the quantile regression estimates. Under the null hypothesis $H_0: \beta_k(0.90) = \beta_k(0.10)$, the test statistic is: | |
| 63 | +\begin{equation} | |
| 64 | + z_k = \frac{\hat{\beta}_k(0.90) - \hat{\beta}_k(0.10)}{\sqrt{\text{Var}[\hat{\beta}_k(0.90)] + \text{Var}[\hat{\beta}_k(0.10)] - 2\,\text{Cov}[\hat{\beta}_k(0.90), \hat{\beta}_k(0.10)]}} | |
| 65 | + \label{eq:iqr_test} | |
| 66 | +\end{equation} | |
| 67 | +Rejection indicates that the attribute's association with listing prices differs significantly between lower-priced and higher-priced properties (conditional on covariates). | |
| 68 | + | |
| 69 | +\subsection{Gradient-Boosted Ensemble Models} | |
| 70 | + | |
| 71 | +\subsubsection{XGBoost} | |
| 72 | + | |
| 73 | +XGBoost \citep{chen2016xgboost} fits an additive ensemble of regression trees by sequentially minimizing a regularized loss function: | |
| 74 | +\begin{equation} | |
| 75 | + \mathcal{L}^{(t)} = \sum_{i=1}^{n} l(y_i, \hat{y}_i^{(t-1)} + f_t(\mathbf{x}_i)) + \Omega(f_t) | |
| 76 | +\end{equation} | |
| 77 | +where $\Omega(f_t) = \gamma T + \frac{1}{2}\lambda \|w\|^2 + \alpha \|w\|_1$ penalizes tree complexity. We use 1,000 trees, depth 8, learning rate 0.05, column and row subsampling ratios of 0.8, and early stopping after 50 rounds without improvement. | |
| 78 | + | |
| 79 | +\subsubsection{LightGBM} | |
| 80 | + | |
| 81 | +LightGBM \citep{ke2017lightgbm} uses Gradient-based One-Side Sampling (GOSS) and Exclusive Feature Bundling (EFB) for computational efficiency. We use matching hyperparameters for comparability. | |
| 82 | + | |
| 83 | +\subsubsection{Additional Benchmarks} | |
| 84 | + | |
| 85 | +We also report results for Ridge regression ($\alpha = 1$), Lasso ($\alpha = 0.001$), Elastic Net, Random Forest (500 trees, depth 20), and OLS with state fixed effects. | |
| 86 | + | |
| 87 | +\textbf{Ablation design.} To understand which feature groups drive the ML predictive gain over OLS, we estimate XGBoost models using progressively richer feature sets: (i) structural attributes only (8 features); (ii) $+$ lot characteristics (9); (iii) $+$ amenity indicators (20); (iv) $+$ neighborhood variables (27); (v) $+$ market status (32); and (vi) the full model including interactions, categorical controls, and region dummies (62). At each stage, we evaluate both random and geographic holdout $R^2$ to identify which feature groups contribute to genuine predictive generalization versus spatial memorization. | |
| 88 | + | |
| 89 | +\subsection{Validation Designs} | |
| 90 | +\label{sec:validation_designs} | |
| 91 | + | |
| 92 | +We employ three validation strategies: | |
| 93 | + | |
| 94 | +\begin{enumerate}[nosep] | |
| 95 | + \item \textbf{Random 80/20 split} (seed = 42): 631,073 training, 157,769 test. This is the standard approach but permits spatial leakage---nearby properties from the same neighborhood can appear in both train and test sets. | |
| 96 | + \item \textbf{Geographic holdout}: 10 states (CA, NY, TX, FL, OH, CO, NC, WA, IL, GA) are held out entirely; models are trained on the remaining 40 states + DC. This tests generalization to markets not seen during training. The held-out states represent the five largest (by sample size) plus five geographically diverse states, totaling 438,315 test observations. | |
| 97 | + \item \textbf{Ablation cascade}: XGBoost models are estimated at six stages of feature inclusion under both random and geographic validation, isolating the marginal contribution of each feature group. | |
| 98 | +\end{enumerate} | |
| 99 | + | |
| 100 | +The gap between random and geographic validation quantifies the extent to which models exploit local spatial structure rather than learning generalizable attribute-price relationships. | |
| 101 | + | |
| 102 | +\subsection{SHAP-Based Model Interpretation and Cross-Model Stability} | |
| 103 | +\label{sec:shap_methodology} | |
| 104 | + | |
| 105 | +SHAP values \citep{lundberg2017unified} decompose each prediction into additive feature contributions: | |
| 106 | +\begin{equation} | |
| 107 | + f(\mathbf{x}) = E[f(\mathbf{X})] + \sum_{k=1}^{K} \phi_k(\mathbf{x}) | |
| 108 | +\end{equation} | |
| 109 | +We compute TreeSHAP values on a random subsample of 10,000 test observations. | |
| 110 | + | |
| 111 | +\textbf{Cross-model stability.} A common criticism of SHAP-based interpretation is that feature importance rankings may be model-specific. To assess this concern, we compute mean absolute SHAP values for three tree-based models (XGBoost, LightGBM, Random Forest) and report the Spearman rank correlation between each pair. High cross-model correlation would support the use of SHAP rankings as a description of the predictive structure of the data, not merely an artifact of a particular model. | |
| 112 | + | |
| 113 | +\textbf{Interpretive caution.} SHAP values are prediction-level contributions, not structural implicit prices. They are: | |
| 114 | +\begin{itemize}[nosep] | |
| 115 | + \item model-dependent (XGBoost SHAP values differ from LightGBM SHAP values); | |
| 116 | + \item affected by feature correlation (correlated features may share SHAP contributions); | |
| 117 | + \item not causal---they decompose predictions, not data-generating processes; | |
| 118 | + \item not equivalent to Rosen's hedonic price gradient $\partial P / \partial z_k$. | |
| 119 | +\end{itemize} | |
| 120 | +We use SHAP to describe which features contribute most to predictions, not to estimate marginal willingness to pay. | |
| 121 | + | |
| 122 | +\subsection{Spatial Autocorrelation Diagnostics} | |
| 123 | +\label{sec:moran_method} | |
| 124 | + | |
| 125 | +We compute Moran's $I$ on OLS residuals using a row-standardized KNN spatial weight matrix ($k = 8$) on random subsamples of 5,000 observations. To assess the stability of this diagnostic, we repeat the computation across three independent subsamples and report the mean, standard deviation, and significance of the resulting $I$ statistics. Moran's $I$ is defined as: | |
| 126 | +\begin{equation} | |
| 127 | + I = \frac{N}{\sum_{i}\sum_{j} w_{ij}} \cdot \frac{\sum_{i}\sum_{j} w_{ij}(e_i - \bar{e})(e_j - \bar{e})}{\sum_{i}(e_i - \bar{e})^2} | |
| 128 | + \label{eq:moran} | |
| 129 | +\end{equation} | |
| 130 | +where $e_i$ are the OLS residuals, $w_{ij}$ are the spatial weights, and $N$ is the subsample size. Under the null of no spatial autocorrelation, $I$ has expected value $-1/(N-1) \approx 0$ and an asymptotically normal distribution. Values significantly above zero indicate positive spatial autocorrelation (nearby residuals are similar), implying that the OLS specification fails to capture local price variation. | |
added
paper/sections/results.tex
+590 −0
@@ -0,0 +1,590 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 5. RESULTS | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Estimation Results} | |
| 7 | +\label{sec:results} | |
| 8 | + | |
| 9 | +\subsection{OLS Baseline, State Fixed Effects, and ZIP3 Fixed Effects} | |
| 10 | +\label{sec:ols_fe} | |
| 11 | + | |
| 12 | +\subsubsection{Standardized Baseline Specification} | |
| 13 | + | |
| 14 | +Table~\ref{tab:ols_results} presents the OLS results with standardized continuous regressors. The model achieves $R^2 = 0.634$ with RMSE $= 0.489$ in log-price units. The $F$-statistic of 18,712.5 strongly rejects the null of jointly zero slope coefficients. | |
| 15 | + | |
| 16 | +\begin{table}[H] | |
| 17 | +\centering | |
| 18 | +\caption{OLS Hedonic Regression Results (Dep.\ Var.: $\ln(\text{Listing Price})$)} | |
| 19 | +\label{tab:ols_results} | |
| 20 | +\begin{threeparttable} | |
| 21 | +\small | |
| 22 | +\begin{tabular}{lrrl} | |
| 23 | +\toprule | |
| 24 | +Variable & Coefficient & Std.\ Error & \\ | |
| 25 | +\midrule | |
| 26 | +\multicolumn{4}{l}{\textit{Panel A: Structural Attributes (standardized)}} \\ | |
| 27 | +$\ln$(Living Area)$^\dagger$ & 0.3119 & 0.0035 & *** \\ | |
| 28 | +Bedrooms$^\dagger$ & $-$0.0675 & 0.0023 & *** \\ | |
| 29 | +Bathrooms$^\dagger$ & 0.2359 & 0.0026 & *** \\ | |
| 30 | +Age$^\dagger$ & $-$0.0418 & 0.0139 & ** \\ | |
| 31 | +Age$^{2\dagger}$ & 0.0507 & 0.0019 & *** \\ | |
| 32 | +Stories$^\dagger$ & 0.0004 & 0.0003 & \\ | |
| 33 | +Bath/Bed Ratio$^\dagger$ & $-$0.0208 & 0.0021 & *** \\ | |
| 34 | +\midrule | |
| 35 | +\multicolumn{4}{l}{\textit{Panel B: Lot and Amenities}} \\ | |
| 36 | +$\ln$(Lot Size)$^\dagger$ & 0.0697 & 0.0010 & *** \\ | |
| 37 | +Pool & 0.0286 & 0.0026 & *** \\ | |
| 38 | +Spa & 0.0327 & 0.0027 & *** \\ | |
| 39 | +Basement & $-$0.0424 & 0.0023 & *** \\ | |
| 40 | +Garage & 0.1094 & 0.0015 & *** \\ | |
| 41 | +Waterfront & $-$0.1349 & 0.0325 & *** \\ | |
| 42 | +Central Air & 0.0693 & 0.0014 & *** \\ | |
| 43 | +Hardwood Floors & 0.1246 & 0.0014 & *** \\ | |
| 44 | +Luxury Score$^\dagger$ & 0.0443 & 0.0018 & *** \\ | |
| 45 | +\midrule | |
| 46 | +\multicolumn{4}{l}{\textit{Panel C: Neighborhood (standardized)}} \\ | |
| 47 | +Walk Score$^\dagger$ & $-$0.0109 & 0.0012 & *** \\ | |
| 48 | +Bike Score$^\dagger$ & 0.0717 & 0.0010 & *** \\ | |
| 49 | +Transit Score$^\dagger$ & 0.0524 & 0.0008 & *** \\ | |
| 50 | +Avg.\ School Rating$^\dagger$ & $-$0.0491 & 0.0007 & *** \\ | |
| 51 | +Property Tax Rate$^\dagger$ & $-$0.0292 & 0.0008 & *** \\ | |
| 52 | +\midrule | |
| 53 | +\multicolumn{4}{l}{\textit{Panel D: Market Status}} \\ | |
| 54 | +Condominium & $-$0.0370 & 0.0037 & *** \\ | |
| 55 | +HOA Member & $-$0.1077 & 0.0024 & *** \\ | |
| 56 | +$\ln$(HOA Fees + 1)$^\dagger$ & 0.0439 & 0.0014 & *** \\ | |
| 57 | +New Construction & 0.1035 & 0.0029 & *** \\ | |
| 58 | +Foreclosure & $-$0.4064 & 0.0098 & *** \\ | |
| 59 | +\midrule | |
| 60 | +\multicolumn{4}{l}{\textit{Panel E: Interactions (standardized)}} \\ | |
| 61 | +$\ln$(sqft) $\times$ Age$^\dagger$ & $-$0.1100 & 0.0140 & *** \\ | |
| 62 | +Pool $\times$ South & 0.0081 & 0.0012 & *** \\ | |
| 63 | +Waterfront $\times$ $\ln$(sqft)$^\dagger$ & 0.1371 & 0.0092 & *** \\ | |
| 64 | +Basement $\times$ North & $-$0.0255 & 0.0012 & *** \\ | |
| 65 | +Condo $\times$ Walk Score$^\dagger$ & 0.0816 & 0.0013 & *** \\ | |
| 66 | +Age $\times$ Luxury$^\dagger$ & 0.0440 & 0.0015 & *** \\ | |
| 67 | +\midrule | |
| 68 | +\multicolumn{4}{l}{\textit{Panel F: Regional Dummies (ref: Midwest)}} \\ | |
| 69 | +Northeast & 0.4869 & 0.0030 & *** \\ | |
| 70 | +South & 0.0220 & 0.0028 & *** \\ | |
| 71 | +West & 0.4008 & 0.0031 & *** \\ | |
| 72 | +\midrule | |
| 73 | +$R^2$ & 0.6343 & & \\ | |
| 74 | +Adj.\ $R^2$ & 0.6342 & & \\ | |
| 75 | +RMSE & 0.4887 & & \\ | |
| 76 | +$F$-statistic & 18,712.5 & & \\ | |
| 77 | +$N$ & 788,842 & & \\ | |
| 78 | +\bottomrule | |
| 79 | +\end{tabular} | |
| 80 | +\begin{tablenotes} | |
| 81 | +\small | |
| 82 | +\item \textit{Notes:} HC3 heteroskedasticity-robust standard errors. $^\dagger$Variables standardized (zero mean, unit variance) before estimation; coefficients represent effects of a one-standard-deviation increase. Binary variables not standardized; percentage effects computed as $100 \times (e^\beta - 1)$. Categorical controls (roof: 6, construction: 8, foundation: 7 dummies) included but not shown. Significance: *** $p<0.001$, ** $p<0.01$, * $p<0.05$. | |
| 83 | +\end{tablenotes} | |
| 84 | +\end{threeparttable} | |
| 85 | +\end{table} | |
| 86 | + | |
| 87 | +\subsubsection{Unstandardized Specification for Economic Interpretation} | |
| 88 | + | |
| 89 | +Table~\ref{tab:ols_unstd} reports key coefficients from the unstandardized specification, which permits direct economic interpretation. The $R^2$ is identical (0.634); only the coefficient magnitudes and interpretations differ. | |
| 90 | + | |
| 91 | +\begin{table}[H] | |
| 92 | +\centering | |
| 93 | +\caption{Unstandardized OLS: Key Coefficients in Natural Units} | |
| 94 | +\label{tab:ols_unstd} | |
| 95 | +\begin{threeparttable} | |
| 96 | +\small | |
| 97 | +\begin{tabular}{lrrrl} | |
| 98 | +\toprule | |
| 99 | +Variable & Coefficient & Std.\ Error & Interpretation & \\ | |
| 100 | +\midrule | |
| 101 | +Constant & 7.7316 & 0.0495 & & *** \\ | |
| 102 | +$\ln$(Living Area) & 0.6281 & 0.0070 & Elasticity: 0.63 & *** \\ | |
| 103 | +Bedrooms & $-$0.0620 & 0.0022 & $-$6.0\% per additional bedroom & *** \\ | |
| 104 | +Bathrooms & 0.2035 & 0.0023 & +22.6\% per additional bathroom & *** \\ | |
| 105 | +Age (years) & $-$0.0013 & 0.0004 & $-$0.13\% per year & ** \\ | |
| 106 | +Age$^2$ ($\times 10^{-5}$) & 1.2 & 0.05 & Positive quadratic (vintage) & *** \\ | |
| 107 | +$\ln$(Lot Size) & 0.0182 & 0.0003 & Elasticity: 0.02 & *** \\ | |
| 108 | +Walk Score (per point) & $-$0.0004 & 0.0000 & $-$0.04\% per point & *** \\ | |
| 109 | +Bike Score (per point) & 0.0040 & 0.0001 & +0.40\% per point & *** \\ | |
| 110 | +Transit Score (per point) & 0.0029 & 0.0000 & +0.29\% per point & *** \\ | |
| 111 | +Avg.\ School Rating (per point) & $-$0.0544 & 0.0008 & $-$5.3\% per rating point & *** \\ | |
| 112 | +Property Tax Rate (per ppt) & $-$0.0553 & 0.0015 & $-$5.4\% per ppt & *** \\ | |
| 113 | +Luxury Score (per unit) & 0.0344 & 0.0014 & +3.5\% per unit & *** \\ | |
| 114 | +\bottomrule | |
| 115 | +\end{tabular} | |
| 116 | +\begin{tablenotes} | |
| 117 | +\small | |
| 118 | +\item \textit{Notes:} Same sample and specification as Table~\ref{tab:ols_results}, but continuous variables enter in natural units. $N = 788{,}842$. HC3 standard errors. | |
| 119 | +\end{tablenotes} | |
| 120 | +\end{threeparttable} | |
| 121 | +\end{table} | |
| 122 | + | |
| 123 | +The elasticity of listing price with respect to living area is 0.63: a 10\% increase in square footage is associated with a 6.3\% higher listing price. This is below unity, consistent with diminishing marginal returns to space. Each additional bathroom is associated with a 22.6\% higher listing price, while each additional bedroom---conditional on total area---is associated with a 6.0\% \textit{lower} price, reflecting the well-documented tradeoff between room count and room size \citep{sirmans2005composition}. | |
| 124 | + | |
| 125 | +\subsubsection{Counterintuitive Coefficient Signs} | |
| 126 | + | |
| 127 | +Several coefficients warrant discussion. | |
| 128 | + | |
| 129 | +\textbf{Negative waterfront coefficient ($-0.135$, standardized).} The standalone waterfront effect is evaluated at the mean of the standardized interaction term $\text{waterfront} \times \ln(\text{sqft})$. Because the interaction is strongly positive ($+0.137$), the net waterfront effect becomes positive for larger properties. This artifact of the interacted specification does not imply that waterfront reduces price on average. | |
| 130 | + | |
| 131 | +\textbf{Negative school rating ($-0.054$ per rating point, unstandardized).} This likely reflects confounding with property tax rates and regional effects. In higher-tax jurisdictions, school quality is partially capitalized through the tax rate, which enters separately. Conditional on tax rate, region, and other controls, the residual school rating variation may capture unobserved factors that correlate negatively with prices. This coefficient should be interpreted with caution. | |
| 132 | + | |
| 133 | +\textbf{Negative Walk Score ($-0.0004$ per point, unstandardized).} Walk Score is highly correlated with Bike Score ($r \approx 0.56$) and Transit Score ($r \approx 0.62$). In the presence of all three accessibility measures, the Walk Score coefficient may reflect residual urban density effects---after controlling for biking and transit access, the remaining variation in walkability may capture denser, smaller-lot neighborhoods. | |
| 134 | + | |
| 135 | +\textbf{Negative basement ($-0.042$).} Basements are predominantly found in older properties in colder regions. Conditional on age, region, and size, the basement indicator may proxy for older construction quality or layout features valued less in modern markets. | |
| 136 | + | |
| 137 | +\textbf{Negative HOA membership ($-0.108$).} HOA properties include condominiums and planned communities that may have lower lot sizes and more restrictions. The negative sign is conditional on the separate log HOA fee variable ($+0.044$), suggesting that the HOA membership dummy captures the property-type effect while fees capture the service-quality gradient. | |
| 138 | + | |
| 139 | +These counterintuitive signs are common in hedonic regressions with many correlated controls and do not necessarily indicate misspecification, though they do limit the attributable economic interpretation of individual coefficients. | |
| 140 | + | |
| 141 | +\subsubsection{Progressive Geographic Fixed Effects} | |
| 142 | +\label{sec:progressive_fe} | |
| 143 | + | |
| 144 | +Table~\ref{tab:fe_comparison} documents how explanatory power increases with geographic granularity. Adding 50 state fixed effects increases the in-sample $R^2$ from 0.634 to 0.681. Replacing state dummies with 886 ZIP3 fixed effects further increases $R^2$ to 0.729 (in-sample) and out-of-sample $R^2$ to 0.725. | |
| 145 | + | |
| 146 | +\begin{table}[H] | |
| 147 | +\centering | |
| 148 | +\caption{OLS $R^2$ Under Progressively Finer Geographic Controls} | |
| 149 | +\label{tab:fe_comparison} | |
| 150 | +\begin{threeparttable} | |
| 151 | +\begin{tabular}{lrrrr} | |
| 152 | +\toprule | |
| 153 | +Specification & Geographic FE & $N_{\text{FE}}$ & In-sample $R^2$ & Out-of-sample $R^2$ \\ | |
| 154 | +\midrule | |
| 155 | +Baseline (4 regions) & Census Region & 3 & 0.634 & 0.630 \\ | |
| 156 | +State FE & State & 50 & 0.681 & 0.678 \\ | |
| 157 | +ZIP3 FE & 3-digit ZIP & 886 & 0.729 & 0.725 \\ | |
| 158 | +\bottomrule | |
| 159 | +\end{tabular} | |
| 160 | +\begin{tablenotes} | |
| 161 | +\small | |
| 162 | +\item \textit{Notes:} Same 62 non-geographic regressors across all specifications; geographic FE replace the 3 regional dummies. Out-of-sample $R^2$ computed on a random 20\% holdout. ZIP3 codes cover 886 unique prefixes in the sample. | |
| 163 | +\end{tablenotes} | |
| 164 | +\end{threeparttable} | |
| 165 | +\end{table} | |
| 166 | + | |
| 167 | +The progression from region to state to ZIP3 fixed effects yields $R^2$ gains of $+4.7$ pp and $+4.7$ pp, respectively. This 9.5 pp total gain from geographic granularity alone---achieved without any additional property-level information---underscores the importance of unobserved local factors in hedonic pricing. Notably, even ZIP3 fixed effects leave substantial residual variation unexplained: the gap between ZIP3 OLS ($R^2 = 0.725$) and XGBoost under random validation ($R^2 = 0.833$) suggests that non-linear attribute effects contribute an additional 10.8 pp of explained variation. | |
| 168 | + | |
| 169 | +\subsubsection{OLS Diagnostics} | |
| 170 | + | |
| 171 | +Figure~\ref{fig:diagnostics} presents diagnostic plots. The residual distribution exhibits excess kurtosis (5.55 vs.\ 3.0 for normality) and mild positive skewness (0.18), as confirmed by the Jarque-Bera test ($JB = 217{,}078$, $p < 0.001$). The heteroskedastic pattern visible in the residuals-versus-fitted plot motivates our use of HC3 standard errors. | |
| 172 | + | |
| 173 | +\begin{figure}[H] | |
| 174 | + \centering | |
| 175 | + \includegraphics[width=\textwidth]{figures/fig2_ols_diagnostics.png} | |
| 176 | + \caption{OLS Diagnostic Plots: (a) Residuals vs.\ Fitted; (b) Normal Q-Q Plot; (c) Residual Distribution; (d) Scale-Location} | |
| 177 | + \label{fig:diagnostics} | |
| 178 | +\end{figure} | |
| 179 | + | |
| 180 | + | |
| 181 | +\subsection{Spatial Autocorrelation} | |
| 182 | +\label{sec:spatial_autocorrelation} | |
| 183 | + | |
| 184 | +We compute Moran's $I$ on OLS residuals using a KNN spatial weight matrix ($k = 8$) on three independent random subsamples of 5,000 observations each. Table~\ref{tab:moran} reports the results. | |
| 185 | + | |
| 186 | +\begin{table}[H] | |
| 187 | +\centering | |
| 188 | +\caption{Moran's $I$ on OLS Residuals: Subsample Stability} | |
| 189 | +\label{tab:moran} | |
| 190 | +\begin{threeparttable} | |
| 191 | +\begin{tabular}{lrrrl} | |
| 192 | +\toprule | |
| 193 | +Subsample & $I$ & $z$-statistic & $p$-value & \\ | |
| 194 | +\midrule | |
| 195 | +Subsample 1 & 0.2742 & 41.8 & $<0.001$ & *** \\ | |
| 196 | +Subsample 2 & 0.2839 & 43.5 & $<0.001$ & *** \\ | |
| 197 | +Subsample 3 & 0.2652 & 40.5 & $<0.001$ & *** \\ | |
| 198 | +\midrule | |
| 199 | +Mean & 0.2745 & & & \\ | |
| 200 | +Std.\ Dev. & 0.0076 & & & \\ | |
| 201 | +\bottomrule | |
| 202 | +\end{tabular} | |
| 203 | +\begin{tablenotes} | |
| 204 | +\small | |
| 205 | +\item \textit{Notes:} Row-standardized KNN weight matrix with $k = 8$. Each subsample draws 5,000 observations at random. All $z$-statistics strongly reject the null of no spatial autocorrelation. The low standard deviation (0.008) across subsamples indicates that the diagnostic is highly stable. | |
| 206 | +\end{tablenotes} | |
| 207 | +\end{threeparttable} | |
| 208 | +\end{table} | |
| 209 | + | |
| 210 | +The mean Moran's $I = 0.2745$ (std $= 0.0076$) across three subsamples confirms \textbf{strong, stable positive spatial autocorrelation} in the OLS residuals. This implies that our regional controls (four Census regions) are insufficient to absorb local price variation. OLS standard errors may understate true uncertainty, and coefficient estimates may be biased if the spatial structure correlates with included regressors. The stability across subsamples (CV $= 2.8\%$) indicates that this is not a sampling artifact but a structural feature of the data. Addressing spatial dependence through spatial econometric models is an important direction for future work; our OLS and quantile regression results should be interpreted with this caveat. | |
| 211 | + | |
| 212 | + | |
| 213 | +\subsection{Quantile Regression and Inter-Quantile Tests} | |
| 214 | +\label{sec:qr_results} | |
| 215 | + | |
| 216 | +\subsubsection{Coefficient Estimates Across Quantiles} | |
| 217 | + | |
| 218 | +Table~\ref{tab:qr_results} presents quantile regression coefficients at the five estimated quantiles. | |
| 219 | + | |
| 220 | +\begin{table}[H] | |
| 221 | +\centering | |
| 222 | +\caption{Quantile Regression Coefficients at Selected Quantiles} | |
| 223 | +\label{tab:qr_results} | |
| 224 | +\begin{threeparttable} | |
| 225 | +\small | |
| 226 | +\begin{tabular}{lrrrrrr} | |
| 227 | +\toprule | |
| 228 | +Variable & $\tau=0.10$ & $\tau=0.25$ & $\tau=0.50$ & $\tau=0.75$ & $\tau=0.90$ & OLS \\ | |
| 229 | +\midrule | |
| 230 | +$\ln$(Living Area) & 0.358*** & 0.313*** & 0.275*** & 0.237*** & 0.251*** & 0.312*** \\ | |
| 231 | +Bedrooms & $-$0.085*** & $-$0.076*** & $-$0.064*** & $-$0.037*** & $-$0.034*** & $-$0.068*** \\ | |
| 232 | +Bathrooms & 0.157*** & 0.195*** & 0.234*** & 0.259*** & 0.259*** & 0.236*** \\ | |
| 233 | +Age & $-$0.430*** & $-$0.201*** & $-$0.066** & $-$0.007 & $-$0.026 & $-$0.042** \\ | |
| 234 | +$\ln$(Lot Size) & 0.045*** & 0.051*** & 0.061*** & 0.070*** & 0.086*** & 0.070*** \\ | |
| 235 | +Pool & 0.008 & 0.008 & 0.019*** & 0.061*** & 0.094*** & 0.029*** \\ | |
| 236 | +Garage & 0.205*** & 0.146*** & 0.102*** & 0.061*** & 0.020*** & 0.109*** \\ | |
| 237 | +Luxury Score & 0.018*** & 0.038*** & 0.054*** & 0.068*** & 0.072*** & 0.044*** \\ | |
| 238 | +Foreclosure & $-$0.494*** & $-$0.473*** & $-$0.379*** & $-$0.327*** & $-$0.338*** & $-$0.406*** \\ | |
| 239 | +Northeast & 0.339*** & 0.395*** & 0.471*** & 0.553*** & 0.603*** & 0.487*** \\ | |
| 240 | +West & 0.365*** & 0.372*** & 0.376*** & 0.399*** & 0.450*** & 0.401*** \\ | |
| 241 | +\bottomrule | |
| 242 | +\end{tabular} | |
| 243 | +\begin{tablenotes} | |
| 244 | +\small | |
| 245 | +\item \textit{Notes:} Estimated on a random subsample of 150,000 observations. Continuous variables standardized. Full set of 62 regressors included; selected coefficients shown. Significance: *** $p<0.001$, ** $p<0.01$, * $p<0.05$. Coefficients reflect one-standard-deviation effects for continuous variables. | |
| 246 | +\end{tablenotes} | |
| 247 | +\end{threeparttable} | |
| 248 | +\end{table} | |
| 249 | + | |
| 250 | +\begin{figure}[H] | |
| 251 | + \centering | |
| 252 | + \includegraphics[width=\textwidth]{figures/fig3_quantile_coefficients.png} | |
| 253 | + \caption{Quantile Regression Coefficients Across the Conditional Price Distribution. Red dashed lines indicate OLS estimates. Coefficients are on standardized variables.} | |
| 254 | + \label{fig:qr_coefficients} | |
| 255 | +\end{figure} | |
| 256 | + | |
| 257 | +Several economically interesting patterns emerge. | |
| 258 | + | |
| 259 | +\textbf{Living area gradient declines at upper quantiles.} The standardized $\ln(\text{sqft})$ coefficient declines from 0.358 ($\tau = 0.10$) to 0.237 ($\tau = 0.75$), indicating that additional space is associated with proportionally larger listing-price differences at the lower end of the conditional distribution. At higher quantiles, other factors (luxury features, location) may dominate size. | |
| 260 | + | |
| 261 | +\textbf{Bathroom gradient increases at upper quantiles.} In contrast, the bathroom gradient rises monotonically from 0.157 ($\tau = 0.10$) to 0.259 ($\tau = 0.90$), suggesting that bathroom count differentiates properties more strongly in the upper market. | |
| 262 | + | |
| 263 | +\textbf{Age penalty concentrated at lower quantiles.} The age gradient is $-0.430$ at $\tau = 0.10$ but statistically insignificant at $\tau = 0.75$ and $\tau = 0.90$. Age-related depreciation appears to be primarily a phenomenon of lower-priced properties, possibly because higher-priced properties are better maintained or benefit from vintage appeal. | |
| 264 | + | |
| 265 | +\textbf{Pool gradient emerges only above the median.} The pool coefficient is insignificant at $\tau = 0.10$ and $\tau = 0.25$ but rises to 0.094 at $\tau = 0.90$ ($\approx 9.9\%$ listing-price difference), consistent with pools being a luxury amenity. | |
| 266 | + | |
| 267 | +\textbf{Garage gradient declines monotonically.} The garage coefficient drops from 0.205 ($\tau = 0.10$) to 0.020 ($\tau = 0.90$), reflecting that garage availability differentiates lower-priced properties much more than upper-priced ones where garages are nearly universal. | |
| 268 | + | |
| 269 | +OLS averages across these heterogeneous gradients and can therefore misrepresent both the lower and upper segments of the market. | |
| 270 | + | |
| 271 | +\subsubsection{Inter-Quantile Wald Tests ($\tau = 0.10$ vs.\ $\tau = 0.90$)} | |
| 272 | + | |
| 273 | +Table~\ref{tab:iqr_tests} reports the inter-quantile Wald tests for equality of coefficients between the 10th and 90th percentiles. | |
| 274 | + | |
| 275 | +\begin{table}[H] | |
| 276 | +\centering | |
| 277 | +\caption{Inter-Quantile Wald Tests: $\hat{\beta}(0.10) - \hat{\beta}(0.90)$} | |
| 278 | +\label{tab:iqr_tests} | |
| 279 | +\begin{threeparttable} | |
| 280 | +\small | |
| 281 | +\begin{tabular}{lrrrl} | |
| 282 | +\toprule | |
| 283 | +Variable & $\hat{\beta}(0.10) - \hat{\beta}(0.90)$ & $z$-statistic & $p$-value & \\ | |
| 284 | +\midrule | |
| 285 | +Garage & $+$0.185 & 28.92 & $<0.001$ & *** \\ | |
| 286 | +Northeast & $-$0.264 & $-$21.43 & $<0.001$ & *** \\ | |
| 287 | +Bathrooms & $-$0.103 & $-$10.48 & $<0.001$ & *** \\ | |
| 288 | +$\ln$(Living Area) & $+$0.107 & 9.52 & $<0.001$ & *** \\ | |
| 289 | +$\ln$(Lot Size) & $-$0.041 & $-$9.49 & $<0.001$ & *** \\ | |
| 290 | +Age & $-$0.405 & $-$7.89 & $<0.001$ & *** \\ | |
| 291 | +Pool & $-$0.087 & $-$7.80 & $<0.001$ & *** \\ | |
| 292 | +Luxury Score & $-$0.054 & $-$6.82 & $<0.001$ & *** \\ | |
| 293 | +West & $-$0.086 & $-$6.26 & $<0.001$ & *** \\ | |
| 294 | +Bedrooms & $-$0.051 & $-$5.95 & $<0.001$ & *** \\ | |
| 295 | +Foreclosure & $-$0.155 & $-$4.18 & $<0.001$ & *** \\ | |
| 296 | +\midrule | |
| 297 | +\multicolumn{5}{l}{\textit{Not significant ($p > 0.05$)}} \\ | |
| 298 | +Waterfront & $+$0.222 & 1.86 & 0.063 & \\ | |
| 299 | +Age$^2$ & $+$0.010 & 1.40 & 0.162 & \\ | |
| 300 | +\bottomrule | |
| 301 | +\end{tabular} | |
| 302 | +\begin{tablenotes} | |
| 303 | +\small | |
| 304 | +\item \textit{Notes:} $z$-statistics computed from the joint covariance matrix of the $\tau = 0.10$ and $\tau = 0.90$ quantile regression estimates; both quantiles estimated on the same 150,000-observation subsample. Variables sorted by $|z|$. 11 of 13 tested variables show statistically significant differences at the 5\% level. *** $p<0.001$, ** $p<0.01$, * $p<0.05$. | |
| 305 | +\end{tablenotes} | |
| 306 | +\end{threeparttable} | |
| 307 | +\end{table} | |
| 308 | + | |
| 309 | +The inter-quantile tests reject coefficient equality for 11 of 13 tested variables at the 5\% level. The largest inter-quantile difference is for garage ($+0.185$, $z = 28.92$), confirming that the garage listing-price gradient is dramatically larger for lower-priced properties. Bathrooms ($-0.103$, $z = -10.48$) and living area ($+0.107$, $z = 9.52$) also exhibit large, highly significant inter-quantile differences. The only two variables for which the null of equal coefficients cannot be rejected are age-squared ($z = 1.40$) and waterfront ($z = 1.86$, $p = 0.063$). | |
| 310 | + | |
| 311 | +These formal tests confirm the visual patterns in Figure~\ref{fig:qr_coefficients}: the conditional distribution of listing prices is not merely shifted by housing attributes but is differentially stretched and compressed, with different attributes mattering more at different points in the distribution. | |
| 312 | + | |
| 313 | + | |
| 314 | +\subsection{Machine Learning Under Random Validation} | |
| 315 | +\label{sec:ml_random} | |
| 316 | + | |
| 317 | +Table~\ref{tab:ml_comparison} summarizes out-of-sample performance under the random 80/20 split. | |
| 318 | + | |
| 319 | +\begin{table}[H] | |
| 320 | +\centering | |
| 321 | +\caption{Out-of-Sample Predictive Performance: Random Validation} | |
| 322 | +\label{tab:ml_comparison} | |
| 323 | +\begin{threeparttable} | |
| 324 | +\small | |
| 325 | +\begin{tabular}{lrrrrrr} | |
| 326 | +\toprule | |
| 327 | + & \multicolumn{3}{c}{Log-Price Scale} & \multicolumn{3}{c}{Dollar Scale} \\ | |
| 328 | +\cmidrule(lr){2-4} \cmidrule(lr){5-7} | |
| 329 | +Model & $R^2$ & RMSE & MAE & MAPE (\%) & MdAPE (\%) & MAE (\$) \\ | |
| 330 | +\midrule | |
| 331 | +OLS & 0.6301 & 0.4911 & 0.3619 & 39.8 & 27.2 & 240,941 \\ | |
| 332 | +Ridge ($\alpha=1$) & 0.6301 & 0.4911 & 0.3619 & --- & --- & --- \\ | |
| 333 | +Lasso ($\alpha=0.001$) & 0.6280 & 0.4925 & 0.3627 & --- & --- & --- \\ | |
| 334 | +Elastic Net & 0.6145 & 0.5014 & 0.3681 & --- & --- & --- \\ | |
| 335 | +OLS + State FE & 0.6775 & 0.4586 & --- & --- & --- & --- \\ | |
| 336 | +OLS + ZIP3 FE & 0.7253 & --- & --- & --- & --- & --- \\ | |
| 337 | +Random Forest & 0.7842 & 0.3751 & 0.2636 & 28.4 & 18.5 & 180,116 \\ | |
| 338 | +LightGBM & 0.8088 & 0.3531 & 0.2519 & 26.9 & 18.2 & 169,495 \\ | |
| 339 | +\textbf{XGBoost} & \textbf{0.8330} & \textbf{0.3300} & \textbf{0.2327} & \textbf{24.7} & \textbf{16.5} & \textbf{157,822} \\ | |
| 340 | +\bottomrule | |
| 341 | +\end{tabular} | |
| 342 | +\begin{tablenotes} | |
| 343 | +\small | |
| 344 | +\item \textit{Notes:} Training: 631,073 (80\%). Test: 157,769 (20\%). Random split, seed 42. MAPE = mean absolute percentage error; MdAPE = median APE. Dollar metrics computed by exponentiating predicted log-prices. Ridge, Lasso, Elastic Net dollar metrics suppressed for brevity (similar to OLS). ZIP3 FE RMSE and dollar-scale metrics not computed for this specification. | |
| 345 | +\end{tablenotes} | |
| 346 | +\end{threeparttable} | |
| 347 | +\end{table} | |
| 348 | + | |
| 349 | +XGBoost achieves the highest $R^2$ of 0.833 under random validation, representing a 20-percentage-point improvement over OLS. Regularized linear models (Ridge, Lasso, Elastic Net) offer no improvement over OLS, confirming that the parametric specification is not overfit---the predictive gap is driven by non-linearity and interaction effects, not overfitting. The ZIP3 FE model ($R^2 = 0.725$) closes roughly half the OLS-to-XGBoost gap, indicating that fine-grained geographic controls capture a substantial portion of the non-linearity that tree models exploit. | |
| 350 | + | |
| 351 | +\begin{figure}[H] | |
| 352 | + \centering | |
| 353 | + \includegraphics[width=\textwidth]{figures/fig4_model_comparison.png} | |
| 354 | + \caption{Predicted vs.\ Actual Log-Prices Under Random Validation: (a) OLS; (b) XGBoost; (c) LightGBM} | |
| 355 | + \label{fig:model_comparison} | |
| 356 | +\end{figure} | |
| 357 | + | |
| 358 | + | |
| 359 | +\subsection{Machine Learning Under Geographic Validation} | |
| 360 | +\label{sec:ml_geo} | |
| 361 | + | |
| 362 | +Table~\ref{tab:geo_holdout} reports performance when 10 states are held out entirely. | |
| 363 | + | |
| 364 | +\begin{table}[H] | |
| 365 | +\centering | |
| 366 | +\caption{Geographic Holdout Validation (10 States Held Out)} | |
| 367 | +\label{tab:geo_holdout} | |
| 368 | +\begin{threeparttable} | |
| 369 | +\begin{tabular}{lrrr} | |
| 370 | +\toprule | |
| 371 | +Model & $R^2$ (random) & $R^2$ (geo holdout) & $\Delta R^2$ \\ | |
| 372 | +\midrule | |
| 373 | +OLS & 0.630 & $<0$ & $>-0.63$ \\ | |
| 374 | +Random Forest & 0.784 & 0.542 & $-0.242$ \\ | |
| 375 | +XGBoost & 0.833 & 0.547 & $-0.286$ \\ | |
| 376 | +XGBoost (no geo features) & 0.830 & 0.519 & $-0.311$ \\ | |
| 377 | +XGBoost (+ lat/lon) & 0.870 & 0.464 & $-0.406$ \\ | |
| 378 | +\bottomrule | |
| 379 | +\end{tabular} | |
| 380 | +\begin{tablenotes} | |
| 381 | +\small | |
| 382 | +\item \textit{Notes:} Geographic holdout: CA, NY, TX, FL, OH, CO, NC, WA, IL, GA held out (438,315 observations). Training on remaining 350,527 observations. OLS with regional dummies fails catastrophically because the held-out states span all four Census regions and include the largest markets. ``No geo features'' removes region dummies from the full model. ``+ lat/lon'' adds raw latitude and longitude as continuous features. | |
| 383 | +\end{tablenotes} | |
| 384 | +\end{threeparttable} | |
| 385 | +\end{table} | |
| 386 | + | |
| 387 | +\textbf{Key finding.} The XGBoost $R^2$ drops from 0.833 (random) to 0.547 (geographic holdout)---a \textbf{28.6-percentage-point decline}. This demonstrates that a substantial portion of the random-validation $R^2$ reflects the model's ability to exploit local spatial structure rather than learning generalizable attribute-price relationships. | |
| 388 | + | |
| 389 | +Two additional experiments sharpen this conclusion: | |
| 390 | +\begin{itemize}[nosep] | |
| 391 | + \item \textbf{Adding latitude and longitude} boosts random $R^2$ from 0.833 to 0.870 ($+3.7$ pp) but \emph{reduces} geographic holdout $R^2$ to 0.464 ($-8.3$ pp relative to the baseline XGBoost). Coordinates allow the model to memorize location-specific price levels, which is useful when nearby properties appear in the test set but harmful when entire states are withheld. | |
| 392 | + \item \textbf{Removing all geographic features} (no region dummies) yields random $R^2 = 0.830$ (essentially unchanged) but geographic holdout $R^2 = 0.519$. Compared to the ablation-design variant of the full model (Geo $R^2 = 0.425$; see Section~\ref{sec:ablation}), dropping geography \emph{improves} geographic holdout by $+9.4$ pp. Region dummies can \emph{harm} geographic generalization because they encode training-set-specific geographic price levels. | |
| 393 | +\end{itemize} | |
| 394 | + | |
| 395 | +This finding is methodologically important: \textbf{random train-test splits can substantially overstate the predictive performance of hedonic models} when applied to geographically diverse national samples. Geographic holdout validation provides a more conservative---and arguably more relevant---assessment of model generalizability. | |
| 396 | + | |
| 397 | + | |
| 398 | +\subsection{Ablation: What Drives the ML Gain?} | |
| 399 | +\label{sec:ablation} | |
| 400 | + | |
| 401 | +Table~\ref{tab:ablation} presents the results of the feature ablation analysis for XGBoost. | |
| 402 | + | |
| 403 | +\begin{table}[H] | |
| 404 | +\centering | |
| 405 | +\caption{XGBoost Ablation Analysis: Marginal Contribution of Feature Groups} | |
| 406 | +\label{tab:ablation} | |
| 407 | +\begin{threeparttable} | |
| 408 | +\small | |
| 409 | +\begin{tabular}{llrrrr} | |
| 410 | +\toprule | |
| 411 | +Stage & Feature Set & $N_{\text{features}}$ & Random $R^2$ & Geo $R^2$ & $\Delta$ Geo $R^2$ \\ | |
| 412 | +\midrule | |
| 413 | +1 & Structural only & 8 & 0.4976 & 0.3481 & --- \\ | |
| 414 | +2 & + Lot & 9 & 0.5464 & 0.3765 & $+$0.028 \\ | |
| 415 | +3 & + Amenities & 20 & 0.6378 & 0.4485 & $+$0.072 \\ | |
| 416 | +4 & + Neighborhood & 27 & 0.8136 & 0.4833 & $+$0.035 \\ | |
| 417 | +5 & + Market status & 32 & 0.8235 & 0.5014 & $+$0.018 \\ | |
| 418 | +6 & Full model (+ interactions/cats/region) & 62 & 0.8327 & 0.4250 & $-$0.076 \\ | |
| 419 | +\bottomrule | |
| 420 | +\end{tabular} | |
| 421 | +\begin{tablenotes} | |
| 422 | +\small | |
| 423 | +\item \textit{Notes:} Each row adds a feature group to the previous stage. Random $R^2$ is computed on the standard 20\% random holdout. Geo $R^2$ is computed on the 10-state geographic holdout. $\Delta$ Geo $R^2$ is the change from the previous stage. | |
| 424 | +\end{tablenotes} | |
| 425 | +\end{threeparttable} | |
| 426 | +\end{table} | |
| 427 | + | |
| 428 | +Three findings emerge from the ablation analysis. | |
| 429 | + | |
| 430 | +\textbf{Neighborhood features provide the largest random-validation gain.} Adding neighborhood variables (Walk Score, Bike Score, Transit Score, school rating, school count, school distance, tax rate) increases random $R^2$ from 0.638 to 0.814---a gain of $+17.6$ pp. This is more than twice the contribution of any other feature group, reflecting the importance of local amenities and public-service quality in explaining listing-price variation. | |
| 431 | + | |
| 432 | +\textbf{The full model hurts geographic generalization.} The transition from Stage 5 (32 features, Geo $R^2 = 0.501$) to Stage 6 (62 features, Geo $R^2 = 0.425$) \emph{reduces} geographic holdout $R^2$ by 7.6 pp. The features added in Stage 6 include region dummies, interaction terms involving region, and categorical controls. These features improve random $R^2$ modestly ($+0.9$ pp) but actively harm geographic generalization by encoding training-set-specific spatial patterns. | |
| 433 | + | |
| 434 | +\textbf{Structural features alone explain 35\% of geographic variation.} Even with only 8 structural variables, XGBoost achieves Geo $R^2 = 0.348$, indicating that basic property characteristics (size, bedrooms, bathrooms, age) carry non-trivial predictive power across geographies. | |
| 435 | + | |
| 436 | + | |
| 437 | +\subsection{The Role of Geographic Features} | |
| 438 | +\label{sec:geo_features} | |
| 439 | + | |
| 440 | +The ablation and geographic holdout results jointly demonstrate a critical tension in hedonic modeling: geographic features improve in-sample and random-holdout fit but degrade geographic generalization. Table~\ref{tab:geo_role} summarizes the evidence. | |
| 441 | + | |
| 442 | +\begin{table}[H] | |
| 443 | +\centering | |
| 444 | +\caption{Impact of Geographic Features on Model Performance} | |
| 445 | +\label{tab:geo_role} | |
| 446 | +\begin{threeparttable} | |
| 447 | +\small | |
| 448 | +\begin{tabular}{lrr} | |
| 449 | +\toprule | |
| 450 | +XGBoost Specification & Random $R^2$ & Geographic Holdout $R^2$ \\ | |
| 451 | +\midrule | |
| 452 | +Full model (62 features, with region) & 0.8327 & 0.4250 \\ | |
| 453 | +No geographic features (no region dummies) & 0.8297 & 0.5190 \\ | |
| 454 | +Full model + lat/lon coordinates & 0.8701 & 0.4644 \\ | |
| 455 | +\bottomrule | |
| 456 | +\end{tabular} | |
| 457 | +\begin{tablenotes} | |
| 458 | +\small | |
| 459 | +\item \textit{Notes:} ``No geographic features'' removes region dummies from the full model. ``+ lat/lon'' adds raw latitude and longitude as continuous features to the full model. Geographic holdout uses 10 states held out entirely. | |
| 460 | +\end{tablenotes} | |
| 461 | +\end{threeparttable} | |
| 462 | +\end{table} | |
| 463 | + | |
| 464 | +Removing region dummies costs only 0.3 pp in random $R^2$ (0.833 to 0.830) but \emph{gains} 9.4 pp in geographic holdout $R^2$ (0.425 to 0.519). Adding coordinates achieves the opposite: $+3.7$ pp random, $-5.5$ pp geographic holdout (relative to the no-geography specification). | |
| 465 | + | |
| 466 | +This pattern has important implications for automated valuation models (AVMs). In deployment contexts where the model must generalize to new geographies, geographic features can be counterproductive. The model without geography exploits only attribute-price relationships that transfer across markets, yielding more conservative but more robust predictions. | |
| 467 | + | |
| 468 | + | |
| 469 | +\subsection{SHAP Analysis and Cross-Model Stability} | |
| 470 | +\label{sec:shap_results} | |
| 471 | + | |
| 472 | +\subsubsection{Feature Importance Rankings} | |
| 473 | + | |
| 474 | +Table~\ref{tab:shap_importance} reports the top 20 features by mean absolute SHAP value from the XGBoost model. | |
| 475 | + | |
| 476 | +\begin{table}[H] | |
| 477 | +\centering | |
| 478 | +\caption{Top 20 Features by Mean Absolute SHAP Value (XGBoost)} | |
| 479 | +\label{tab:shap_importance} | |
| 480 | +\begin{threeparttable} | |
| 481 | +\begin{tabular}{rlr} | |
| 482 | +\toprule | |
| 483 | +Rank & Feature & Mean $|\phi|$ \\ | |
| 484 | +\midrule | |
| 485 | +1 & $\ln$(Living Area) & 0.1740 \\ | |
| 486 | +2 & Bathrooms & 0.1610 \\ | |
| 487 | +3 & Avg.\ School Rating & 0.0881 \\ | |
| 488 | +4 & Region: West & 0.0751 \\ | |
| 489 | +5 & $\ln$(Lot Size) & 0.0747 \\ | |
| 490 | +6 & Luxury Score & 0.0713 \\ | |
| 491 | +7 & Property Tax Rate & 0.0694 \\ | |
| 492 | +8 & Nearest School Distance & 0.0472 \\ | |
| 493 | +9 & Region: Northeast & 0.0425 \\ | |
| 494 | +10 & Bike Score & 0.0400 \\ | |
| 495 | +11 & $\ln$(HOA Fees + 1) & 0.0398 \\ | |
| 496 | +12 & Garage & 0.0378 \\ | |
| 497 | +13 & Age & 0.0369 \\ | |
| 498 | +14 & Hardwood Floors & 0.0311 \\ | |
| 499 | +15 & Waterfront $\times$ $\ln$(sqft) & 0.0292 \\ | |
| 500 | +16 & Sqft per Bedroom & 0.0289 \\ | |
| 501 | +17 & Walk Score & 0.0276 \\ | |
| 502 | +18 & Central Air & 0.0273 \\ | |
| 503 | +19 & Region: South & 0.0250 \\ | |
| 504 | +20 & Transit Score & 0.0213 \\ | |
| 505 | +\bottomrule | |
| 506 | +\end{tabular} | |
| 507 | +\begin{tablenotes} | |
| 508 | +\small | |
| 509 | +\item \textit{Notes:} TreeSHAP values computed on 10,000 random test-set observations. Mean $|\phi|$ is the average absolute contribution of each feature to deviations from the expected predicted log-price. These are predictive contributions, not structural implicit prices. | |
| 510 | +\end{tablenotes} | |
| 511 | +\end{threeparttable} | |
| 512 | +\end{table} | |
| 513 | + | |
| 514 | +\begin{figure}[H] | |
| 515 | + \centering | |
| 516 | + \includegraphics[width=0.85\textwidth]{figures/fig5_shap_summary.png} | |
| 517 | + \caption{SHAP Summary Plot for XGBoost. Each dot is one observation; horizontal position shows the feature's SHAP value (contribution to predicted log-price deviation from the mean); color indicates feature value (red = high, blue = low). These are predictive contributions, not causal effects.} | |
| 518 | + \label{fig:shap_summary} | |
| 519 | +\end{figure} | |
| 520 | + | |
| 521 | +Living area and bathrooms are the two dominant predictive contributors, with mean absolute SHAP values of 0.174 and 0.161. School quality (0.088), regional location (West: 0.075; Northeast: 0.043), and lot size (0.075) form a second tier. | |
| 522 | + | |
| 523 | +\textbf{SHAP dependence plots.} Figure~\ref{fig:shap_dependence} shows how the SHAP contribution of key features varies with feature values. The non-linear shapes---particularly the strongly concave living-area SHAP and the threshold-like behavior of school rating and property tax rate---explain why tree-based models outperform linear OLS. These non-linearities cannot be captured by the semi-log specification even with interaction terms. | |
| 524 | + | |
| 525 | +\begin{figure}[H] | |
| 526 | + \centering | |
| 527 | + \includegraphics[width=\textwidth]{figures/fig10_shap_dependence.png} | |
| 528 | + \caption{SHAP Dependence Plots for Eight Key Features. Each panel shows how the SHAP contribution varies with the feature value. Non-linear patterns (concavity, thresholds, saturation) explain the predictive advantage of tree-based models over linear specifications.} | |
| 529 | + \label{fig:shap_dependence} | |
| 530 | +\end{figure} | |
| 531 | + | |
| 532 | +\subsubsection{Cross-Model SHAP Stability} | |
| 533 | + | |
| 534 | +A common concern with SHAP-based interpretation is that rankings may be model-specific artifacts. Table~\ref{tab:shap_stability} reports pairwise Spearman rank correlations of mean absolute SHAP values across three tree-based models. | |
| 535 | + | |
| 536 | +\begin{table}[H] | |
| 537 | +\centering | |
| 538 | +\caption{Cross-Model SHAP Importance Stability: Spearman Rank Correlations} | |
| 539 | +\label{tab:shap_stability} | |
| 540 | +\begin{threeparttable} | |
| 541 | +\begin{tabular}{lrrr} | |
| 542 | +\toprule | |
| 543 | +& XGBoost & LightGBM & Random Forest \\ | |
| 544 | +\midrule | |
| 545 | +XGBoost & 1.000 & 0.9898 & 0.9060 \\ | |
| 546 | +LightGBM & 0.9898 & 1.000 & 0.8917 \\ | |
| 547 | +Random Forest & 0.9060 & 0.8917 & 1.000 \\ | |
| 548 | +\bottomrule | |
| 549 | +\end{tabular} | |
| 550 | +\begin{tablenotes} | |
| 551 | +\small | |
| 552 | +\item \textit{Notes:} Spearman $\rho$ computed on the full vector of mean $|\phi|$ values across all features. The same six features---$\ln$(Living Area), Bathrooms, Avg.\ School Rating, Region: West, $\ln$(Lot Size), and Luxury Score---rank in the top six across all three models. | |
| 553 | +\end{tablenotes} | |
| 554 | +\end{threeparttable} | |
| 555 | +\end{table} | |
| 556 | + | |
| 557 | +The cross-model correlations are remarkably high. XGBoost and LightGBM produce nearly identical rankings ($\rho = 0.990$), while even the structurally different Random Forest model yields $\rho > 0.89$ with both gradient-boosted methods. The same six features appear in the top six across all three models. This stability lends credibility to the SHAP-based feature importance ranking as a description of the predictive structure of the data, not merely an artifact of a particular algorithm. | |
| 558 | + | |
| 559 | +\textbf{Note on SHAP versus OLS comparisons.} SHAP importance rankings differ from both OLS $t$-statistics and XGBoost gain importance. School rating ranks 3rd by SHAP but has a counterintuitive negative OLS sign; property tax rate ranks 7th by SHAP but 28th by standardized OLS coefficient magnitude. These discrepancies reflect the non-linear and interactive effects that SHAP captures but OLS cannot. However, even though SHAP rankings are stable across tree-based models, they should not be interpreted as revealing the ``true'' importance hierarchy of housing attributes---they remain predictive decompositions, not structural parameters. | |
| 560 | + | |
| 561 | + | |
| 562 | +\subsection{Geographic Heterogeneity} | |
| 563 | + | |
| 564 | +Figure~\ref{fig:geographic} maps the spatial distribution of listing prices. The well-documented coastal price gradient is clearly visible, with California, the Northeast corridor, Hawaii, and Colorado exhibiting the highest prices. The map also illustrates the non-representativeness concern: listing density is highly uneven, with sparse coverage in some rural areas. | |
| 565 | + | |
| 566 | +\begin{figure}[H] | |
| 567 | + \centering | |
| 568 | + \includegraphics[width=\textwidth]{figures/fig7_geographic_prices.png} | |
| 569 | + \caption{Geographic Distribution of Log Listing Prices. Each dot represents one property (50,000 random subsample). Color indicates log-price. Note: listing density is not uniform; areas with few dots may have fewer Zillow listings, not necessarily fewer properties.} | |
| 570 | + \label{fig:geographic} | |
| 571 | +\end{figure} | |
| 572 | + | |
| 573 | +\begin{figure}[H] | |
| 574 | + \centering | |
| 575 | + \includegraphics[width=0.85\textwidth]{figures/fig8_regional_prices.png} | |
| 576 | + \caption{Listing Price Distribution by Census Region} | |
| 577 | + \label{fig:regional} | |
| 578 | +\end{figure} | |
| 579 | + | |
| 580 | + | |
| 581 | +\subsection{Non-Linear Relationships} | |
| 582 | + | |
| 583 | +Figure~\ref{fig:marginal} presents bivariate (unconditional) relationships between key attributes and log listing prices. These plots illustrate the non-linearities that motivate the machine learning approach but should not be confused with conditional (ceteris paribus) effects. | |
| 584 | + | |
| 585 | +\begin{figure}[H] | |
| 586 | + \centering | |
| 587 | + \includegraphics[width=\textwidth]{figures/fig9_marginal_effects.png} | |
| 588 | + \caption{Unconditional Bivariate Relationships Between Key Attributes and Log Listing Price. These are raw scatter plots and binned means; they do not control for other attributes and should not be interpreted as conditional effects.} | |
| 589 | + \label{fig:marginal} | |
| 590 | +\end{figure} | |
added
paper/sections/robustness.tex
+164 −0
@@ -0,0 +1,164 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 4 | +% 6. ROBUSTNESS | |
| 5 | +% ═══════════════════════════════════════════════════════════════════════ | |
| 6 | +\section{Robustness Checks} | |
| 7 | +\label{sec:robustness} | |
| 8 | + | |
| 9 | +\subsection{Imputation Sensitivity} | |
| 10 | +\label{sec:imputation_sensitivity} | |
| 11 | + | |
| 12 | +Two variables with substantial missingness---year built (19.4\%) and lot size (16.3\%)---are imputed using state-level medians. To assess the consequences, we re-estimate the baseline OLS model on subsamples that exclude imputed observations. Table~\ref{tab:imputation} reports the results. | |
| 13 | + | |
| 14 | +\begin{table}[H] | |
| 15 | +\centering | |
| 16 | +\caption{Imputation Sensitivity: Unstandardized OLS on Complete-Case Subsamples} | |
| 17 | +\label{tab:imputation} | |
| 18 | +\begin{threeparttable} | |
| 19 | +\small | |
| 20 | +\begin{tabular}{lrrrr} | |
| 21 | +\toprule | |
| 22 | +Specification & $N$ & $R^2$ & $\hat{\beta}_{\ln(\text{lot})}$ & Notes \\ | |
| 23 | +\midrule | |
| 24 | +Full sample (baseline) & 788,842 & 0.634 & 0.018 & All imputed values included \\ | |
| 25 | +Drop missing year\_built & 640,055 & 0.638 & 0.019 & 19\% fewer obs. \\ | |
| 26 | +Drop missing lot\_size & 664,900 & 0.651 & 0.059 & 16\% fewer obs. \\ | |
| 27 | +Drop both missing & 534,803 & 0.654 & 0.059 & 32\% fewer obs. \\ | |
| 28 | +\bottomrule | |
| 29 | +\end{tabular} | |
| 30 | +\begin{tablenotes} | |
| 31 | +\small | |
| 32 | +\item \textit{Notes:} All specifications use the same 62-regressor unstandardized OLS model with HC3 standard errors. $\hat{\beta}_{\ln(\text{lot})}$ is the coefficient on $\ln$(lot size) in natural units. Other coefficients are broadly stable across specifications (not shown for brevity). | |
| 33 | +\end{tablenotes} | |
| 34 | +\end{threeparttable} | |
| 35 | +\end{table} | |
| 36 | + | |
| 37 | +The key finding is that the \textbf{lot-size coefficient triples} from 0.018 to 0.059 when imputed lot sizes are dropped. This suggests that state-median imputation attenuates the lot-size gradient substantially: imputed properties receive the state median lot size regardless of their actual lot, washing out the true lot-price relationship. The $R^2$ also increases modestly ($+2.0$ pp) when imputed lots are excluded, indicating that imputation introduces noise. | |
| 38 | + | |
| 39 | +Other coefficients (living area, bathrooms, age) are broadly stable across subsamples, suggesting that the imputation concern is primarily concentrated in the lot-size variable. Dropping observations with missing year built has minimal effect on the lot-size coefficient (0.018 to 0.019) and only slightly increases $R^2$ ($+0.4$ pp). | |
| 40 | + | |
| 41 | +This sensitivity finding has implications for interpretation: the baseline elasticity of listing price with respect to lot size ($0.02$) likely understates the true association, and the complete-case estimate ($0.06$) may be more informative---though it applies to a selected (non-randomly missing) subsample. | |
| 42 | + | |
| 43 | + | |
| 44 | +\subsection{Winsorization Sensitivity} | |
| 45 | +\label{sec:winsorization} | |
| 46 | + | |
| 47 | +To assess the influence of outliers, we winsorize the two variables with implausible extreme values---lot size, capped at its 99.5th percentile (3,944 observations affected), and Bike Score, capped at its nominal maximum of 100 (38 observations)---and re-estimate the baseline OLS model. | |
| 48 | + | |
| 49 | +\begin{table}[H] | |
| 50 | +\centering | |
| 51 | +\caption{Winsorization Sensitivity: Baseline vs.\ Winsorized OLS} | |
| 52 | +\label{tab:winsorization} | |
| 53 | +\begin{threeparttable} | |
| 54 | +\small | |
| 55 | +\begin{tabular}{lrr} | |
| 56 | +\toprule | |
| 57 | +Metric / Coefficient & Baseline & Winsorized \\ | |
| 58 | +\midrule | |
| 59 | +$R^2$ & 0.634 & 0.640 \\ | |
| 60 | +$\hat{\beta}_{\ln(\text{lot})}$ & 0.018 & 0.058 \\ | |
| 61 | +$\hat{\beta}_{\ln(\text{sqft})}$ & 0.628 & 0.610 \\ | |
| 62 | +$\hat{\beta}_{\text{bathrooms}}$ & 0.204 & 0.211 \\ | |
| 63 | +$\hat{\beta}_{\text{garage}}$ & 0.109 & 0.111 \\ | |
| 64 | +\bottomrule | |
| 65 | +\end{tabular} | |
| 66 | +\begin{tablenotes} | |
| 67 | +\small | |
| 68 | +\item \textit{Notes:} Lot size winsorized at its 99.5th percentile ($\approx 3.04$ million sqft; 3,944 observations capped); Bike Score capped at 100 (38 observations). Unstandardized OLS, $N = 788{,}842$. Other coefficients broadly stable (not shown). The large change in $\hat{\beta}_{\ln(\text{lot})}$ mirrors the imputation sensitivity finding, likely reflecting outlier lot sizes in imputed observations. | |
| 69 | +\end{tablenotes} | |
| 70 | +\end{threeparttable} | |
| 71 | +\end{table} | |
| 72 | + | |
| 73 | +Winsorization increases $R^2$ modestly from 0.634 to 0.640, suggesting that extreme values in continuous variables account for a small portion of unexplained variation. The lot-size coefficient again jumps substantially (0.018 to 0.058), consistent with the imputation sensitivity analysis: extreme imputed lot sizes attenuate the gradient. Other coefficients are broadly stable, indicating that the main OLS results are not driven by outliers. | |
| 74 | + | |
| 75 | + | |
| 76 | +\subsection{Quantile Regression Subsample Stability} | |
| 77 | +\label{sec:qr_stability} | |
| 78 | + | |
| 79 | +We repeat the median quantile regression ($\tau = 0.50$) across 10 random subsamples of 150,000 observations each. Table~\ref{tab:qr_stability} reports the coefficient of variation (CV) and sign stability for key variables. | |
| 80 | + | |
| 81 | +\begin{table}[H] | |
| 82 | +\centering | |
| 83 | +\caption{Quantile Regression Stability: $\tau = 0.50$ Across 10 Subsamples} | |
| 84 | +\label{tab:qr_stability} | |
| 85 | +\begin{threeparttable} | |
| 86 | +\small | |
| 87 | +\begin{tabular}{lrrl} | |
| 88 | +\toprule | |
| 89 | +Variable & CV (\%) & Sign Stability (\%) & Assessment \\ | |
| 90 | +\midrule | |
| 91 | +West & 1.0 & 100 & Highly stable \\ | |
| 92 | +Northeast & 1.6 & 100 & Highly stable \\ | |
| 93 | +$\ln$(Lot Size) & 2.7 & 100 & Highly stable \\ | |
| 94 | +$\ln$(Living Area) & 2.9 & 100 & Highly stable \\ | |
| 95 | +Bathrooms & 2.9 & 100 & Highly stable \\ | |
| 96 | +Garage & 4.1 & 100 & Highly stable \\ | |
| 97 | +Luxury Score & 4.8 & 100 & Highly stable \\ | |
| 98 | +Age$^2$ & 5.3 & 100 & Highly stable \\ | |
| 99 | +Foreclosure & 7.1 & 100 & Stable \\ | |
| 100 | +Bedrooms & 7.3 & 100 & Stable \\ | |
| 101 | +\midrule | |
| 102 | +\multicolumn{4}{l}{\textit{Less stable variables}} \\ | |
| 103 | +Pool & 33.4 & 100 & Moderately unstable \\ | |
| 104 | +Waterfront & 42.2 & 100 & Moderately unstable \\ | |
| 105 | +Age & 78.6 & 80 & Unstable \\ | |
| 106 | +\bottomrule | |
| 107 | +\end{tabular} | |
| 108 | +\begin{tablenotes} | |
| 109 | +\small | |
| 110 | +\item \textit{Notes:} CV = coefficient of variation across 10 random subsamples of 150,000 observations. Sign stability = percentage of subsamples in which the coefficient has the same sign as the pooled estimate. Variables sorted by CV. The instability of age (CV = 78.6\%) likely reflects heterogeneity in the age-price relationship across geographic subsamples. The waterfront and pool coefficients vary in magnitude but never change sign; their instability reflects low variation in these binary variables in some subsamples. | |
| 111 | +\end{tablenotes} | |
| 112 | +\end{threeparttable} | |
| 113 | +\end{table} | |
| 114 | + | |
| 115 | +Most variables exhibit high stability (CV $< 8\%$, 100\% sign stability), confirming that the quantile regression results are robust to subsample variation. Three variables are exceptions: \textbf{age} (CV = 78.6\%, sign stability = 80\%), \textbf{waterfront} (CV = 42.2\%), and \textbf{pool} (CV = 33.4\%). The instability of age is consistent with substantial geographic heterogeneity in the age-price relationship---old properties may command a premium in some markets (historic charm) and a discount in others (obsolescence). Waterfront and pool instability reflects the relatively low prevalence of these features in some subsamples. | |
| 116 | + | |
| 117 | + | |
| 118 | +\subsection{Robustness Summary Matrix} | |
| 119 | +\label{sec:robustness_matrix} | |
| 120 | + | |
| 121 | +Table~\ref{tab:robustness_matrix} provides a comprehensive summary of all robustness findings. | |
| 122 | + | |
| 123 | +\begin{table}[H] | |
| 124 | +\centering | |
| 125 | +\caption{Robustness Summary Matrix} | |
| 126 | +\label{tab:robustness_matrix} | |
| 127 | +\begin{threeparttable} | |
| 128 | +\footnotesize | |
| 129 | +\begin{tabular}{p{3.5cm}p{2.5cm}p{2.5cm}p{4.5cm}} | |
| 130 | +\toprule | |
| 131 | +Test & Baseline & Robustness & Key Finding \\ | |
| 132 | +\midrule | |
| 133 | +\multicolumn{4}{l}{\textit{OLS Specification}} \\ | |
| 134 | +Region FE $\rightarrow$ State FE & $R^2 = 0.630$ & $R^2 = 0.678$ & $+$4.8 pp from state-level controls \\ | |
| 135 | +Region FE $\rightarrow$ ZIP3 FE & $R^2 = 0.630$ & $R^2 = 0.725$ & $+$9.5 pp from ZIP3 controls \\ | |
| 136 | +Drop imputed lot sizes & $\hat{\beta}_{\ln\text{lot}} = 0.018$ & $\hat{\beta}_{\ln\text{lot}} = 0.059$ & Coefficient triples; imputation attenuates \\ | |
| 137 | +Winsorization (1\%/99\%) & $R^2 = 0.634$ & $R^2 = 0.640$ & Modest improvement; lot coeff.\ sensitive \\ | |
| 138 | +\midrule | |
| 139 | +\multicolumn{4}{l}{\textit{Spatial Diagnostics}} \\ | |
| 140 | +Moran's $I$ (3 subsamples) & $I = 0.2745$ & std $= 0.0076$ & Highly stable; strong spatial autocorrelation \\ | |
| 141 | +\midrule | |
| 142 | +\multicolumn{4}{l}{\textit{Quantile Regression}} \\ | |
| 143 | +QR stability (10 draws) & Most CV $< 8\%$ & Age CV = 78.6\% & Age unstable; all others sign-stable \\ | |
| 144 | +Inter-quantile Wald & --- & 11/13 significant & Distribution varies for most attributes \\ | |
| 145 | +\midrule | |
| 146 | +\multicolumn{4}{l}{\textit{Machine Learning}} \\ | |
| 147 | +Random vs.\ geo holdout & $R^2 = 0.833$ & $R^2 = 0.425$ & 40.8 pp gap; spatial leakage \\ | |
| 148 | +Remove geo features & Geo $R^2 = 0.425$ & Geo $R^2 = 0.519$ & $+$9.4 pp; geography hurts generalization \\ | |
| 149 | +Add lat/lon & Random $R^2 = 0.870$ & Geo $R^2 = 0.464$ & Coordinates memorize location \\ | |
| 150 | +Ablation: neighborhood & --- & $+$17.6 pp random & Largest single feature-group contribution \\ | |
| 151 | +Ablation: full model & --- & $-$7.6 pp geo & Region dummies harm generalization \\ | |
| 152 | +\midrule | |
| 153 | +\multicolumn{4}{l}{\textit{SHAP Stability}} \\ | |
| 154 | +XGBoost vs.\ LightGBM & --- & $\rho = 0.990$ & Near-identical rankings \\ | |
| 155 | +XGBoost vs.\ RF & --- & $\rho = 0.906$ & Consistent top-6 features \\ | |
| 156 | +LightGBM vs.\ RF & --- & $\rho = 0.892$ & Consistent top-6 features \\ | |
| 157 | +\bottomrule | |
| 158 | +\end{tabular} | |
| 159 | +\begin{tablenotes} | |
| 160 | +\footnotesize | |
| 161 | +\item \textit{Notes:} This table summarizes all robustness checks conducted in the paper. ``pp'' = percentage points. All findings refer to the same base sample of $N = 788{,}842$ unless noted. The geographic holdout $R^2$ of 0.425 for the full XGBoost model reflects the variant with region dummies included in the ablation design; the main specification yields 0.547 due to differences in training-set composition. | |
| 162 | +\end{tablenotes} | |
| 163 | +\end{threeparttable} | |
| 164 | +\end{table} | |
added
paper/sections/titlepage.tex
+80 −0
@@ -0,0 +1,80 @@ | ||
| 1 | +% Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +% | |
| 3 | +% ============================================================================ | |
| 4 | +% Title Page | |
| 5 | +% ============================================================================ | |
| 6 | +\thispagestyle{empty} | |
| 7 | + | |
| 8 | +\begin{center} | |
| 9 | + | |
| 10 | +% --- Logo --- | |
| 11 | +\includegraphics[width=3.5cm]{uq_logo.jpg} | |
| 12 | + | |
| 13 | +\vspace{0.6cm} | |
| 14 | + | |
| 15 | +{\footnotesize\textsc{Universit\'e du Qu\'ebec en Outaouais}}\\[0.15cm] | |
| 16 | +{\footnotesize\textsc{D\'epartement des sciences administratives}} | |
| 17 | + | |
| 18 | +\vspace{0.8cm} | |
| 19 | + | |
| 20 | +{\footnotesize\textsc{Working Paper No.~\WPnumber}} | |
| 21 | + | |
| 22 | +\vspace{1.2cm} | |
| 23 | + | |
| 24 | +% --- Title --- | |
| 25 | +{\LARGE\bfseries \WPtitle\par} | |
| 26 | + | |
| 27 | +\ifx\WPsubtitle\empty\else | |
| 28 | + \vspace{0.3cm} | |
| 29 | + {\large\itshape \WPsubtitle\par} | |
| 30 | +\fi | |
| 31 | + | |
| 32 | +\vspace{1.2cm} | |
| 33 | + | |
| 34 | +% --- Author --- | |
| 35 | +{\large \WPauthor\footnotemark[1]}\\[0.3cm] | |
| 36 | +{\normalsize\itshape \WPaffiliation} | |
| 37 | + | |
| 38 | +\footnotetext[1]{D\'epartement des sciences administratives, Universit\'e du Qu\'ebec en Outaouais (UQO), 283 boulevard Alexandre-Tach\'e, Gatineau, QC J9A 1L8, Canada. Email: \href{mailto:\WPemail}{\WPemail}. All errors are my own.} | |
| 39 | + | |
| 40 | +\vspace{0.8cm} | |
| 41 | + | |
| 42 | +% --- Date & Version --- | |
| 43 | +{\normalsize First draft: May 2026}\\[0.1cm] | |
| 44 | +{\normalsize This version: \today}\\[0.1cm] | |
| 45 | +{\small\itshape Version~\WPversion} | |
| 46 | + | |
| 47 | +\end{center} | |
| 48 | + | |
| 49 | +\vfill | |
| 50 | + | |
| 51 | +\newpage | |
| 52 | + | |
| 53 | +% ============================================================================ | |
| 54 | +% Abstract Page | |
| 55 | +% ============================================================================ | |
| 56 | +\thispagestyle{empty} | |
| 57 | + | |
| 58 | +\vspace*{1cm} | |
| 59 | + | |
| 60 | +\noindent\rule{\textwidth}{0.4pt} | |
| 61 | +\vspace{0.3cm} | |
| 62 | + | |
| 63 | +\noindent\textbf{Abstract} | |
| 64 | + | |
| 65 | +\vspace{0.15cm} | |
| 66 | + | |
| 67 | +\noindent\WPabstract | |
| 68 | + | |
| 69 | +\vspace{0.4cm} | |
| 70 | + | |
| 71 | +\noindent\textbf{Keywords:} \WPkeywords | |
| 72 | + | |
| 73 | +\vspace{0.15cm} | |
| 74 | + | |
| 75 | +\noindent\textbf{JEL Classification:} \WPjel | |
| 76 | + | |
| 77 | +\vspace{0.3cm} | |
| 78 | +\noindent\rule{\textwidth}{0.4pt} | |
| 79 | + | |
| 80 | +\newpage | |
added
paper/uq_logo.jpg
+0 −0
Binary file not shown.
added
requirements.txt
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +# Pinned to the environment used to validate the restructured pipeline (2026-08-05). | |
| 2 | +numpy==2.4.4 | |
| 3 | +pandas==3.0.2 | |
| 4 | +scikit-learn==1.6.1 | |
| 5 | +xgboost==3.2.0 | |
| 6 | +lightgbm==4.6.0 | |
| 7 | +statsmodels==0.14.6 | |
| 8 | +duckdb==1.5.2 | |
| 9 | +matplotlib==3.10.9 | |
| 10 | +seaborn==0.13.2 | |
| 11 | +shap==0.48.0 | |
| 12 | +scipy==1.17.1 | |
| 13 | +# Spatial statistics (Moran's I) | |
| 14 | +libpysal==4.14.1 | |
| 15 | +esda==2.8.2 | |
added
results/tables/imputation_sensitivity.csv
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +scenario,N,R2,beta_ln_sqft,beta_bedrooms,beta_bathrooms,beta_age,beta_age_sq,beta_ln_lot,beta_has_pool,beta_has_garage,beta_luxury_score,beta_tag_foreclosure,beta_on_waterfront | |
| 2 | +Full sample (baseline),788842,0.6342645712398405,0.6291497085860492,-0.06253937708298193,0.2046302108480406,-0.0013063239445552082,1.2157460512409509e-05,0.01825913172966425,0.028846538996710375,0.10948343518943941,0.034159885302552165,-0.40626594489646634,0.003407492048199623 | |
| 3 | +Drop missing year_built,640055,0.6382685712453622,0.6068184942086208,-0.0628192249474294,0.20265651716406818,-0.0035467801729982054,1.5989806760788056e-05,0.01875321767374919,0.02835688186909949,0.09735360471060732,0.03611810016845016,-0.39330863787402526,-0.19706571029128697 | |
| 4 | +Drop missing lot_size_sqft,664900,0.6505266867333319,0.5774727426791402,-0.0358274818203093,0.1774986550325188,0.002425769595256238,1.0796066581130782e-05,0.058552920800207575,0.03252197316521902,0.10779101746564317,0.027971908904656895,-0.3993766892734994,0.533120329107087 | |
| 5 | +Drop both missing,534803,0.6535187225011015,0.550142646057374,-0.03715353325671315,0.17847739888996103,-0.00024671950231596124,1.4263503675634656e-05,0.05886080812071794,0.0316361881240981,0.09615864029099051,0.030589359019737115,-0.3840536665551645,0.45098599860952976 | |
added
results/tables/iqr_wald_tests.csv
+14 −0
@@ -0,0 +1,14 @@ | ||
| 1 | +variable,beta_010,beta_090,difference,z_stat,significant_5pct | |
| 2 | +ln_sqft,0.35790604655562674,0.25124064331976115,0.1066654032358656,9.523903002291814,True | |
| 3 | +bedrooms,-0.08509454138379624,-0.034109420294328174,-0.050985121089468066,-5.952703196706474,True | |
| 4 | +bathrooms,0.15664322672346032,0.25928991503769794,-0.10264668831423762,-10.479274246632242,True | |
| 5 | +age,-0.4300734891044158,-0.025484597728791414,-0.40458889137562437,-7.8914083434103945,True | |
| 6 | +age_sq,0.04795531448411339,0.037677669962704385,0.010277644521409002,1.3986940652791868,False | |
| 7 | +ln_lot,0.04514788731722774,0.08595968701297352,-0.040811799695745776,-9.492067306312144,True | |
| 8 | +has_pool,0.007618423137671471,0.09431528870845796,-0.08669686557078649,-7.800515479396527,True | |
| 9 | +has_garage,0.2053782963639037,0.020382505062571,0.1849957913013327,28.91856779366193,True | |
| 10 | +luxury_score,0.017959966439434538,0.07189800507285327,-0.05393803863341873,-6.824197492223778,True | |
| 11 | +tag_foreclosure,-0.49355434150106703,-0.3383248078930592,-0.15522953360800784,-4.179376201356296,True | |
| 12 | +on_waterfront,-0.0999201633494522,-0.32230770918795315,0.22238754583850096,1.859780923597348,False | |
| 13 | +region_Northeast,0.33867926474317755,0.602684380188748,-0.2640051154455705,-21.427901680593553,True | |
| 14 | +region_West,0.36468702598631353,0.4503481148149149,-0.08566108882860135,-6.256099295659817,True | |
added
results/tables/morans_i_robustness.csv
+4 −0
@@ -0,0 +1,4 @@ | ||
| 1 | +subsample,morans_i,p_value | |
| 2 | +0,0.2742001982779295,0.001 | |
| 3 | +1,0.28394686013492765,0.001 | |
| 4 | +2,0.26523500038289916,0.001 | |
added
results/tables/ols_standardized.csv
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +variable,coef,se,t,p | |
| 2 | +const,12.510260893818074,0.010085015251814244,1240.48011643488,0.0 | |
| 3 | +ln_sqft,0.3118769507828863,0.0034727326696340393,89.8073593484385,0.0 | |
| 4 | +bedrooms,-0.06750174671812204,0.0023431142166706552,-28.80856009402549,1.6757391475966402e-182 | |
| 5 | +bathrooms,0.23592022288160894,0.0026239092536372664,89.91173096195153,0.0 | |
| 6 | +age,-0.041806282051593036,0.013909448519080027,-3.0056031333122983,0.002650546930905638 | |
| 7 | +age_sq,0.05073769636560884,0.0019160687299147237,26.480102500220315,1.6430989616855433e-154 | |
| 8 | +stories,0.00044870103270214104,0.0002607722243428601,1.7206626734609376,0.08531205343573296 | |
| 9 | +bath_per_bed,-0.02077860220396728,0.002051692538745553,-10.127541925298294,4.170002974353909e-24 | |
| 10 | +sqft_per_bed,-0.002202279119295951,0.002213081859263558,-0.9951186893867533,0.3196785418546034 | |
| 11 | +ln_lot,0.06972230732352985,0.0009582388098584828,72.76088862840649,0.0 | |
| 12 | +has_pool,0.02855245643801721,0.002591720748690671,11.016795097404618,3.1715077869439134e-28 | |
| 13 | +has_spa,0.03268203638057495,0.002650077990726293,12.332480966576368,6.055316663484659e-35 | |
| 14 | +has_basement,-0.042397383881889336,0.0022583155165494286,-18.773897434256668,1.2348489251883573e-78 | |
| 15 | +has_fireplace,-0.002124639739702301,0.0017914558195680202,-1.185985005320769,0.2356282211729842 | |
| 16 | +has_garage,0.10941860136933954,0.0015042257300469397,72.74081222232856,0.0 | |
| 17 | +on_waterfront,-0.13490439969823786,0.03245190385505747,-4.157056556705337,3.223742044568921e-05 | |
| 18 | +has_central_air,0.0692645579279192,0.0013900291557197633,49.82957202221683,0.0 | |
| 19 | +has_forced_air,-0.023327166565504515,0.0015639401751570228,-14.915638677267447,2.6077011459501143e-50 | |
| 20 | +has_hardwood,0.12458888245821044,0.001407077304798409,88.54444743962394,0.0 | |
| 21 | +parking_spaces,0.00024230082313270347,0.0988318278790555,0.0024516476962180315,0.9980438701142379 | |
| 22 | +luxury_score,0.044335991893873175,0.0017593377861741552,25.200386328475297,3.966794290395653e-140 | |
| 23 | +walk_score,-0.010863127056879623,0.001161661699081678,-9.351368875695213,8.652059868843034e-21 | |
| 24 | +bike_score,0.07173946375525904,0.0010054812549646509,71.3483850653995,0.0 | |
| 25 | +transit_score,0.052385177194669896,0.0008181487703601662,64.02891392430847,0.0 | |
| 26 | +avg_school_rating,-0.049120492380129686,0.0007225216024663806,-67.98480794546943,0.0 | |
| 27 | +school_count,-0.0023576570032662423,0.0005212984764327702,-4.522662370700981,6.106660809993482e-06 | |
| 28 | +nearest_school_distance,-0.015745415127310227,0.0007542084928952413,-20.876740683291732,8.713685374626717e-97 | |
| 29 | +property_tax_rate,-0.029150384331210356,0.0007839212795117585,-37.18534640285028,1.1773622067067655e-302 | |
| 30 | +is_condo,-0.037024849313309405,0.0037108756776390367,-9.977388770099044,1.914384690833806e-23 | |
| 31 | +has_hoa,-0.10766211099280076,0.002380035025504934,-45.23551537648477,0.0 | |
| 32 | +ln_hoa,0.0438747141921382,0.001393218972071835,31.49161407620855,1.131527122346012e-217 | |
| 33 | +tag_new_construction,0.10348069905156648,0.002906503876685734,35.60315191100478,1.2521284768511052e-277 | |
| 34 | +tag_foreclosure,-0.4063900836874578,0.00977256203842917,-41.5848046898437,0.0 | |
| 35 | +sqft_x_age,-0.1099721543345997,0.014046827651987046,-7.8289672984663685,4.918935540007701e-15 | |
| 36 | +pool_x_south,0.008119596563043502,0.0011898853194987377,6.823848004498484,8.86336606398563e-12 | |
| 37 | +waterfront_x_sqft,0.13705683824892476,0.009184379717094819,14.922819229024457,2.3416684450919193e-50 | |
| 38 | +basement_x_north,-0.025490040780514485,0.001202556709646256,-21.19653948628554,1.027818251641276e-99 | |
| 39 | +condo_x_walkscore,0.08159583868728772,0.0013346339142952237,61.13724356418426,0.0 | |
| 40 | +age_x_luxury,0.044015662289863276,0.0014835104533609512,29.669937404312904,1.8762436530339638e-193 | |
| 41 | +roof_cat_Metal,0.07017351405102361,0.0080064581660963,8.764613839884465,1.8741795429592264e-18 | |
| 42 | +roof_cat_Other,0.033111615787464,0.007943972240990034,4.1681434404581195,3.0709067058971555e-05 | |
| 43 | +roof_cat_Shingle_Comp,-0.07371516502128181,0.0075964779839938976,-9.703860812418966,2.903009800312546e-22 | |
| 44 | +roof_cat_Slate,0.008371081455198371,0.01719552074117138,0.48681756029379325,0.6263876248732123 | |
| 45 | +roof_cat_Tile,-0.11015041370372027,0.007886469200242828,-13.967012475028582,2.478054122661378e-44 | |
| 46 | +roof_cat_Unknown,0.003744911159938564,0.007631325099806241,0.49072881982627725,0.623618260018022 | |
| 47 | +construction_cat_Concrete_Block,0.2688178184390626,0.002708326314442638,99.25606711626406,0.0 | |
| 48 | +construction_cat_Other,0.07490637003451775,0.0022916057084639745,32.68728549499305,2.3676170023017508e-234 | |
| 49 | +construction_cat_Steel,0.027121960831319965,0.013693857852171594,1.9805931333673747,0.047636919615265276 | |
| 50 | +construction_cat_Stone,0.14499960080534224,0.0045728392047608216,31.70887807609372,1.1721807008428776e-220 | |
| 51 | +construction_cat_Stucco,0.14861249647987582,0.002094455581432234,70.95519131432303,0.0 | |
| 52 | +construction_cat_Unknown,0.17738776599333783,0.0020449810016221395,86.74298971610426,0.0 | |
| 53 | +construction_cat_Vinyl,0.012414692892664397,0.0017968240696294958,6.909242313981413,4.872490696681538e-12 | |
| 54 | +construction_cat_Wood_Frame,0.10255991191206666,0.002145447623394766,47.80350300502091,0.0 | |
| 55 | +foundation_cat_Concrete,0.21729813210649174,0.005164834871110541,42.07261946010064,0.0 | |
| 56 | +foundation_cat_Crawlspace,0.18081354415653514,0.005767725527513882,31.34919359355731,9.97833010620953e-216 | |
| 57 | +foundation_cat_Other,0.14354906108536017,0.005836220968048647,24.59623476753933,1.38597531000125e-133 | |
| 58 | +foundation_cat_Pier_Raised,0.16118491556172415,0.006397232498833677,25.19603837926961,4.426861389485397e-140 | |
| 59 | +foundation_cat_Slab,0.12228167343158515,0.005202499054813795,23.504410504109508,3.676434363492889e-122 | |
| 60 | +foundation_cat_Stone,0.03176682392847073,0.00979433301550258,3.2433881794901036,0.001181171647467094 | |
| 61 | +foundation_cat_Unknown,0.17384703490821618,0.005063287670697755,34.33481291499657,2.373741544030717e-258 | |
| 62 | +region_Northeast,0.48694797327620326,0.003000424496972807,162.29302679254073,0.0 | |
| 63 | +region_South,0.022033665918830242,0.00275039786770285,8.011083115488633,1.1370256980880176e-15 | |
| 64 | +region_West,0.4007941459324695,0.0031009925516071127,129.24705211715354,0.0 | |
added
results/tables/ols_unstandardized.csv
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +variable,coef,se,t,p | |
| 2 | +const,7.731552411430626,0.04952381509131435,156.11786767991975,0.0 | |
| 3 | +ln_sqft,0.6280515162982743,0.006993319037443324,89.8073594148341,0.0 | |
| 4 | +bedrooms,-0.062016885446280354,0.0021527242347853448,-28.80856007665295,1.6757399872754306e-182 | |
| 5 | +bathrooms,0.2034950951356044,0.002263276327734436,89.91173222728185,0.0 | |
| 6 | +age,-0.0013285740540762035,0.00044203175286894314,-3.0056077316918657,0.002650506852571522 | |
| 7 | +age_sq,1.2163220147762203e-05,4.5933432841380117e-07,26.480102607973826,1.643094266731313e-154 | |
| 8 | +stories,1.999196808002408e-05,1.1618760733380352e-05,1.7206626884559,0.08531205071314489 | |
| 9 | +bath_per_bed,-0.07274218850489828,0.007182610295009158,-10.127542149327416,4.1699934226668284e-24 | |
| 10 | +sqft_per_bed,-8.773375098681142e-06,8.816401781370576e-06,-0.9951197003316761,0.3196780502285953 | |
| 11 | +ln_lot,0.01822835968192002,0.0002505241511473948,72.76088791613326,0.0 | |
| 12 | +has_pool,0.02855245680857336,0.0025917207349386235,11.01679529883822,3.1715006918214863e-28 | |
| 13 | +has_spa,0.03268203629160967,0.002650077980658137,12.33248097985902,6.055315665136236e-35 | |
| 14 | +has_basement,-0.042397382730469994,0.002258315445194868,-18.773897517585976,1.2348469879222912e-78 | |
| 15 | +has_fireplace,-0.002124639132704362,0.001791455815808547,-1.1859846689802047,0.23562835400069249 | |
| 16 | +has_garage,0.10941860111370356,0.0015042257290490259,72.7408121006401,0.0 | |
| 17 | +on_waterfront,-0.13490439923749814,0.03245190385518212,-4.157056542491753,3.223742245060588e-05 | |
| 18 | +has_central_air,0.06926455743806333,0.0013900291417454746,49.82957217075828,0.0 | |
| 19 | +has_forced_air,-0.023327165736501523,0.0015639401660140502,-14.91563823439263,2.607718448604021e-50 | |
| 20 | +has_hardwood,0.12458888333245341,0.0014070772993942245,88.5444484010164,0.0 | |
| 21 | +parking_spaces,1.937098870290347e-12,2.8817329670441795e-10,0.0067219929550837165,0.9946366659938872 | |
| 22 | +luxury_score,0.03437015165885438,0.0013638738621125687,25.200388843596453,3.9665424802537024e-140 | |
| 23 | +walk_score,-0.0004098255927286921,4.382519877282263e-05,-9.351368714905583,8.652073023660174e-21 | |
| 24 | +bike_score,0.003969993111833388,5.564236779095722e-05,71.34838558179717,0.0 | |
| 25 | +transit_score,0.0028678286070474898,4.4789586849616713e-05,64.02891405710815,0.0 | |
| 26 | +avg_school_rating,-0.05438967755695603,0.0008000269159661202,-67.98480960015517,0.0 | |
| 27 | +school_count,-0.027348566100848577,0.006047005000764767,-4.522663053427241,6.10664110604767e-06 | |
| 28 | +nearest_school_distance,-0.025822796444772905,0.0012369169938977887,-20.876741585868082,8.713520810475241e-97 | |
| 29 | +property_tax_rate,-0.05528041535348997,0.0014866182034689792,-37.18534807692706,1.1772888639394465e-302 | |
| 30 | +is_condo,-0.037024850230877274,0.003710875674145318,-9.977389026757079,1.9143797402253623e-23 | |
| 31 | +has_hoa,-0.10766211089957795,0.002380034986829691,-45.235516072387036,0.0 | |
| 32 | +ln_hoa,0.011705846177746759,0.0003717130993953816,31.49161597153065,1.1314595192202749e-217 | |
| 33 | +tag_new_construction,0.10348069841334447,0.0029065038727997703,35.603151739021705,1.2521361498711193e-277 | |
| 34 | +tag_foreclosure,-0.4063900837611966,0.009772562040118154,-41.58480469020212,0.0 | |
| 35 | +sqft_x_age,-0.0004717742412965806,6.026012228242753e-05,-7.828962561434343,4.9191208536752785e-15 | |
| 36 | +pool_x_south,0.0181012579304409,0.0026526467707544865,6.823847837566552,8.863376368791729e-12 | |
| 37 | +waterfront_x_sqft,0.06377842531998063,0.004273885816671549,14.922819199145218,2.3416694938484985e-50 | |
| 38 | +basement_x_north,-0.06999128650419585,0.0033020147568158273,-21.19653958533162,1.0278160890252323e-99 | |
| 39 | +condo_x_walkscore,0.003903762187208463,6.385244028989087e-05,61.13724345514962,0.0 | |
| 40 | +age_x_luxury,0.0006905404957930882,2.3274083216529612e-05,29.669933263049252,1.876474464500293e-193 | |
| 41 | +roof_cat_Metal,0.07017351491500201,0.008006458162752392,8.764613951455203,1.8741776869743367e-18 | |
| 42 | +roof_cat_Other,0.03311161692800065,0.0079439722376021,4.168143585808331,3.070904748110175e-05 | |
| 43 | +roof_cat_Shingle_Comp,-0.0737151644870279,0.0075964779808091,-9.703860746158117,2.9030116863303636e-22 | |
| 44 | +roof_cat_Slate,0.008371082721379038,0.017195520750739067,0.4868176336572562,0.6263875728786577 | |
| 45 | +roof_cat_Tile,-0.11015041269051362,0.007886469195774888,-13.967012354467297,2.478058316589432e-44 | |
| 46 | +roof_cat_Unknown,0.003744911784157261,0.007631325096876657,0.49072890181155765,0.6236182020239405 | |
| 47 | +construction_cat_Concrete_Block,0.2688178184033233,0.0027083260980781017,99.25607503250195,0.0 | |
| 48 | +construction_cat_Other,0.07490636957988644,0.002291605707764882,32.687285306575006,2.367631597824313e-234 | |
| 49 | +construction_cat_Steel,0.027121961014964464,0.013693857844589276,1.9805931478747538,0.04763691798703864 | |
| 50 | +construction_cat_Stone,0.1449996007089799,0.004572839207940861,31.708878032970002,1.1721823052803796e-220 | |
| 51 | +construction_cat_Stucco,0.14861249635213813,0.0020944555786406188,70.95519134790784,0.0 | |
| 52 | +construction_cat_Unknown,0.17738776496809056,0.0020449810000075746,86.742989283242,0.0 | |
| 53 | +construction_cat_Vinyl,0.01241469287770574,0.0017968240689596822,6.909242308231962,4.872490894138278e-12 | |
| 54 | +construction_cat_Wood_Frame,0.1025599113278911,0.0021454476196064425,47.80350281714384,0.0 | |
| 55 | +foundation_cat_Concrete,0.21729813260367084,0.005164834849797572,42.072619729977916,0.0 | |
| 56 | +foundation_cat_Crawlspace,0.18081354506771502,0.005767725519314945,31.34919379609988,9.978266684204534e-216 | |
| 57 | +foundation_cat_Other,0.1435490617128185,0.005836220961168693,24.59623490404535,1.3859706488829183e-133 | |
| 58 | +foundation_cat_Pier_Raised,0.16118491616568323,0.006397232489311616,25.19603851118248,4.426846652926976e-140 | |
| 59 | +foundation_cat_Slab,0.12228167350340387,0.005202499046751097,23.50441055434069,3.6764300150687384e-122 | |
| 60 | +foundation_cat_Stone,0.031766824922115286,0.009794333014593305,3.2433882812421766,0.0011811712255876852 | |
| 61 | +foundation_cat_Unknown,0.17384703479286615,0.005063287662034982,34.334812950958316,2.373738610597118e-258 | |
| 62 | +region_Northeast,0.48694797339119117,0.0030004244939206615,162.29302699595522,0.0 | |
| 63 | +region_South,0.022033664372614146,0.0027503978413142283,8.011082630171698,1.1370301856407947e-15 | |
| 64 | +region_West,0.4007941448424641,0.0031009925521863872,129.24705174150773,0.0 | |
added
results/tables/qr_coefficients.csv
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +variable,tau_0.1,tau_0.25,tau_0.5,tau_0.75,tau_0.9 | |
| 2 | +const,11.849922509837597,12.9340569752776,12.905365898221135,12.759226198375472,12.595247685585491 | |
| 3 | +ln_sqft,0.357906558015884,0.31315890465566554,0.27515897025811853,0.2371397708989207,0.2512239633365425 | |
| 4 | +bedrooms,-0.08509469951411575,-0.07625745069513812,-0.0638549497104961,-0.03746177047793253,-0.03410255263050033 | |
| 5 | +bathrooms,0.1566429969949361,0.1950009997400457,0.23448904213131527,0.25865196614199704,0.25930494669773907 | |
| 6 | +age,-0.4300731898485992,-0.2013515666214971,-0.06554365595138734,-0.007241910385346273,-0.02550229170219609 | |
| 7 | +age_sq,0.04795536590722804,0.049199292479329415,0.047199019410783194,0.0371015179084111,0.03769790562205344 | |
| 8 | +stories,0.0010185912817231024,0.0007388941498257529,0.0004229117881728861,6.514740521970652e-05,-0.0003417682422606205 | |
| 9 | +bath_per_bed,-0.0099584967795181,-0.034250431416580795,-0.043648046694777065,-0.04211958023972784,-0.0280950675683016 | |
| 10 | +sqft_per_bed,-0.058342276302596474,-0.016745977628147557,0.01765566532267826,0.06485749954231323,0.07920784764269229 | |
| 11 | +ln_lot,0.04514794272755296,0.05073463930068556,0.061395118988230024,0.07026873810184497,0.08596878008472686 | |
| 12 | +has_pool,0.007619114257181536,0.007567236582929127,0.01852448921101768,0.06089096990388043,0.0943265007304035 | |
| 13 | +has_spa,0.03874780628530061,0.0180789066290572,0.022480035755336303,0.01713714070928063,0.0005072080659260791 | |
| 14 | +has_basement,-0.014815995963431305,-0.021878514973792562,-0.03292261803750307,-0.05359667516574973,-0.07553003676675019 | |
| 15 | +has_fireplace,0.04143406040316222,0.009673776584136817,-0.012110259046437477,-0.030380737792256696,-0.032952579822222106 | |
| 16 | +has_garage,0.2053782753120288,0.14566117388744715,0.10218886135783123,0.06149881536930479,0.020372741030087127 | |
| 17 | +on_waterfront,-0.09992358214367414,-0.1768454478269632,-0.25268568768296973,-0.13252779024332995,-0.32238715834546383 | |
| 18 | +has_central_air,0.11740902357462346,0.08029035727824407,0.0520164489072954,0.0327931879610901,0.016032078730745525 | |
| 19 | +has_forced_air,0.0008684938962915112,-0.005696801255226092,-0.011830238361042689,-0.023570379195859914,-0.01813350176678341 | |
| 20 | +has_hardwood,0.10016602236504468,0.10380894856638179,0.12116497595869191,0.13110461290477815,0.1330114799447415 | |
| 21 | +parking_spaces,-0.013349256571308032,691.2545724222437,317.1821111976169,-97.82401308976114,-527.3458635360003 | |
| 22 | +luxury_score,0.01795960286455056,0.03786385584298557,0.05448826598863832,0.06812524466482728,0.07188080508194616 | |
| 23 | +walk_score,-0.028821622997128316,-0.022953879673423216,-0.009698874555368775,0.0032705419677938607,0.005246771724500832 | |
| 24 | +bike_score,0.04614100701697055,0.05065453518817975,0.06054683836375574,0.07629735539659066,0.08579684220330819 | |
| 25 | +transit_score,0.04534395234946942,0.04497839103453316,0.04730634611325968,0.05451710282051181,0.046503147576459014 | |
| 26 | +avg_school_rating,-0.030127607614179652,-0.02716668107517961,-0.030166600347527606,-0.035105525183567465,-0.0420894113329382 | |
| 27 | +school_count,-0.004427547246081076,-0.0030891381105391247,-0.0018832999121798366,-0.0006446806877562494,0.002757283807554245 | |
| 28 | +nearest_school_distance,-0.009825656338108502,-0.011944345715932059,-0.012153764041791248,-0.011324228484667742,-0.010279004824106153 | |
| 29 | +property_tax_rate,-0.004190249743572849,-0.009798506651442551,-0.019304189575279906,-0.03103226526980052,-0.036209969742230896 | |
| 30 | +is_condo,-0.16580809676270292,-0.1452260638673124,-0.07976984357242145,-0.010947594474828293,0.061623717147995194 | |
| 31 | +has_hoa,-0.10534155607763296,-0.1127840169685328,-0.10753730031827047,-0.10247201722563659,-0.09284689174728555 | |
| 32 | +ln_hoa,0.07649782561563256,0.06853089826370251,0.05281376308886365,0.03951118575315604,0.017864670559447404 | |
| 33 | +tag_new_construction,0.07909377418218355,0.06849862210100355,0.07785836930290202,0.09124375008067886,0.09600302724129506 | |
| 34 | +tag_foreclosure,-0.49355306301913515,-0.4725296085081965,-0.37856554145173504,-0.3266230110490369,-0.33831893924320866 | |
| 35 | +sqft_x_age,0.18235567125043417,0.012388534724777855,-0.08005913709064316,-0.09555883228858875,-0.046699518800778606 | |
| 36 | +pool_x_south,0.006391342583270898,0.005893986986379218,0.005282726790028391,-0.008522821525474455,-0.022043326090674498 | |
| 37 | +waterfront_x_sqft,0.09101773103611777,0.12208861861631704,0.1619417999977486,0.14575440967382747,0.21681571210791617 | |
| 38 | +basement_x_north,-0.01805429847563289,-0.01824052704745327,-0.01526305193999633,-0.013064216110876714,-0.009582837122181331 | |
| 39 | +condo_x_walkscore,0.09998897118856576,0.10163621372083753,0.08864137304550468,0.06819214478559843,0.06868347322483714 | |
| 40 | +age_x_luxury,0.053901540188448394,0.04394070996114863,0.038229670244220415,0.037654097542031195,0.03712006468231266 | |
| 41 | +roof_cat_Metal,-0.00815807405609803,0.04031307685249885,0.035074643255370574,0.07286993336492742,0.12787981100700563 | |
| 42 | +roof_cat_Other,-0.0019282099452198054,0.03822601443198437,-0.0007021735834769061,0.007828057809319944,0.030573480300517986 | |
| 43 | +roof_cat_Shingle_Comp,-0.03762212077720051,-0.02165351267672122,-0.09329276732273684,-0.12490439282146326,-0.1581275001166489 | |
| 44 | +roof_cat_Slate,0.03864796910443147,0.006514172531296936,-0.016504582610650687,0.0021526665204874007,-0.011575405365874758 | |
| 45 | +roof_cat_Tile,-0.08108337379560737,-0.075032508809727,-0.13314550130314728,-0.12073042187557803,-0.11173602040753394 | |
| 46 | +roof_cat_Unknown,0.0013777838218942406,0.029756474222040197,-0.02072716278189546,-0.03015086826167135,-0.04263688984366354 | |
| 47 | +construction_cat_Concrete_Block,0.17894633103583146,0.20191172753504816,0.26795002454690575,0.326430228381696,0.3860183817255347 | |
| 48 | +construction_cat_Other,0.013509989809676881,0.03728601950288635,0.08198672867032997,0.12232397260754624,0.15456783013382847 | |
| 49 | +construction_cat_Steel,-0.01462339848103511,0.03626694733884506,0.05645142016214777,0.07732077046580343,0.07193904185021438 | |
| 50 | +construction_cat_Stone,0.08832289135634142,0.10476105408173453,0.13258447511839222,0.1606987953701946,0.2185698552420945 | |
| 51 | +construction_cat_Stucco,0.07880345120270249,0.10940080498653515,0.15060305662780138,0.1851044073843866,0.22283256319275324 | |
| 52 | +construction_cat_Unknown,0.08293869922476915,0.09932516403432601,0.14293863326452083,0.2040907843756372,0.2694218066098415 | |
| 53 | +construction_cat_Vinyl,0.006936825872799091,0.013151897358341103,0.022626894378319662,0.02607179824077832,0.02853035768589507 | |
| 54 | +construction_cat_Wood_Frame,0.01844167037043576,0.059284417295643266,0.10662683558730635,0.1512926040130651,0.1999362962654061 | |
| 55 | +foundation_cat_Concrete,0.30559956260003673,0.2723207419226128,0.20628665991966955,0.16804842561191435,0.16303328340791268 | |
| 56 | +foundation_cat_Crawlspace,0.27979075512394047,0.2534320335907978,0.18767545719674672,0.14296826369127302,0.11444678607631431 | |
| 57 | +foundation_cat_Other,0.21193125589070405,0.2005622279691579,0.15862356213256135,0.10320323952657873,0.1215280674286987 | |
| 58 | +foundation_cat_Pier_Raised,0.1307646212853797,0.18539704590347128,0.18245021145594364,0.17506331676088394,0.210221242634816 | |
| 59 | +foundation_cat_Slab,0.21363530834241828,0.18968869366472063,0.12643082982253873,0.06004888367613148,0.02197799127475264 | |
| 60 | +foundation_cat_Stone,0.05098890904173459,0.06928267514427944,0.07719787535438627,-0.012921236867214247,-0.07104368534783134 | |
| 61 | +foundation_cat_Unknown,0.22939124871663807,0.22059736013875408,0.17578605651146972,0.15176033675561484,0.14755042134856922 | |
| 62 | +region_Northeast,0.338679523534438,0.39451647456701266,0.47075842575593896,0.5527407433669396,0.6027001958389064 | |
| 63 | +region_South,0.03587540300497949,0.04136913543207754,0.04233883223227508,0.05352910650231024,0.04943155846103764 | |
| 64 | +region_West,0.36468765557879124,0.3722635475263587,0.3759893920067725,0.3989336587744958,0.4503405640348319 | |
added
results/tables/qr_pseudo_r2.csv
+2 −0
@@ -0,0 +1,2 @@ | ||
| 1 | +,tau_0.1,tau_0.25,tau_0.5,tau_0.75,tau_0.9 | |
| 2 | +pseudo_r2,0.5834790121665371,0.5977991277203497,0.6006880166255077,0.5798794175850837,0.5435221562010766 | |
added
results/tables/qr_stability.csv
+14 −0
@@ -0,0 +1,14 @@ | ||
| 1 | +variable,mean,std,cv,min,max,sign_stability_pct | |
| 2 | +ln_sqft,0.27558566047790267,0.007979984014337372,0.02895645586384649,0.262514683814004,0.2900398080532227,100.0 | |
| 3 | +bedrooms,-0.06142257416409365,0.004480907820546131,0.07295213334066966,-0.06913189094325389,-0.054417035188123464,100.0 | |
| 4 | +bathrooms,0.23220617716563724,0.006744238765673522,0.029044183268486966,0.22281742264428317,0.24461819377052052,100.0 | |
| 5 | +age,-0.03969330329951788,0.031183454933027,0.7856099729902236,-0.07583002057288013,0.022312495054848114,80.0 | |
| 6 | +age_sq,0.050343360580962934,0.0026491273568429734,0.05262118631477944,0.046627963846447074,0.05592350627859943,100.0 | |
| 7 | +ln_lot,0.06060109559464877,0.0016125727580704344,0.02660963044062241,0.05783164475802849,0.0641852605609472,100.0 | |
| 8 | +has_pool,0.016577784234322122,0.0055332431489157,0.33377459078396304,0.0035241913606114394,0.02496500668226531,100.0 | |
| 9 | +has_garage,0.09990595309004817,0.004129161681175362,0.04133048685751116,0.0901809332771677,0.10381395666040305,100.0 | |
| 10 | +luxury_score,0.05756019615266013,0.0027435091359714605,0.047663304146760975,0.05414168863399027,0.06371949082392803,100.0 | |
| 11 | +tag_foreclosure,-0.3562744038685913,0.02524205704178479,0.07085004358352698,-0.39371881834512124,-0.3221903705132263,100.0 | |
| 12 | +on_waterfront,-0.2042347839611466,0.08628320336420031,0.4224706570092131,-0.3556472523987395,-0.10086182450322667,100.0 | |
| 13 | +region_Northeast,0.46859634937051736,0.007284731033566231,0.015545855283234872,0.4566879873166698,0.48347329526316685,100.0 | |
| 14 | +region_West,0.3766292734287077,0.0038280863563525894,0.010164070151804622,0.3697378442065542,0.3830368936470734,100.0 | |
added
results/tables/shap_cross_model_stability.csv
+4 −0
@@ -0,0 +1,4 @@ | ||
| 1 | +pair,spearman_rho | |
| 2 | +xgb_lgb,0.9897761325577297 | |
| 3 | +xgb_rf,0.9059706378585279 | |
| 4 | +lgb_rf,0.8917176600941804 | |
added
results/tables/shap_importance_xgb.csv
+63 −0
@@ -0,0 +1,63 @@ | ||
| 1 | +variable,mean_abs_shap | |
| 2 | +ln_sqft,0.17400743067264557 | |
| 3 | +bathrooms,0.16102507710456848 | |
| 4 | +avg_school_rating,0.08814097940921783 | |
| 5 | +region_West,0.0750875398516655 | |
| 6 | +ln_lot,0.07474827021360397 | |
| 7 | +luxury_score,0.07130001485347748 | |
| 8 | +property_tax_rate,0.06940716505050659 | |
| 9 | +nearest_school_distance,0.047175612300634384 | |
| 10 | +region_Northeast,0.04249763488769531 | |
| 11 | +bike_score,0.03995014727115631 | |
| 12 | +ln_hoa,0.039756275713443756 | |
| 13 | +has_garage,0.03778256103396416 | |
| 14 | +age,0.03694174066185951 | |
| 15 | +has_hardwood,0.03108942322432995 | |
| 16 | +waterfront_x_sqft,0.029179977253079414 | |
| 17 | +sqft_per_bed,0.0289430171251297 | |
| 18 | +walk_score,0.027586886659264565 | |
| 19 | +has_central_air,0.02725778892636299 | |
| 20 | +region_South,0.02497166208922863 | |
| 21 | +transit_score,0.02128736861050129 | |
| 22 | +bath_per_bed,0.018676621839404106 | |
| 23 | +roof_cat_Shingle_Comp,0.016837917268276215 | |
| 24 | +sqft_x_age,0.012873374857008457 | |
| 25 | +construction_cat_Unknown,0.012313016690313816 | |
| 26 | +construction_cat_Stucco,0.011333988048136234 | |
| 27 | +parking_spaces,0.010989967733621597 | |
| 28 | +construction_cat_Vinyl,0.01090232189744711 | |
| 29 | +age_sq,0.00922967866063118 | |
| 30 | +construction_cat_Concrete_Block,0.00876736268401146 | |
| 31 | +on_waterfront,0.00808318704366684 | |
| 32 | +stories,0.007524613291025162 | |
| 33 | +foundation_cat_Concrete,0.006802935153245926 | |
| 34 | +has_basement,0.006560285110026598 | |
| 35 | +has_hoa,0.006379136350005865 | |
| 36 | +age_x_luxury,0.006090256851166487 | |
| 37 | +is_condo,0.005778271704912186 | |
| 38 | +condo_x_walkscore,0.005612872075289488 | |
| 39 | +pool_x_south,0.005442899186164141 | |
| 40 | +tag_new_construction,0.00536991935223341 | |
| 41 | +has_forced_air,0.00524168973788619 | |
| 42 | +foundation_cat_Slab,0.004994309041649103 | |
| 43 | +roof_cat_Unknown,0.004852370359003544 | |
| 44 | +bedrooms,0.004661286249756813 | |
| 45 | +construction_cat_Wood_Frame,0.00442053796723485 | |
| 46 | +has_pool,0.004373344127088785 | |
| 47 | +foundation_cat_Unknown,0.004030699376016855 | |
| 48 | +roof_cat_Metal,0.003528366796672344 | |
| 49 | +has_fireplace,0.003385707037523389 | |
| 50 | +construction_cat_Other,0.0029181374702602625 | |
| 51 | +tag_foreclosure,0.00278071197681129 | |
| 52 | +basement_x_north,0.0024371198378503323 | |
| 53 | +foundation_cat_Pier_Raised,0.002085316926240921 | |
| 54 | +construction_cat_Stone,0.0020824067760258913 | |
| 55 | +has_spa,0.0017726804362609982 | |
| 56 | +roof_cat_Tile,0.0017147306352853775 | |
| 57 | +roof_cat_Other,0.0014831859152764082 | |
| 58 | +foundation_cat_Crawlspace,0.0010341695742681623 | |
| 59 | +foundation_cat_Other,0.0008585686446167529 | |
| 60 | +school_count,0.0008040699176490307 | |
| 61 | +foundation_cat_Stone,0.0003373564686626196 | |
| 62 | +construction_cat_Steel,0.00010494200978428125 | |
| 63 | +roof_cat_Slate,7.787979120621458e-05 | |
added
results/tables/winsorization_sensitivity.csv
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +metric,baseline,winsorized | |
| 2 | +R2,0.6342645712398404,0.6401619168061571 | |
| 3 | +ln_sqft,0.6291497085846827,0.6099742481392126 | |
| 4 | +bedrooms,-0.06253937708152302,-0.0656463321516763 | |
| 5 | +bathrooms,0.20463021084777544,0.21129309773012286 | |
| 6 | +age,-0.0013063239445989644,-0.0013267039528491729 | |
| 7 | +age_sq,1.2157460512799388e-05,1.2749921975046489e-05 | |
| 8 | +ln_lot,0.018259131729344444,0.05797235290868282 | |
| 9 | +has_pool,0.02884653899651412,0.04653675127720655 | |
| 10 | +has_garage,0.10948343519065148,0.11090106864123753 | |
| 11 | +luxury_score,0.03415988530255404,0.029578966638762333 | |
| 12 | +tag_foreclosure,-0.4062659448964657,-0.399588031598668 | |
| 13 | +on_waterfront,0.003407492048275243,0.002838262287207377 | |
added
results/tables/xgb_geography_and_ablation.csv
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +model,r2_random,rmse_random,r2_geo,rmse_geo,best_iter_random,best_iter_geo,n_features | |
| 2 | +xgb_with_latlon,0.8701343524505045,0.29100481864238364,0.4643710647418329,0.5907699864382361,999,999,64 | |
| 3 | +xgb_no_geography,0.8297270178380317,0.3332160476706253,0.5189513788612397,0.5598618763028691,999,999,59 | |
| 4 | +ablation_1_structural,0.49759250513418396,0.5723755745047215,0.3480964725892921,0.6517455487276005,447,247,8 | |
| 5 | +ablation_2_plus_lot,0.5463896097218952,0.5438692873177148,0.37648383980897226,0.6373973670896643,757,511,9 | |
| 6 | +ablation_3_plus_amenities,0.6378381360594687,0.48596433334928646,0.44853685927476183,0.5994385280796517,994,987,20 | |
| 7 | +ablation_4_plus_neighborhood,0.8135639213624183,0.34867275840756023,0.48333530521849133,0.5802174702473957,999,999,27 | |
| 8 | +ablation_5_plus_market,0.8234756311061102,0.3392777438168081,0.5013714590700792,0.5700001565163754,999,999,32 | |
| 9 | +ablation_6_full_model,0.8326609311614387,0.33033280996484216,0.42501500587502516,0.6120891251660002,999,999,62 | |
added
results/tables/zip3_fixed_effects.csv
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +,0 | |
| 2 | +r2_insample,0.7286602580843261 | |
| 3 | +r2_outsample,0.7253003287542753 | |
| 4 | +rmse_outsample,0.423235518860347 | |
| 5 | +n_zip3,886.0 | |
| 6 | +n_features,947.0 | |
added
scripts/01_spatial_robustness.py
+167 −0
@@ -0,0 +1,167 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 3 | +# | |
| 4 | +"""Spatial and robustness analysis (refactor of the original run_v3_spatial.py). | |
| 5 | + | |
| 6 | +Produces results/v3_spatial_results.pkl with: | |
| 7 | + 1. ZIP3 fixed-effects OLS (in/out-of-sample R2) | |
| 8 | + 2. Moran's I on OLS residuals (3 random subsamples of 5,000, KNN k=8) | |
| 9 | + 3. XGBoost with latitude/longitude | |
| 10 | + 4. XGBoost without any geographic features | |
| 11 | + 5. Ablation analysis over 6 progressive feature sets | |
| 12 | + | |
| 13 | +Usage: python scripts/01_spatial_robustness.py [--output results/v3_spatial_results.pkl] | |
| 14 | +""" | |
| 15 | + | |
| 16 | +import argparse | |
| 17 | +import pickle | |
| 18 | +import sys | |
| 19 | +import time | |
| 20 | +import warnings | |
| 21 | +from pathlib import Path | |
| 22 | + | |
| 23 | +import numpy as np | |
| 24 | +import pandas as pd | |
| 25 | +from sklearn.linear_model import LinearRegression | |
| 26 | +from sklearn.metrics import mean_squared_error, r2_score | |
| 27 | +from sklearn.model_selection import train_test_split | |
| 28 | + | |
| 29 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | |
| 30 | +from wp3 import config, data | |
| 31 | +from wp3.models import train_xgb | |
| 32 | + | |
| 33 | +warnings.filterwarnings("ignore") | |
| 34 | + | |
| 35 | + | |
| 36 | +def zip3_fixed_effects_ols(df, X_full, y, idx_train, idx_test): | |
| 37 | + """OLS with 3-digit-ZIP fixed effects, evaluated on the random holdout.""" | |
| 38 | + zip3 = df["address_zipcode"].fillna("000").str[:3] | |
| 39 | + zip3_dummies = pd.get_dummies(zip3, prefix="zip3", drop_first=True).astype(np.int8) | |
| 40 | + X_zip3 = pd.concat([X_full, zip3_dummies], axis=1) | |
| 41 | + | |
| 42 | + ols = LinearRegression(n_jobs=-1) | |
| 43 | + ols.fit(X_zip3.iloc[idx_train].values, y[idx_train]) | |
| 44 | + y_pred_train = ols.predict(X_zip3.iloc[idx_train].values) | |
| 45 | + y_pred_test = ols.predict(X_zip3.iloc[idx_test].values) | |
| 46 | + | |
| 47 | + return { | |
| 48 | + "r2_insample": r2_score(y[idx_train], y_pred_train), | |
| 49 | + "r2_outsample": r2_score(y[idx_test], y_pred_test), | |
| 50 | + "rmse_outsample": float(np.sqrt(mean_squared_error(y[idx_test], y_pred_test))), | |
| 51 | + "n_zip3": int(zip3.nunique()), | |
| 52 | + "n_features": X_zip3.shape[1], | |
| 53 | + } | |
| 54 | + | |
| 55 | + | |
| 56 | +def morans_i_robustness(df, X_full, y, idx_train, n_subsamples=3, size=5000, k=8): | |
| 57 | + """Moran's I on baseline-OLS residuals over independent random subsamples.""" | |
| 58 | + from esda.moran import Moran | |
| 59 | + from libpysal.weights import KNN | |
| 60 | + | |
| 61 | + ols = LinearRegression(n_jobs=-1) | |
| 62 | + ols.fit(X_full.iloc[idx_train].values, y[idx_train]) | |
| 63 | + residuals = y - ols.predict(X_full.values) | |
| 64 | + | |
| 65 | + coords = df[["latitude", "longitude"]].values | |
| 66 | + values, pvalues = [], [] | |
| 67 | + for i in range(n_subsamples): | |
| 68 | + rng = np.random.RandomState(config.RANDOM_STATE + i) | |
| 69 | + idx_sub = rng.choice(len(df), size=size, replace=False) | |
| 70 | + w = KNN.from_array(coords[idx_sub], k=k) | |
| 71 | + w.transform = "r" | |
| 72 | + mi = Moran(residuals[idx_sub], w) | |
| 73 | + values.append(mi.I) | |
| 74 | + pvalues.append(mi.p_sim) | |
| 75 | + print(f" Subsample {i + 1}: Moran's I = {mi.I:.4f}, p = {mi.p_sim:.4f}") | |
| 76 | + | |
| 77 | + return { | |
| 78 | + "values": values, | |
| 79 | + "pvalues": pvalues, | |
| 80 | + "mean": float(np.mean(values)), | |
| 81 | + "std": float(np.std(values)), | |
| 82 | + "pval_mean": float(np.mean(pvalues)), | |
| 83 | + "n_subsamples": n_subsamples, | |
| 84 | + "subsample_size": size, | |
| 85 | + "k_neighbors": k, | |
| 86 | + } | |
| 87 | + | |
| 88 | + | |
| 89 | +def main(): | |
| 90 | + parser = argparse.ArgumentParser(description=__doc__) | |
| 91 | + parser.add_argument("--output", type=Path, | |
| 92 | + default=config.RESULTS_DIR / "v3_spatial_results.pkl") | |
| 93 | + args = parser.parse_args() | |
| 94 | + | |
| 95 | + t0 = time.time() | |
| 96 | + print("Loading analytical sample...") | |
| 97 | + df = data.load_analytical_sample() | |
| 98 | + X_full, parts = data.build_feature_matrix(df) | |
| 99 | + y = df["ln_price"].values | |
| 100 | + print(f" {df.shape[0]:,} rows, {X_full.shape[1]} regressors") | |
| 101 | + | |
| 102 | + idx_train, idx_test = train_test_split( | |
| 103 | + np.arange(len(df)), test_size=0.2, random_state=config.RANDOM_STATE) | |
| 104 | + geo_mask = df["address_state"].isin(config.HOLDOUT_STATES).values | |
| 105 | + idx_geo_train, idx_geo_test = np.where(~geo_mask)[0], np.where(geo_mask)[0] | |
| 106 | + | |
| 107 | + results = {} | |
| 108 | + | |
| 109 | + print("\n1. ZIP3 fixed-effects OLS...") | |
| 110 | + results["zip3_fe_ols"] = zip3_fixed_effects_ols(df, X_full, y, idx_train, idx_test) | |
| 111 | + print(f" R2 in={results['zip3_fe_ols']['r2_insample']:.4f} " | |
| 112 | + f"out={results['zip3_fe_ols']['r2_outsample']:.4f}") | |
| 113 | + | |
| 114 | + print("\n2. Moran's I robustness...") | |
| 115 | + results["morans_i_robustness"] = morans_i_robustness(df, X_full, y, idx_train) | |
| 116 | + | |
| 117 | + print("\n3. XGBoost with latitude/longitude...") | |
| 118 | + X_latlon = pd.concat([X_full, df[["latitude", "longitude"]]], axis=1) | |
| 119 | + results["xgb_with_latlon"] = train_xgb( | |
| 120 | + X_latlon.values.astype(np.float32), y, | |
| 121 | + idx_train, idx_test, idx_geo_train, idx_geo_test) | |
| 122 | + print(f" R2 random={results['xgb_with_latlon']['r2_random']:.4f} " | |
| 123 | + f"geo={results['xgb_with_latlon']['r2_geo']:.4f}") | |
| 124 | + | |
| 125 | + print("\n4. XGBoost without geographic features...") | |
| 126 | + X_nogeo = pd.concat( | |
| 127 | + [df[config.ALL_BASE_FEATS], | |
| 128 | + parts["cat_dummies"][parts["pure_categorical_dummies"]]], axis=1) | |
| 129 | + results["xgb_no_geography"] = train_xgb( | |
| 130 | + X_nogeo.values.astype(np.float32), y, | |
| 131 | + idx_train, idx_test, idx_geo_train, idx_geo_test) | |
| 132 | + print(f" R2 random={results['xgb_no_geography']['r2_random']:.4f} " | |
| 133 | + f"geo={results['xgb_no_geography']['r2_geo']:.4f}") | |
| 134 | + | |
| 135 | + print("\n5. Ablation analysis...") | |
| 136 | + ablation_specs = [ | |
| 137 | + ("1_structural", config.STRUCTURAL_FEATS, []), | |
| 138 | + ("2_plus_lot", config.STRUCTURAL_FEATS + config.LOT_FEATS, []), | |
| 139 | + ("3_plus_amenities", | |
| 140 | + config.STRUCTURAL_FEATS + config.LOT_FEATS + config.AMENITY_FEATS, []), | |
| 141 | + ("4_plus_neighborhood", | |
| 142 | + config.STRUCTURAL_FEATS + config.LOT_FEATS + config.AMENITY_FEATS | |
| 143 | + + config.NEIGHBORHOOD_FEATS, []), | |
| 144 | + ("5_plus_market", | |
| 145 | + config.STRUCTURAL_FEATS + config.LOT_FEATS + config.AMENITY_FEATS | |
| 146 | + + config.NEIGHBORHOOD_FEATS + config.MARKET_FEATS, []), | |
| 147 | + ("6_full_model", config.ALL_BASE_FEATS, list(parts["cat_dummies"].columns)), | |
| 148 | + ] | |
| 149 | + ablation = {} | |
| 150 | + for name, feats, dummy_cols in ablation_specs: | |
| 151 | + X_abl = (pd.concat([df[feats], parts["cat_dummies"][dummy_cols]], axis=1) | |
| 152 | + if dummy_cols else df[feats]) | |
| 153 | + res = train_xgb(X_abl.values.astype(np.float32), y, | |
| 154 | + idx_train, idx_test, idx_geo_train, idx_geo_test) | |
| 155 | + res["features"] = list(X_abl.columns) | |
| 156 | + ablation[name] = res | |
| 157 | + print(f" {name}: R2 random={res['r2_random']:.4f} geo={res['r2_geo']:.4f}") | |
| 158 | + results["ablation"] = ablation | |
| 159 | + | |
| 160 | + args.output.parent.mkdir(parents=True, exist_ok=True) | |
| 161 | + with open(args.output, "wb") as f: | |
| 162 | + pickle.dump(results, f) | |
| 163 | + print(f"\nSaved to {args.output} ({time.time() - t0:.0f}s total)") | |
| 164 | + | |
| 165 | + | |
| 166 | +if __name__ == "__main__": | |
| 167 | + main() | |
added
scripts/02_qr_imputation.py
+210 −0
@@ -0,0 +1,210 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 3 | +# | |
| 4 | +"""Quantile-regression stability, inter-quantile tests, imputation and | |
| 5 | +winsorization sensitivity (refactor of the original v3_qr_imputation_analysis.py). | |
| 6 | + | |
| 7 | +Produces results/v3_qr_imputation_results.pkl with: | |
| 8 | + 1. QR coefficient stability at tau=0.50 over 10 subsamples of 150,000 | |
| 9 | + 2. Inter-quantile Wald tests (tau=0.10 vs tau=0.90) | |
| 10 | + 3. Imputation sensitivity: OLS re-estimated after dropping observations with | |
| 11 | + originally missing year_built / lot_size_sqft (missingness read from DuckDB) | |
| 12 | + 4. Winsorization sensitivity: lot size capped at the 99.5th percentile, | |
| 13 | + bike score capped at 100 | |
| 14 | + | |
| 15 | +Usage: python scripts/02_qr_imputation.py [--output results/v3_qr_imputation_results.pkl] | |
| 16 | +""" | |
| 17 | + | |
| 18 | +import argparse | |
| 19 | +import pickle | |
| 20 | +import sys | |
| 21 | +import time | |
| 22 | +import warnings | |
| 23 | +from pathlib import Path | |
| 24 | + | |
| 25 | +import duckdb | |
| 26 | +import numpy as np | |
| 27 | +import pandas as pd | |
| 28 | +import statsmodels.api as sm | |
| 29 | +from sklearn.linear_model import LinearRegression | |
| 30 | + | |
| 31 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | |
| 32 | +from wp3 import config, data | |
| 33 | + | |
| 34 | +warnings.filterwarnings("ignore") | |
| 35 | + | |
| 36 | +OLS_FEATURES = ["ln_sqft", "bedrooms", "bathrooms", "age", "age_sq", | |
| 37 | + "ln_lot", "has_pool", "has_garage", "luxury_score", | |
| 38 | + "tag_foreclosure", "on_waterfront"] | |
| 39 | + | |
| 40 | + | |
| 41 | +def qr_stability(X_const, y_clean): | |
| 42 | + """Median-regression coefficient stability across 10 random subsamples.""" | |
| 43 | + coef_matrix = {v: [] for v in config.KEY_VARS} | |
| 44 | + for i, seed in enumerate(config.QR_STABILITY_SEEDS): | |
| 45 | + rng = np.random.RandomState(seed) | |
| 46 | + idx = rng.choice(len(X_const), size=config.QR_SUBSAMPLE_SIZE, replace=False) | |
| 47 | + print(f" QR subsample {i + 1}/{len(config.QR_STABILITY_SEEDS)} (seed={seed})...") | |
| 48 | + res = sm.QuantReg(y_clean.iloc[idx], X_const.iloc[idx]).fit(q=0.50, max_iter=1000) | |
| 49 | + for v in config.KEY_VARS: | |
| 50 | + coef_matrix[v].append(res.params[v]) | |
| 51 | + | |
| 52 | + stability = {} | |
| 53 | + for v, vals_list in coef_matrix.items(): | |
| 54 | + vals = np.array(vals_list) | |
| 55 | + m, s = vals.mean(), vals.std() | |
| 56 | + stability[v] = { | |
| 57 | + "mean": m, "std": s, | |
| 58 | + "cv": abs(s / m) if abs(m) > 1e-10 else np.inf, | |
| 59 | + "min": vals.min(), "max": vals.max(), | |
| 60 | + "sign_stability_pct": float(np.mean(np.sign(vals) == np.sign(m)) * 100), | |
| 61 | + } | |
| 62 | + return stability | |
| 63 | + | |
| 64 | + | |
| 65 | +def inter_quantile_test(X_const, y_clean): | |
| 66 | + """Wald z-tests for coefficient equality between tau=0.10 and tau=0.90.""" | |
| 67 | + rng = np.random.RandomState(config.RANDOM_STATE) | |
| 68 | + idx = rng.choice(len(X_const), size=config.QR_SUBSAMPLE_SIZE, replace=False) | |
| 69 | + X_sub, y_sub = X_const.iloc[idx], y_clean.iloc[idx] | |
| 70 | + | |
| 71 | + print(" Fitting QR at tau=0.10 and tau=0.90...") | |
| 72 | + res10 = sm.QuantReg(y_sub, X_sub).fit(q=0.10, max_iter=1000) | |
| 73 | + res90 = sm.QuantReg(y_sub, X_sub).fit(q=0.90, max_iter=1000) | |
| 74 | + | |
| 75 | + out = {} | |
| 76 | + for v in config.KEY_VARS: | |
| 77 | + diff = res10.params[v] - res90.params[v] | |
| 78 | + z = diff / np.sqrt(res10.bse[v] ** 2 + res90.bse[v] ** 2) | |
| 79 | + out[v] = { | |
| 80 | + "beta_010": res10.params[v], "beta_090": res90.params[v], | |
| 81 | + "difference": diff, "z_stat": z, | |
| 82 | + "significant_5pct": bool(abs(z) > 1.96), | |
| 83 | + } | |
| 84 | + return out | |
| 85 | + | |
| 86 | + | |
| 87 | +def load_missingness_flags(): | |
| 88 | + """Read original missingness of year_built / lot_size_sqft from the raw DB.""" | |
| 89 | + con = duckdb.connect(str(config.DUCKDB_PATH), read_only=True) | |
| 90 | + flags = con.execute(""" | |
| 91 | + SELECT zpid, | |
| 92 | + CASE WHEN year_built IS NULL THEN 1 ELSE 0 END AS yb_missing, | |
| 93 | + CASE WHEN lot_size_sqft IS NULL THEN 1 ELSE 0 END AS lot_missing | |
| 94 | + FROM properties | |
| 95 | + """).fetchdf() | |
| 96 | + con.close() | |
| 97 | + flags["zpid"] = flags["zpid"].astype(np.int64) | |
| 98 | + return flags | |
| 99 | + | |
| 100 | + | |
| 101 | +def build_unstandardized_matrix(df_merged, X_const): | |
| 102 | + """Rebuild the OLS design matrix on the raw (unstandardized) scale.""" | |
| 103 | + xcols = [c for c in X_const.columns if c != "const"] | |
| 104 | + missing = [c for c in xcols if c not in df_merged.columns] | |
| 105 | + if missing: | |
| 106 | + for cat_col in config.CATEGORICAL_COLS: | |
| 107 | + if cat_col not in df_merged.columns: | |
| 108 | + continue | |
| 109 | + dummies = pd.get_dummies(df_merged[cat_col], prefix=cat_col) | |
| 110 | + for dc in dummies.columns: | |
| 111 | + if dc in missing: | |
| 112 | + df_merged[dc] = dummies[dc].astype(float) | |
| 113 | + return df_merged[xcols].values.astype(np.float64), xcols | |
| 114 | + | |
| 115 | + | |
| 116 | +def imputation_sensitivity(df_merged, X_unstd, xcols): | |
| 117 | + """OLS on subsets that drop originally-missing year_built / lot_size rows.""" | |
| 118 | + y = df_merged["ln_price"].values | |
| 119 | + | |
| 120 | + def run(mask, label): | |
| 121 | + lr = LinearRegression() | |
| 122 | + lr.fit(X_unstd[mask], y[mask]) | |
| 123 | + coefs = dict(zip(xcols, lr.coef_)) | |
| 124 | + return {"label": label, "N": int(mask.sum()), | |
| 125 | + "R2": lr.score(X_unstd[mask], y[mask]), | |
| 126 | + "key_coefficients": {v: coefs[v] for v in OLS_FEATURES}} | |
| 127 | + | |
| 128 | + no_yb = df_merged["yb_missing"].values == 0 | |
| 129 | + no_lot = df_merged["lot_missing"].values == 0 | |
| 130 | + scenarios = [ | |
| 131 | + (np.ones(len(df_merged), dtype=bool), "Full sample (baseline)"), | |
| 132 | + (no_yb, "Drop missing year_built"), | |
| 133 | + (no_lot, "Drop missing lot_size_sqft"), | |
| 134 | + (no_yb & no_lot, "Drop both missing"), | |
| 135 | + ] | |
| 136 | + out = [] | |
| 137 | + for mask, label in scenarios: | |
| 138 | + r = run(mask, label) | |
| 139 | + out.append(r) | |
| 140 | + print(f" {label:<30} N={r['N']:>9,} R2={r['R2']:.4f}") | |
| 141 | + return out | |
| 142 | + | |
| 143 | + | |
| 144 | +def winsorization_sensitivity(df_merged, X_unstd, xcols): | |
| 145 | + """Re-estimate OLS with lot size winsorized at p99.5 and bike score capped at 100.""" | |
| 146 | + df_w = df_merged.copy() | |
| 147 | + y = df_merged["ln_price"].values | |
| 148 | + | |
| 149 | + lot_p995 = df_w["lot_size_sqft"].quantile(0.995) | |
| 150 | + n_lot = int((df_w["lot_size_sqft"] > lot_p995).sum()) | |
| 151 | + df_w["lot_size_sqft"] = df_w["lot_size_sqft"].clip(upper=lot_p995) | |
| 152 | + df_w["ln_lot"] = np.where(df_w["lot_size_sqft"] > 0, np.log(df_w["lot_size_sqft"]), 0.0) | |
| 153 | + | |
| 154 | + n_bike = int((df_w["bike_score"] > 100).sum()) | |
| 155 | + df_w["bike_score"] = df_w["bike_score"].clip(upper=100) | |
| 156 | + | |
| 157 | + def fit(X): | |
| 158 | + lr = LinearRegression() | |
| 159 | + lr.fit(X, y) | |
| 160 | + coefs = dict(zip(xcols, lr.coef_)) | |
| 161 | + return {"R2": lr.score(X, y), | |
| 162 | + "key_coefficients": {v: coefs[v] for v in OLS_FEATURES}} | |
| 163 | + | |
| 164 | + X_w = df_w[xcols].values.astype(np.float64) | |
| 165 | + return { | |
| 166 | + "baseline": fit(X_unstd), | |
| 167 | + "winsorized": fit(X_w), | |
| 168 | + "lot_p995": lot_p995, | |
| 169 | + "n_lot_winsorized": n_lot, | |
| 170 | + "n_bike_capped": n_bike, | |
| 171 | + } | |
| 172 | + | |
| 173 | + | |
| 174 | +def main(): | |
| 175 | + parser = argparse.ArgumentParser(description=__doc__) | |
| 176 | + parser.add_argument("--output", type=Path, | |
| 177 | + default=config.RESULTS_DIR / "v3_qr_imputation_results.pkl") | |
| 178 | + args = parser.parse_args() | |
| 179 | + | |
| 180 | + t0 = time.time() | |
| 181 | + print("Loading model data...") | |
| 182 | + md = data.load_model_data() | |
| 183 | + X_const, y_clean, df_clean = md["X_const"], md["y_clean"], md["df_clean"] | |
| 184 | + | |
| 185 | + results = {} | |
| 186 | + | |
| 187 | + print("\n1. QR stability (tau=0.50, 10 subsamples)...") | |
| 188 | + results["qr_stability"] = qr_stability(X_const, y_clean) | |
| 189 | + | |
| 190 | + print("\n2. Inter-quantile difference test (tau=0.10 vs 0.90)...") | |
| 191 | + results["iqr_test"] = inter_quantile_test(X_const, y_clean) | |
| 192 | + | |
| 193 | + print("\n3. Imputation sensitivity...") | |
| 194 | + flags = load_missingness_flags() | |
| 195 | + df_merged = df_clean.merge(flags, on="zpid", how="left") | |
| 196 | + X_unstd, xcols = build_unstandardized_matrix(df_merged, X_const) | |
| 197 | + results["imputation_sensitivity"] = imputation_sensitivity(df_merged, X_unstd, xcols) | |
| 198 | + | |
| 199 | + print("\n4. Winsorization sensitivity...") | |
| 200 | + results["winsorization_sensitivity"] = winsorization_sensitivity( | |
| 201 | + df_merged, X_unstd, xcols) | |
| 202 | + | |
| 203 | + args.output.parent.mkdir(parents=True, exist_ok=True) | |
| 204 | + with open(args.output, "wb") as f: | |
| 205 | + pickle.dump(results, f) | |
| 206 | + print(f"\nSaved to {args.output} ({time.time() - t0:.0f}s total)") | |
| 207 | + | |
| 208 | + | |
| 209 | +if __name__ == "__main__": | |
| 210 | + main() | |
added
scripts/03_make_figures.py
+426 −0
@@ -0,0 +1,426 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 3 | +# | |
| 4 | +"""Regenerate all 11 paper figures from the stored pickles. | |
| 5 | + | |
| 6 | +Every figure is rebuilt from the data actually used in the paper | |
| 7 | +(analytical_sample.pkl, model_data.pkl, ml_results.pkl, qr_results.pkl, | |
| 8 | +shap_data.pkl) in a unified publication style. The script prints | |
| 9 | +verification statistics (Jarque-Bera, skewness, kurtosis, model R2, OLS | |
| 10 | +reference coefficients) so the regenerated content can be checked against | |
| 11 | +the numbers reported in the paper. The six original PNGs remain untouched | |
| 12 | +in the source archive (immo-wp3-spb-20260519/figures/). | |
| 13 | + | |
| 14 | +Usage: | |
| 15 | + python scripts/03_make_figures.py # all 11 figures | |
| 16 | + python scripts/03_make_figures.py --figs fig2 fig4 | |
| 17 | +""" | |
| 18 | + | |
| 19 | +import argparse | |
| 20 | +import sys | |
| 21 | +from pathlib import Path | |
| 22 | + | |
| 23 | +import matplotlib | |
| 24 | + | |
| 25 | +matplotlib.use("Agg") | |
| 26 | +import matplotlib.pyplot as plt | |
| 27 | +import numpy as np | |
| 28 | +import pandas as pd | |
| 29 | +import scipy.stats as st | |
| 30 | + | |
| 31 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | |
| 32 | +from wp3 import config, data | |
| 33 | +from wp3.plotting import (ACCENT, CMAP_DIV, CMAP_SEQ, NEUTRAL, PRIMARY, | |
| 34 | + PRIMARY_LIGHT, apply_paper_style, label, panel_title) | |
| 35 | + | |
| 36 | +RNG = np.random.RandomState(config.RANDOM_STATE) | |
| 37 | + | |
| 38 | +FILE_NAMES = { | |
| 39 | + "fig1": "fig1_price_distribution.png", | |
| 40 | + "fig2": "fig2_ols_diagnostics.png", | |
| 41 | + "fig3": "fig3_quantile_coefficients.png", | |
| 42 | + "fig4": "fig4_model_comparison.png", | |
| 43 | + "fig5": "fig5_shap_summary.png", | |
| 44 | + "fig6": "fig6_shap_importance.png", | |
| 45 | + "fig7": "fig7_geographic_prices.png", | |
| 46 | + "fig8": "fig8_regional_prices.png", | |
| 47 | + "fig9": "fig9_marginal_effects.png", | |
| 48 | + "fig10": "fig10_shap_dependence.png", | |
| 49 | + "fig11": "fig11_shap_interactions.png", | |
| 50 | +} | |
| 51 | + | |
| 52 | + | |
| 53 | +def fig1_price_distribution(path): | |
| 54 | + """Histograms of price (thousands) and log-price with median markers.""" | |
| 55 | + df = data.load_analytical_sample() | |
| 56 | + price_k = df["price"].values / 1000.0 | |
| 57 | + | |
| 58 | + fig, axes = plt.subplots(1, 2, figsize=(16, 6)) | |
| 59 | + ax = axes[0] | |
| 60 | + ax.hist(price_k[price_k <= 3000], bins=100, color=PRIMARY, alpha=0.85, | |
| 61 | + edgecolor="white", linewidth=0.2) | |
| 62 | + med = np.median(price_k) | |
| 63 | + ax.axvline(med, color=ACCENT, linestyle="--", linewidth=1.8, | |
| 64 | + label=f"Median: \\${med:,.0f}K") | |
| 65 | + ax.set_xlabel("Listing Price (\\$ thousands)") | |
| 66 | + ax.set_ylabel("Frequency") | |
| 67 | + ax.yaxis.set_major_formatter(plt.matplotlib.ticker.StrMethodFormatter("{x:,.0f}")) | |
| 68 | + panel_title(ax, "(a) Price Distribution") | |
| 69 | + ax.legend() | |
| 70 | + | |
| 71 | + ax = axes[1] | |
| 72 | + ax.hist(df["ln_price"], bins=100, color=PRIMARY, alpha=0.85, | |
| 73 | + edgecolor="white", linewidth=0.2) | |
| 74 | + med_ln = df["ln_price"].median() | |
| 75 | + ax.axvline(med_ln, color=ACCENT, linestyle="--", linewidth=1.8, | |
| 76 | + label=f"Median: {med_ln:.2f}") | |
| 77 | + ax.set_xlabel("Log(Price)") | |
| 78 | + ax.set_ylabel("Frequency") | |
| 79 | + ax.yaxis.set_major_formatter(plt.matplotlib.ticker.StrMethodFormatter("{x:,.0f}")) | |
| 80 | + panel_title(ax, "(b) Log-Price Distribution") | |
| 81 | + ax.legend() | |
| 82 | + | |
| 83 | + fig.tight_layout() | |
| 84 | + fig.savefig(path) | |
| 85 | + plt.close(fig) | |
| 86 | + | |
| 87 | + | |
| 88 | +def fig2_ols_diagnostics(path): | |
| 89 | + """OLS diagnostics: residuals vs fitted, Q-Q, distribution, scale-location.""" | |
| 90 | + md = data.load_model_data() | |
| 91 | + resid = np.asarray(md["residuals"], dtype=float) | |
| 92 | + fitted = np.asarray(md["y_clean"], dtype=float) - resid | |
| 93 | + | |
| 94 | + jb = st.jarque_bera(resid)[0] | |
| 95 | + print(f" [verify] skew={st.skew(resid):.2f} (paper: 0.18) " | |
| 96 | + f"kurtosis={st.kurtosis(resid, fisher=False):.2f} (paper: 5.55) " | |
| 97 | + f"JB={jb:,.0f} (paper: 217,078)") | |
| 98 | + | |
| 99 | + idx = RNG.choice(len(resid), size=100_000, replace=False) | |
| 100 | + | |
| 101 | + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) | |
| 102 | + | |
| 103 | + ax = axes[0, 0] | |
| 104 | + ax.hexbin(fitted[idx], resid[idx], gridsize=60, cmap=CMAP_SEQ, mincnt=1) | |
| 105 | + ax.axhline(0, color=ACCENT, linestyle="--", linewidth=1.5) | |
| 106 | + ax.set_xlabel("Fitted Values") | |
| 107 | + ax.set_ylabel("Residuals") | |
| 108 | + panel_title(ax, "(a) Residuals vs. Fitted") | |
| 109 | + | |
| 110 | + ax = axes[0, 1] | |
| 111 | + (osm, osr), (slope, intercept, _) = st.probplot(resid[idx], dist="norm") | |
| 112 | + ax.plot(osm, osr, ".", color=PRIMARY, markersize=2.5, rasterized=True) | |
| 113 | + ax.plot(osm, slope * osm + intercept, color=ACCENT, linestyle="--", linewidth=1.5) | |
| 114 | + ax.set_xlabel("Theoretical Quantiles") | |
| 115 | + ax.set_ylabel("Sample Quantiles") | |
| 116 | + panel_title(ax, "(b) Normal Q-Q Plot") | |
| 117 | + | |
| 118 | + ax = axes[1, 0] | |
| 119 | + ax.hist(resid, bins=100, color=PRIMARY, alpha=0.85, density=True, | |
| 120 | + edgecolor="white", linewidth=0.2) | |
| 121 | + x = np.linspace(resid.min(), resid.max(), 400) | |
| 122 | + ax.plot(x, st.norm.pdf(x, resid.mean(), resid.std()), color=ACCENT, | |
| 123 | + linestyle="--", linewidth=1.6, label="Normal density") | |
| 124 | + ax.set_xlabel("Residuals") | |
| 125 | + ax.set_ylabel("Density") | |
| 126 | + panel_title(ax, "(c) Residual Distribution") | |
| 127 | + ax.legend() | |
| 128 | + | |
| 129 | + ax = axes[1, 1] | |
| 130 | + ax.hexbin(fitted[idx], np.sqrt(np.abs(resid[idx] / resid.std())), | |
| 131 | + gridsize=60, cmap=CMAP_SEQ, mincnt=1) | |
| 132 | + ax.set_xlabel("Fitted Values") | |
| 133 | + ax.set_ylabel(r"$\sqrt{|\mathrm{Standardized\ Residuals}|}$") | |
| 134 | + panel_title(ax, "(d) Scale-Location") | |
| 135 | + | |
| 136 | + fig.tight_layout() | |
| 137 | + fig.savefig(path) | |
| 138 | + plt.close(fig) | |
| 139 | + | |
| 140 | + | |
| 141 | +def fig3_quantile_coefficients(path): | |
| 142 | + """Quantile-regression coefficient paths with the OLS benchmark.""" | |
| 143 | + qr = data.load_result("qr_results.pkl") | |
| 144 | + md = data.load_model_data() | |
| 145 | + ols_params = md["ols_results"]["params"] | |
| 146 | + taus = sorted(qr.keys()) | |
| 147 | + | |
| 148 | + plot_vars = ["ln_sqft", "bathrooms", "age", "ln_lot", | |
| 149 | + "has_pool", "has_garage", "luxury_score", "tag_foreclosure"] | |
| 150 | + print(f" [verify] OLS ref ln_sqft={ols_params['ln_sqft']:.3f} " | |
| 151 | + f"(original figure: 0.312)") | |
| 152 | + | |
| 153 | + fig, axes = plt.subplots(2, 4, figsize=(19, 9)) | |
| 154 | + for k, (ax, v) in enumerate(zip(axes.ravel(), plot_vars)): | |
| 155 | + coefs = [qr[t]["params"][v] for t in taus] | |
| 156 | + ax.plot(taus, coefs, color=PRIMARY, marker="o", markersize=6, | |
| 157 | + linewidth=2, zorder=3) | |
| 158 | + ax.axhline(ols_params[v], color=ACCENT, linestyle="--", linewidth=1.5, | |
| 159 | + label="OLS") | |
| 160 | + ax.set_xticks(taus) | |
| 161 | + ax.set_xlabel(r"Quantile $\tau$") | |
| 162 | + if k % 4 == 0: | |
| 163 | + ax.set_ylabel("Coefficient") | |
| 164 | + panel_title(ax, f"({chr(97 + k)}) {label(v)}") | |
| 165 | + if k == 0: | |
| 166 | + ax.legend(loc="best") | |
| 167 | + | |
| 168 | + fig.tight_layout() | |
| 169 | + fig.savefig(path) | |
| 170 | + plt.close(fig) | |
| 171 | + | |
| 172 | + | |
| 173 | +def fig4_model_comparison(path): | |
| 174 | + """Predicted vs actual log-prices on the random test set: OLS, XGB, LGBM.""" | |
| 175 | + from sklearn.metrics import r2_score | |
| 176 | + | |
| 177 | + ml = data.load_result("ml_results.pkl") | |
| 178 | + y_test = np.asarray(ml["y_test"], dtype=float) | |
| 179 | + panels = [ | |
| 180 | + ("(a) OLS", ml["y_pred_ols"]), | |
| 181 | + ("(b) XGBoost", ml["y_pred_xgb"]), | |
| 182 | + ("(c) LightGBM", ml["y_pred_lgb"]), | |
| 183 | + ] | |
| 184 | + | |
| 185 | + idx = RNG.choice(len(y_test), size=50_000, replace=False) | |
| 186 | + lims = (y_test.min() - 0.1, y_test.max() + 0.1) | |
| 187 | + | |
| 188 | + fig, axes = plt.subplots(1, 3, figsize=(18, 6), sharex=True, sharey=True) | |
| 189 | + for ax, (title, y_pred) in zip(axes, panels): | |
| 190 | + y_pred = np.asarray(y_pred, dtype=float) | |
| 191 | + r2 = r2_score(y_test, y_pred) | |
| 192 | + print(f" [verify] {title.split(') ')[1]}: R2={r2:.4f} " | |
| 193 | + f"(paper: OLS 0.630 / XGBoost 0.833 / LightGBM 0.809)") | |
| 194 | + ax.hexbin(y_test[idx], y_pred[idx], gridsize=70, cmap=CMAP_SEQ, mincnt=1) | |
| 195 | + ax.plot(lims, lims, color=ACCENT, linestyle="--", linewidth=1.5) | |
| 196 | + ax.set_xlim(lims) | |
| 197 | + ax.set_ylim(lims) | |
| 198 | + ax.set_xlabel("Actual Log(Price)") | |
| 199 | + panel_title(ax, title) | |
| 200 | + ax.text(0.05, 0.92, f"$R^2 = {r2:.3f}$", transform=ax.transAxes, | |
| 201 | + fontsize=13) | |
| 202 | + axes[0].set_ylabel("Predicted Log(Price)") | |
| 203 | + | |
| 204 | + fig.tight_layout() | |
| 205 | + fig.savefig(path) | |
| 206 | + plt.close(fig) | |
| 207 | + | |
| 208 | + | |
| 209 | +def _shap_frame(): | |
| 210 | + """SHAP values and features with human-readable column labels.""" | |
| 211 | + sd = data.load_shap_data() | |
| 212 | + X = sd["X_shap"].copy() | |
| 213 | + return sd, X | |
| 214 | + | |
| 215 | + | |
| 216 | +def fig5_shap_summary(path): | |
| 217 | + """SHAP beeswarm summary plot for the XGBoost model (top 20 features).""" | |
| 218 | + import shap | |
| 219 | + | |
| 220 | + sd, X = _shap_frame() | |
| 221 | + X_lab = X.rename(columns={c: label(c) for c in X.columns}) | |
| 222 | + plt.figure(figsize=(12, 10)) | |
| 223 | + shap.summary_plot(sd["shap_values_xgb"], X_lab, max_display=20, show=False, | |
| 224 | + plot_size=None) | |
| 225 | + ax = plt.gca() | |
| 226 | + ax.set_xlabel("SHAP Value (impact on predicted log-price)") | |
| 227 | + ax.grid(axis="y", visible=False) | |
| 228 | + plt.tight_layout() | |
| 229 | + plt.savefig(path) | |
| 230 | + plt.close() | |
| 231 | + | |
| 232 | + | |
| 233 | +def fig6_shap_importance(path): | |
| 234 | + """Mean |SHAP| feature-importance bar chart (top 20).""" | |
| 235 | + sd, _ = _shap_frame() | |
| 236 | + mean_shap = pd.Series(sd["mean_shap"]).sort_values(ascending=True).tail(20) | |
| 237 | + | |
| 238 | + fig, ax = plt.subplots(figsize=(11, 9)) | |
| 239 | + ax.barh([label(v) for v in mean_shap.index], mean_shap.values, | |
| 240 | + color=PRIMARY, alpha=0.9, height=0.72) | |
| 241 | + ax.set_xlabel(r"Mean $|$SHAP Value$|$") | |
| 242 | + ax.grid(axis="x", linestyle=":", linewidth=0.7, alpha=0.45, color="#999999") | |
| 243 | + ax.grid(axis="y", visible=False) | |
| 244 | + fig.tight_layout() | |
| 245 | + fig.savefig(path) | |
| 246 | + plt.close(fig) | |
| 247 | + | |
| 248 | + | |
| 249 | +def fig7_geographic_prices(path): | |
| 250 | + """Map of log listing prices (50,000-listing random subsample).""" | |
| 251 | + df = data.load_analytical_sample() | |
| 252 | + idx = RNG.choice(len(df), size=50_000, replace=False) | |
| 253 | + sub = df.iloc[idx] | |
| 254 | + | |
| 255 | + fig, ax = plt.subplots(figsize=(15, 9)) | |
| 256 | + sc = ax.scatter(sub["longitude"], sub["latitude"], c=sub["ln_price"], | |
| 257 | + s=3.5, cmap="viridis", alpha=0.75, linewidths=0, | |
| 258 | + rasterized=True) | |
| 259 | + cbar = fig.colorbar(sc, ax=ax, shrink=0.75, pad=0.02) | |
| 260 | + cbar.set_label("Log(Price)") | |
| 261 | + cbar.outline.set_visible(False) | |
| 262 | + ax.set_xlabel("Longitude") | |
| 263 | + ax.set_ylabel("Latitude") | |
| 264 | + ax.grid(False) | |
| 265 | + ax.set_axisbelow(True) | |
| 266 | + | |
| 267 | + fig.tight_layout() | |
| 268 | + fig.savefig(path) | |
| 269 | + plt.close(fig) | |
| 270 | + | |
| 271 | + | |
| 272 | +def fig8_regional_prices(path): | |
| 273 | + """Distribution of log listing prices by Census region.""" | |
| 274 | + df = data.load_analytical_sample() | |
| 275 | + order = ["Northeast", "Midwest", "South", "West"] | |
| 276 | + groups = [df.loc[df["region"] == r, "ln_price"].values for r in order] | |
| 277 | + labels_n = [f"{r}\n(N = {len(g):,})" for r, g in zip(order, groups)] | |
| 278 | + | |
| 279 | + fig, ax = plt.subplots(figsize=(10, 6.5)) | |
| 280 | + ax.boxplot(groups, tick_labels=labels_n, showfliers=False, patch_artist=True, | |
| 281 | + widths=0.55, | |
| 282 | + medianprops=dict(color=ACCENT, linewidth=1.8), | |
| 283 | + boxprops=dict(facecolor=PRIMARY, alpha=0.75, edgecolor="#333333"), | |
| 284 | + whiskerprops=dict(color="#333333", linewidth=1.0), | |
| 285 | + capprops=dict(color="#333333", linewidth=1.0)) | |
| 286 | + ax.set_ylabel("Log(Price)") | |
| 287 | + fig.tight_layout() | |
| 288 | + fig.savefig(path) | |
| 289 | + plt.close(fig) | |
| 290 | + | |
| 291 | + | |
| 292 | +def fig9_marginal_effects(path): | |
| 293 | + """Unconditional bivariate relationships: scatter plus binned means.""" | |
| 294 | + df = data.load_analytical_sample() | |
| 295 | + specs = [ | |
| 296 | + ("living_area_sqft", "Living Area (sqft)", (0, 8000)), | |
| 297 | + ("age", "Property Age (years)", (0, 150)), | |
| 298 | + ("lot_size_sqft", "Lot Size (sqft)", (0, 45000)), | |
| 299 | + ("bedrooms", "Bedrooms", None), | |
| 300 | + ("bathrooms", "Bathrooms", (0, 8)), | |
| 301 | + ("avg_school_rating", "Avg. School Rating", None), | |
| 302 | + ] | |
| 303 | + idx = RNG.choice(len(df), size=30_000, replace=False) | |
| 304 | + sub = df.iloc[idx] | |
| 305 | + | |
| 306 | + fig, axes = plt.subplots(2, 3, figsize=(18, 10)) | |
| 307 | + for k, (ax, (col, xlabel, xlim)) in enumerate(zip(axes.ravel(), specs)): | |
| 308 | + x, y = sub[col].values, sub["ln_price"].values | |
| 309 | + if xlim is not None: | |
| 310 | + keep = (x >= xlim[0]) & (x <= xlim[1]) | |
| 311 | + x, y = x[keep], y[keep] | |
| 312 | + ax.scatter(x, y, s=2.5, color=PRIMARY_LIGHT, alpha=0.2, linewidths=0, | |
| 313 | + rasterized=True) | |
| 314 | + | |
| 315 | + # Binned means computed on the full sample | |
| 316 | + xf, yf = df[col].values, df["ln_price"].values | |
| 317 | + if xlim is not None: | |
| 318 | + keepf = (xf >= xlim[0]) & (xf <= xlim[1]) | |
| 319 | + xf, yf = xf[keepf], yf[keepf] | |
| 320 | + bins = np.linspace(np.nanmin(xf), np.nanmax(xf), 25) | |
| 321 | + which = np.digitize(xf, bins) | |
| 322 | + centers = [xf[which == b].mean() for b in range(1, len(bins)) | |
| 323 | + if (which == b).sum() > 50] | |
| 324 | + means = [yf[which == b].mean() for b in range(1, len(bins)) | |
| 325 | + if (which == b).sum() > 50] | |
| 326 | + ax.plot(centers, means, color=ACCENT, linewidth=2.2, marker="o", | |
| 327 | + markersize=4.5, label="Binned mean", zorder=3) | |
| 328 | + | |
| 329 | + ax.set_xlabel(xlabel) | |
| 330 | + if k % 3 == 0: | |
| 331 | + ax.set_ylabel("Log(Price)") | |
| 332 | + if col in ("living_area_sqft", "lot_size_sqft"): | |
| 333 | + ax.xaxis.set_major_formatter( | |
| 334 | + plt.matplotlib.ticker.StrMethodFormatter("{x:,.0f}")) | |
| 335 | + panel_title(ax, f"({chr(97 + k)}) {xlabel}") | |
| 336 | + if k == 0: | |
| 337 | + ax.legend(loc="lower right") | |
| 338 | + | |
| 339 | + fig.tight_layout() | |
| 340 | + fig.savefig(path) | |
| 341 | + plt.close(fig) | |
| 342 | + | |
| 343 | + | |
| 344 | +def fig10_shap_dependence(path): | |
| 345 | + """SHAP dependence plots for the eight features shown in the paper.""" | |
| 346 | + sd, X = _shap_frame() | |
| 347 | + shap_vals = sd["shap_values_xgb"] | |
| 348 | + feats = ["ln_sqft", "bathrooms", "ln_lot", "avg_school_rating", | |
| 349 | + "property_tax_rate", "age", "walk_score", "luxury_score"] | |
| 350 | + | |
| 351 | + fig, axes = plt.subplots(2, 4, figsize=(20, 9.5)) | |
| 352 | + for k, (ax, feat) in enumerate(zip(axes.ravel(), feats)): | |
| 353 | + j = list(X.columns).index(feat) | |
| 354 | + xv = X[feat].values | |
| 355 | + ax.scatter(xv, shap_vals[:, j], c=xv, cmap=CMAP_DIV, s=4, alpha=0.6, | |
| 356 | + linewidths=0, rasterized=True) | |
| 357 | + ax.axhline(0, color=NEUTRAL, linestyle="--", linewidth=1.1) | |
| 358 | + ax.set_xlabel(label(feat)) | |
| 359 | + if k % 4 == 0: | |
| 360 | + ax.set_ylabel("SHAP Value") | |
| 361 | + panel_title(ax, f"({chr(97 + k)}) {label(feat)}") | |
| 362 | + | |
| 363 | + fig.tight_layout() | |
| 364 | + fig.savefig(path) | |
| 365 | + plt.close(fig) | |
| 366 | + | |
| 367 | + | |
| 368 | +def fig11_shap_interactions(path): | |
| 369 | + """SHAP values of the two interaction features highlighted in the paper.""" | |
| 370 | + sd, X = _shap_frame() | |
| 371 | + shap_vals = sd["shap_values_xgb"] | |
| 372 | + panels = [ | |
| 373 | + ("waterfront_x_sqft", "(a) Waterfront $\\times$ Living Area Interaction"), | |
| 374 | + ("pool_x_south", "(b) Pool $\\times$ South Region Interaction"), | |
| 375 | + ] | |
| 376 | + | |
| 377 | + fig, axes = plt.subplots(1, 2, figsize=(16, 6.5)) | |
| 378 | + for ax, (feat, title) in zip(axes, panels): | |
| 379 | + j = list(X.columns).index(feat) | |
| 380 | + ax.scatter(X[feat].values, shap_vals[:, j], s=6, color=PRIMARY, | |
| 381 | + alpha=0.35, linewidths=0, rasterized=True) | |
| 382 | + ax.axhline(0, color=NEUTRAL, linestyle="--", linewidth=1.1) | |
| 383 | + ax.set_xlabel(label(feat)) | |
| 384 | + ax.set_ylabel("SHAP Value") | |
| 385 | + panel_title(ax, title) | |
| 386 | + | |
| 387 | + fig.tight_layout() | |
| 388 | + fig.savefig(path) | |
| 389 | + plt.close(fig) | |
| 390 | + | |
| 391 | + | |
| 392 | +BUILDERS = { | |
| 393 | + "fig1": fig1_price_distribution, | |
| 394 | + "fig2": fig2_ols_diagnostics, | |
| 395 | + "fig3": fig3_quantile_coefficients, | |
| 396 | + "fig4": fig4_model_comparison, | |
| 397 | + "fig5": fig5_shap_summary, | |
| 398 | + "fig6": fig6_shap_importance, | |
| 399 | + "fig7": fig7_geographic_prices, | |
| 400 | + "fig8": fig8_regional_prices, | |
| 401 | + "fig9": fig9_marginal_effects, | |
| 402 | + "fig10": fig10_shap_dependence, | |
| 403 | + "fig11": fig11_shap_interactions, | |
| 404 | +} | |
| 405 | + | |
| 406 | + | |
| 407 | +def main(): | |
| 408 | + parser = argparse.ArgumentParser(description=__doc__) | |
| 409 | + parser.add_argument("--figs", nargs="+", choices=sorted(BUILDERS), | |
| 410 | + help="build only these figures") | |
| 411 | + args = parser.parse_args() | |
| 412 | + | |
| 413 | + apply_paper_style() | |
| 414 | + config.FIGURES_DIR.mkdir(parents=True, exist_ok=True) | |
| 415 | + | |
| 416 | + names = args.figs if args.figs else list(BUILDERS) | |
| 417 | + for name in names: | |
| 418 | + path = config.FIGURES_DIR / FILE_NAMES[name] | |
| 419 | + print(f"Building {name} -> {path.relative_to(config.PROJECT_ROOT)}") | |
| 420 | + BUILDERS[name](path) | |
| 421 | + | |
| 422 | + print("Done.") | |
| 423 | + | |
| 424 | + | |
| 425 | +if __name__ == "__main__": | |
| 426 | + main() | |
added
scripts/04_export_tables.py
+135 −0
@@ -0,0 +1,135 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 3 | +# | |
| 4 | +"""Export the key result tables of the paper as CSV files into results/tables/. | |
| 5 | + | |
| 6 | +Each CSV mirrors a table in the paper and is generated directly from the | |
| 7 | +stored result pickles, providing a machine-readable audit trail between the | |
| 8 | +pickles and the numbers reported in the LaTeX source. | |
| 9 | + | |
| 10 | +Usage: python scripts/04_export_tables.py | |
| 11 | +""" | |
| 12 | + | |
| 13 | +import sys | |
| 14 | +from pathlib import Path | |
| 15 | + | |
| 16 | +import numpy as np | |
| 17 | +import pandas as pd | |
| 18 | + | |
| 19 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | |
| 20 | +from wp3 import config, data | |
| 21 | + | |
| 22 | +OUT = config.RESULTS_DIR / "tables" | |
| 23 | + | |
| 24 | + | |
| 25 | +def export_qr_coefficients(): | |
| 26 | + """Quantile regression coefficients across taus (paper Table: QR results).""" | |
| 27 | + qr = data.load_result("qr_results.pkl") | |
| 28 | + taus = sorted(qr.keys()) | |
| 29 | + rows = {} | |
| 30 | + for t in taus: | |
| 31 | + rows[f"tau_{t}"] = pd.Series(qr[t]["params"]) | |
| 32 | + df = pd.DataFrame(rows) | |
| 33 | + df.index.name = "variable" | |
| 34 | + df.to_csv(OUT / "qr_coefficients.csv") | |
| 35 | + pd.DataFrame({f"tau_{t}": {"pseudo_r2": qr[t]["pseudo_r2"]} for t in taus}) \ | |
| 36 | + .to_csv(OUT / "qr_pseudo_r2.csv") | |
| 37 | + | |
| 38 | + | |
| 39 | +def export_iqr_tests(): | |
| 40 | + """Inter-quantile Wald tests, tau=0.10 vs tau=0.90.""" | |
| 41 | + r = data.load_result("v3_qr_imputation_results.pkl") | |
| 42 | + df = pd.DataFrame(r["iqr_test"]).T | |
| 43 | + df.index.name = "variable" | |
| 44 | + df.to_csv(OUT / "iqr_wald_tests.csv") | |
| 45 | + | |
| 46 | + | |
| 47 | +def export_qr_stability(): | |
| 48 | + """QR subsample stability (CV, sign stability).""" | |
| 49 | + r = data.load_result("v3_qr_imputation_results.pkl") | |
| 50 | + df = pd.DataFrame(r["qr_stability"]).T | |
| 51 | + df.index.name = "variable" | |
| 52 | + df.to_csv(OUT / "qr_stability.csv") | |
| 53 | + | |
| 54 | + | |
| 55 | +def export_imputation_winsorization(): | |
| 56 | + """Imputation and winsorization sensitivity.""" | |
| 57 | + r = data.load_result("v3_qr_imputation_results.pkl") | |
| 58 | + imp = pd.DataFrame([ | |
| 59 | + {"scenario": s["label"], "N": s["N"], "R2": s["R2"], | |
| 60 | + **{f"beta_{k}": v for k, v in s["key_coefficients"].items()}} | |
| 61 | + for s in r["imputation_sensitivity"] | |
| 62 | + ]) | |
| 63 | + imp.to_csv(OUT / "imputation_sensitivity.csv", index=False) | |
| 64 | + | |
| 65 | + w = r["winsorization_sensitivity"] | |
| 66 | + win = pd.DataFrame({ | |
| 67 | + "baseline": {"R2": w["baseline"]["R2"], **w["baseline"]["key_coefficients"]}, | |
| 68 | + "winsorized": {"R2": w["winsorized"]["R2"], **w["winsorized"]["key_coefficients"]}, | |
| 69 | + }) | |
| 70 | + win.index.name = "metric" | |
| 71 | + win.to_csv(OUT / "winsorization_sensitivity.csv") | |
| 72 | + | |
| 73 | + | |
| 74 | +def export_spatial_and_ablation(): | |
| 75 | + """ZIP3 FE, Moran robustness, XGBoost geography variants, ablation.""" | |
| 76 | + v3 = data.load_result("v3_spatial_results.pkl") | |
| 77 | + | |
| 78 | + pd.Series(v3["zip3_fe_ols"]).to_csv(OUT / "zip3_fixed_effects.csv") | |
| 79 | + | |
| 80 | + m = v3["morans_i_robustness"] | |
| 81 | + pd.DataFrame({"morans_i": m["values"], "p_value": m["pvalues"]}) \ | |
| 82 | + .to_csv(OUT / "morans_i_robustness.csv", index_label="subsample") | |
| 83 | + | |
| 84 | + rows = [] | |
| 85 | + for name in ["xgb_with_latlon", "xgb_no_geography"]: | |
| 86 | + rows.append({"model": name, **{k: v for k, v in v3[name].items()}}) | |
| 87 | + for stage, res in v3["ablation"].items(): | |
| 88 | + rows.append({"model": f"ablation_{stage}", | |
| 89 | + **{k: v for k, v in res.items() if k != "features"}}) | |
| 90 | + pd.DataFrame(rows).to_csv(OUT / "xgb_geography_and_ablation.csv", index=False) | |
| 91 | + | |
| 92 | + | |
| 93 | +def export_shap(): | |
| 94 | + """Mean |SHAP| importance and cross-model stability.""" | |
| 95 | + sd = data.load_shap_data() | |
| 96 | + pd.Series(sd["mean_shap"], name="mean_abs_shap") \ | |
| 97 | + .sort_values(ascending=False) \ | |
| 98 | + .to_csv(OUT / "shap_importance_xgb.csv", index_label="variable") | |
| 99 | + | |
| 100 | + st = data.load_result("v3_shap_stability.pkl") | |
| 101 | + pd.DataFrame({ | |
| 102 | + "pair": ["xgb_lgb", "xgb_rf", "lgb_rf"], | |
| 103 | + "spearman_rho": [float(st["rho_xgb_lgb"]), float(st["rho_xgb_rf"]), | |
| 104 | + float(st["rho_lgb_rf"])], | |
| 105 | + }).to_csv(OUT / "shap_cross_model_stability.csv", index=False) | |
| 106 | + | |
| 107 | + | |
| 108 | +def export_ols(): | |
| 109 | + """Standardized and unstandardized OLS coefficients.""" | |
| 110 | + md = data.load_model_data() | |
| 111 | + ols = md["ols_results"] | |
| 112 | + df = pd.DataFrame({"coef": ols["params"], "se": ols["bse"], | |
| 113 | + "t": ols["tvalues"], "p": ols["pvalues"]}) | |
| 114 | + df.index.name = "variable" | |
| 115 | + df.to_csv(OUT / "ols_standardized.csv") | |
| 116 | + | |
| 117 | + ext = data.load_result("extended_results.pkl") | |
| 118 | + df_u = pd.DataFrame({"coef": ext["unstd_params"], "se": ext["unstd_bse"], | |
| 119 | + "t": ext["unstd_tvalues"], "p": ext["unstd_pvalues"]}) | |
| 120 | + df_u.index.name = "variable" | |
| 121 | + df_u.to_csv(OUT / "ols_unstandardized.csv") | |
| 122 | + | |
| 123 | + | |
| 124 | +def main(): | |
| 125 | + OUT.mkdir(parents=True, exist_ok=True) | |
| 126 | + for fn in [export_ols, export_qr_coefficients, export_iqr_tests, | |
| 127 | + export_qr_stability, export_imputation_winsorization, | |
| 128 | + export_spatial_and_ablation, export_shap]: | |
| 129 | + print(f"Exporting {fn.__name__.replace('export_', '')}...") | |
| 130 | + fn() | |
| 131 | + print(f"Done. CSVs written to {OUT}") | |
| 132 | + | |
| 133 | + | |
| 134 | +if __name__ == "__main__": | |
| 135 | + main() | |
added
src/wp3/__init__.py
+3 −0
@@ -0,0 +1,3 @@ | ||
| 1 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +# | |
| 3 | +"""UQO Working Paper 3 — hedonic housing price analysis (US Zillow listings).""" | |
added
src/wp3/config.py
+54 −0
@@ -0,0 +1,54 @@ | ||
| 1 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +# | |
| 3 | +"""Project paths and analysis constants. | |
| 4 | + | |
| 5 | +All paths are derived from the repository root so the pipeline can be run | |
| 6 | +from any working directory. Override the data location by setting the | |
| 7 | +environment variable ``WP3_ROOT`` before running any script. | |
| 8 | +""" | |
| 9 | + | |
| 10 | +import os | |
| 11 | +from pathlib import Path | |
| 12 | + | |
| 13 | +PROJECT_ROOT = Path(os.environ.get("WP3_ROOT", Path(__file__).resolve().parents[2])) | |
| 14 | + | |
| 15 | +DATA_RAW = PROJECT_ROOT / "data" / "raw" | |
| 16 | +DATA_PROCESSED = PROJECT_ROOT / "data" / "processed" | |
| 17 | +RESULTS_DIR = PROJECT_ROOT / "results" | |
| 18 | +FIGURES_DIR = PROJECT_ROOT / "figures" | |
| 19 | + | |
| 20 | +DUCKDB_PATH = DATA_RAW / "us_housing.duckdb" | |
| 21 | +ANALYTICAL_SAMPLE = DATA_PROCESSED / "analytical_sample.pkl" | |
| 22 | +MODEL_DATA = DATA_PROCESSED / "model_data.pkl" | |
| 23 | +SHAP_DATA = DATA_PROCESSED / "shap_data.pkl" | |
| 24 | + | |
| 25 | +# Reproducibility | |
| 26 | +RANDOM_STATE = 42 | |
| 27 | +QR_STABILITY_SEEDS = [42, 100, 200, 300, 400, 500, 600, 700, 800, 900] | |
| 28 | +QR_SUBSAMPLE_SIZE = 150_000 | |
| 29 | + | |
| 30 | +# States held out entirely in the geographic-validation exercise | |
| 31 | +HOLDOUT_STATES = ["CA", "NY", "TX", "FL", "OH", "CO", "NC", "WA", "IL", "GA"] | |
| 32 | + | |
| 33 | +# Feature blocks (raw, unstandardized names in analytical_sample) | |
| 34 | +STRUCTURAL_FEATS = ["ln_sqft", "bedrooms", "bathrooms", "age", "age_sq", "stories", | |
| 35 | + "bath_per_bed", "sqft_per_bed"] | |
| 36 | +LOT_FEATS = ["ln_lot"] | |
| 37 | +AMENITY_FEATS = ["has_pool", "has_spa", "has_basement", "has_fireplace", "has_garage", | |
| 38 | + "on_waterfront", "has_central_air", "has_forced_air", "has_hardwood", | |
| 39 | + "parking_spaces", "luxury_score"] | |
| 40 | +NEIGHBORHOOD_FEATS = ["walk_score", "bike_score", "transit_score", "avg_school_rating", | |
| 41 | + "school_count", "nearest_school_distance", "property_tax_rate"] | |
| 42 | +MARKET_FEATS = ["is_condo", "has_hoa", "ln_hoa", "tag_new_construction", "tag_foreclosure"] | |
| 43 | +INTERACTION_FEATS = ["sqft_x_age", "pool_x_south", "waterfront_x_sqft", "basement_x_north", | |
| 44 | + "condo_x_walkscore", "age_x_luxury"] | |
| 45 | + | |
| 46 | +ALL_BASE_FEATS = (STRUCTURAL_FEATS + LOT_FEATS + AMENITY_FEATS + NEIGHBORHOOD_FEATS + | |
| 47 | + MARKET_FEATS + INTERACTION_FEATS) | |
| 48 | + | |
| 49 | +CATEGORICAL_COLS = ["roof_cat", "construction_cat", "foundation_cat", "region"] | |
| 50 | + | |
| 51 | +# Variables tracked in the quantile-regression robustness exercises | |
| 52 | +KEY_VARS = ["ln_sqft", "bedrooms", "bathrooms", "age", "age_sq", | |
| 53 | + "ln_lot", "has_pool", "has_garage", "luxury_score", | |
| 54 | + "tag_foreclosure", "on_waterfront", "region_Northeast", "region_West"] | |
added
src/wp3/data.py
+58 −0
@@ -0,0 +1,58 @@ | ||
| 1 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +# | |
| 3 | +"""Loaders for the processed datasets and stored result pickles.""" | |
| 4 | + | |
| 5 | +import pickle | |
| 6 | + | |
| 7 | +import pandas as pd | |
| 8 | + | |
| 9 | +from . import config | |
| 10 | + | |
| 11 | + | |
| 12 | +def load_analytical_sample() -> pd.DataFrame: | |
| 13 | + """Return the analytical sample (788,842 listings x 68 engineered columns).""" | |
| 14 | + return pd.read_pickle(config.ANALYTICAL_SAMPLE) | |
| 15 | + | |
| 16 | + | |
| 17 | +def load_model_data() -> dict: | |
| 18 | + """Return model-ready data: standardized X_const, y_clean, df_clean, OLS results.""" | |
| 19 | + with open(config.MODEL_DATA, "rb") as f: | |
| 20 | + return pickle.load(f) | |
| 21 | + | |
| 22 | + | |
| 23 | +def load_shap_data() -> dict: | |
| 24 | + """Return SHAP values for the 10,000-observation XGBoost explanation sample.""" | |
| 25 | + with open(config.SHAP_DATA, "rb") as f: | |
| 26 | + return pickle.load(f) | |
| 27 | + | |
| 28 | + | |
| 29 | +def load_result(name: str) -> dict: | |
| 30 | + """Load a stored result pickle from results/ by file name (with or without .pkl).""" | |
| 31 | + if not name.endswith(".pkl"): | |
| 32 | + name += ".pkl" | |
| 33 | + with open(config.RESULTS_DIR / name, "rb") as f: | |
| 34 | + return pickle.load(f) | |
| 35 | + | |
| 36 | + | |
| 37 | +def build_feature_matrix(df: pd.DataFrame): | |
| 38 | + """Build the 62-regressor design matrix used by the tree models and FE OLS. | |
| 39 | + | |
| 40 | + Returns | |
| 41 | + ------- | |
| 42 | + X_full : pd.DataFrame | |
| 43 | + Base features plus one-hot categorical dummies (drop-first). | |
| 44 | + parts : dict | |
| 45 | + Column groups: 'region_dummies', 'pure_categorical_dummies', | |
| 46 | + and the raw dummy frame under 'cat_dummies'. | |
| 47 | + """ | |
| 48 | + cat_dummies = pd.get_dummies(df[config.CATEGORICAL_COLS], drop_first=True).astype(int) | |
| 49 | + region_cols = [c for c in cat_dummies.columns if c.startswith("region_")] | |
| 50 | + pure_cat_cols = [c for c in cat_dummies.columns if not c.startswith("region_")] | |
| 51 | + | |
| 52 | + X_full = pd.concat([df[config.ALL_BASE_FEATS], cat_dummies], axis=1) | |
| 53 | + parts = { | |
| 54 | + "cat_dummies": cat_dummies, | |
| 55 | + "region_dummies": region_cols, | |
| 56 | + "pure_categorical_dummies": pure_cat_cols, | |
| 57 | + } | |
| 58 | + return X_full, parts | |
added
src/wp3/models.py
+62 −0
@@ -0,0 +1,62 @@ | ||
| 1 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +# | |
| 3 | +"""Shared model-training helpers.""" | |
| 4 | + | |
| 5 | +import numpy as np | |
| 6 | +from sklearn.metrics import mean_squared_error, r2_score | |
| 7 | +from sklearn.model_selection import train_test_split | |
| 8 | +from xgboost import XGBRegressor | |
| 9 | + | |
| 10 | +from .config import RANDOM_STATE | |
| 11 | + | |
| 12 | +XGB_PARAMS = dict( | |
| 13 | + n_estimators=1000, | |
| 14 | + max_depth=8, | |
| 15 | + learning_rate=0.05, | |
| 16 | + subsample=0.8, | |
| 17 | + colsample_bytree=0.8, | |
| 18 | + min_child_weight=5, | |
| 19 | + reg_alpha=0.1, | |
| 20 | + reg_lambda=1.0, | |
| 21 | + random_state=RANDOM_STATE, | |
| 22 | + n_jobs=-1, | |
| 23 | + early_stopping_rounds=50, | |
| 24 | +) | |
| 25 | + | |
| 26 | + | |
| 27 | +def _fit_and_score(X, y, idx_train, idx_test): | |
| 28 | + """Fit XGBoost on one split (10% of train reserved for early stopping).""" | |
| 29 | + X_tr, X_te = X[idx_train], X[idx_test] | |
| 30 | + y_tr, y_te = y[idx_train], y[idx_test] | |
| 31 | + | |
| 32 | + X_fit, X_eval, y_fit, y_eval = train_test_split( | |
| 33 | + X_tr, y_tr, test_size=0.1, random_state=RANDOM_STATE) | |
| 34 | + | |
| 35 | + model = XGBRegressor(**XGB_PARAMS) | |
| 36 | + model.fit(X_fit, y_fit, eval_set=[(X_eval, y_eval)], verbose=0) | |
| 37 | + | |
| 38 | + y_pred = model.predict(X_te) | |
| 39 | + return { | |
| 40 | + "r2": r2_score(y_te, y_pred), | |
| 41 | + "rmse": float(np.sqrt(mean_squared_error(y_te, y_pred))), | |
| 42 | + "best_iter": getattr(model, "best_iteration", XGB_PARAMS["n_estimators"]), | |
| 43 | + } | |
| 44 | + | |
| 45 | + | |
| 46 | +def train_xgb(X, y, idx_train, idx_test, idx_geo_train, idx_geo_test): | |
| 47 | + """Train XGBoost under both validation schemes and return the metrics. | |
| 48 | + | |
| 49 | + Two independent models are fit: one on the random 80/20 split and one on | |
| 50 | + the geographic (state-holdout) split. | |
| 51 | + """ | |
| 52 | + random_split = _fit_and_score(X, y, idx_train, idx_test) | |
| 53 | + geo_split = _fit_and_score(X, y, idx_geo_train, idx_geo_test) | |
| 54 | + return { | |
| 55 | + "r2_random": random_split["r2"], | |
| 56 | + "rmse_random": random_split["rmse"], | |
| 57 | + "r2_geo": geo_split["r2"], | |
| 58 | + "rmse_geo": geo_split["rmse"], | |
| 59 | + "best_iter_random": random_split["best_iter"], | |
| 60 | + "best_iter_geo": geo_split["best_iter"], | |
| 61 | + "n_features": X.shape[1], | |
| 62 | + } | |
added
src/wp3/plotting.py
+112 −0
@@ -0,0 +1,112 @@ | ||
| 1 | +# Author: Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +# | |
| 3 | +"""Publication style shared by all paper figures. | |
| 4 | + | |
| 5 | +Design goals (housing/urban economics journal standard): | |
| 6 | + - serif typography consistent with the paper's newtx text font; | |
| 7 | + - muted, colorblind-safe palette (no default-Stata / default-matplotlib look); | |
| 8 | + - no chart junk: top/right spines removed, light dotted y-grid only; | |
| 9 | + - human-readable variable labels instead of raw column names; | |
| 10 | + - panel titles "(a) ..." left-aligned, no redundant figure-level titles | |
| 11 | + (the LaTeX caption carries the title). | |
| 12 | +""" | |
| 13 | + | |
| 14 | +import matplotlib.pyplot as plt | |
| 15 | + | |
| 16 | +# Muted palette | |
| 17 | +PRIMARY = "#3B6B9B" # deep steel blue (marks, bars, boxes) | |
| 18 | +PRIMARY_LIGHT = "#7FA5C6" # scatter clouds | |
| 19 | +ACCENT = "#A63E38" # muted brick red (reference lines, binned means) | |
| 20 | +NEUTRAL = "#6E6E6E" # gray for zero lines / secondary elements | |
| 21 | +CMAP_SEQ = "Blues" # densities (hexbin) | |
| 22 | +CMAP_DIV = "coolwarm" # SHAP dependence coloring | |
| 23 | +DPI = 300 | |
| 24 | + | |
| 25 | +# Human-readable labels for model variables (used in axis labels and SHAP plots) | |
| 26 | +LABELS = { | |
| 27 | + "ln_sqft": "Log(Living Area)", | |
| 28 | + "ln_lot": "Log(Lot Size)", | |
| 29 | + "ln_hoa": "Log(HOA Fee)", | |
| 30 | + "ln_price": "Log(Price)", | |
| 31 | + "bedrooms": "Bedrooms", | |
| 32 | + "bathrooms": "Bathrooms", | |
| 33 | + "age": "Property Age", | |
| 34 | + "age_sq": "Property Age$^2$", | |
| 35 | + "stories": "Stories", | |
| 36 | + "bath_per_bed": "Baths per Bedroom", | |
| 37 | + "sqft_per_bed": "Sqft per Bedroom", | |
| 38 | + "parking_spaces": "Parking Spaces", | |
| 39 | + "luxury_score": "Luxury Score", | |
| 40 | + "walk_score": "Walk Score", | |
| 41 | + "bike_score": "Bike Score", | |
| 42 | + "transit_score": "Transit Score", | |
| 43 | + "school_count": "School Count", | |
| 44 | + "avg_school_rating": "Avg. School Rating", | |
| 45 | + "nearest_school_distance": "Nearest School Dist.", | |
| 46 | + "property_tax_rate": "Property Tax Rate", | |
| 47 | + "has_pool": "Pool", | |
| 48 | + "has_spa": "Spa", | |
| 49 | + "has_basement": "Basement", | |
| 50 | + "has_fireplace": "Fireplace", | |
| 51 | + "has_garage": "Garage", | |
| 52 | + "on_waterfront": "Waterfront", | |
| 53 | + "has_central_air": "Central Air", | |
| 54 | + "has_forced_air": "Forced Air", | |
| 55 | + "has_hardwood": "Hardwood Floors", | |
| 56 | + "is_condo": "Condo", | |
| 57 | + "has_hoa": "HOA", | |
| 58 | + "tag_new_construction": "New Construction", | |
| 59 | + "tag_foreclosure": "Foreclosure", | |
| 60 | + "region_Northeast": "Region: Northeast", | |
| 61 | + "region_South": "Region: South", | |
| 62 | + "region_West": "Region: West", | |
| 63 | + "region_Midwest": "Region: Midwest", | |
| 64 | + "sqft_x_age": "Sqft $\\times$ Age", | |
| 65 | + "pool_x_south": "Pool $\\times$ South", | |
| 66 | + "waterfront_x_sqft": "Waterfront $\\times$ Log(sqft)", | |
| 67 | + "basement_x_north": "Basement $\\times$ North", | |
| 68 | + "condo_x_walkscore": "Condo $\\times$ Walk Score", | |
| 69 | + "age_x_luxury": "Age $\\times$ Luxury", | |
| 70 | +} | |
| 71 | + | |
| 72 | + | |
| 73 | +def label(var: str) -> str: | |
| 74 | + """Human-readable label for a model variable (falls back to the raw name).""" | |
| 75 | + return LABELS.get(var, var) | |
| 76 | + | |
| 77 | + | |
| 78 | +def apply_paper_style() -> None: | |
| 79 | + """Set the rcParams used for every figure in the paper.""" | |
| 80 | + plt.rcParams.update({ | |
| 81 | + "font.family": "serif", | |
| 82 | + "mathtext.fontset": "dejavuserif", | |
| 83 | + "font.size": 13, | |
| 84 | + "axes.titlesize": 14, | |
| 85 | + "axes.labelsize": 13.5, | |
| 86 | + "legend.fontsize": 12, | |
| 87 | + "xtick.labelsize": 12, | |
| 88 | + "ytick.labelsize": 12, | |
| 89 | + # Clean journal look | |
| 90 | + "axes.spines.top": False, | |
| 91 | + "axes.spines.right": False, | |
| 92 | + "axes.linewidth": 0.9, | |
| 93 | + "axes.edgecolor": "#333333", | |
| 94 | + "xtick.direction": "out", | |
| 95 | + "ytick.direction": "out", | |
| 96 | + "axes.grid": True, | |
| 97 | + "axes.grid.axis": "y", | |
| 98 | + "grid.linestyle": ":", | |
| 99 | + "grid.linewidth": 0.7, | |
| 100 | + "grid.alpha": 0.45, | |
| 101 | + "grid.color": "#999999", | |
| 102 | + "legend.frameon": False, | |
| 103 | + "figure.dpi": 100, | |
| 104 | + "savefig.dpi": DPI, | |
| 105 | + "savefig.bbox": "tight", | |
| 106 | + "savefig.facecolor": "white", | |
| 107 | + }) | |
| 108 | + | |
| 109 | + | |
| 110 | +def panel_title(ax, text: str) -> None: | |
| 111 | + """Left-aligned panel title, e.g. '(a) OLS'.""" | |
| 112 | + ax.set_title(text, loc="left", fontweight="regular", pad=8) | |
| 113 | ||