| 1 | −<!-- Author: Simon-Pierre Boucher — contact@spboucher.ai --> |
1 | +<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca --> |
| 2 | # 🏇 Half a Million Prices, Twenty Models |
2 | # 🏇 Half a Million Prices, Twenty Models |
| 3 | |
3 | |
| 4 | **UQO Working Paper No. 11** — *A Systematic Assessment of Hedonic Specifications |
4 | **UQO Working Paper No. 11** — *A Systematic Assessment of Hedonic Specifications |
| 6 | |
6 | |
| 7 | [-b31b1b?logo=latex&logoColor=white)](paper/main.pdf) |
7 | [-b31b1b?logo=latex&logoColor=white)](paper/main.pdf) |
| 8 | [](requirements.txt) |
8 | [](requirements.txt) |
| 9 | −[](data/raw/README.md) |
9 | +[](data/raw/README.md) |
| 10 | [](src/wp11/models.py) |
10 | [](src/wp11/models.py) |
| 11 | [](scripts/) |
11 | [](scripts/) |
| 12 | [](https://uqo.ca) |
12 | [](https://uqo.ca) |
| 13 | −[](mailto:contact@spboucher.ai) |
13 | +[](mailto:simon-pierre.boucher@uqo.ca) |
| 14 | −[](mailto:contact@spboucher.ai) |
| |
| 15 | |
14 | |
| 16 | > **TL;DR** — A controlled horse race of **20 hedonic models** on 514,212 Quebec sales |
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 × |
16 | +> (2021–2026) with assessor-grade structural attributes: 6 functional forms × a time-FE |
| 18 | −> spatial-FE ladder × 6 estimation methods, all scored on the **same price-level |
17 | +> ladder × a spatial-FE ladder × 6 estimation methods, all scored on the **same |
| 19 | −> scoreboard** (Duan smearing / Box–Cox inversion) under **two holdouts** (random 80/20 |
18 | +> price-level scoreboard** (Duan smearing / Box–Cox inversion) under **two holdouts** |
| 20 | −> and forward-in-time). Functional form is second-order (±2 pp of MdAPE); spatial |
19 | +> (random 80/20 and forward-in-time). Functional form is second-order (±2 pp of MdAPE); |
| 21 | −> controls are first-order (~10 pp) but overfit at ~1 km granularity; **gradient |
20 | +> spatial 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 |
21 | +> boosting wins under random validation** (MdAPE 14.4% vs 16.5% for the best linear |
| 23 | > model) — but under the forward-in-time split *every* model degrades and the ML |
22 | > model) — but under the forward-in-time split *every* model degrades and the ML |
| 24 | > advantage **reverses** (splines 19.1% vs boosting 21.2%, identical R²_ln): random |
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 |
24 | +> cross-validation is the wrong experiment for valuation models. |
| 26 | −> and implicit-price profiles agree across forms far more than the accuracy gap suggests. |
| |
| 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 | | Axis | Models | Question | |
63 | | Axis | Models | Question | |
| 33 | |---|---|---| |
64 | |---|---|---| |
| 36 | | **C. Spatial controls** | none / municipality / ~5.5 km grid / ~1.1 km grid FE | how fine can location FE get before overfitting? | |
67 | | **C. Spatial controls** | none / municipality / ~5.5 km grid / ~1.1 km grid FE | how fine can location FE get before overfitting? | |
| 37 | | **D. Estimation method** | OLS, ridge (CV), random forest, gradient boosting (± coordinates), spatial k-NN comparables | what do the machines actually buy? | |
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, |
70 | +Every model sees the same sales and the same attributes (floor area, lot, age, storeys, |
| 40 | −class, physical link), metrics on price levels (MdAPE, MAPE, RMSE_ln, R²_ln), |
71 | +units, class, physical link). Accuracy is measured on **price levels** (MdAPE, MAPE, |
| 41 | −municipality-clustered design, and **two splits** — random 80/20 and |
72 | +RMSE_ln, R²_ln) — log models are Duan-retransformed, Box–Cox models inverted — under a |
| 42 | −train < 2025 / test 2025–26 with carry-forward time effects. |
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 | ## 🗂 Repository layout |
98 | ## 🗂 Repository layout |
| 45 | |
99 | |
| 46 | ``` |
100 | ``` |
| 47 | wp11_uqo/ |
101 | wp11_uqo/ |
| 48 | −├── data/raw/ # snapshot (NOT in git — see data/raw/README.md) |
102 | +├── README.md |
| 49 | −├── src/wp11/ # config, sample, models (the harness), plotstyle |
103 | +├── requirements.txt |
| 50 | −├── scripts/ |
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 | │ ├── 01_build_sample.py # 745,119 → 514,212 sales |
114 | │ ├── 01_build_sample.py # 745,119 → 514,212 sales |
| 52 | │ ├── 02_horserace.py # 20 models × 2 splits + Box–Cox profile |
115 | │ ├── 02_horserace.py # 20 models × 2 splits + Box–Cox profile |
| 53 | │ ├── 03_extensions.py # indices, implicit-price profiles, learning curves, segments |
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 | │ └── 05_make_tables.py # 7 LaTeX tables |
118 | │ └── 05_make_tables.py # 7 LaTeX tables |
| 56 | −├── results/{reproduced,tables}/ |
119 | +├── results/ |
| 57 | −├── figures/ |
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 | └── paper/ # main.tex + sections/ + references.bib → main.pdf |
123 | └── paper/ # main.tex + sections/ + references.bib → main.pdf |
| 59 | ``` |
124 | ``` |
| 60 | |
125 | |
| 61 | ## 🚀 Quick start |
126 | ## 🚀 Quick start |
| 62 | |
127 | |
| 63 | ```bash |
128 | ```bash |
| 64 | −cd wp11_uqo |
| |
| 65 | python3 -m pip install -r requirements.txt |
129 | python3 -m pip install -r requirements.txt |
| 66 | −# place the parquet snapshot in data/raw/ (not distributed), then: |
130 | + |
| 67 | −for s in scripts/0*.py; do python3 "$s"; done |
131 | +# place the parquet snapshot in data/raw/ (see data/raw/README.md), then: |
| 68 | −cd paper && make |
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 |
141 | +Runtime ≈ 10 minutes on an Apple-silicon laptop (the boosting and forest fits dominate); |
| 72 | −dominate); peak RAM ≈ 8 GB. |
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 | ## 📝 Citation |
212 | ## 📝 Citation |
| 75 | |
213 | |
| 78 | author = {Boucher, Simon-Pierre}, |
216 | author = {Boucher, Simon-Pierre}, |
| 79 | title = {Half a Million Prices, Twenty Models: A Systematic Assessment |
217 | title = {Half a Million Prices, Twenty Models: A Systematic Assessment |
| 80 | of Hedonic Specifications and Estimation Methods for the |
218 | of Hedonic Specifications and Estimation Methods for the |
| 81 | − Quebec Housing Market}, |
219 | + Quebec Housing Market, 2021--2026}, |
| 82 | institution = {Universit\'e du Qu\'ebec en Outaouais, |
220 | institution = {Universit\'e du Qu\'ebec en Outaouais, |
| 83 | D\'epartement des sciences administratives}, |
221 | D\'epartement des sciences administratives}, |
| 84 | type = {Working Paper}, |
222 | type = {Working Paper}, |
| 92 | |
230 | |
| 93 | **Simon-Pierre Boucher** — Département des sciences administratives, |
231 | **Simon-Pierre Boucher** — Département des sciences administratives, |
| 94 | Université du Québec en Outaouais (UQO), Gatineau, QC. |
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). |