SPB Git

spb/wp26_uqo

Public
5.4 KB · 105 lines python
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca3"""Phase 3 — fit the non-LLM baselines (A hedonic, B hierarchical/repeat-sales, C LightGBM [+CatBoost/XGBoost],4D AVM with conformal quantiles) on train, tune on validation, and write prediction records for the validation,5test and holdout partitions (full partitions) plus the spatial-holdout experiment.67Usage: python3 scripts/10_train_baselines.py [--synthetic] [--trials 12] [--skip-robustness]8Outputs: results/runs/baselines[_synthetic]/predictions.parquet, manifest.json, search logs.9"""10from __future__ import annotations1112import argparse13import json14import platform15import sys16import time17from pathlib import Path1819import numpy as np20import polars as pl2122sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))23from agentic_valuation.config import PROCESSED, RESULTS, SYNTHETIC, load_config  # noqa: E40224from agentic_valuation.baselines.gbm import AVM, CatBoostModel, LightGBMModel, XGBoostModel, dump_search_log  # noqa: E40225from agentic_valuation.baselines.hedonic import HedonicRidge, HierarchicalHedonic  # noqa: E402262728def main(argv: list[str] | None = None) -> None:29    ap = argparse.ArgumentParser()30    ap.add_argument("--synthetic", action="store_true")31    ap.add_argument("--trials", type=int, default=12)32    ap.add_argument("--skip-robustness", action="store_true")33    ap.add_argument("--max-train", type=int, default=0, help="subsample train rows (speed; 0 = all)")34    a = ap.parse_args(argv)35    base = SYNTHETIC if a.synthetic else PROCESSED36    run_id = "baselines_synthetic" if a.synthetic else "baselines"37    out = RESULTS / "runs" / run_id38    out.mkdir(parents=True, exist_ok=True)39    fm = pl.read_parquet(base / "feature_matrix.parquet")40    if "split" not in fm.columns:41        fm = fm.join(pl.read_parquet(base / "splits.parquet").select(["tx_id", "split"]), on="tx_id", how="left")42    spatial_regions = load_config("data")["spatial_holdout"]["regions"]43    train = fm.filter(pl.col("split") == "train")44    if a.max_train and train.height > a.max_train:45        train = train.sample(a.max_train, seed=20261027)46    valid = fm.filter(pl.col("split") == "validation")47    evals = {s: fm.filter(pl.col("split") == s) for s in ("validation", "test", "holdout")}48    print(f"train {train.height:,} | valid {valid.height:,} | test {evals['test'].height:,} | holdout {evals['holdout'].height:,}")4950    # Arm C = gradient boosting on attributes, roll values and location (no as-of market / neighbourhood lag features);51    # arm D = AVM with the full spatial-temporal feature set + quantile models + conformal scaling.52    LAGS = ("mun_", "nb_")53    models = {54        "A_hedonic": lambda: HedonicRidge(),55        "B_hierarchical": lambda: HierarchicalHedonic(),56        "C_gbm": lambda: LightGBMModel(n_trials=a.trials, exclude_prefixes=LAGS),57        "D_avm": lambda: AVM(n_trials=a.trials),58    }59    if not a.skip_robustness:60        models["C_catboost"] = lambda: CatBoostModel(exclude_prefixes=LAGS)61        models["C_xgboost"] = lambda: XGBoostModel(exclude_prefixes=LAGS)62    records, manifest = [], {"run_id": run_id, "synthetic": a.synthetic, "python": platform.python_version(), "machine": platform.machine(), "arms": {}}63    for arm, ctor in models.items():64        t0 = time.perf_counter()65        m = ctor().fit(train, valid)66        fit_s = time.perf_counter() - t067        info = {"fit_seconds": round(fit_s, 1)}68        for split, df in evals.items():69            if df.height == 0:70                continue71            t1 = time.perf_counter()72            pred = m.predict(df)73            records.append(pred.to_records(split, arm, run_id, latency_s=time.perf_counter() - t1))74        if isinstance(m, LightGBMModel):75            dump_search_log(m, out / f"{arm}_search.json")76            info["params"] = m.params77        if isinstance(m, AVM):78            dump_search_log(m.point, out / f"{arm}_search.json")79            info["params"] = m.point.params80            info["conformal_scale"] = m.scale81        if isinstance(m, HierarchicalHedonic):82            info["w_repeat"] = m.w_repeat83        manifest["arms"][arm] = info84        print(f"{arm}: fit {fit_s:.0f}s {info}")85    # spatial holdout: retrain A, C, D without the withheld regions; predict test rows of those regions86    sp_train = train.filter(~pl.col("region_name").is_in(spatial_regions))87    sp_valid = valid.filter(~pl.col("region_name").is_in(spatial_regions))88    sp_test = evals["test"].filter(pl.col("region_name").is_in(spatial_regions))89    if sp_test.height > 0 and sp_train.height > 1000:90        for arm in ("A_hedonic", "C_gbm", "D_avm"):91            m = models[arm]().fit(sp_train, sp_valid)92            records.append(m.predict(sp_test).to_records("test", f"{arm}_spatial", run_id))93        manifest["spatial_holdout"] = {"regions": spatial_regions, "n_test": sp_test.height, "n_train": sp_train.height}94    preds = pl.concat(records)95    preds.write_parquet(out / "predictions.parquet")96    (out / "manifest.json").write_text(json.dumps(manifest, indent=1, default=float))97    # quick in-sample-free summary (validation/test mean abs log error) for the console98    truth = fm.select(["tx_id", "log_price"])99    s = preds.join(truth, on="tx_id").with_columns((pl.col("point").log() - pl.col("log_price")).abs().alias("ae")).group_by(["arm", "split"]).agg(pl.col("ae").mean().round(4), pl.len()).sort(["split", "arm"])100    print(s)101102103if __name__ == "__main__":104    main()105
Files · main
Menu

Theme