spb/qwhpi Public
QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.
Python 63.9%
TypeScript 25.4%
CSS 5.5%
TeX 3.5%
SQL 0.8%
Makefile 0.5%
Dockerfile 0.5%
1#!/usr/bin/env python32# =============================================================================3# QWHPI — Quebec Weekly Housing Price Index4# Author : Simon-Pierre Boucher5# Contact : contact@spboucher.ai6# File : engine/scripts/04_baseline.py7# Purpose : Step 4 — pooled hedonic time-dummy baseline for Quebec + the four8# major cities × All/Unifamilial/Condo/Plex (20 cells).9# =============================================================================10"""Baseline index build (Execution Order step 4).1112For every cell: pooled Model A time-dummy regression (2021→present),13week coefficients rebased to 2021 avg = 100, raw weekly median attached for14comparison. Results land in ``data/interim/baseline_indexes.parquet``.15"""1617from __future__ import annotations1819import sys20import time21from pathlib import Path2223sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))2425import polars as pl2627from qwhpi.clean import CLEAN_PARQUET, research_sample28from qwhpi.config import INTERIM_DIR, TABLES_DIR, ensure_dirs29from qwhpi.features import build_features30from qwhpi.hedonic import estimate_time_dummy3132BASELINE_PARQUET = INTERIM_DIR / "baseline_indexes.parquet"3334GEOGRAPHIES = [35 ("quebec", "Québec (province)", "province", None),36 ("montreal", "Montréal", "municipality", "Montréal"),37 ("quebec-city", "Québec", "municipality", "Québec"),38 ("laval", "Laval", "municipality", "Laval"),39 ("gatineau", "Gatineau", "municipality", "Gatineau"),40]41TYPE_SETS = ["all", "unifamilial", "condo", "plex"]424344def main() -> None:45 ensure_dirs()46 clean = pl.read_parquet(CLEAN_PARQUET)47 feat = build_features(research_sample(clean))4849 frames: list[pl.DataFrame] = []50 summary_rows: list[dict] = []5152 for geo_id, geo_name, level, mun_filter in GEOGRAPHIES:53 base = feat if mun_filter is None else feat.filter(pl.col("municipality") == mun_filter)54 for ptype in TYPE_SETS:55 cell = base if ptype == "all" else base.filter(pl.col("propertyType") == ptype)56 t0 = time.time()57 res = estimate_time_dummy(cell, absorb="loc_fine",58 include_property_type=(ptype == "all"))59 if res is None:60 print(f"[skip] {geo_id} x {ptype}: n={cell.height} too thin")61 summary_rows.append({"geography": geo_id, "property_type": ptype,62 "n_obs": cell.height, "estimated": False,63 "r2": None, "median_weekly_tx": None})64 continue6566 # Raw weekly median + volume for comparison/validation.67 weekly_raw = (68 cell.group_by("week_str")69 .agg(pl.col("amount").median().alias("raw_median"),70 pl.len().alias("transactions"))71 .rename({"week_str": "week"})72 )73 tbl = (74 res.table.join(weekly_raw, on="week", how="left")75 .with_columns(76 pl.lit(geo_id).alias("geography_id"),77 pl.lit(geo_name).alias("geography_name"),78 pl.lit(level).alias("geography_level"),79 pl.lit(ptype).alias("property_type"),80 )81 )82 frames.append(tbl)83 med_tx = weekly_raw["transactions"].median()84 summary_rows.append({"geography": geo_id, "property_type": ptype,85 "n_obs": res.n_obs, "estimated": True,86 "r2": round(res.r2, 4),87 "median_weekly_tx": med_tx})88 print(f"[ok] {geo_id} x {ptype}: n={res.n_obs:,} r2={res.r2:.3f} "89 f"({time.time() - t0:.1f}s)")9091 out = pl.concat(frames)92 out.write_parquet(BASELINE_PARQUET)93 pl.DataFrame(summary_rows).write_csv(TABLES_DIR / "baseline_model_summary.csv")94 print(f"\nSaved {out.height:,} index observations to {BASELINE_PARQUET}")959697if __name__ == "__main__":98 main()99