SPB Git

spb/wp2_uqo Public

UQO Working Paper No. 2 — Decoding Real Estate Descriptions: text-based hedonic analysis of housing listings.

TeX 73.8% Python 26%
ZIP
NameLast commitUpdated
data Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
figures Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
paper Complete bibliography audit (100% verified) + README refresh 5 days ago
results Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
scripts Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
src Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
.gitignore Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
AUDIT.md Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
CHANGES.md Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
PAPER_REVIEW.md Scholarly upgrade: verified bibliography (59→86 refs), expanded... 5 days ago
README.md Complete bibliography audit (100% verified) + README refresh 5 days ago
requirements.txt Initial commit: semantic hedonic pricing pipeline, figures, and paper... 5 days ago
UPGRADE_REPORT.md Complete bibliography audit (100% verified) + README refresh 5 days ago
README.md

# 🏠 Decoding Real Estate Descriptions

# Semantic Embeddings and Hedonic Pricing of Residential Properties in Quebec

UQO Working Paper No. 2

Author Contact Institution

Python LaTeX sentence-transformers statsmodels scikit-learn

Reproducible Sample Model Paper References JEL


TL;DR — Listing descriptions written by real estate agents contain price-relevant information that classic hedonic variables miss. This project embeds the free-text PublicRemarks of 17,087 Quebec single-family house listings with a sentence transformer, projects each embedding onto 20 researcher-defined semantic reference descriptions (Luxury, Needs Renovation, Waterfront, Motivated Seller, …) via cosine similarity, and adds those 20 interpretable scores to a log-price hedonic OLS model. Adjusted R² rises from 0.452 to 0.511 (joint F = 99.53, p < 0.001), and each semantic dimension carries a named, signed, economically meaningful implicit price.


# 📖 Table of Contents


# 🔍 Overview

Hedonic pricing models decompose a property's price into the implicit prices of its characteristics — bedrooms, bathrooms, lot size. But the narrative of a listing ("cuisine gastronomique, comptoirs de quartz…" vs. "besoin de rénovation, vendu tel quel…") carries quality information that no structured field captures.

The catch, historically, is a depth-vs-interpretability trade-off:

Approach Semantic depth Economic interpretability
Keyword counts / bag-of-words ❌ shallow ✅ high
Sentiment scores ❌ one dimension ✅ high
LDA topics 🟡 moderate ❌ unstable
Raw BERT/transformer embeddings ✅ deep ❌ opaque (384–768 anonymous dims)
Reference-based cosine projection (this paper) deep high

The idea: instead of feeding 384 anonymous embedding dimensions into a regression, define 20 short reference descriptions — synthetic French paragraphs, each embodying one qualitative housing dimension — embed them with the same model, and use the cosine similarity between each listing and each reference as 20 named regressors. Every coefficient then reads directly as "the implicit price of sounding more luxury / more fixer-upper / more waterfront", per standard deviation.

This is conceptually analogous to factor-mimicking portfolios in asset pricing, and to concept-bottleneck models in interpretable ML: the deep representation is channeled through human-named concepts before inference.


# 📊 Key Results

# Model comparison (n = 17,087)

Model Specification Adj. R² AIC BIC k
A Structural only 0.4525 0.4523 26,165 26,219 6
B A + description length 0.4647 0.4645 25,779 25,841 7
C A + 20 semantic similarities 0.4962 0.4954 24,783 24,992 26
D Full (B + C) 0.5122 0.5115 24,232 24,448 27
E Parsimonious (16 significant dims) 0.5118 0.5111 24,239 24,425 23

Joint significance of the 21 text variables (D vs. A): F = 99.53, p < 0.001. Variance decomposition of Model D: structural 88.3 %, text length 2.4 %, semantics 9.3 %.

# Implicit semantic price gradients (Model D, per +1 SD, HC3 errors)

📈 Positive Impact 📉 Negative Impact
Modern/Contemporary +16.4 %* Family-Friendly −11.6 %*
Luxury +14.2 %* New Construction −10.5 %*
Land & Nature +13.0 %* Quiet & Peaceful −9.5 %***
Entry-Level +10.4 %*** Bright & Spacious −8.9 %***
Waterfront +4.7 %*** Motivated Seller −8.5 %*
Panoramic View +3.0 %** Needs Renovation −7.9 %*

*** p < 0.001, ** p < 0.01. Negative coefficients on Family-Friendly / New Construction largely proxy suburban location (see paper §6). Impact = (e^β − 1) × 100 %.

