1"""Invariants of the reconstructed panel against the raw event log."""2import duckdb, pandas as pd, numpy as np, pytest3from src.common import ROOT45P = ROOT / "data" / "processed"; I = ROOT / "data" / "interim"67@pytest.fixture(scope="module")8def d():9 return pd.read_parquet(P / "offers_daily.parquet")1011def test_daily_bounds(d):12 assert (d.hours_obs <= 24.0 + 1e-6).all()13 assert (d.p_min <= d.p_mode + 1e-9).all() and (d.p_mode <= d.p_max + 1e-9).all()14 assert (d.p_min <= d.p_twmean + 1e-9).all() and (d.p_twmean <= d.p_max + 1e-9).all()15 assert (d.n_distinct >= 1).all() and (d.mode_share <= 1.0 + 1e-9).all()1617def test_total_hours_equal_span(d):18 prod = pd.read_parquet(I / "raw_products.parquet", columns=["uid", "first_seen", "last_seen"])19 pl = pd.read_parquet(I / "raw_price_log.parquet")20 # products with >=1 priced event: covered hours == span from first event to last_seen (NULL segments excluded)21 tot = d.groupby("uid").hours_obs.sum()22 first_ev = pl.groupby("uid").ts.min()23 span = ((prod.set_index("uid").last_seen - first_ev) / 3600.0).dropna()24 common = tot.index.intersection(span.index)25 diff = (tot.loc[common] - span.loc[common]).abs()26 assert diff.quantile(0.99) < 1e-3, diff.describe()2728def test_known_sequence(d):29 # metro:068400662600 (mayonnaise): raw events on 2026-08-13 local = 7.49 carried, then 5.77 at 07h UTC... bounds must hold30 x = d[d.uid == "metro:068400662600"].sort_values("day")31 assert x.day.min() == pd.Timestamp("2026-08-12").date()32 assert set(x.loc[x.day == pd.Timestamp("2026-08-13").date(), "p_min"]) == {5.77}33 assert np.allclose(x.hours_obs.iloc[1:-1], 24.0, atol=1e-6)3435def test_single_price_products_have_one_distinct(d):36 pl = pd.read_parquet(I / "raw_price_log.parquet")37 one = pl.groupby("uid").size(); one = one[one == 1].index38 sub = d[d.uid.isin(one)]39 assert (sub.n_distinct == 1).all() and (sub.n_changes == 0).all()40