SPB Git

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%
3.9 KB · 99 lines python
Raw Blame History
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