🏇 Half a Million Prices, Twenty Models
UQO Working Paper No. 11 — A Systematic Assessment of Hedonic Specifications and Estimation Methods for the Quebec Housing Market, 2021–2026
TL;DR — A controlled horse race of 20 hedonic models on 514,212 Quebec sales (2021–2026) with assessor-grade structural attributes: 6 functional forms × a time-FE ladder × a spatial-FE ladder × 6 estimation methods, all scored on the same price-level scoreboard (one retransformation rule for every model) under two holdouts (random 80/20 and forward-in-time), with municipality-clustered bootstrap intervals and Diebold–Mariano-type tests on every comparison. Functional form is second-order (1.5 pp of MdAPE); spatial controls are first-order (~10 pp), and a ~1 km grid overfits when used alone but becomes the best linear specification once nested in municipalities; gradient boosting wins under random validation (14.4% vs 17.6% for the best linear model, −3.2 pp [−4.7, −1.9]), a gain that survives parcel- and building-grouped holdouts. Under the forward-in-time split (train < 2025, test 2025–26) every model degrades and the gap shrinks to 0.5 pp and to insignificance — but the decomposition shows this is a level (staleness) effect: re-levelled, boosting still leads by 2.9 pp. Random cross-validation hides the staleness cost; it does not reverse the ranking (the reversal reported in v1.0 was an artefact of an asymmetric retransformation, corrected in v1.1).
📌 Status
| Item | Value |
|---|---|
| Version | 1.1 (October 2026) — revised 2026-10-17 by the editorial-board review (see docs/); v1.0 = August 2026 |
| SSRN | 7626698 — v1.0 uploaded 2026-10-17; v1.1 PDF to be uploaded as a revision |
| Journal | not yet submitted |
| Code & data | reproducible from scripts/ (see Quick start); raw parquet snapshot not redistributed |
📖 Table of contents
- Status
- What this paper does
- Headline results
- Repository layout
- Quick start
- The pipeline, step by step
- Data
- Methodology
- Figures & tables inventory
- Limitations
- Citation
- Author & contact
- Notes
🎯 What this paper does
Fifty years after Rosen (1974), applied hedonic practice still rests on specification choices — functional form, temporal controls, spatial controls, estimation method — that are made by convention and rarely stress-tested jointly. This paper runs the debate as a controlled experiment: twenty models organised along four design axes, one attribute set, one scoreboard, two holdouts, half a million sales.
| Axis | Models | Question |
|---|---|---|
| A. Functional form | linear, semi-log, log-log, Box–Cox (λ̂ by profile likelihood), quadratics, cubic splines | does curvature matter? |
| B. Time effects | none / year / quarter / month FE | how fine must the time dummies be? |
| C. Spatial controls | none / municipality / ~5.5 km grid / ~1.1 km grid FE | how fine can location FE get before overfitting? |
| D. Estimation method | OLS, ridge (CV), random forest, gradient boosting (± coordinates), spatial k-NN comparables | what do the machines actually buy? |
Every model sees the same sales and the same attributes (floor area, lot, age, storeys, units, class, physical link). Accuracy is measured on price levels (MdAPE, MAPE, RMSE_ln, R²_ln) — log models are Duan-retransformed, Box–Cox models inverted — under a random 80/20 split and a forward-in-time split (train < 2025, test 2025–26, with carry-forward time effects). Extensions compare the constant-quality monthly indices and the implicit age / floor-area profiles implied by the competing forms, learning curves, and accuracy by market segment.
🏆 Headline results
All MdAPE in %, median retransformation rule for every model (v1.1 scoreboard).
| Object | Random 80/20 holdout | Forward-in-time holdout |
|---|---|---|
| Functional-form range (A1–A6) | 17.6 (splines) – 19.2 (quadratics) | 20.4 (linear) – 23.3 |
| Splines vs. semi-log | −1.4 pp [−1.8, −1.1] | −1.4 pp [−2.3, −0.8] |
| Data-driven Box–Cox exponent | λ̂ = 0.25 (0.35 on a fine grid), −0.5 pp vs. semi-log, worse on MAPE | — |
| Dropping spatial FE (C1 vs C2) | 28.8 vs 19.2 (+9.6 pp) | 29.0 vs 23.3 |
| ~5.5 km grid vs. municipality FE (C3 vs C2) | 18.7 vs 19.2 — not significant (−0.45 [−1.07, +0.37]) | 24.3 vs 23.3 |
| ~1.1 km grid alone (C4) | 21.0 — overfits (+1.8 pp [+1.2, +2.3]) | 25.3 |
| ~1.1 km grid nested in municipalities (C4′) | 17.3 — best linear specification | 21.6 |
| Dropping time FE (B1 vs B3) | 22.0 vs 19.2 | 29.3 vs 23.3 |
| Gradient boosting (D4) vs. best linear (A6) | 14.4 vs 17.6, −3.2 pp [−4.7, −1.9], DM t = −3.6 | 21.2 vs 21.7, −0.5 pp [−1.7, +0.6], n.s. |
| Random forest (D3) | 14.6 | 21.3 |
| Boosting without coordinates (D5) | 23.2 | 26.2 |
| Parcel- / building-grouped holdout, D4 vs A6 | 14.5 vs 17.6 / 14.6 vs 17.8 (gap unchanged) | — |
| Staleness cost (forward − same-period random) | — | D4 +7.5 pp, A6 +4.7, A1 +3.3 |
| Median log level error, forward split | — | D4 +0.18, A6 +0.16, A1 +0.10 |
| Re-levelled forward MdAPE | — | D4 13.9 vs A6 16.8 (gap 2.9 ≈ random 3.5) |
| Boosting lead over splines by test quarter | — | 1.5 pp (2025Q1) → 0.0 (2026Q3) |
| Monthly indices, four linear forms | agree within 2.5 index points | |
| Boosted imputation index, July 2026 | 160.5 vs 168.6–171.0 for the linear forms | |
| Boosted age profile vs. quadratic OLS | same shape, 0.10–0.13 log points flatter from age 20 |
Practical synthesis: a semi-log with splines, quarter effects and spatial fixed effects nested from municipality down to a fine grid is within three points of the frontier under random validation and indistinguishable from it in deployment. Supply the price level from an index or a frequent refit, spend the flexibility budget on the spatial surface, and validate against the future, not a shuffle of the past.
🗂 Repository layout
wp11_uqo/
├── README.md
├── requirements.txt
├── .gitignore
├── data/
│ ├── raw/ # transactions_700k_avec_registre_foncier.parquet — NOT in git (see data/raw/README.md)
│ └── processed/ # estimation sample (generated by step 01)
├── src/wp11/ # reusable package
│ ├── config.py # paths, thresholds, grid sizes, seeds
│ ├── sample.py # sample construction & variable definitions
│ ├── models.py # the harness: 20 models, 2 splits, scoreboard, Box–Cox profile
│ └── plotstyle.py # journal-calibre matplotlib style
├── scripts/ # the pipeline, in order
│ ├── 01_build_sample.py # 745,119 → 514,212 sales
│ ├── 02_horserace.py # 20 models × 2 splits + Box–Cox profile
│ ├── 03_extensions.py # indices, implicit-price profiles, learning curves, segments
│ ├── 04_make_figures.py # 9 figures
│ └── 05_make_tables.py # 7 LaTeX tables
├── results/
│ ├── reproduced/ # every CSV the scripts emit (scoreboards, Box–Cox, index, profiles, …)
│ └── tables/ # LaTeX tables input by the paper
├── figures/ # 9 publication figures (PNG, 300 dpi)
└── paper/ # main.tex + sections/ + references.bib → main.pdf🚀 Quick start
python3 -m pip install -r requirements.txt
# place the parquet snapshot in data/raw/ (see data/raw/README.md), then:
python3 scripts/01_build_sample.py # 745,119 → 514,212 sales
python3 scripts/02_horserace.py # 20 models × 2 splits + Box–Cox profile
python3 scripts/03_extensions.py # indices, profiles, learning curves, segments
python3 scripts/04_make_figures.py # 9 figures
python3 scripts/05_make_tables.py # 7 LaTeX tables
cd paper && make # compile main.pdf (latexmk)Runtime ≈ 10 minutes on an Apple-silicon laptop (the boosting and forest fits dominate); peak RAM ≈ 8 GB.
🔬 The pipeline, step by step
| Step | Script | What it does | Output |
|---|---|---|---|
| 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 |
| 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 |
| 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 |
| 04 | 04_make_figures.py |
the 10 figures | figures/fig_*.png |
| 05 | 05_make_tables.py |
the 16 LaTeX tables | results/tables/*.tex |
| 06 | 06_robustness.py |
cluster-bootstrap CIs + clustered DM tests, same-period and by-quarter decomposition, level re-levelling, leakage decomposition, parcel- and building-grouped races, nested spatial FE, fine Box–Cox grid, boosting hyper-parameter sensitivity (run after 02; ≈ 20 min) | inference_*.csv, pairs.csv, same_period.csv, by_quarter.csv, relevel.csv, leakage.csv, horserace_group_*.csv, nested_fe.csv, boxcox_fine.csv, hgb_sensitivity.csv |
🗃 Data
745,119 Quebec residential-market transactions (January 2021 – July 2026) matched at the parcel level to the municipal assessment roll in force at the sale date (median match distance 0.6 m; same snapshot as WP 10). The roll supplies the structural descriptors missing from transaction registries: floor area, lot area, year built, units, storeys, physical configuration, use code. Sample cascade (paper §3): residential use codes → high-confidence match → price ≥ $50,000 → plausible floor area → known year built → 1/99 % trims ⇒ 514,212 sales (348,253 single-family, 75,734 condominiums, 74,909 plexes, 10,349 cottages, 4,881 mobile homes) over 1,115 municipalities, 5,255 cells of ~5.5 km and 30,548 cells of ~1.1 km. Mean price $440,207 (median $397,375); 86 further dwellings of undetermined class. One fifth of sales are repeat sales of the same parcel and 30% share exact coordinates with another sale (92% of condominiums). The raw parquet (100 MB, transaction microdata) is not redistributed.
🔬 Methodology
- One attribute set, one sample for every model (
src/wp11/sample.py). - Axis A — six functional forms on a municipality + quarter FE backbone; Box–Cox λ by
profile likelihood. Every transformed-scale prediction is inverted with the same
median rule (plain inversion); Duan smearing is computed for every model and
reported as a robustness scoreboard (
src/wp11/models.py). - Axis B / C — time-FE ladder (none → month) and spatial-FE ladder (none → ~1.1 km grid) on the quadratic semi-log backbone.
- Axis D — OLS, cross-validated ridge, random forest, gradient boosting with and without coordinates, spatial k-NN comparables, all on the identical attribute set.
- Two holdouts — random 80/20 and forward-in-time (train < 2025, test 2025–26, time
effects carried forward), plus parcel- and building-grouped holdouts; metrics on price
levels; municipality-clustered bootstrap intervals and clustered Diebold–Mariano tests
(
src/wp11/inference.py); level/skill decomposition of the forward error. - Extensions — implied monthly indices, implicit-price profiles, learning curves,
segment accuracy (
scripts/03_extensions.py). - Robustness — retransformation rule, leakage-free holdouts, nested spatial FE, fine
Box–Cox grid, boosting hyper-parameters (
scripts/06_robustness.py).
🖼 Figures & tables inventory
| Figure | Content |
|---|---|
fig_forms |
Axis A — functional form, MdAPE on the random holdout |
fig_time |
Axis B — time effects |
fig_space |
Axis C — spatial controls (overfitting at ~1 km) |
fig_methods |
Axis D — estimation methods on the identical attribute set |
fig_generalization |
the generalization gap: random vs forward-in-time MdAPE, all 20 models |
fig_index |
constant-quality monthly price indices implied by competing forms |
fig_profiles |
implicit ln-price profiles: quadratic OLS vs gradient boosting |
fig_learning |
learning curves: MdAPE against training-set size |
fig_segments |
MdAPE by market segment (random holdout) |
fig_horizon |
forward-in-time MdAPE by test quarter (distance from the training window) |
Tables (results/tables/*.tex): summary statistics, scoreboard random holdout,
scoreboard forward-in-time holdout, inference (bootstrap CIs + DM tests), targeted pairs,
Box–Cox profile, generalization gap, same-period / level decomposition, by-quarter
accuracy, learning curves, segments, median vs. Duan retransformation, grouped holdouts,
seen/unseen leakage decomposition, nested spatial FE, boosting hyper-parameters.
⚠️ Limitations
- Sales below $50k are absent from the source registry; residual non-arm's-length transfers may survive the screens.
- 2021–2026 spans a boom, a rate correction and a recovery — level statistics are period-specific; the forward-in-time test is deliberately demanding.
- Attributes are those of the assessment roll (no interior quality, renovations or listing text); the ML advantage measured here is the value of flexible spatial learning on that attribute set, not of richer data.
- Tree ensembles use fixed, conventional hyper-parameters; four materially different configurations (including early stopping on a time-ordered validation set) leave the ranking unchanged (paper §7.5), but no linear model with a continuous spatial surface (kriging, bivariate splines) is estimated as a competitor.
- The forward-in-time test covers one regime (the 2025–26 recovery); the level re-levelling uses the test-set median ratio (an oracle upper bound for what a published index would deliver).
📝 Citation
@techreport{boucher2026horserace,
author = {Boucher, Simon-Pierre},
title = {Half a Million Prices, Twenty Models: A Systematic Assessment
of Hedonic Specifications and Estimation Methods for the
Quebec Housing Market, 2021--2026},
institution = {Universit\'e du Qu\'ebec en Outaouais,
D\'epartement des sciences administratives},
type = {Working Paper},
number = {11},
year = {2026},
month = {October},
note = {Version 1.1}
}👤 Author & contact
Simon-Pierre Boucher — Département des sciences administratives, Université du Québec en Outaouais (UQO), Gatineau, QC. 📧 simon-pierre.boucher@uqo.ca
🗒 Notes
- Numbering: UQO Working Paper No. 11 belongs to this paper (August 2026). Two other manuscripts briefly carried the label "WP 11": Hedonic House Pricing with Language-Model and Vision Features (now WP 14) and Does the Sales Comparison Approach Actually Work? (now WP 16).
- Cover page, metadata and layout follow
Recherche-UQO/HARMONISATION.md(2026-10-17). - Revision 2026-10-17 (v1.1): full editorial-board review. Scientific report in
docs/REVIEW-REPORT.md, audit trail (every changed number, old → new) indocs/CHANGELOG-REVISION.md; pre-revision PDF kept asdocs/main_pre-revision-20261017.pdf(git tagpre-revision-20261017). Main correction: v1.0 applied Duan smearing to the linear log models but not to the tree ensembles; under a uniform rule the forward-in-time "reversal" disappears (the gap shrinks to 0.5 pp, n.s.) and the paper now decomposes the forward error into a common level (staleness) component and intact relative skill.