TeX 55.1%
Python 44.2%
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Index panel assembly (protocol §17 schema), normalisation, growth rates, reliability grades."""3from __future__ import annotations45import numpy as np6import polars as pl78SCHEMA_COLS = ["geography_id", "geography_name", "geography_level", "property_type", "month", "index_value", "base_period",9 "log_monthly_change", "monthly_pct_change", "annual_pct_change", "actual_transaction_count", "effective_sample_size",10 "local_data_weight", "lower_95", "upper_95", "reliability_grade", "direct_or_model_assisted", "data_cutoff",11 "publication_vintage", "model_version", "index_method", "partial_month_flag", "suppression_flag"]121314def normalise(log_index: np.ndarray, months: list[str], base: str = "2021") -> tuple[np.ndarray, str]:15 """2021 average = 100 (geometric mean over the available 2021 months) or a single month = 100."""16 li = np.asarray(log_index, float)17 if len(base) == 4:18 sel = [i for i, m in enumerate(months) if m.startswith(base) and not np.isnan(li[i])]19 if not sel:20 return np.full_like(li, np.nan), f"{base} average = 100 (unavailable)"21 c = float(np.mean(li[sel]))22 label = f"{base} average = 100" + ("" if len(sel) == 12 else f" ({len(sel)} months available)")23 else:24 i = months.index(base); c = li[i]; label = f"{base} = 100"25 return 100.0 * np.exp(li - c), label262728def growth(log_index: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:29 li = np.asarray(log_index, float)30 d1 = np.full_like(li, np.nan); d1[1:] = li[1:] - li[:-1]31 d12 = np.full_like(li, np.nan); d12[12:] = li[12:] - li[:-12]32 return d1, 100 * (np.exp(d1) - 1), 100 * (np.exp(d12) - 1)333435def grade(n: np.ndarray, ess: np.ndarray, se_log: np.ndarray) -> np.ndarray:36 """A–E reliability grade per month from effective sample size and posterior se of the log level.37 Thresholds are anchored on the Monte Carlo results (results/simulation) — see paper §7."""38 g = np.full(len(n), "E", dtype=object)39 g[(ess >= 15) & (se_log < 0.05)] = "D"40 g[(ess >= 30) & (se_log < 0.035)] = "C"41 g[(ess >= 75) & (se_log < 0.025)] = "B"42 g[(ess >= 150) & (se_log < 0.015)] = "A"43 return g444546def build_rows(geo_id: str, geo_name: str, level: str, cls: str, months: list[str], log_index: np.ndarray, var: np.ndarray,47 n: np.ndarray, ess: np.ndarray, weight: np.ndarray, method: str, data_cutoff: str, vintage: str, model_version: str,48 base: str = "2021", partial_last: bool = True, direct_rule: str = "n>0") -> pl.DataFrame:49 idx, base_label = normalise(log_index, months, base)50 d1, p1, p12 = growth(log_index)51 se = np.sqrt(np.maximum(np.asarray(var, float), 0))52 lo = idx * np.exp(-1.96 * se); hi = idx * np.exp(1.96 * se)53 gr = grade(np.asarray(n, float), np.asarray(ess, float), se)54 direct = np.where(np.asarray(n) > 0, "direct", "model_assisted")55 direct = np.where(np.asarray(ess) < 5, "insufficiently_identified", direct)56 suppress = (np.asarray(ess) < 5) | np.isnan(idx)57 partial = np.zeros(len(months), bool)58 if partial_last:59 partial[-1] = True60 return pl.DataFrame({61 "geography_id": geo_id, "geography_name": geo_name, "geography_level": level, "property_type": cls, "month": months,62 "index_value": idx, "base_period": base_label, "log_monthly_change": d1, "monthly_pct_change": p1, "annual_pct_change": p12,63 "actual_transaction_count": np.asarray(n, float).astype(int), "effective_sample_size": np.asarray(ess, float),64 "local_data_weight": np.asarray(weight, float), "lower_95": lo, "upper_95": hi, "reliability_grade": gr.astype(str),65 "direct_or_model_assisted": direct.astype(str), "data_cutoff": data_cutoff, "publication_vintage": vintage,66 "model_version": model_version, "index_method": method, "partial_month_flag": partial, "suppression_flag": suppress,67 }).select(SCHEMA_COLS)68