# Heterogeneity & robustness (highlights)

  • Quantile regressions (τ = 0.25 / 0.50 / 0.75): the Luxury premium rises monotonically from 9.7 % at the 25th percentile to 17.3 % at the 75th; urgency and condition discounts attenuate at the top of the market.
  • Bootstrap (1,000 replications, seed 42): SEs within 9 % of HC3 (15/20 within 5 %).
  • Outlier trimming (1st/99th price percentiles): no sign reversal, all 16 significant dimensions stay significant.
  • Multicollinearity is real and disclosed: similarity dimensions correlate at 0.66–0.97 (mean VIF 17.2). Block-level inference is unaffected; individual coefficients are partial associations within a correlated block.
  • PCA benchmark: 20 principal components of the raw embeddings fit better (ΔR² +0.070 vs. +0.044) but are economically unreadable — the paper quantifies the interpretability-vs-fit trade-off explicitly.

# 🧪 Methodology

┌─────────────┐   ┌──────────────┐   ┌──────────────┐   ┌─────────────┐   ┌──────────────┐
│  Property   │   │   Sentence   │   │    Cosine    │   │   Hedonic   │   │   Implicit   │
│  Listings   ├──▶│  Embeddings  ├──▶│  Similarity  ├──▶│  OLS Model  ├──▶│    Price     │
│ (n=17,087)  │   │   (384-d)    │   │  (20 refs)   │   │ (HC3 s.e.)  │   │  Estimates   │
└─────────────┘   └──────────────┘   └──────────────┘   └─────────────┘   └──────────────┘
 PublicRemarks     all-MiniLM-L6-v2    S = E · Rᵀ          log(P) = α + β'X + δℓ + γ's + ε
  1. Extraction — houses only, price > 0, description ≥ 20 characters; minimal preprocessing (the transformer handles raw text).
  2. Embeddingall-MiniLM-L6-v2 (22.7M params, 384-d, L2-normalized), batch 256; the whole corpus encodes in seconds on Apple Silicon.
  3. Projection — 20 French reference descriptions (146–257 chars each, full verbatim text in paper Appendix A), designed for semantic saturation, dimensional specificity, linguistic consistency. One matrix product yields the 17,087 × 20 similarity matrix.
  4. Estimation — nested OLS models A–E on log price, all covariates standardized, HC3 robust errors; joint F-tests; quantile regressions; bootstrap; winsorization; VIF; Lasso/Elastic-Net selection; PCA benchmark.

The 20 dimensions span six domains: Quality & Standing (Luxury, Entry-Level) · Condition (Renovated, Needs Renovation) · Physical (Bright & Spacious, Garage & Parking, Finished Basement, Pool & Landscaping) · Location & Setting (Land & Nature, Panoramic View, Premium Location, Quiet & Peaceful, Waterfront) · Style & Character (Modern/Contemporary, Heritage/Character, New Construction, Energy Efficient, Family-Friendly) · Market Signals (Income/Investment, Motivated Seller).


# 📁 Repository Structure

wp2_uqo/
├── README.md                ← you are here
├── AUDIT.md                 ← forensic audit of the original (pre-restructuring) project
├── CHANGES.md               ← everything that was moved / refactored / corrected, with diffs
├── PAPER_REVIEW.md          ← critical pre-upgrade assessment of the paper's scholarship
├── UPGRADE_REPORT.md        ← literature expansion: refs added/replaced, claims flagged
├── requirements.txt         ← pinned dependencies (exact versions used for verification)

├── data/                    ← NOT tracked by git (see data/README.md)
│   ├── raw/louka.db         │  SQLite, 268 MB, 46,479 listings (5 categories)
│   └── processed/           │  parquet sample, embeddings cache, similarity matrix,
│       └── legacy/          │  analysis CSV + pre-refactor artifacts kept for comparison

├── src/                     ← analysis package (imported by scripts/)
│   ├── config.py            │  repo-relative paths, model name, seed, structural vars
│   ├── references.py        │  the 20 French reference descriptions + English labels
│   ├── data.py              │  SQLite → DataFrame extraction (n = 17,087)
│   ├── embeddings.py        │  encoding, caching, cosine-similarity features
│   └── models.py            │  standardized-design OLS A–E, HC3

