1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca3"""Baseline feature matrix with as-of comparable summaries.45 python3 scripts/03_features.py [--synthetic] [--no-geo]6→ data/{processed|synthetic}/feature_matrix.parquet (+ results/data/feature_matrix_summary*.json).7Geo features are joined when data/<base>/geo_features.parquet exists (run scripts/04_geo.py first).8"""9from __future__ import annotations1011import argparse12import json13import time1415import polars as pl1617from agentic_valuation.config import PROCESSED, RESULTS, SYNTHETIC18from agentic_valuation.features.matrix import build_feature_matrix, feature_columns192021def main() -> None:22 ap = argparse.ArgumentParser(description=__doc__)23 ap.add_argument("--synthetic", action="store_true")24 ap.add_argument("--no-geo", action="store_true", help="ignore geo_features.parquet even if present")25 args = ap.parse_args()26 base = SYNTHETIC if args.synthetic else PROCESSED27 t0 = time.perf_counter()28 analytic = pl.read_parquet(base / "analytic.parquet")29 splits = pl.read_parquet(base / "splits.parquet")30 geo = None31 geo_path = base / "geo_features.parquet"32 if geo_path.exists() and not args.no_geo:33 geo = pl.read_parquet(geo_path)34 fm = build_feature_matrix(analytic, splits, geo=geo, comps_summary=True)35 fm.write_parquet(base / "feature_matrix.parquet", compression="zstd")36 num, cat = feature_columns(fm)37 summary = {38 "synthetic": args.synthetic,39 "rows": fm.height,40 "by_split": {k: int(v) for k, v in fm.group_by("split").len().iter_rows()},41 "numeric_features": num,42 "categorical_features": cat,43 "geo_joined": geo is not None,44 "geo_rows": None if geo is None else geo.height,45 "missingness_pct": {c: round(100.0 * fm[c].null_count() / fm.height, 3) for c in num + cat},46 "nb_n_12m_quantiles": {q: float(fm["nb_n_12m"].quantile(q)) for q in (0.05, 0.25, 0.5, 0.75, 0.95)},47 "nb_mean_dist_km_median": float(fm["nb_mean_dist_km"].median()),48 "seconds": round(time.perf_counter() - t0, 2),49 }50 out = RESULTS / "data" / ("feature_matrix_summary_synthetic.json" if args.synthetic else "feature_matrix_summary.json")51 out.parent.mkdir(parents=True, exist_ok=True)52 out.write_text(json.dumps(summary, indent=1, ensure_ascii=False))53 print(json.dumps({k: summary[k] for k in ("rows", "by_split", "geo_joined", "nb_n_12m_quantiles", "nb_mean_dist_km_median", "seconds")}, ensure_ascii=False, indent=1))545556if __name__ == "__main__":57 main()58