TeX 52.9%
Python 45.7%
Shell 1.4%
1# -*- coding: utf-8 -*-2"""02 — Indice temporel régional « point dans le temps ».3Pour chaque mois d'évaluation m, on ré-estime un indice hédonique mensuel par région administrative4avec toutes les ventes antérieures au mois m (fenêtre expansive) (jamais le mois m lui-même). Niveau de référence = m-1,5dernier mois entièrement connu. Sortie : index_asof.npy [m_eval, région, mois] (log), regions.json,6et un indice « plein échantillon » descriptif (index_full.csv)."""7from common import *89df = load_parquet("sales.parquet")10regions = sorted(df["region"].unique().tolist())11rid = {r: i for i, r in enumerate(regions)}12df["rid"] = df["region"].map(rid).astype(int)13M = int(df["month"].max()) + 114M_EVAL0 = int(month_index(pd.Series([VAL_START]))[0])15WINDOW = 999 # fenêtre « expansive » : toutes les ventes antérieures au mois m16SHRINK = 40.0 # n/(n+SHRINK) : rétrécissement de l'effet mensuel régional vers l'effet provincial1718X_cols = ["ln_floor", "ln_land", "age"]19def design(d):20 X = np.column_stack([np.ones(len(d)), d["ln_floor"], d["ln_land"], d["age"], d["age"] ** 2 / 100,21 (d["lien"] == 1), (d["lien"] >= 2), (d["genre"] == 1), (d["genre"] == 2), (d["genre"] == 3),22 d["etages"], d["neuf"]]).astype(float)23 return X2425def fit_month_effects(d, months):26 """OLS à effets fixes de municipalité (estimateur within) : ln P ~ X + effets mensuels (base = dernier mois),27 toutes les variables étant centrées par municipalité pour que l'indice ne soit pas contaminé par les28 déplacements du mélange des ventes entre municipalités chères et bon marché. Renvoie effets et n par mois."""29 X = design(d)[:, 1:]30 mm = d["month"].to_numpy()31 base = months[-1]32 dums = np.column_stack([(mm == m).astype(float) for m in months if m != base])33 A = np.hstack([X, dums]); y = d["ln_price"].to_numpy(float)34 codes, inv = np.unique(d["code_mun"].to_numpy(), return_inverse=True)35 cnt = np.bincount(inv).astype(float)36 mA = np.zeros((len(codes), A.shape[1])); np.add.at(mA, inv, A); A = A - mA[inv] / cnt[inv, None]37 my = np.bincount(inv, y) / cnt; y = y - my[inv]38 beta, *_ = np.linalg.lstsq(A, y, rcond=None)39 eff = {base: 0.0}40 k = X.shape[1]41 for j, m in enumerate([m for m in months if m != base]):42 eff[m] = float(beta[k + j])43 n = {m: int((mm == m).sum()) for m in months}44 return eff, n4546idx = np.full((M - M_EVAL0, len(regions), M), np.nan, dtype=np.float32)47log(f"indice point-dans-le-temps : mois d'évaluation {M_EVAL0}..{M-1}, {len(regions)} régions")48for m in range(M_EVAL0, M):49 months = list(range(max(0, m - WINDOW), m))50 win = df[(df["month"] >= months[0]) & (df["month"] < m)]51 prov_eff, _ = fit_month_effects(win, months)52 for r in regions:53 dr = win[win["region"] == r]54 if len(dr) < 200:55 for mo in months: idx[m - M_EVAL0, rid[r], mo] = prov_eff[mo]56 continue57 eff, n = fit_month_effects(dr, months)58 for mo in months:59 w = n[mo] / (n[mo] + SHRINK)60 idx[m - M_EVAL0, rid[r], mo] = w * eff[mo] + (1 - w) * prov_eff[mo]61 if m % 6 == 0: log(f" mois {m} ({len(win):,} ventes dans la fenêtre)")62np.save(OUT / "index_asof.npy", idx)63save_json({"regions": regions, "M_EVAL0": M_EVAL0, "M": M, "window": WINDOW, "shrink": SHRINK}, "index_meta.json")6465# indice descriptif plein échantillon (pour le rapport seulement — pas utilisé dans la rétro-évaluation)66rows = []67months = list(range(0, M))68eff_p, n_p = fit_month_effects(df, months)69for mo in months:70 rows.append({"month": mo, "ym": f"{2021 + mo // 12}-{mo % 12 + 1:02d}", "region": "QC", "log_idx": eff_p[mo] - eff_p[months[0]], "n": n_p[mo]})71for r in regions:72 dr = df[df["region"] == r]73 if len(dr) < 2000: continue74 eff, n = fit_month_effects(dr, months)75 for mo in months:76 rows.append({"month": mo, "ym": f"{2021 + mo // 12}-{mo % 12 + 1:02d}", "region": r, "log_idx": eff[mo] - eff[months[0]], "n": n[mo]})77pd.DataFrame(rows).to_csv(OUT / "index_full.csv", index=False)78log("indice terminé")79