SPB Git forge

spb/wp22_uqo

Public
6commits 1branches 0releases
142.4 MBsize
maindefault branch
25 min agolast push
TeX 55.1% Python 44.2%
5.2 KB · 102 lines python
Raw Blame History
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Unit / integration tests for the SHARP-HPI code base (run: python3 -m pytest -q tests)."""3import sys4from pathlib import Path56sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))7import numpy as np8import polars as pl9import pytest1011from sharp_hpi import estimators as E12from sharp_hpi.hedonic import assign_loc_fe, build_features, fit_fe13from sharp_hpi.panel import growth, normalise14from sharp_hpi.sharp import cell_log_index, run_sharp15from sharp_hpi.simulate import Scenario, simulate16from sharp_hpi.statespace import eb_prior, fit_cell, profile_loglik171819def test_month_grid():20    g = E.month_grid("2021-11", "2022-02")21    assert g == ["2021-11", "2021-12", "2022-01", "2022-02"]222324def test_normalisation_does_not_change_growth():25    months = E.month_grid("2021-01", "2022-12"); li = np.cumsum(np.full(24, 0.01))26    i1, _ = normalise(li, months, "2021"); i2, _ = normalise(li, months, "2021-01")27    g1 = np.diff(np.log(i1)); g2 = np.diff(np.log(i2))28    assert np.allclose(g1, g2) and np.isclose(np.mean(np.log(i1[:12])), np.log(100)) and np.isclose(i2[0], 100)29    d1, p1, p12 = growth(li); assert np.isclose(p12[12], 100 * (np.exp(0.12) - 1))303132def test_local_level_recovers_signal_and_tau():33    rng = np.random.default_rng(3); T = 12034    d = np.cumsum(rng.normal(0, 0.01, T)); n = rng.poisson(20, T); y = d + rng.normal(0, 0.2 / np.sqrt(np.maximum(n, 1))); y[n == 0] = np.nan35    f = fit_cell(y, n, 0.04, model="ll", robust=False)36    assert np.sqrt(np.mean((f.smoothed - d) ** 2)) < np.sqrt(np.nanmean((y - d) ** 2))   # smoother beats raw means37    assert 0.004 < np.sqrt(f.tau2) < 0.02538    assert np.all((f.gain >= 0) & (f.gain <= 1)) and np.all(f.gain[n == 0] == 0)394041def test_eb_prior_recovers_common_tau():42    rng = np.random.default_rng(5); profs = []43    for _ in range(25):44        d = np.cumsum(rng.normal(0, 0.008, 60)); n = rng.poisson(3, 60); y = d + rng.normal(0, 0.2 / np.sqrt(np.maximum(n, 1))); y[n == 0] = np.nan45        profs.append(profile_loglik(y, n, 0.04))46    m, v = eb_prior(profs)47    assert 0.003 < np.exp(m / 2) < 0.02484950def test_simulation_schema_and_truth_shapes():51    s = simulate(Scenario(T=24, n_regions=2, mrc_per_region=2, mun_per_mrc=2, seed=1))52    for c in ["tx_id", "parcel_id", "date", "month", "price", "log_price", "property_class", "region_code", "mrc_code", "mun_code", "floor_area_m2", "age", "grid500", "available_date"]:53        assert c in s.df.columns54    assert s.truth_mun.shape == (8, 24) and s.truth_prov.shape == (24,) and len(s.months) == 24555657def test_pooled_td_recovers_known_path():58    s = simulate(Scenario(T=36, n_regions=2, mrc_per_region=2, mun_per_mrc=3, seed=2, size_logmean=2.5, tau_mun=0.0, tau_mrc=0.0, tau_region=0.0, contamination=0.0))59    f = assign_loc_fe(build_features(s.df), min_n=15)60    td = E.pooled_td(f, s.months, robust=True).log_index61    truth = s.truth_prov62    err = np.diff(td) - np.diff(truth)63    assert np.sqrt(np.mean(err ** 2)) < 0.02646566def test_sharp_end_to_end_and_hierarchy_consistency():67    s = simulate(Scenario(T=36, n_regions=2, mrc_per_region=2, mun_per_mrc=3, seed=7, size_logmean=1.8))68    f = build_features(s.df)69    r = run_sharp(f, s.months)70    assert len(r.stage1.mu) == 36 and r.stage1.mu[0] == 0.071    assert any(k[0] == "municipality" for k in r.cells)72    for k in [k for k in r.cells if k[0] == "municipality"][:3]:73        p, v = cell_log_index(r, *k)74        assert len(p) == 36 and np.all(np.isfinite(p)) and np.all(v > 0)75        # child path = parent path + own deviation (nested additivity)76        c = r.cells[k]; parent = cell_log_index(r, "mrc", c.parent_id)[0]77        assert np.allclose(p - parent, c.fit.smoothed)78    # real-time (filtered) path must not depend on later months: truncating the sample reproduces the earlier filtered values up to79    # stage-1 re-estimation; check that the filter is one-sided on fixed inputs80    i = s.months.index("2022-06")81    k = [k for k in r.cells if k[0] == "region"][0]; c = r.cells[k]82    f1 = fit_cell(c.ybar[: i + 1], c.n[: i + 1], c.sigma2_cell, log_tau2=np.log(c.fit.tau2), robust=False)83    f2 = fit_cell(c.ybar, c.n, c.sigma2_cell, log_tau2=np.log(c.fit.tau2), robust=False)84    assert np.allclose(f1.filtered, f2.filtered[: i + 1])858687def test_repeat_sales_on_synthetic_pairs():88    s = simulate(Scenario(T=30, n_regions=1, mrc_per_region=1, mun_per_mrc=4, seed=11, size_logmean=3.0, resale_prob=0.3, drift=0.02, tau_mun=0.0, tau_mrc=0.0, tau_region=0.0))89    f = build_features(s.df); rs = E.repeat_sales(f, s.months)90    ok = ~np.isnan(rs.log_index)91    assert rs.meta["pairs"] > 200 and ok.sum() > 2092    assert np.corrcoef(rs.log_index[ok], s.truth_prov[ok])[0, 1] > 0.8939495def test_fit_fe_residual_alignment_with_singletons():96    s = simulate(Scenario(T=12, n_regions=1, mrc_per_region=1, mun_per_mrc=2, seed=4, size_logmean=2.0))97    f = build_features(s.df)98    # half of the rows get their own (singleton) fixed effect, the rest share one cell99    f = f.with_columns(pl.when(pl.int_range(pl.len()) % 2 == 0).then(pl.col("tx_id")).otherwise(pl.lit("shared")).alias("loc_fe"))100    res, m = fit_fe(f, "lfa", "loc_fe", robust=False)101    assert len(res.resid) == f.height and res.n == (f.height + 1) // 2 and np.isnan(res.resid).sum() == f.height // 2102