├── scripts/                 ← numbered pipeline entry points (run in order)
│   ├── 01_prepare_data.py   │  louka.db → data/processed/houses.parquet
│   ├── 02_similarities.py   │  embeddings (cached) + 20 similarity features → CSV
│   ├── 03_models.py         │  models A–E → results/*.csv
│   ├── 04_robustness.py     │  VIF · BP · quantile · winsor · bootstrap · Lasso/ENet
│   ├── 05_figures.py        │  figures fig1–fig11 (PDF + PNG)
│   └── 06_tables.py         │  LaTeX tables generated from results (no hand transcription)

├── figures/                 ← all 11 paper figures, regenerated from the pipeline
├── results/                 ← model & robustness outputs (CSV, versioned)

└── paper/                   ← LaTeX source (UQO working-paper style)
    ├── main.tex             │  metadata + section inputs (53-page PDF)
    ├── preamble.tex         │  packages, layout, custom macros
    ├── main.pdf             │  compiled paper (committed for convenience)
    ├── Makefile             │  latexmk build
    ├── references.bib       │  86 entries, every one verified (OpenAlex + DOI)
    ├── sections/            │  titlepage · intro · literature · methodology · data ·
    │                        │  results · robustness · discussion · conclusion · appendix
    └── tables/              │  7 machine-generated + 3 hand-maintained booktabs tables

# ⚙️ The Pipeline, Script by Script

# Script Input Output Runtime*
1 01_prepare_data.py data/raw/louka.db houses.parquet (17,087 × 14) ~15 s
2 02_similarities.py parquet + refs embeddings .npy (cached), sim_matrix.npy, analysis CSV ~5 s cached / ~3 min fresh
3 03_models.py analysis CSV model_comparison.csv, coefficient tables, descriptive stats ~5 s
4 04_robustness.py analysis CSV robustness_results.csv (163 rows: VIF, BP, QR, winsor, bootstrap, Lasso) ~4 min (bootstrap)
5 05_figures.py analysis CSV figures/fig1…fig11.{pdf,png} ~30 s
6 06_tables.py results/*.csv paper/tables/*.tex (7 tables) ~2 s

*Apple M-series laptop. All stochastic steps are seeded (seed = 42): bootstrap resampling, CV folds, scatter subsampling.

Design principles applied during the refactor (see CHANGES.md for the full story):

  • 🚫 No hardcoded paths — everything resolves from the repo root (src/config.py); the DB location can be overridden with WP2_DB_PATH.
  • 🧬 Single source of truth — the extraction logic and the 20 reference texts, previously copy-pasted across five scripts (in two languages), now live in exactly one place each.
  • 🔁 Deterministic & cached — embeddings are deterministic for a given model version and cached; --force re-encodes from scratch.
  • 📋 Tables are compiled artifacts — every number in the paper's statistical tables is written by 06_tables.py from the results CSVs. The original project transcribed numbers by hand, which introduced errors that this restructuring caught and fixed (documented, with old→new values, in CHANGES.md §4).

# 💾 Data

The raw data is a SQLite database (louka.db, 268 MB) of Quebec residential listings collected from Realtor.ca / Centris: 46,479 listings across five categories — houses (17,089), condos (8,586), land (8,506), rentals (8,240), plexes (4,058). Each row carries structured fields (price, bedrooms, bathrooms) plus a full JSON blob including the agent-written PublicRemarks.

The analysis sample: single-family houses with a positive price and a description of ≥ 20 characters → n = 17,087. Median listing price $589,900 (mean $807,259, max $25M). Descriptions average 510 characters and are truncated at ~700 characters in the export (disclosed in the paper).

⚠️ data/ is not tracked by git: the raw database exceeds GitHub's 100 MB file limit, contains a leftover application users table, and the listing texts are proprietary platform content. data/README.md explains how to place a copy of louka.db to re-run the pipeline from scratch. All derived, aggregate outputs (results CSVs, figures, tables, paper) are versioned, so every number in the paper is inspectable without the raw data.


# 🔄 Reproducing Everything

git clone https://github.com/spboucher-ai/wp2_uqo.git
cd wp2_uqo
python3 -m pip install -r requirements.txt

# place louka.db in data/raw/ (see data/README.md), then:
python3 scripts/01_prepare_data.py     # extract the house sample
python3 scripts/02_similarities.py     # embed + project (add --force to re-encode)
python3 scripts/03_models.py           # hedonic models A–E
python3 scripts/04_robustness.py       # full robustness battery (~4 min)
python3 scripts/05_figures.py          # all 11 figures
python3 scripts/06_tables.py           # LaTeX tables from results

cd paper && latexmk -pdf main.tex      # 53-page PDF, 0 errors

Expected checkpoints along the way:

  • Step 1 prints 17,087 houses with a valid price and description.
  • Step 3 prints the model table with R² = 0.4525 / 0.4647 / 0.4962 / 0.5122 / 0.5118 and F = 99.53.
  • Step 4 prints Winsorized R2=0.4867 and the Lasso selection (4/20 at the CV penalty).

# 📝 Building the Paper

The paper (paper/) follows the UQO working-paper format: title page with logo, abstract page with keywords + JEL codes, IMRaD body (Introduction · Literature · Methodology · Data · Results · Robustness · Discussion · Conclusion), natbib/apalike bibliography, and two appendices (verbatim reference texts; parsimonious-model estimates).

cd paper
make            # or: latexmk -pdf main.tex

Figures are pulled from ../figures/ via \graphicspath, so the paper always reflects the latest pipeline run. The build is clean: 0 errors, 0 undefined references, 0 missing citations (59/59 bib keys resolve).


# ✅ Verification & Provenance

This repository is a restructuring of an earlier research folder. Before anything was rewritten, the pipeline was validated against the original artifacts:

Check Result
Analysis dataset (17,087 × 30) ✅ identical, column by column
Similarity matrix (17,087 × 20) ✅ bit-identical
Models A–E fit statistics ✅ identical to the paper's Table 4
Model D coefficients (27 params) ✅ identical to the paper's Table 5
Figures fig1, fig4–fig11 ✅ regenerated, byte-size within ±2 B
Figures fig2, fig3 🔧 were missing from the original repo (placeholder boxes in the old PDF) — regenerated
robustness_results.csv 🔧 lost in the original — regenerated (seeded)
Descriptive/robustness numbers in the old LaTeX ⚠️ several did not match the code's own outputs — corrected and now machine-generated; full old→new list in CHANGES.md §4

AUDIT.md documents the original project's layout, dead files, and defects; CHANGES.md documents every move, refactor, and numeric correction.


# Figure What it shows
1 fig1_model_comparison R² / adj. R² across models A–E
2 fig2_coefficient_plot Forest plot of the 20 semantic implicit prices (Model D)
3 fig3_similarity_distributions Box plots of the 20 cosine-similarity features
4 fig4_quintile_heatmap Mean similarity by price quintile × dimension
5 fig5_correlation_matrix Inter-dimension Pearson correlations (0.66–0.97)
6 fig6_scatter_plots Four illustrative similarity–price scatters
7 fig7_methodology Pipeline diagram
8 fig8_r2_decomposition Stacked decomposition of Model D's R²
9 fig9_price_distribution Price and log-price histograms
10 fig10_structural_coefficients Structural coefficients with 95 % CIs
11 fig11_residual_diagnostics Residuals vs. fitted + normal Q-Q

All figures exist as publication PDF (vector) and PNG (300 dpi).


# 📦 Requirements

Package Version Used for
Python 3.14
numpy 2.4.4 linear algebra
pandas 3.0.2 data wrangling
pyarrow 24.0.0 parquet I/O
scipy 1.17.1 tests, distributions
scikit-learn 1.6.1 PCA, Lasso/ElasticNet CV, scaling
statsmodels 0.14.6 OLS/HC3, QuantReg, BP test, VIF
matplotlib 3.10.9 figures
sentence-transformers 5.5.0 embeddings
torch 2.12.0 transformer backend (MPS on Apple Silicon)

LaTeX: TeX Live 2026 with latexmk (newtx, booktabs, threeparttable, natbib, hyperref…).


# 📚 Citation

@techreport{boucher2026decoding,
  author      = {Boucher, Simon-Pierre},
  title       = {Decoding Real Estate Descriptions: Semantic Embeddings and
                 Hedonic Pricing of Residential Properties in Quebec},
  institution = {Universit\'e du Qu\'ebec en Outaouais,
                 D\'epartement des sciences administratives},
  type        = {Working Paper},
  number      = {2},
  year        = {2026},
  month       = {May}
}

# 👤 Author & Contact

Simon-Pierre Boucher Département des sciences administratives Université du Québec en Outaouais (UQO) 283, boulevard Alexandre-Taché, Gatineau (Québec) J9A 1L8, Canada

📧 contact@spboucher.ai

Comments and suggestions are welcome. All errors are my own.


© 2026 Simon-Pierre Boucher. Paper, code, and figures — all rights reserved.