1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Baselines on a small synthetic feature matrix built here (independent of the feature pipeline)."""3from __future__ import annotations45import numpy as np6import polars as pl7import pytest89from agentic_valuation.baselines.gbm import AVM, LightGBMModel10from agentic_valuation.baselines.hedonic import HedonicRidge, HierarchicalHedonic11from agentic_valuation.data.synthetic import generate121314@pytest.fixture(scope="module")15def fm():16 df = generate(4000, seed=1)17 df = df.with_columns(18 pl.col("price").log().alias("log_price"),19 pl.col("floor_area_m2").log().alias("log_floor_area"),20 (2026 - pl.col("year_built")).alias("age"),21 pl.col("roll_total_value").log().alias("log_roll_total"),22 (pl.col("lat") * 100).floor().cast(pl.Utf8).alias("grid1k"),23 (pl.col("lat") * 20).floor().cast(pl.Utf8).alias("grid5k"),24 pl.col("region_name").alias("region_group"),25 pl.when(pl.col("date") < pl.date(2024, 7, 1)).then(pl.lit("train")).when(pl.col("date") < pl.date(2025, 4, 1)).then(pl.lit("validation")).otherwise(pl.lit("test")).alias("split"),26 ).drop(["exclude_reason", "borough_name", "roll_reference_date", "parcel_id"])27 return df282930def _split(fm):31 return (fm.filter(pl.col("split") == s) for s in ("train", "validation", "test"))323334def test_hedonic_beats_naive(fm):35 tr, va, te = _split(fm)36 m = HedonicRidge().fit(tr, va)37 p = m.predict(te)38 ae = np.abs(np.log(p.point) - te["log_price"].to_numpy()).mean()39 naive = np.abs(te["log_price"].to_numpy() - tr["log_price"].mean()).mean()40 assert ae < 0.75 * naive41 assert p.quantiles.shape == (te.height, 5)42 assert np.all(np.diff(p.quantiles, axis=1) >= 0)434445def test_hierarchical_runs(fm):46 tr, va, te = _split(fm)47 m = HierarchicalHedonic().fit(tr, va)48 p = m.predict(te)49 assert np.isfinite(p.point).all()505152def test_lightgbm_and_avm(fm):53 tr, va, te = _split(fm)54 m = LightGBMModel(n_trials=2, max_rounds=300).fit(tr, va)55 p = m.predict(te)56 ae = np.abs(np.log(p.point) - te["log_price"].to_numpy()).mean()57 assert ae < 0.3058 avm = AVM(n_trials=1)59 avm.point.max_rounds = 30060 avm.fit(tr, va)61 # conformal guarantee holds on the calibration partition; under the synthetic trend drift the test-period62 # coverage is lower (documented behaviour, measured in the paper's drift section)63 q = avm.predict(va)64 y = va["log_price"].to_numpy()65 cov = np.mean((np.log(q.quantiles[:, 0]) <= y) & (y <= np.log(q.quantiles[:, 4])))66 assert 0.85 <= cov <= 0.9567 q = avm.predict(te)68 rec = q.to_records("test", "D_avm", "t")69 assert set(rec.columns) == {"tx_id", "split", "arm", "committee", "aggregation", "point", "q05", "q25", "q50", "q75", "q95", "cost_usd", "latency_s", "tokens", "run_id"}70