SPB Git forge

spb/qc-election

Public
20commits 1branches 0releases
4.9 MBsize
maindefault branch
20 days agolast push
Python 66.6% HTML 24.8% CSS 4.9% JavaScript 3.6%
3.8 KB · 88 lines python
Raw Blame History
1# QC Élection Forecast — Plateforme de prévision électorale du Québec 20262# Auteur : Simon-Pierre Boucher3# Contact : contact@spboucher.ai4# https://www.qc-election.com5"""Cadre d'ablation (v3 « 127 », règle §22) — chaque couche doit prouver6qu'elle ne dégrade pas la prévision.78Rejoue le backtest 2022 pour une série de VARIANTES (couche retirée une à une)9et compare les scores : une couche qui dégrade systématiquement Brier/EAM doit10être retirée, reparamétrée ou reléguée au statut expérimental. Le rapport est11archivé (`data/ablation_report.json`) et exposé par l'API — aucune couche ne12peut modifier silencieusement le forecast.13"""14from __future__ import annotations1516import json17from contextlib import contextmanager18from datetime import date1920from sqlalchemy.orm import Session2122from ...config import DATA_DIR, settings2324# Variantes d'ablation : nom → surcharges de configuration temporaires.25VARIANTS: dict[str, dict] = {26    "FULL": {},27    "sans_fondamentaux": {"fundamentals_enabled": False},28    "sans_partielles": {"byelections_enabled": False},29    "sans_medias": {"media_nudge_enabled": False},30    "sans_volatilite_sociale": {"enable_social_volatility": False},31    "sondages_seuls": {"fundamentals_enabled": False,32                       "byelections_enabled": False,33                       "media_nudge_enabled": False,34                       "enable_social_volatility": False},35}363738@contextmanager39def _overrides(over: dict):40    """Applique temporairement des surcharges de settings (restaurées ensuite)."""41    saved = {k: getattr(settings, k) for k in over}42    for k, v in over.items():43        object.__setattr__(settings, k, v)44    try:45        yield46    finally:47        for k, v in saved.items():48            object.__setattr__(settings, k, v)495051def run_ablation(db: Session, n_sims: int = 6000,52                 horizons: list[int] | None = None) -> dict:53    """Backtest 2022 par variante → tableau comparatif des scores.54    NB : les couches non rejouables en 2022 (partielles, médias, volatilité —55    données indisponibles) affichent des scores identiques au FULL : le cadre56    les journalise comme « non identifiées en 2022 » plutôt que de simuler un57    effet inexistant (règle §1.4, aucun data leakage)."""58    from ..backtest import run_backtest59    horizons_keep = horizons or [120, 60, 30, 7, 1]60    report: dict = {"generated": date.today().isoformat(),61                    "model_version": settings.model_version,62                    "n_sims": n_sims, "variants": {}}63    for name, over in VARIANTS.items():64        with _overrides(over):65            bt = run_backtest(db, n_sims=n_sims, save_runs=False)66        rows = []67        for h in bt["horizons"]:68            if h.get("horizon") in horizons_keep and "national" in h:69                rows.append({"horizon": h["horizon"],70                             "mae_pp": h["national"]["mae_pp"],71                             "coverage_95": h["national"]["coverage_95"],72                             "seat_mae": h["seats"]["mae"],73                             "brier": h["districts"]["brier"],74                             "winner_accuracy": h["districts"]["winner_accuracy"]})75        avg = {k: round(sum(r[k] for r in rows) / len(rows), 4)76               for k in ("mae_pp", "seat_mae", "brier", "winner_accuracy")} if rows else {}77        report["variants"][name] = {"overrides": over, "horizons": rows,78                                    "avg": avg}79    full = report["variants"].get("FULL", {}).get("avg", {})80    for name, v in report["variants"].items():81        if name == "FULL" or not v["avg"]:82            continue83        v["delta_vs_full"] = {k: round(v["avg"][k] - full.get(k, 0), 4)84                              for k in v["avg"]}85    (DATA_DIR / "ablation_report.json").write_text(86        json.dumps(report, indent=1, ensure_ascii=False))87    return report88