SPB Git

spb/wp3_uqo Public

UQO Working Paper No. 3 — Hedonic housing price models for the US: parametric, quantile, and machine-learning approaches.

TeX 77.8% Python 22.1%

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>
simon-pierre boucher committed 5 days ago (Aug 5, 2026)

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 +[![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
10 +[![LaTeX](https://img.shields.io/badge/LaTeX-pdflatex%20%2B%20BibTeX-008080?logo=latex&logoColor=white)](https://www.latex-project.org/)
11 +[![XGBoost](https://img.shields.io/badge/XGBoost-3.2.0-EB5E28)](https://xgboost.readthedocs.io/)
12 +[![LightGBM](https://img.shields.io/badge/LightGBM-4.6.0-9ACD32)](https://lightgbm.readthedocs.io/)
13 +[![SHAP](https://img.shields.io/badge/SHAP-0.48.0-blueviolet)](https://shap.readthedocs.io/)
14 +[![statsmodels](https://img.shields.io/badge/statsmodels-0.14.6-4051B5)](https://www.statsmodels.org/)
15 +[![DuckDB](https://img.shields.io/badge/DuckDB-1.5.2-FFF000?logo=duckdb&logoColor=black)](https://duckdb.org/)
16 +
17 +[![Paper](https://img.shields.io/badge/Paper-52%20pages%20PDF-B31B1B?logo=adobeacrobatreader&logoColor=white)](paper/main.pdf)
18 +[![Sample](https://img.shields.io/badge/Sample-788%2C842%20listings-2B5C8A)](#-data)
19 +[![Coverage](https://img.shields.io/badge/Coverage-50%20states%20%2B%20DC-2B5C8A)](#-data)
20 +[![Figures](https://img.shields.io/badge/Figures-11%20publication--ready-A63E38)](#-figures)
21 +[![Verified](https://img.shields.io/badge/Results-verified%20against%20stored%20artifacts-2E8B57)](#-verification--integrity)
22 +[![Status](https://img.shields.io/badge/Status-Working%20Paper%20v1.0-orange)](#-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