SPB Git
12.1 KB · 162 lines python
1#!/usr/bin/env python32"""Figures du chapitre 37 — Apprentissage automatique et explicabilité (cas 1, Gatineau, DONNÉES SYNTHÉTIQUES, graine 20261031).3Produit dans assets/figures/ch37/ :4  ch37-fig-arbre.pdf          arbre de régression de profondeur 3 estimé sur les 600 ventes (feuilles en $)5  ch37-fig-apprentissage.pdf  erreur d'entraînement et de validation croisée du boosting selon le nombre d'arbres et la profondeur6  ch37-fig-erreurs.pdf        distribution des erreurs hors échantillon (log, en %) par modèle et par schéma de validation7  ch37-fig-predit-observe.pdf prix prédit hors échantillon (plis aléatoires) contre prix observé : hédonique, forêt, boosting8  ch37-fig-shap-beeswarm.pdf  valeurs SHAP des 600 ventes (boosting) : importance et sens des effets9  ch37-fig-pdp.pdf            dépendance partielle : superficie et âge (hédonique, forêt, boosting, loi de génération) ; âge dans le scénario non linéaire10  ch37-fig-waterfall.pdf      cascade SHAP de la prédiction du sujet par le boosting (en dollars)11Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch37_figures.py"""12from __future__ import annotations13import sys, pathlib, math, re14ROOT = pathlib.Path(__file__).resolve().parents[3]15sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))16import numpy as np, pandas as pd17import matplotlib.pyplot as plt18from matplotlib.ticker import FuncFormatter19from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers20import ch37_ml as c372122COURT = {"Hédonique (référence)": "Hédonique", "Arbre (profondeur 3)": "Arbre", "Forêt aléatoire": "Forêt", "Gradient boosting": "Boosting", "XGBoost": "XGBoost",23         "Hédonique respécifié": "Hédonique respécifié"}24COUL = {"Hédonique (référence)": palette.cat[0], "Arbre (profondeur 3)": palette.cat[4], "Forêt aléatoire": palette.cat[1], "Gradient boosting": palette.cat[2], "XGBoost": palette.cat[3]}25def _k(x, pos=None): return f"{x/1000:,.0f}".replace(",", " ")2627def fig_arbre(res):28    from sklearn.tree import plot_tree29    arbre = res["ajustes"]["Arbre (profondeur 3)"]30    COURTS = {"superficie": "Superficie (m²)", "terrain": "Terrain (m²)", "age": "Âge", "qualite": "Qualité", "garage": "Garage", "sous_sol": "Sous-sol", "piscine": "Piscine",31              "etages": "À étages", "dist_centre": "Dist. centre (km)", "lat": "Latitude", "lon": "Longitude", "mois": "Mois"}32    noms = [COURTS.get(c, c.replace("sect_", "Secteur ")) for c in res["Xm"].columns]33    fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))34    plot_tree(arbre, feature_names=noms, filled=False, impurity=False, rounded=True, fontsize=6, ax=ax, precision=1)35    for t in ax.texts:36        s = t.get_text()37        s = re.sub(r"samples = (\d+)", lambda m: f"n = {m.group(1)}", s)38        s = re.sub(r"value = (-?\d+\.\d+)", lambda m: f"≈ {math.exp(float(m.group(1)))/1000:,.0f} k$".replace(",", " "), s)39        s = s.replace(" <= ", " ≤ ")40        t.set_text(s); t.set_color(palette.ink)41        bb = t.get_bbox_patch()42        if bb is not None: bb.set_edgecolor(palette.ink2); bb.set_facecolor(palette.surface); bb.set_linewidth(0.6)43    ax.set_axis_off(); ax.grid(False)44    savefig(fig, "ch37/ch37-fig-arbre.pdf")4546def fig_apprentissage(res):47    c = res["courbes"]; ref = res["comparaison"]["metriques"]["Hédonique (référence)"]["aleatoire"]["rmse_log"]48    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))49    it = np.arange(1, 1201)50    for i, (d, cc) in enumerate(sorted(c.items())):51        ax.plot(it, cc["validation"], color=palette.cat[i], lw=1.6, label=f"validation croisée, profondeur {d}")52        ax.plot(it, cc["entrainement"], color=palette.cat[i], lw=1.0, ls="--", alpha=0.85)53        ax.plot([cc["optimum"]], [cc["rmse_min"]], marker="o", ms=4, color=palette.cat[i])54    ax.axhline(ref, color=palette.ink2, lw=0.9, ls=":", label=f"hédonique de référence (validation croisée) : {ref:.4f}".replace(".", ","))55    ax.plot([], [], color=palette.ink2, lw=1.0, ls="--", label="erreur d'entraînement (tiretés)")56    ax.set_xlabel("Nombre d'arbres (itérations du boosting, ν = 0,03)"); ax.set_ylabel("Racine de l'erreur quadratique moyenne (log du prix)")57    ax.set_ylim(0.0, 0.20); ax.set_xlim(0, 1200); ax.legend(loc="upper right", ncol=1)58    savefig(fig, "ch37/ch37-fig-apprentissage.pdf")5960def fig_erreurs(res):61    comp = res["comparaison"]; y = res["y"]; noms = list(c37.MODELES)62    fig, axes = plt.subplots(1, 3, figsize=(LARGEUR, 3.3), sharey=True)63    titres = {"aleatoire": "Plis aléatoires (10)", "spatial": "Blocs spatiaux par secteur (10)", "temporel": "Prospectif : ventes de 2026"}64    for ax, sch in zip(axes, ["aleatoire", "spatial", "temporel"]):65        data = []66        for nom in noms:67            p = comp["pred"][nom][sch]; m = ~np.isnan(p); data.append((np.exp(p[m] - y[m]) - 1) * 100)68        bp = ax.boxplot(data, widths=0.6, patch_artist=True, showfliers=True, flierprops=dict(marker=".", ms=2.5, markeredgecolor=palette.ink2, alpha=0.5),69                        medianprops=dict(color=palette.ink, lw=1.2), whiskerprops=dict(color=palette.ink2, lw=0.8), capprops=dict(color=palette.ink2, lw=0.8))70        for patch, nom in zip(bp["boxes"], noms): patch.set_facecolor(COUL[nom]); patch.set_alpha(0.55); patch.set_edgecolor(palette.ink2)71        ax.axhline(0, color=palette.ink2, lw=0.8, ls=":")72        ax.set_xticks(range(1, len(noms) + 1)); ax.set_xticklabels([COURT[n] for n in noms], rotation=30, ha="right", fontsize=7.2)73        ax.set_title(titres[sch], fontsize=8.5)74        for i, nom in enumerate(noms):75            ax.text(i + 1, -59, f"COD {comp['metriques'][nom][sch]['COD']:.1f}".replace(".", ","), ha="center", va="bottom", fontsize=6.4, color=palette.ink2, rotation=90)76    axes[0].set_ylabel("Erreur de prédiction hors échantillon (%)"); axes[0].set_ylim(-60, 45)77    savefig(fig, "ch37/ch37-fig-erreurs.pdf")7879def fig_predit_observe(res):80    comp = res["comparaison"]; P = np.exp(res["y"])81    fig, axes = plt.subplots(1, 3, figsize=(LARGEUR, 2.9), sharex=True, sharey=True)82    for ax, nom in zip(axes, ["Hédonique (référence)", "Forêt aléatoire", "Gradient boosting"]):83        pred = np.exp(comp["pred"][nom]["aleatoire"]) * comp["duan"][nom]84        ax.scatter(P, pred, s=7, color=COUL[nom], alpha=0.55, edgecolors="none")85        lim = [180_000, 900_000]; ax.plot(lim, lim, color=palette.ink2, lw=0.9)86        ax.plot(lim, [l * 1.18 for l in lim], color=palette.ink2, lw=0.6, ls="--"); ax.plot(lim, [l / 1.18 for l in lim], color=palette.ink2, lw=0.6, ls="--")87        m = comp["metriques"][nom]["aleatoire"]88        ax.set_title(COURT[nom], fontsize=8.5)89        ax.text(0.03, 0.97, f"RMSE log {m['rmse_log']:.3f}\nMdAPE {m['mdape']:.1f} %\nCOD {m['COD']:.2f}".replace(".", ","), transform=ax.transAxes, va="top", ha="left", fontsize=7.2)90        ax.set_xlim(lim); ax.set_ylim(lim); ax.set_xlabel("Prix observé (k$)")91        ax.xaxis.set_major_formatter(FuncFormatter(_k)); ax.yaxis.set_major_formatter(FuncFormatter(_k))92    axes[0].set_ylabel("Prix prédit hors échantillon (k$)")93    savefig(fig, "ch37/ch37-fig-predit-observe.pdf")9495def fig_beeswarm(res, top: int = 11):96    sh = res["shap"]; Xm = res["Xm"]; sv = sh["valeurs"]; imp = sh["importance"]97    cols = list(imp.index[:top]); rng = np.random.default_rng(20261031)98    fig, ax = plt.subplots(figsize=(LARGEUR, 3.7)); cmap = palette.cmap_seq()99    for i, c in enumerate(cols[::-1]):100        j = list(Xm.columns).index(c); x = sv[:, j] * 100; val = Xm[c].to_numpy()101        lo, hi = np.nanpercentile(val, [5, 95]); col = np.clip((val - lo) / (hi - lo if hi > lo else 1), 0, 1)102        # essaim : décalage vertical proportionnel à la densité locale103        ordre = np.argsort(x); yj = np.zeros(len(x)); bins = np.digitize(x, np.linspace(x.min(), x.max() + 1e-9, 60))104        for b in np.unique(bins):105            idx = np.where(bins == b)[0]; k = len(idx); yj[idx] = (np.arange(k) - (k - 1) / 2) * min(0.32 / max(k, 1) * 2, 0.06) + rng.normal(0, 0.004, k)106        ax.scatter(x, i + yj, c=cmap(col), s=6, edgecolors="none", alpha=0.85)107        ax.text(ax.get_xlim()[1] if False else 20.5, i, f"{imp[c]*100:.2f}".replace(".", ","), va="center", ha="left", fontsize=7.2, color=palette.ink2)108    ax.axvline(0, color=palette.ink2, lw=0.8)109    ax.set_yticks(range(len(cols))); ax.set_yticklabels([c37.ETIQUETTES[c] for c in cols[::-1]])110    ax.set_xlabel("Valeur SHAP φ (points de pourcentage du logarithme du prix)"); ax.set_xlim(-22, 26)111    ax.text(20.5, len(cols) - 0.35, "moy. |φ|", fontsize=7.2, color=palette.ink2, ha="left", va="bottom")112    sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(0, 1)); sm.set_array([])113    cb = fig.colorbar(sm, ax=ax, pad=0.01, fraction=0.03, ticks=[0, 1]); cb.ax.set_yticklabels(["faible", "élevée"]); cb.set_label("Valeur de la variable", fontsize=7.5)114    cb.outline.set_visible(False)115    savefig(fig, "ch37/ch37-fig-shap-beeswarm.pdf")116117def _centrer(c): return c - np.mean(c)118119def fig_pdp(res):120    pdp = res["pdp"]; v = res["ventes"]121    fig, axes = plt.subplots(1, 3, figsize=(LARGEUR, 2.9))122    series = [("vraie", "loi de génération", palette.ink, ":"), ("Hédonique (référence)", "hédonique", palette.cat[0], "-"), ("Forêt aléatoire", "forêt aléatoire", palette.cat[1], "-"), ("Gradient boosting", "boosting", palette.cat[2], "-")]123    for ax, (cle, var, xlab) in zip(axes, [("superficie", "superficie_habitable_m2", "Superficie habitable (m²)"), ("age", "age", "Âge (années)"), ("age_scenario", "age", "Âge (années), scénario")]):124        d = pdp[cle]; g = d["grille"]125        for k, lab, col, ls in series:126            if k in d: ax.plot(g, _centrer(d[k]) * 100, color=col, ls=ls, lw=1.7 if k == "vraie" else 1.4, label=lab)127        q = np.percentile(v[var], np.arange(5, 100, 5)); ax.plot(q, np.full_like(q, ax.get_ylim()[0] if False else -38), "|", color=palette.ink2, ms=5, alpha=0.8)128        ax.set_xlabel(xlab); ax.set_ylim(-42, 42)129    axes[0].set_ylabel("Dépendance partielle, écart au niveau moyen (%)"); axes[0].legend(loc="upper left", fontsize=7)130    axes[2].text(0.97, 0.97, "prime +12 % (qualité ≥ 4, âge ≥ 40)\n« mur » −10 % (25 à 45 ans)", transform=axes[2].transAxes, ha="right", va="top", fontsize=6.8, color=palette.ink2)131    savefig(fig, "ch37/ch37-fig-pdp.pdf")132133def fig_waterfall(res, top: int = 8):134    sh = res["shap"]; phi = sh["sujet"].copy(); base = sh["base"]135    ordre = phi.abs().sort_values(ascending=False); princip = list(ordre.index[:top]); autres = float(phi.drop(princip).sum())136    etapes = [(c37.ETIQUETTES[c], float(phi[c])) for c in princip] + [("Autres variables (somme)", autres)]137    fig, ax = plt.subplots(figsize=(LARGEUR, 3.3))138    cur = base; ys = []139    for i, (lab, ph) in enumerate(etapes):140        a, b = math.exp(cur), math.exp(cur + ph); col = palette.div["pos"] if ph >= 0 else palette.div["neg"]141        ax.barh(i, b - a, left=a, color=col, height=0.62, edgecolor="none")142        ax.text(max(a, b) + 4000, i, f"{ph*100:+.1f} %".replace(".", ","), va="center", ha="left", fontsize=7.5, color=palette.ink)143        if i + 1 < len(etapes): ax.plot([b, b], [i + 0.31, i + 0.69], color=palette.ink2, lw=0.6)144        cur += ph; ys.append(lab)145    fin = math.exp(cur)146    ax.axvline(math.exp(base), color=palette.ink2, lw=0.9, ls=":"); ax.axvline(fin, color=palette.ink, lw=0.9, ls="--")147    ax.text(math.exp(base) - 3000, -0.9, f"base : {math.exp(base):,.0f} $".replace(",", " "), ha="right", va="top", fontsize=7.5, color=palette.ink2)148    ax.text(fin + 3000, -0.9, f"prédiction brute : {fin:,.0f} $".replace(",", " "), ha="left", va="top", fontsize=7.5, color=palette.ink)149    ax.set_yticks(range(len(etapes))); ax.set_yticklabels(ys); ax.invert_yaxis()150    ax.set_xlabel("Prix prédit (k$) — chaque contribution est un facteur multiplicatif exp(φ)")151    ax.xaxis.set_major_formatter(FuncFormatter(_k)); ax.set_xlim(430_000, 700_000); ax.set_ylim(len(etapes) - 0.3, -1.5)152    savefig(fig, "ch37/ch37-fig-waterfall.pdf")153154def main():155    setup(); res = c37.main(verbose=False)156    fig_arbre(res); fig_apprentissage(res); fig_erreurs(res); fig_predit_observe(res); fig_beeswarm(res); fig_pdp(res); fig_waterfall(res)157    print("figures ch37 écrites dans", ROOT / "assets/figures/ch37")158    return res159160if __name__ == "__main__":161    main()162
Files · 031fae1bd368de12dc60ff542f1be9a8fd73e699
Menu

Theme