🏘️ A Grand Hedonic Model of the Canadian Housing Market
UQO Working Paper No. 9 — Decomposing the Value of Structure and Location across 142,121 MLS Listings with High-Dimensional Neighbourhood Fixed Effects
TL;DR — On 142,121 Canadian MLS listings (asking prices), absorbing 1,155 neighbourhood (FSA) fixed effects lifts explained log-price variance from 44% → 78%. A Shapley–Owen decomposition assigns ≈34 pp to structure and ≈36 pp to location: the two are of comparable importance, and location is the single largest block once province and neighbourhood are grouped (the sequential ladder would say 44 vs 32). Living-area elasticity 0.53, +12% per full bathroom, bedrooms ≈ 0. Out of sample: MdAPE 15.6% (R² 0.77) in neighbourhoods seen in training, 17.3% (R² 0.74) in neighbourhoods never seen when the FSA effect is interpolated from its neighbours.
📌 Status
| Item | Value |
|---|---|
| Version | 1.3 (October 2026) — revised 2026-10-17 by the editorial-board review (docs/REVIEW-REPORT.md, docs/CHANGELOG-REVISION.md); v1.2 (May 2026) archived as docs/main_pre-revision-20261017.pdf and git tag pre-revision-20261017 |
| SSRN | 7626678 — v1.2 uploaded 2026-10-17; v1.3 to be uploaded (title N changed 140,931 → 142,121) |
| Journal | not yet submitted |
| Code & data | fully reproducible from scripts/ on the committed data/processed/analysis.parquet; raw DuckDB snapshot not redistributed |
📖 Table of contents
- Status
- What this paper does
- Headline results
- Repository layout
- Quick start
- The pipeline, step by step
- Data
- Methodology
- The two results tiers (reproduced vs. reference)
- Figures & tables inventory
- Limitations
- Citation
- Author & contact
🎯 What this paper does
A dwelling is the archetypal heterogeneous good: no two houses are identical, and the most important attribute — where it stands — is unobservable as a scalar. This project estimates a semi-logarithmic hedonic price equation at national scale for Canada:
ln P_i = α + β·ln(Area_i) + x_i'γ + d_i'δ + μ_f(i) + ε_iwhere P_i is the asking price of listing i and μ_f(i) is a fixed effect for its
Forward Sortation Area (FSA — the first three characters of the postal code). With 1,155
absorbed FSA intercepts (1,146 FSAs + 9 province-level pools of small FSAs), every
neighbourhood gets an arbitrary price level that soaks up schools, transit, coastline,
employment density — observed or not — and the structural implicit prices (β, γ) are
identified purely from within-neighbourhood variation. Estimation is by absorbing
least squares (numerically identical to full-dummy OLS); baseline inference is clustered by FSA.
Beyond the headline decomposition the paper delivers: a Shapley–Owen decomposition of R² across regressor blocks (order-free alternative to the sequential ladder); inference under HC1 / FSA-cluster / province-cluster / two-way SEs and a province-level wild-cluster bootstrap; out-of-sample valuation accuracy (repeated random 80/20 splits and a spatial holdout of entire FSAs with interpolated effects, Duan-smeared retransformation); robustness across eight sample cuts; quantile hedonic regressions; quadratic and spline tests of diminishing returns to floor space; an urban price gradient in distance to nine metros (pooled, metro intercepts, quadratic); Moran's I diagnostics with k-sensitivity; leave-one-province-out transferability; dwelling-type/ownership effects with the Kennedy correction; and a ranked map of Canada's most and least expensive neighbourhoods net of structure.
🏆 Headline results
| Quantity | Value |
|---|---|
| Estimation sample | 142,121 listings — 83,524 houses, 57,853 condos, 744 other; 9 provinces (province from FSA letter) |
| Absorbed neighbourhood effects | 1,155 (1,146 FSAs ≥ 25 listings + 9 pooled residual categories) |
| R² houses: M1 structural / M2 +type / M3 +province / M4 +FSA | 0.467 / 0.470 / 0.605 / 0.769 |
| R²: grand model, all residential, FSA FE (M5) | 0.776 (structure-only on the same sample: 0.436) |
| Variance decomposition (pp of R², all residential) | sequential: structure 43.6, type 1.6, province 14.3, FSA 18.1 · Shapley: 33.0 / 7.5 / 8.9 / 28.2 · Owen: structure 34.0, type 7.3, location 36.3 |
| Living-area elasticity (M5) | 0.529 (FSA-cluster SE 0.009; province-cluster 0.037; wild-cluster p = 0.004) |
| Full-bathroom premium | 0.112 log points ≈ +11.9% per bathroom (0.148 ≈ +16% on houses with province FE) |
| Bedrooms, half baths (conditional on area) | −0.006, −0.034 — not significant under province-level inference |
| Dwelling type (vs apartment, same size & FSA) | house +46%, triplex +43%, fourplex +39%, duplex +16%, townhouse ≈ 0 (Kennedy-corrected) |
| Out-of-sample, random 80/20 (within support) | R² 0.772, MdAPE 15.6%, 60% within ±20% (5 seeds: 0.769 ± 0.010, 15.5 ± 0.2%) |
| Out-of-sample, held-out FSAs (20% of neighbourhoods) | province-mean effect: R² 0.52, MdAPE 25% · IDW of 5 nearest FSAs: R² 0.74, MdAPE 17.3% |
| Moran's I of residuals, structural → grand (k = 10) | 0.50 → 0.06 (−87%); k = 5: 0.52 → 0.11; k = 20: 0.47 → 0.03 |
| Floor space | quadratic: elasticity 0.63 at 60 m² → 0.41 at 350 m²; spline: 0.57 / 0.65 / 0.58 / 0.41 / 0.48 by quintile; ΔR² = 0.001 |
| Urban gradient | −0.082 per log unit of distance ⇒ −5.5% per doubling; +21% within 5 km, −38% beyond 320 km |
| LOPO transfer (within-province R²) | mean 0.34; MB/AB/NB/ON/SK/NS 0.46–0.52; QC 0.19; BC −0.49 |
| Neighbourhood premia span | ×10.5 between extremes (V6S +226% vs A0B −69%), ×6.7 between P99 and P1 |
The most expensive neighbourhoods net of structure are on Vancouver's west side (V6S Dunbar–Point Grey, V6T UBC), in Whistler (V8E) and West Vancouver (V7T); the first Ontario FSA (M5R, the Annex) ranks 14th. The cheapest are in rural Newfoundland (A0B, A0G), Saskatchewan (S0N, S0C, S0H, S0A), Manitoba and New Brunswick.
📁 Repository layout
wp9_uqo/
├── README.md ← you are here
├── docs/ REVIEW-REPORT.md + CHANGELOG-REVISION.md (editorial-board
│ revision, 2026-10-17), main_pre-revision-20261017.pdf (v1.2),
│ AUDIT.md, CHANGES.md, PAPER_REVIEW.md, UPGRADE_REPORT.md (history)
├── requirements.txt pinned Python dependencies
├── .gitignore
├── data/
│ ├── raw/
│ │ ├── realtor_mls_unique.duckdb ⚠ 747 MB — NOT in git (see Data section)
│ │ └── README.md
│ └── processed/
│ └── analysis.parquet estimation sample, 142k rows (generated by step 01)
├── src/wp9/ analysis package
│ ├── config.py paths, constants, seeds, metro coordinates
│ ├── parsing.py parsers for the semi-structured MLS fields
│ ├── sample.py raw DuckDB → estimation sample
│ ├── models.py design matrix, M1–M5 ladder, AbsorbingLS, FE recovery
│ └── plotstyle.py shared matplotlib style
├── scripts/ numbered pipeline entry points
│ ├── 01_build_sample.py
│ ├── 02_estimate_core.py
│ ├── 03_estimate_extended.py
│ ├── 04_make_figures.py (--results reproduced|reference; default reproduced)
│ ├── 05_make_tables.py (--results reproduced|reference; default reproduced)
│ └── 06_revision_analyses.py Shapley/Owen, SE comparison + wild bootstrap, spline,
│ spatial holdout, Moran sensitivity, metro gradient
├── figures/ all 17 paper figures (PNG, generated)
├── results/
│ ├── reference/ archived outputs of the May 2026 version (v1.2)
│ ├── reproduced/ outputs of the current pipeline — THE PAPER'S NUMBERS (v1.3)
│ └── tables/ the 13 LaTeX tables consumed by the paper
└── paper/
├── main.tex preamble + metadata; \input's the sections
├── main.pdf compiled paper (52 pages)
├── sections/ titlepage, introduction, literature, data,
│ methodology, results, robustness, discussion, conclusion
├── appendix/appendix.tex variable construction, Moran sensitivity, v1.2 ↔ v1.3 reconciliation
├── references.bib 91 entries (all Crossref-verified, DOIs), natbib author-year, aer style
├── Makefile / .latexmkrc build config
└── uq_logo.jpg🚀 Quick start
git clone https://github.com/spboucher-ai/wp9_uqo.git
cd wp9_uqo
python3 -m pip install -r requirements.txt
# Full pipeline (needs data/raw/realtor_mls_unique.duckdb — see Data section):
python3 scripts/01_build_sample.py # duckdb → data/processed/analysis.parquet
python3 scripts/02_estimate_core.py # M1–M5 ladder → results/reproduced/
python3 scripts/03_estimate_extended.py # OOS, robustness, quantile, Moran, LOPO…
python3 scripts/06_revision_analyses.py # Shapley/Owen, SE comparison, spline, spatial holdout…
python3 scripts/04_make_figures.py # figures/ (17 PNG)
python3 scripts/05_make_tables.py # results/tables/ (13 .tex)
# Paper:
cd paper && latexmk -pdf main.tex # or `make`Without the raw DuckDB you can still run steps 02→06: the committed
data/processed/analysis.parquet (6 MB) is the estimation sample produced by step 01.
The whole pipeline (01→06) runs in about 2 minutes on an Apple M5 Max.
🔬 The pipeline, step by step
| Step | Script | Input | Output | Runtime* |
|---|---|---|---|---|
| 01 | 01_build_sample.py |
raw DuckDB (172,019 rows) | analysis.parquet (142k rows, 21 cols) |
~40 s |
| 02 | 02_estimate_core.py |
parquet | fit.json, coef_M{1,2,3,5}.csv, grand_model.parquet, fsa_premia.csv |
~1 min |
| 03 | 03_estimate_extended.py |
parquet + step 02 | oos.json, robustness.csv (8 cuts), heterogeneity.csv, quantile.csv, lopo.csv, ext2.json, Moran arrays, gradient/nonlinearity bands |
~10 s |
| 06 | 06_revision_analyses.py |
parquet + step 02 | shapley.csv, se_comparison.csv, spline.csv, spatial_holdout.json, moran_sensitivity.csv, gradient_metro.json |
~35 s |
| 04 | 04_make_figures.py |
results tier + parquet | 17 PNG figures | ~30 s |
| 05 | 05_make_tables.py |
results tier | 13 LaTeX tables | ~5 s |
* Apple Silicon, single process.
Step 01 in detail (documented in src/wp9/parsing.py / sample.py):
- bedrooms
"3 + 1"(main + basement) → summed; - living area: prefers the explicit floor-area measurement; banded entries
(
"1100-1500 sqft") map to the upper bound of the band; falls back tosize_interior; sqft → m² at 0.0929; - lot size parsed from free text (
"6000 sqft","0.14 ac","under 1/2 acre","50 x 120"frontage×depth) with a missingness indicator; the top 1% of positive parsed lots (multi-acre rural acreage strings) treated as "no usable lot info"; - dwelling type consolidated to 8 groups, ownership to 6 groups;
- FSA from the postal code; province from the first letter of the FSA (Canada Post
convention; the scraper's
_provinceseed mislabelled 11.7% of listings and is kept asprov_seed); territories / invalid letters (27 records) dropped; FSAs with < 25 listings pooled into<PROV>_other; - filters: strictly positive price, non-missing living area / bedrooms / bathrooms, valid FSA (147,236); then the extreme 1% tails of price and living area are trimmed (142,121).
📊 Data
Source file: data/raw/realtor_mls_unique.duckdb — a de-duplicated snapshot of
Canadian MLS "for-sale" listings: 172,019 rows × 81 columns (single table
listings), with list price, geocoded coordinates, postal code, and semi-structured
building/lot attributes.
⚠️ The raw DuckDB (747 MB) is not distributed in this repository (GitHub's 100 MB
file limit, plus its content is scraped listing data). It lives on the author's machines;
place it at data/raw/realtor_mls_unique.duckdb to run step 01. All downstream artifacts
— including the committed estimation sample — derive from it programmatically.
Estimation-sample snapshot (Table 1 of the paper): median asking price $639,900; median living area 135 m²; 3 bedrooms; 2 full bathrooms. Both price and price/m² are strongly right-skewed, motivating the log transformation. ON 55,897 · BC 32,994 · QC 19,949 · AB 19,463 · SK 3,458 · MB 3,062 · NS 3,044 · NB 2,980 · NL 1,274; PEI and the territories have no listings. Prices are asking prices, not transactions — every coefficient is an implicit list price.
📐 Methodology
- Specification ladder — M1 structural only → M2 + type/ownership → M3 + province FE (house subsample) → M4 houses + FSA FE → M5 grand model (all residential, FSA FE).
- Variance decomposition — sequential increments and Shapley values over the four blocks (16 regressions) plus Owen values grouping province + FSA into one location block (Israeli 2007; Huettner & Sunder 2012). The sequential ladder credits the ~10 pp shared by structure and location to whichever enters first.
- Absorbing least squares (
linearmodels.AbsorbingLS) sweeps out the 1,155 FSA intercepts; slopes identical to full-dummy OLS. Baseline SEs clustered by FSA; also HC1, province-cluster, two-way, and a restricted wild-cluster bootstrap-t at the province level (Rademacher, full enumeration of 2⁹ = 512 draws). - Percentage effects — e^γ − 1 for counts; Kennedy (1981) correction for indicators.
- Functional form — quadratic in ln(area) and a linear spline with quintile knots.
- Retransformation — Duan (1983) smearing.
- Validation — (i) random 80/20 splits × 5 seeds, evaluation on FSAs seen in training; (ii) spatial holdout: 20% of FSAs held out entirely × 5 reps, FSA effect imputed as the province mean or as the inverse-distance mean of the 5 nearest training FSAs; (iii) leave-one-province-out with province intercepts.
- Spatial diagnostics — Moran's I, row-standardised k-NN weights (k = 5, 10, 20), two 15,000-listing subsamples, 199 permutations.
- Urban gradient — location residual on ln(distance to nearest of 9 metros): pooled, with nearest-metro intercepts, quadratic; FSA-clustered SEs.
🧭 The two results tiers (reproduced vs. reference)
results/reproduced/— the paper's numbers since v1.3. Regenerated end-to-end by scripts 01–06 on the committed sample, with the province derived from the FSA.results/reference/— the archived outputs of the May 2026 version (v1.2), whose upstream cleaning code was lost (seedocs/AUDIT.md). Kept for the reconciliation table in Appendix C of the paper.python3 scripts/0{4,5}_*.py --results referencerebuilds the v1.2 figures/tables (the six original tables only).
🖼️ Figures & tables inventory
| # | Figure | Shows |
|---|---|---|
| 1 | fig_price_dist |
raw vs. log asking price |
| 2 | fig_province_ppm2 |
median $/m² by province (FSA-derived) |
| 3 | fig_r2 |
R² across the M1–M5 ladder |
| 4 | fig_decomp |
sequential vs. Shapley variance decomposition |
| 5 | fig_forest |
structural implicit prices, 95% CI (M3) |
| 6 | fig_size_gradient |
median price by size bin × dwelling type |
| 7a/7b | fig_map / fig_fsa_map |
listing map & FSA medians, ln $/m² |
| 8 | fig_premia |
top/bottom 12 FSA premia vs. median FSA |
| 9 | fig_nonlinear |
marginal elasticity vs. size: quadratic + spline |
| 10 | fig_gradient |
location premium by distance band to metro |
| 11 | fig_quantile |
quantile coefficients τ = 0.1…0.9 |
| 12 | fig_moran |
Moran scatterplots, structural vs. grand |
| 13 | fig_heterogeneity |
size elasticity by province, 95% CI |
| 14 | fig_oos |
OOS error buckets (±10/±20%) |
| 15 | fig_fit / fig_resid |
predicted vs. actual; residual diagnostics |
Tables (results/tables/, 13): summary_stats, shapley, regression (M1/M2/M3/M5),
type_effects, spline, gradient, robustness (8 cuts), se_comparison, quantile,
lopo, oos, spatial_holdout, moran_sensitivity (appendix).
⚠️ Limitations
Asking prices, not transactions (implicit list prices; accuracy is an upper bound for transaction-price models); no construction year / renovation / interior quality (within-FSA sorting remains); noisy fields (lot parsed from free text, storeys/parking with implausible extremes and zero when unreported, province reconstructed from the postal code); a single cross-section of one platform; FSAs are postal, not planning, units and 9 pooled residual categories are not neighbourhoods; structural prices do not transfer to BC (LOPO R² < 0). See §7 of the paper.
📚 Citation
@techreport{boucher2026grandhedonic,
author = {Boucher, Simon-Pierre},
title = {A Grand Hedonic Model of the Canadian Housing Market:
Decomposing the Value of Structure and Location across
142,121 MLS Listings with High-Dimensional Neighbourhood
Fixed Effects},
institution = {Universit\'e du Qu\'ebec en Outaouais,
D\'epartement des sciences administratives},
type = {Working Paper},
number = {9},
year = {2026},
month = {October},
note = {Version 1.3}
}👤 Author & contact
Simon-Pierre Boucher Département des sciences administratives, Université du Québec en Outaouais Gatineau — Pavillon Alexandre-Taché, 283 boulevard Alexandre-Taché, Gatineau (QC) J9A 1L8
📧 simon-pierre.boucher@uqo.ca · 🌐 spboucher.ai
All code files carry the header Author: Simon-Pierre Boucher — contact@spboucher.ai.
© 2026 Simon-Pierre Boucher. All rights reserved. The listing data snapshot is not redistributed; code and paper are shared for research reproducibility.
🗒 Notes
- Numbering: UQO Working Paper No. 9 since the first version (May 2026).
- The original flat May 2026 project (
immo-wp9-spb-20260529) is archived inRecherche-UQO/_archives/versions-mai-2026/grand-hedonic-model-canada--immo-wp9-20260529;docs/AUDIT.mdanddocs/CHANGES.mddescribe how this repository was rebuilt from it. - Cover page, metadata and layout follow
Recherche-UQO/HARMONISATION.md(2026-10-17). - Revision 2026-10-17 (v1.2 → v1.3): full scientific revision by the editorial-board
protocol (
Recherche-UQO/REVISION-MASTER-PROMPT.md). Report:docs/REVIEW-REPORT.md; audit trail of every change (old → new numbers, reasons, verification sources):docs/CHANGELOG-REVISION.md. The paper now reports the fully reproducible pipeline (results/reproduced/) with FSA-derived provinces; the N in the subtitle changed from 140,931 to 142,121 accordingly. The pre-revision PDF isdocs/main_pre-revision-20261017.pdf. Deviation from the standard: none (appendix added per the template).