SPB Git
7.9 KB · 115 lines python
1#!/usr/bin/env python32"""Chapitre 14 — figures (données SYNTHÉTIQUES, graine 20261031). Idempotent.3Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch14_figures.py4Produit assets/figures/ch14/ch14-fig-{avant-apres,scores-robustes,levier-cook,geocodage,sensibilite-seuils}.pdf"""5from __future__ import annotations6import sys, pathlib7import numpy as np, pandas as pd8import matplotlib.pyplot as plt9from matplotlib.ticker import FuncFormatter10sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))11sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))12from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars, fmt_milliers13import ch14_nettoyage as c141415def fig_avant_apres(R):16    b = R["brut"]; P = R["pipeline"]; j = P["journal"]17    excl = set(j.loc[j["action"] == "exclusion", "id_inscription"]); corr = set(j.loc[j["action"].isin(["correction"]), "id_inscription"]) - excl18    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.4))19    ax = axes[0]20    for m, col, lab, z in [(~b["id_inscription"].isin(excl | corr), palette.grid, "inscription intacte", 1),21                           (b["id_inscription"].isin(corr), palette.cat[0], "corrigée (R2, R3, R5, R6)", 2),22                           (b["id_inscription"].isin(excl), palette.cat[1], "exclue (R1, R4, R7, S1, E1)", 3)]:23        w = b[m]; ax.scatter(w["superficie_habitable_m2"], w["prix_vente"].clip(lower=1), s=9 if z == 1 else 16, color=col, alpha=0.8, label=f"{lab} (n = {len(w)})", zorder=z)24    ax.set_yscale("log"); ax.set_xscale("log")25    ax.set_xlabel("Superficie habitable saisie (m², échelle log)"); ax.set_ylabel("Prix déclaré ($, échelle log)")26    ax.set_title("Jeu brut : 618 inscriptions", loc="left"); ax.legend(fontsize=6.8, loc="lower right")27    ax.annotate("prix symboliques (1 $)", (120, 1.6), fontsize=7, color=palette.ink2, xytext=(0, 8), textcoords="offset points")28    ax.annotate("superficies en pi²", (1600, 3.5e5), fontsize=7, color=palette.ink2, ha="center", xytext=(0, 10), textcoords="offset points")29    ax = axes[1]; d = P["propre"]30    ax.scatter(d["superficie_habitable_m2"], d["prix_vente"], s=9, color=palette.cat[0], alpha=0.7)31    ax.set_xlabel("Superficie habitable (m²)"); ax.set_ylabel("Prix net de meubles ($)")32    ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_title(f"Jeu nettoyé : {len(d)} ventes", loc="left")33    savefig(fig, "ch14/ch14-fig-avant-apres.pdf")3435def fig_scores(R):36    M = R["masquage"]; df = M["df"]; hull = df[df["secteur"] == "Hull"]37    x = hull["lpm"].values; zc = (x - x.mean()) / x.std(ddof=1); zr = c14.z_robuste(x)38    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), sharey=False)39    for ax, z, titre, col in [(axes[0], zc, "Score classique $(x-\\bar x)/s$", palette.cat[0]), (axes[1], zr, "Score robuste $(x-\\mathrm{méd})/(1{,}4826\\,\\mathrm{MAD})$", palette.cat[1])]:40        zz = np.clip(z, -8, 8); nb_hors = int((np.abs(z) > 8).sum())41        ax.hist(zz, bins=np.arange(-8.25, 8.5, 0.5), color=col, alpha=0.85, edgecolor=palette.surface)42        for s in (-3.5, 3.5): ax.axvline(s, color=palette.ink2, ls="--", lw=0.9)43        n_fl = int((np.abs(z) > 3.5).sum())44        ax.set_title(titre, loc="left", fontsize=8.5); ax.set_xlabel("score (tronqué à ±8)"); ax.set_ylabel("ventes")45        ax.text(0.02, 0.95, f"|z| > 3,5 : {n_fl} ventes\n(dont {nb_hors} au-delà de ±8)", transform=ax.transAxes, va="top", fontsize=7.5, color=palette.ink)46        ax.set_xlim(-8.5, 8.5)47    fig.suptitle("Secteur Hull, ln du prix au m², après R1–R3 (n = %d, dont 2 prix à 1 $)" % len(x), fontsize=8.5, x=0.01, ha="left")48    savefig(fig, "ch14/ch14-fig-scores-robustes.pdf")4950def fig_levier_cook(R):51    E = R["pipeline"]["regression"]; S = R["pipeline"]["scores"]["garde"].copy()52    X = c14.cas1.matrice(S); y = np.log(S["prix_vente"].astype(float)); dg = c14.diagnostics(X.values, y.values)53    n, k = dg["n"], dg["k"]54    fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))55    size = 40 + 2500 * dg["cook"]56    ax.scatter(dg["h"], dg["t_ext"], s=size, facecolor=palette.cat[0], alpha=0.35, edgecolor=palette.cat[0], linewidth=0.5)57    for s in (-3, 3): ax.axhline(s, color=palette.ink2, ls="--", lw=0.9)58    ax.axvline(2 * k / n, color=palette.ink2, ls=":", lw=0.9)59    ax.text(2 * k / n, ax.get_ylim()[1] * 0.95 if False else 3.6, f" levier = 2k/n = {2*k/n:.3f}", fontsize=7.5, color=palette.ink2, va="top")60    ax.text(0.0, 3.1, " |t| = 3", fontsize=7.5, color=palette.ink2)61    excl = E["exclus"]62    for _, r in excl.iterrows():63        ax.annotate(r["id_inscription"], (r["levier"], r["t_ext"]), xytext=(8, -4), textcoords="offset points", fontsize=7.5, color=palette.ink)64    for ci in (0.01, 0.04):65        ax.scatter([], [], s=40 + 2500 * ci, facecolor="none", edgecolor=palette.ink2, label=f"Cook $D_i$ = {ci:.2f}")66    ax.legend(loc="upper right", fontsize=7.5, labelspacing=1.4, borderpad=1.0)67    ax.set_xlabel("Levier $h_{ii}$"); ax.set_ylabel("Résidu studentisé externe $t_i$")68    ax.set_title(f"Première itération E1 : n = {n}, k = {k} ; taille des points proportionnelle à la distance de Cook", loc="left", fontsize=8.5)69    savefig(fig, "ch14/ch14-fig-levier-cook.pdf")7071def fig_geocodage(R):72    G = R["geocodage"]; tab = G["tableau"]73    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2), gridspec_kw={"width_ratios": [1.25, 1]})74    ax = axes[0]75    for i, (nom, e) in enumerate(G["erreurs"].items()):76        xs = np.sort(e); ys = np.arange(1, len(xs) + 1) / len(xs)77        ax.plot(xs, ys, color=palette.cat[i], label=nom)78    ax.set_xscale("log"); ax.set_xlabel("Erreur de position (m, échelle log)"); ax.set_ylabel("Fonction de répartition")79    ax.axvline(3000 * 0.1, color=palette.ink2, ls=":", lw=0.8)80    ax.legend(fontsize=6.6, loc="upper left"); ax.set_title("Erreur simulée par technique", loc="left", fontsize=8.5)81    ax = axes[1]82    ypos = np.arange(len(tab))[::-1]83    ax.barh(ypos + 0.18, tab["pct_voisins_conserves"], height=0.36, color=palette.cat[0], label="8 plus proches voisins conservés")84    ax.barh(ypos - 0.18, 100 - tab["pct_rayon_change"], height=0.36, color=palette.cat[1], label="appartenance au rayon de 3 km inchangée")85    ax.set_yticks(ypos); ax.set_yticklabels([t.replace(" (", "\n(") for t in tab.index], fontsize=6.8)86    ax.set_xlabel("% des 600 ventes"); ax.set_xlim(0, 100); ax.legend(fontsize=6.6, loc="lower left")87    ax.set_title("Conséquences sur le voisinage", loc="left", fontsize=8.5)88    savefig(fig, "ch14/ch14-fig-geocodage.pdf")8990def fig_sensibilite(R):91    T = R["sensibilite"]92    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0))93    ax = axes[0]94    for i, zt in enumerate([2.5, 3.0]):95        w = T[T["seuil_t"] == zt]96        ax.plot(w["seuil_mad"], w["faux_positifs"], marker="o", color=palette.cat[i], label=f"faux positifs, seuil $|t|$ = {zt}")97        ax.plot(w["seuil_mad"], w["liees_detectees"], marker="s", ls="--", color=palette.cat[i], label=f"ventes liées détectées (sur 6), seuil $|t|$ = {zt}")98    ax.set_xlabel("Seuil du score robuste |z|"); ax.set_ylabel("Nombre de ventes"); ax.legend(fontsize=6.6); ax.set_title("Exclusions S1 + E1", loc="left", fontsize=8.5)99    ax = axes[1]100    for i, zt in enumerate([2.5, 3.0]):101        w = T[T["seuil_t"] == zt]102        ax.plot(w["seuil_mad"], w["sigma"], marker="o", color=palette.cat[i], label=f"seuil $|t|$ = {zt}")103    ax.axhline(0.0830, color=palette.ink2, ls=":", lw=0.9); ax.text(2.52, 0.0832, "σ du jeu propre (0,083)", fontsize=7, color=palette.ink2)104    ax.set_xlabel("Seuil du score robuste |z|"); ax.set_ylabel("σ du modèle hédonique"); ax.legend(fontsize=7); ax.set_title("Écart-type résiduel", loc="left", fontsize=8.5)105    savefig(fig, "ch14/ch14-fig-sensibilite-seuils.pdf")106107def main():108    setup()109    R = c14.main(ecrire=False, verbose=False)110    fig_avant_apres(R); fig_scores(R); fig_levier_cook(R); fig_geocodage(R); fig_sensibilite(R)111    print("figures écrites dans assets/figures/ch14/")112113if __name__ == "__main__":114    main()115
Files · 185d04a62ff66f3d004407340590d377c512efce
Menu

Theme