TeX 62.3%
Python 37.6%
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca3"""Interpretability (protocol §11): SHAP values of the Task A LightGBM at three origins (importance and its stability over4time), accumulated local effects of the market features, and the DSGFN attention weights / RAREF regime probabilities for Task B.5Runs in the tree family process (no torch) for SHAP; the DSGFN/RAREF diagnostics are produced by 36_ablation.py."""6from __future__ import annotations78import sys9from pathlib import Path1011sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))12import numpy as np13import polars as pl1415from wp23 import config as C16from wp23 import data as D17from wp23.models_a import LGBM1819OUT = C.RESULTS / "interpret"; OUT.mkdir(parents=True, exist_ok=True)202122def main() -> None:23 import shap24 sales = D.load_sales(); rows_imp, rows_ale = [], []25 for o in ["2023-03", "2024-03", "2025-03"]:26 tr, te = D.task_a_sets(sales, o); Xtr, ytr, cols, cats = D.design(tr); Xte, yte, _, _ = D.design(te, cats)27 m = LGBM(quantiles=False).fit(Xtr, ytr); rng = np.random.default_rng(0); idx = rng.choice(len(Xte), min(4000, len(Xte)), replace=False)28 ex = shap.TreeExplainer(m.m); sv = ex.shap_values(Xte[idx])29 imp = np.abs(sv).mean(axis=0)30 for c, v in zip(cols, imp):31 rows_imp.append({"origin": o, "feature": c, "mean_abs_shap": float(v)})32 # ALE for selected continuous market features (10 bins)33 for feat in ["mun_mom_3_12", "reg_mom_3_12", "knn_q", "mun_q_3", "mun_vol_ratio", "lfa", "age_f"]:34 j = cols.index(feat); x = Xte[idx][:, j]; q = np.quantile(x, np.linspace(0, 1, 11)); q[-1] += 1e-935 ale = []36 for b in range(10):37 sel = (x >= q[b]) & (x < q[b + 1])38 if sel.sum() < 20: ale.append(np.nan); continue39 lo = Xte[idx][sel].copy(); hi = lo.copy(); lo[:, j] = q[b]; hi[:, j] = q[b + 1]; ale.append(float(np.mean(m.m.predict(hi) - m.m.predict(lo))))40 acc = np.nancumsum(np.nan_to_num(ale)); acc -= np.nanmean(acc)41 for b in range(10):42 rows_ale.append({"origin": o, "feature": feat, "bin": b, "x_lo": float(q[b]), "x_hi": float(q[b + 1]), "ale": float(acc[b])})43 print(o, "done", flush=True)44 pl.DataFrame(rows_imp).write_csv(OUT / "shap_importance.csv"); pl.DataFrame(rows_ale).write_csv(OUT / "ale.csv")45 imp = pl.DataFrame(rows_imp).pivot(on="origin", index="feature", values="mean_abs_shap")46 cols_o = [c for c in imp.columns if c != "feature"]; imp = imp.with_columns(pl.mean_horizontal(cols_o).alias("mean")).sort("mean", descending=True)47 imp.write_csv(OUT / "shap_importance_wide.csv"); print(imp.head(20))484950if __name__ == "__main__":51 main()52