#!/usr/bin/env python3 """Chapitre 14 — figures (données SYNTHÉTIQUES, graine 20261031). Idempotent. Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch14_figures.py Produit assets/figures/ch14/ch14-fig-{avant-apres,scores-robustes,levier-cook,geocodage,sensibilite-seuils}.pdf""" from __future__ import annotations import sys, pathlib import numpy as np, pandas as pd import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars, fmt_milliers import ch14_nettoyage as c14 def fig_avant_apres(R): b = R["brut"]; P = R["pipeline"]; j = P["journal"] excl = set(j.loc[j["action"] == "exclusion", "id_inscription"]); corr = set(j.loc[j["action"].isin(["correction"]), "id_inscription"]) - excl fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.4)) ax = axes[0] for m, col, lab, z in [(~b["id_inscription"].isin(excl | corr), palette.grid, "inscription intacte", 1), (b["id_inscription"].isin(corr), palette.cat[0], "corrigée (R2, R3, R5, R6)", 2), (b["id_inscription"].isin(excl), palette.cat[1], "exclue (R1, R4, R7, S1, E1)", 3)]: 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) ax.set_yscale("log"); ax.set_xscale("log") ax.set_xlabel("Superficie habitable saisie (m², échelle log)"); ax.set_ylabel("Prix déclaré ($, échelle log)") ax.set_title("Jeu brut : 618 inscriptions", loc="left"); ax.legend(fontsize=6.8, loc="center left", bbox_to_anchor=(0.02, 0.5)) ax.annotate("prix symboliques (1 $)", (120, 1.6), fontsize=7, color=palette.ink2, xytext=(0, 8), textcoords="offset points") ax.annotate("superficies en pi²", (1600, 3.5e5), fontsize=7, color=palette.ink2, ha="center", xytext=(0, 10), textcoords="offset points") ax = axes[1]; d = P["propre"] ax.scatter(d["superficie_habitable_m2"], d["prix_vente"], s=9, color=palette.cat[0], alpha=0.7) ax.set_xlabel("Superficie habitable (m²)"); ax.set_ylabel("Prix net de meubles ($)") ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_title(f"Jeu nettoyé : {len(d)} ventes", loc="left") fig.subplots_adjust(wspace=0.42) savefig(fig, "ch14/ch14-fig-avant-apres.pdf") def fig_scores(R): M = R["masquage"]; df = M["df"]; hull = df[df["secteur"] == "Hull"] x = hull["lpm"].values; zc = (x - x.mean()) / x.std(ddof=1); zr = c14.z_robuste(x) fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), sharey=False) 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])]: zz = np.clip(z, -8, 8); nb_hors = int((np.abs(z) > 8).sum()) ax.hist(zz, bins=np.arange(-8.25, 8.5, 0.5), color=col, alpha=0.85, edgecolor=palette.surface) for s in (-3.5, 3.5): ax.axvline(s, color=palette.ink2, ls="--", lw=0.9) n_fl = int((np.abs(z) > 3.5).sum()) ax.set_title(titre, loc="left", fontsize=8.5); ax.set_xlabel("score (tronqué à ±8)"); ax.set_ylabel("ventes") 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) ax.set_xlim(-8.5, 8.5) 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, y=1.04, ha="left") savefig(fig, "ch14/ch14-fig-scores-robustes.pdf") def fig_levier_cook(R): E = R["pipeline"]["regression"]; S = R["pipeline"]["scores"]["garde"].copy() X = c14.cas1.matrice(S); y = np.log(S["prix_vente"].astype(float)); dg = c14.diagnostics(X.values, y.values) n, k = dg["n"], dg["k"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.6)) size = 40 + 2500 * dg["cook"] ax.scatter(dg["h"], dg["t_ext"], s=size, facecolor=palette.cat[0], alpha=0.35, edgecolor=palette.cat[0], linewidth=0.5) for s in (-3, 3): ax.axhline(s, color=palette.ink2, ls="--", lw=0.9) ax.axvline(2 * k / n, color=palette.ink2, ls=":", lw=0.9) 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") ax.text(0.009, 2.55, "|t| = 3", fontsize=7.5, color=palette.ink2) excl = E["exclus"] for _, r in excl.iterrows(): ax.annotate(r["id_inscription"], (r["levier"], r["t_ext"]), xytext=(8, -4), textcoords="offset points", fontsize=7.5, color=palette.ink) for ci in (0.01, 0.04): ax.scatter([], [], s=40 + 2500 * ci, facecolor="none", edgecolor=palette.ink2, label=f"Cook $D_i$ = {ci:.2f}") ax.legend(loc="upper right", fontsize=7.5, labelspacing=1.4, borderpad=1.0) ax.set_xlabel("Levier $h_{ii}$"); ax.set_ylabel("Résidu studentisé externe $t_i$") 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) savefig(fig, "ch14/ch14-fig-levier-cook.pdf") def fig_geocodage(R): G = R["geocodage"]; tab = G["tableau"] fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2), gridspec_kw={"width_ratios": [1.25, 1]}) ax = axes[0] for i, (nom, e) in enumerate(G["erreurs"].items()): xs = np.sort(e); ys = np.arange(1, len(xs) + 1) / len(xs) ax.plot(xs, ys, color=palette.cat[i], label=nom) ax.set_xscale("log"); ax.set_xlabel("Erreur de position (m, échelle log)"); ax.set_ylabel("Fonction de répartition") ax.axvline(3000 * 0.1, color=palette.ink2, ls=":", lw=0.8) ax.legend(fontsize=6.6, loc="upper left"); ax.set_title("Erreur simulée par technique", loc="left", fontsize=8.5) ax = axes[1] ypos = np.arange(len(tab))[::-1] ax.barh(ypos + 0.18, tab["pct_voisins_conserves"], height=0.36, color=palette.cat[0], label="8 plus proches voisins conservés") 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") ax.set_yticks(ypos); ax.set_yticklabels([t.replace(" (", "\n(") for t in tab.index], fontsize=6.8); ax.yaxis.tick_right() ax.set_xlabel("% des 600 ventes"); ax.set_xlim(0, 100); ax.legend(fontsize=6.6, loc="upper center", bbox_to_anchor=(0.5, -0.22), ncol=1) ax.set_title("Conséquences sur le voisinage", loc="left", fontsize=8.5) savefig(fig, "ch14/ch14-fig-geocodage.pdf") def fig_sensibilite(R): T = R["sensibilite"] fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.0)) ax = axes[0] for i, zt in enumerate([2.5, 3.0]): w = T[T["seuil_t"] == zt] ax.plot(w["seuil_mad"], w["faux_positifs"], marker="o", color=palette.cat[i], label=f"faux positifs, seuil $|t|$ = {zt}") 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}") ax.set_xlabel("Seuil du score robuste |z|"); ax.set_ylabel("Nombre de ventes"); ax.set_title("Exclusions S1 + E1", loc="left", fontsize=8.5) hnd, lab = ax.get_legend_handles_labels(); fig.legend(hnd, lab, loc="upper center", bbox_to_anchor=(0.5, -0.06), ncol=2, fontsize=6.8) ax = axes[1] for i, zt in enumerate([2.5, 3.0]): w = T[T["seuil_t"] == zt] ax.plot(w["seuil_mad"], w["sigma"], marker="o", color=palette.cat[i], label=f"seuil $|t|$ = {zt}") ax.axhline(0.0830, color=palette.ink2, ls=":", lw=0.9); ax.text(3.05, 0.08255, "σ du jeu propre (0,083)", fontsize=7, color=palette.ink2) 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) fig.subplots_adjust(wspace=0.38) savefig(fig, "ch14/ch14-fig-sensibilite-seuils.pdf") def main(): setup() R = c14.main(ecrire=False, verbose=False) fig_avant_apres(R); fig_scores(R); fig_levier_cook(R); fig_geocodage(R); fig_sensibilite(R) print("figures écrites dans assets/figures/ch14/") if __name__ == "__main__": main()