SPB Git

spb/wp9_uqo

Public

UQO Working Paper No. 9 — A grand hedonic model of the Canadian housing market: decomposing structure and location value.

This looks like a research repository (LaTeX, bibliography, figures). Preview Research Mode· enable it with research: true on the API.

History
NameLast commitUpdated
data révision scientifique complète (editorial board, 2026-10-17)
docs révision scientifique complète (editorial board, 2026-10-17)
figures révision scientifique complète (editorial board, 2026-10-17)
paper révision scientifique complète (editorial board, 2026-10-17)
results révision scientifique complète (editorial board, 2026-10-17)
scripts révision scientifique complète (editorial board, 2026-10-17)
src révision scientifique complète (editorial board, 2026-10-17)
.gitignore harmonisation standard UQO WP (2026-10-17)
README.md révision scientifique complète (editorial board, 2026-10-17)
requirements.txt WP9 — A Grand Hedonic Model of the Canadian Housing Market
README.md source

# 🏘️ 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

Paper Python DuckDB Estimation Reproducible Revised Institution Author Contact

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

  1. Status
  2. What this paper does
  3. Headline results
  4. Repository layout
  5. Quick start
  6. The pipeline, step by step
  7. Data
  8. Methodology
  9. The two results tiers (reproduced vs. reference)
  10. Figures & tables inventory
  11. Limitations
  12. Citation
  13. 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:

text
ln P_i = α + β·ln(Area_i) + x_i'γ + d_i'δ + μ_f(i) + ε_i

where 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

text
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

bash
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 to size_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 _province seed mislabelled 11.7% of listings and is kept as prov_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 (see docs/AUDIT.md). Kept for the reconciliation table in Appendix C of the paper. python3 scripts/0{4,5}_*.py --results reference rebuilds 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

bibtex
@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 in Recherche-UQO/_archives/versions-mai-2026/grand-hedonic-model-canada--immo-wp9-20260529; docs/AUDIT.md and docs/CHANGES.md describe 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 is docs/main_pre-revision-20261017.pdf. Deviation from the standard: none (appendix added per the template).

Clone wp9_uqo

Anonymous clone and fetch. Pushing requires the owner's personal access token.

ZIP tar.gz snapshot of main
Files · main
Menu

Theme