| 1 |
|
−<!-- Author: Simon-Pierre Boucher — contact@spboucher.ai --> |
|
1 |
+<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca --> |
| 2 |
2 |
# 🏇 Half a Million Prices, Twenty Models |
| 3 |
3 |
|
| 4 |
4 |
**UQO Working Paper No. 11** — *A Systematic Assessment of Hedonic Specifications |
| 6 |
6 |
|
| 7 |
7 |
[-b31b1b?logo=latex&logoColor=white)](paper/main.pdf) |
| 8 |
8 |
[](requirements.txt) |
| 9 |
|
−[](data/raw/README.md) |
|
9 |
+[](data/raw/README.md) |
| 10 |
10 |
[](src/wp11/models.py) |
| 11 |
11 |
[](scripts/) |
| 12 |
12 |
[](https://uqo.ca) |
| 13 |
|
−[](mailto:contact@spboucher.ai) |
| 14 |
|
−[](mailto:contact@spboucher.ai) |
|
13 |
+[](mailto:simon-pierre.boucher@uqo.ca) |
| 15 |
14 |
|
| 16 |
15 |
> **TL;DR** — A controlled horse race of **20 hedonic models** on 514,212 Quebec sales |
| 17 |
|
−> with assessor-grade structural attributes: 6 functional forms × time-FE ladder × |
| 18 |
|
−> spatial-FE ladder × 6 estimation methods, all scored on the **same price-level |
| 19 |
|
−> scoreboard** (Duan smearing / Box–Cox inversion) under **two holdouts** (random 80/20 |
| 20 |
|
−> and forward-in-time). Functional form is second-order (±2 pp of MdAPE); spatial |
| 21 |
|
−> controls are first-order (~10 pp) but overfit at ~1 km granularity; **gradient |
| 22 |
|
−> boosting wins under random validation** (MdAPE ≈ 14.4% vs 16.5% for the best linear |
|
16 |
+> (2021–2026) with assessor-grade structural attributes: 6 functional forms × a time-FE |
|
17 |
+> ladder × a spatial-FE ladder × 6 estimation methods, all scored on the **same |
|
18 |
+> price-level scoreboard** (Duan smearing / Box–Cox inversion) under **two holdouts** |
|
19 |
+> (random 80/20 and forward-in-time). Functional form is second-order (±2 pp of MdAPE); |
|
20 |
+> spatial controls are first-order (~10 pp) but overfit at ~1 km granularity; **gradient |
|
21 |
+> boosting wins under random validation** (MdAPE 14.4% vs 16.5% for the best linear |
| 23 |
22 |
> model) — but under the forward-in-time split *every* model degrades and the ML |
| 24 |
23 |
> advantage **reverses** (splines 19.1% vs boosting 21.2%, identical R²_ln): random |
| 25 |
|
−> cross-validation is the wrong experiment for valuation models. Implied price indices |
| 26 |
|
−> and implicit-price profiles agree across forms far more than the accuracy gap suggests. |
|
24 |
+> cross-validation is the wrong experiment for valuation models. |
| 27 |
25 |
|
| 28 |
26 |
--- |
| 29 |
27 |
|
| 30 |
|
−## 🎯 The four design axes |
|
28 |
+## 📌 Status |
|
29 |
+ |
|
30 |
+| Item | Value | |
|
31 |
+|---|---| |
|
32 |
+| Version | 1.0 (August 2026) | |
|
33 |
+| SSRN | [7626698](https://ssrn.com/abstract=7626698) — submitted 2026-10-17 (preliminary upload) | |
|
34 |
+| Journal | not yet submitted | |
|
35 |
+| Code & data | reproducible from `scripts/` (see Quick start); raw parquet snapshot not redistributed | |
|
36 |
+ |
|
37 |
+## 📖 Table of contents |
|
38 |
+ |
|
39 |
+0. [Status](#-status) |
|
40 |
+1. [What this paper does](#-what-this-paper-does) |
|
41 |
+2. [Headline results](#-headline-results) |
|
42 |
+3. [Repository layout](#-repository-layout) |
|
43 |
+4. [Quick start](#-quick-start) |
|
44 |
+5. [The pipeline, step by step](#-the-pipeline-step-by-step) |
|
45 |
+6. [Data](#-data) |
|
46 |
+7. [Methodology](#-methodology) |
|
47 |
+8. [Figures & tables inventory](#-figures--tables-inventory) |
|
48 |
+9. [Limitations](#-limitations) |
|
49 |
+10. [Citation](#-citation) |
|
50 |
+11. [Author & contact](#-author--contact) |
|
51 |
+12. [Notes](#-notes) |
|
52 |
+ |
|
53 |
+--- |
|
54 |
+ |
|
55 |
+## 🎯 What this paper does |
|
56 |
+ |
|
57 |
+Fifty years after Rosen (1974), applied hedonic practice still rests on specification |
|
58 |
+choices — functional form, temporal controls, spatial controls, estimation method — that |
|
59 |
+are made by convention and rarely stress-tested jointly. This paper runs the debate as a |
|
60 |
+**controlled experiment**: twenty models organised along four design axes, one attribute |
|
61 |
+set, one scoreboard, two holdouts, half a million sales. |
| 31 |
62 |
|
| 32 |
63 |
| Axis | Models | Question | |
| 33 |
64 |
|---|---|---| |
| 36 |
67 |
| **C. Spatial controls** | none / municipality / ~5.5 km grid / ~1.1 km grid FE | how fine can location FE get before overfitting? | |
| 37 |
68 |
| **D. Estimation method** | OLS, ridge (CV), random forest, gradient boosting (± coordinates), spatial k-NN comparables | what do the machines actually buy? | |
| 38 |
69 |
|
| 39 |
|
−Every model: same sales, same attributes (floor area, lot, age, storeys, units, |
| 40 |
|
−class, physical link), metrics on price levels (MdAPE, MAPE, RMSE_ln, R²_ln), |
| 41 |
|
−municipality-clustered design, and **two splits** — random 80/20 and |
| 42 |
|
−train < 2025 / test 2025–26 with carry-forward time effects. |
|
70 |
+Every model sees the same sales and the same attributes (floor area, lot, age, storeys, |
|
71 |
+units, class, physical link). Accuracy is measured on **price levels** (MdAPE, MAPE, |
|
72 |
+RMSE_ln, R²_ln) — log models are Duan-retransformed, Box–Cox models inverted — under a |
|
73 |
+random 80/20 split and a **forward-in-time** split (train < 2025, test 2025–26, with |
|
74 |
+carry-forward time effects). Extensions compare the constant-quality monthly indices and |
|
75 |
+the implicit age / floor-area profiles implied by the competing forms, learning curves, |
|
76 |
+and accuracy by market segment. |
|
77 |
+ |
|
78 |
+## 🏆 Headline results |
|
79 |
+ |
|
80 |
+| Object | Random 80/20 holdout | Forward-in-time holdout | |
|
81 |
+|---|---|---| |
|
82 |
+| Functional-form range (A1–A6), MdAPE | 16.5 % (splines) – 18.6 % (Box–Cox) | 19.1 % – 21.0 % | |
|
83 |
+| Data-driven Box–Cox exponent | λ̂ = 0.25, buys nothing out of sample | — | |
|
84 |
+| Dropping spatial FE (C1 vs C2) | 27.9 % vs 18.1 % (+10 pp) | 26.5 % vs 20.5 % | |
|
85 |
+| ~1.1 km grid FE (C4) | 19.4 % — **overfits** vs 17.5 % at ~5.5 km | 22.1 % | |
|
86 |
+| Dropping time FE (B1 vs B3) | 21.1 % vs 18.1 % | 25.4 % vs 20.5 % | |
|
87 |
+| Gradient boosting (D4) | **14.4 %** (−13 % vs best linear 16.5 %) | 21.2 % | |
|
88 |
+| Random forest (D3) | 14.6 % | 21.3 % | |
|
89 |
+| Boosting without coordinates (D5) | 23.2 % | 26.2 % | |
|
90 |
+| Semi-log + splines (A6) | 16.5 % | **19.1 %** — beats boosting, same R²_ln (0.52) | |
|
91 |
+| Implied monthly indices across forms | agree within a few index points | | |
|
92 |
+| Boosted implicit age / area profiles | track quadratic OLS closely | | |
|
93 |
+ |
|
94 |
+Practical synthesis: a semi-log with splines, quarter effects and spatial FE matched to |
|
95 |
+market thickness is within two points of the frontier in deployment conditions — |
|
96 |
+transparent, auditable, fast. Validate against the future, not a shuffle of the past. |
| 43 |
97 |
|
| 44 |
98 |
## 🗂 Repository layout |
| 45 |
99 |
|
| 46 |
100 |
``` |
| 47 |
101 |
wp11_uqo/ |
| 48 |
|
−├── data/raw/ # snapshot (NOT in git — see data/raw/README.md) |
| 49 |
|
−├── src/wp11/ # config, sample, models (the harness), plotstyle |
| 50 |
|
−├── scripts/ |
|
102 |
+├── README.md |
|
103 |
+├── requirements.txt |
|
104 |
+├── .gitignore |
|
105 |
+├── data/ |
|
106 |
+│ ├── raw/ # transactions_700k_avec_registre_foncier.parquet — NOT in git (see data/raw/README.md) |
|
107 |
+│ └── processed/ # estimation sample (generated by step 01) |
|
108 |
+├── src/wp11/ # reusable package |
|
109 |
+│ ├── config.py # paths, thresholds, grid sizes, seeds |
|
110 |
+│ ├── sample.py # sample construction & variable definitions |
|
111 |
+│ ├── models.py # the harness: 20 models, 2 splits, scoreboard, Box–Cox profile |
|
112 |
+│ └── plotstyle.py # journal-calibre matplotlib style |
|
113 |
+├── scripts/ # the pipeline, in order |
| 51 |
114 |
│ ├── 01_build_sample.py # 745,119 → 514,212 sales |
| 52 |
115 |
│ ├── 02_horserace.py # 20 models × 2 splits + Box–Cox profile |
| 53 |
116 |
│ ├── 03_extensions.py # indices, implicit-price profiles, learning curves, segments |
| 54 |
|
−│ ├── 04_make_figures.py # 9 journal-calibre figures |
|
117 |
+│ ├── 04_make_figures.py # 9 figures |
| 55 |
118 |
│ └── 05_make_tables.py # 7 LaTeX tables |
| 56 |
|
−├── results/{reproduced,tables}/ |
| 57 |
|
−├── figures/ |
|
119 |
+├── results/ |
|
120 |
+│ ├── reproduced/ # every CSV the scripts emit (scoreboards, Box–Cox, index, profiles, …) |
|
121 |
+│ └── tables/ # LaTeX tables input by the paper |
|
122 |
+├── figures/ # 9 publication figures (PNG, 300 dpi) |
| 58 |
123 |
└── paper/ # main.tex + sections/ + references.bib → main.pdf |
| 59 |
124 |
``` |
| 60 |
125 |
|
| 61 |
126 |
## 🚀 Quick start |
| 62 |
127 |
|
| 63 |
128 |
```bash |
| 64 |
|
−cd wp11_uqo |
| 65 |
129 |
python3 -m pip install -r requirements.txt |
| 66 |
|
−# place the parquet snapshot in data/raw/ (not distributed), then: |
| 67 |
|
−for s in scripts/0*.py; do python3 "$s"; done |
| 68 |
|
−cd paper && make |
|
130 |
+ |
|
131 |
+# place the parquet snapshot in data/raw/ (see data/raw/README.md), then: |
|
132 |
+python3 scripts/01_build_sample.py # 745,119 → 514,212 sales |
|
133 |
+python3 scripts/02_horserace.py # 20 models × 2 splits + Box–Cox profile |
|
134 |
+python3 scripts/03_extensions.py # indices, profiles, learning curves, segments |
|
135 |
+python3 scripts/04_make_figures.py # 9 figures |
|
136 |
+python3 scripts/05_make_tables.py # 7 LaTeX tables |
|
137 |
+ |
|
138 |
+cd paper && make # compile main.pdf (latexmk) |
| 69 |
139 |
``` |
| 70 |
140 |
|
| 71 |
|
−Runtime ≈ 10 minutes on an Apple-silicon laptop (the boosting and forest fits |
| 72 |
|
−dominate); peak RAM ≈ 8 GB. |
|
141 |
+Runtime ≈ 10 minutes on an Apple-silicon laptop (the boosting and forest fits dominate); |
|
142 |
+peak RAM ≈ 8 GB. |
|
143 |
+ |
|
144 |
+## 🔬 The pipeline, step by step |
|
145 |
+ |
|
146 |
+| Step | Script | What it does | Output | |
|
147 |
+|---|---|---|---| |
|
148 |
+| 01 | `01_build_sample.py` | residential use codes, roll match ≤ 50 m / score ≥ 150, price ≥ $50k, floor area 20–2,000 m², known year built, 1/99 % trims within sale year; builds grid cells | `data/processed/`, `results/reproduced/sample_counts.csv`, `summary_stats.csv` | |
|
149 |
+| 02 | `02_horserace.py` | fits the 20 models on each split, retransforms predictions to price levels, scores MdAPE/MAPE/RMSE_ln/R²_ln; Box–Cox profile likelihood | `horserace_random.csv`, `horserace_temporal.csv`, `boxcox.csv` | |
|
150 |
+| 03 | `03_extensions.py` | constant-quality monthly indices by form, implicit ln-price profiles (quadratic OLS vs boosting), learning curves, accuracy by segment | `index.csv`, `profiles.csv`, `learning.csv`, `segments.csv` | |
|
151 |
+| 04 | `04_make_figures.py` | the 9 figures | `figures/fig_*.png` | |
|
152 |
+| 05 | `05_make_tables.py` | the 7 LaTeX tables | `results/tables/*.tex` | |
|
153 |
+ |
|
154 |
+## 🗃 Data |
|
155 |
+ |
|
156 |
+745,119 Quebec residential-market transactions (January 2021 – July 2026) matched at the |
|
157 |
+**parcel level** to the municipal assessment roll in force at the sale date (median match |
|
158 |
+distance 0.6 m; same snapshot as WP 10). The roll supplies the structural descriptors |
|
159 |
+missing from transaction registries: floor area, lot area, year built, units, storeys, |
|
160 |
+physical configuration, use code. Sample cascade (paper §3): residential use codes → |
|
161 |
+high-confidence match → price ≥ $50,000 → plausible floor area → known year built → |
|
162 |
+1/99 % trims ⇒ **514,212 sales** (348,253 single-family, 75,734 condominiums, 74,909 |
|
163 |
+plexes, 10,349 cottages, 4,881 mobile homes) over 1,115 municipalities, 5,255 cells of |
|
164 |
+~5.5 km and 30,548 cells of ~1.1 km. Mean price $462,000 (median $399,000). The raw |
|
165 |
+parquet (100 MB, transaction microdata) is **not redistributed**. |
|
166 |
+ |
|
167 |
+## 🔬 Methodology |
|
168 |
+ |
|
169 |
+1. **One attribute set, one sample** for every model (`src/wp11/sample.py`). |
|
170 |
+2. **Axis A** — six functional forms on a municipality + quarter FE backbone; Box–Cox λ by |
|
171 |
+ profile likelihood; log models retransformed with Duan smearing, Box–Cox inverted |
|
172 |
+ (`src/wp11/models.py`). |
|
173 |
+3. **Axis B / C** — time-FE ladder (none → month) and spatial-FE ladder (none → ~1.1 km |
|
174 |
+ grid) on the quadratic semi-log backbone. |
|
175 |
+4. **Axis D** — OLS, cross-validated ridge, random forest, gradient boosting with and |
|
176 |
+ without coordinates, spatial k-NN comparables, all on the identical attribute set. |
|
177 |
+5. **Two holdouts** — random 80/20 and forward-in-time (train < 2025, test 2025–26, time |
|
178 |
+ effects carried forward); metrics on price levels, design clustered by municipality. |
|
179 |
+6. **Extensions** — implied monthly indices, implicit-price profiles, learning curves, |
|
180 |
+ segment accuracy (`scripts/03_extensions.py`). |
|
181 |
+ |
|
182 |
+## 🖼 Figures & tables inventory |
|
183 |
+ |
|
184 |
+| Figure | Content | |
|
185 |
+|---|---| |
|
186 |
+| `fig_forms` | Axis A — functional form, MdAPE on the random holdout | |
|
187 |
+| `fig_time` | Axis B — time effects | |
|
188 |
+| `fig_space` | Axis C — spatial controls (overfitting at ~1 km) | |
|
189 |
+| `fig_methods` | Axis D — estimation methods on the identical attribute set | |
|
190 |
+| `fig_generalization` | the generalization gap: random vs forward-in-time MdAPE, all 20 models | |
|
191 |
+| `fig_index` | constant-quality monthly price indices implied by competing forms | |
|
192 |
+| `fig_profiles` | implicit ln-price profiles: quadratic OLS vs gradient boosting | |
|
193 |
+| `fig_learning` | learning curves: MdAPE against training-set size | |
|
194 |
+| `fig_segments` | MdAPE by market segment (random holdout) | |
|
195 |
+ |
|
196 |
+Tables (`results/tables/*.tex`): summary statistics, scoreboard random holdout, |
|
197 |
+scoreboard forward-in-time holdout, Box–Cox profile, generalization gap, learning |
|
198 |
+curves, segments. |
|
199 |
+ |
|
200 |
+## ⚠️ Limitations |
|
201 |
+ |
|
202 |
+- Sales below $50k are absent from the source registry; residual non-arm's-length |
|
203 |
+ transfers may survive the screens. |
|
204 |
+- 2021–2026 spans a boom, a rate correction and a recovery — level statistics are |
|
205 |
+ period-specific; the forward-in-time test is deliberately demanding. |
|
206 |
+- Attributes are those of the assessment roll (no interior quality, renovations or |
|
207 |
+ listing text); the ML advantage measured here is the value of flexible *spatial* |
|
208 |
+ learning on that attribute set, not of richer data. |
|
209 |
+- Tree ensembles are tuned lightly (fixed hyper-parameters); the ranking is robust to |
|
210 |
+ the choices tried but not exhaustively optimised. |
| 73 |
211 |
|
| 74 |
212 |
## 📝 Citation |
| 75 |
213 |
|
| 78 |
216 |
author = {Boucher, Simon-Pierre}, |
| 79 |
217 |
title = {Half a Million Prices, Twenty Models: A Systematic Assessment |
| 80 |
218 |
of Hedonic Specifications and Estimation Methods for the |
| 81 |
|
− Quebec Housing Market}, |
|
219 |
+ Quebec Housing Market, 2021--2026}, |
| 82 |
220 |
institution = {Universit\'e du Qu\'ebec en Outaouais, |
| 83 |
221 |
D\'epartement des sciences administratives}, |
| 84 |
222 |
type = {Working Paper}, |
| 92 |
230 |
|
| 93 |
231 |
**Simon-Pierre Boucher** — Département des sciences administratives, |
| 94 |
232 |
Université du Québec en Outaouais (UQO), Gatineau, QC. |
| 95 |
|
−📧 [contact@spboucher.ai](mailto:contact@spboucher.ai) |
|
233 |
+📧 [simon-pierre.boucher@uqo.ca](mailto:simon-pierre.boucher@uqo.ca) |
|
234 |
+ |
|
235 |
+## 🗒 Notes |
|
236 |
+ |
|
237 |
+- Numbering: UQO Working Paper No. 11 belongs to this paper (August 2026). Two other |
|
238 |
+ manuscripts briefly carried the label "WP 11": *Hedonic House Pricing with |
|
239 |
+ Language-Model and Vision Features* (now **WP 14**) and *Does the Sales Comparison |
|
240 |
+ Approach Actually Work?* (now **WP 16**). |
|
241 |
+- Cover page, metadata and layout follow `Recherche-UQO/HARMONISATION.md` (2026-10-17). |