#!/usr/bin/env python3 """Chapitre 17 — figures : cascade d'ajustements (G0404), dispersion des paires, prix bruts vs ajustés, tornade de sensibilité. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch17_figures.py""" from __future__ import annotations import sys, pathlib import numpy as np 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 import ch17_ajustements as c17 def fig_cascade(r): c = r["cascade_G0404"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.7)) n = len(c); x = np.arange(n) bases = c["base_apres"].values; adj = c["ajustement"].values for i in range(n): if i == 0 or i == n - 1: ax.bar(x[i], bases[i], color=palette.cat[0] if i == 0 else palette.brand, width=0.7) ax.text(x[i], bases[i] + 900, fmt_dollars(bases[i]), ha="center", va="bottom", fontsize=7.5) elif adj[i] == 0: ax.plot([x[i] - 0.35, x[i] + 0.35], [bases[i], bases[i]], color=palette.ink2, lw=1.2) ax.text(x[i], bases[i] + 900, "0", ha="center", va="bottom", fontsize=7, color=palette.ink2) else: bottom = min(bases[i - 1], bases[i]) couleur = palette.cat[2] if adj[i] > 0 else palette.cat[1] ax.bar(x[i], abs(adj[i]), bottom=bottom, color=couleur, width=0.7) lab = ("+" if adj[i] > 0 else "\u2212") + f"{abs(adj[i]):,.0f}".replace(",", " ") if adj[i] > 0: ax.text(x[i], bases[i] + 900, lab, ha="center", va="bottom", fontsize=7.5) else: ax.text(x[i], bases[i] - 900, lab, ha="center", va="top", fontsize=7.5) if 0 < i < n: ax.plot([x[i - 1] + 0.35, x[i] - 0.35], [bases[i - 1], bases[i - 1]], color=palette.ink2, lw=0.6) ax.set_ylim(495000, 558000); ax.set_xticks(x) ax.set_xticklabels(c["etape"], rotation=35, ha="right", fontsize=7.5) ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_ylabel("Base après l'étape") savefig(fig, "ch17/ch17-fig-cascade-g0404.pdf") def fig_paires(r): fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.3), sharey=True) for ax, attr, titre in zip(axes, ("garage", "sous_sol"), ("Garage (une place de plus)", "Sous-sol fini")): p = r[f"paires_{attr}"].sort_values("diff_temps").reset_index(drop=True) rs = r[f"resume_{attr}"]; im = r[f"implicite_{attr}"] y = p["diff_temps"].values; x = np.arange(len(y)) ax.axhline(0, color=palette.ink2, lw=0.6) ax.axhspan(rs["moyenne"] - 1.96 * rs["erreur_type"], rs["moyenne"] + 1.96 * rs["erreur_type"], color=palette.cat[0], alpha=0.12, lw=0) ax.axhline(rs["moyenne"], color=palette.cat[0], lw=1.4, label="moyenne des paires (± 1,96 erreur-type)") ax.axhline(rs["mediane"], color=palette.cat[0], lw=1.0, ls=":", label="médiane des paires") ax.axhline(im["au_prix_median_paires"], color=palette.cat[1], lw=1.4, ls="--", label="prix implicite hédonique") ax.scatter(x, y, s=22, color=palette.ink, zorder=3) ax.set_title(f"{titre} — n = {rs['n']} paires", fontsize=9) ax.set_xticks([]); ax.set_xlabel("Paires classées par écart croissant") ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)) ax.set_ylim(-160000, 160000) axes[0].set_ylabel("Écart de prix « avec » − « sans » (date alignée)") axes[0].legend(loc="upper left", fontsize=7) savefig(fig, "ch17/ch17-fig-paires.pdf") def fig_bruts_ajustes(r): b = r["base"]; a = b["adj"]; rec = r["reconciliation"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.2)) y = np.arange(len(a))[::-1] for yi, (_, row) in zip(y, a.iterrows()): ax.plot([row["prix_vente"], row["prix_ajuste"]], [yi, yi], color=palette.grid, lw=2.2, zorder=1) ax.scatter(a["prix_vente"], y, s=42, facecolor="white", edgecolor=palette.cat[0], linewidth=1.6, zorder=3, label="prix de vente") ax.scatter(a["prix_ajuste"], y, s=42, color=palette.cat[0], zorder=4, label="prix ajusté (grille hédonique)") w = rec["poids_inverse_brut"] for yi, (_, row), wi in zip(y, a.iterrows(), w): ax.text(max(row["prix_vente"], row["prix_ajuste"]) + 4000, yi, f"brut {row['ajust_brut_pct']*100:.0f} % · poids {wi:.2f}".replace(".", ","), va="center", fontsize=7.2, color=palette.ink2) vp = rec["inverse_brut"]["valeur_ponderee"]; pred = b["prediction_hedonique"] ax.axvline(vp, color=palette.brand, lw=1.3, ls="-") ax.text(vp, len(a) - 0.45, "valeur pondérée\n" + fmt_dollars(vp), ha="center", va="bottom", fontsize=7.3, color=palette.brand) ax.axvline(pred, color=palette.cat[1], lw=1.3, ls="--") ax.text(pred, len(a) - 0.45, "prédiction hédonique\n" + fmt_dollars(pred), ha="center", va="bottom", fontsize=7.3, color=palette.cat[1]) ax.set_yticks(y); ax.set_yticklabels(a["id_vente"]); ax.set_ylim(-0.7, len(a) + 0.6) ax.set_xlim(490000, 720000); ax.xaxis.set_major_formatter(FuncFormatter(fmt_dollars)) ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.14), ncol=2, fontsize=7.3) savefig(fig, "ch17/ch17-fig-bruts-ajustes.pdf") def fig_sensibilite(r): s = r["sensibilite"]; ref = float(s.iloc[0]["bas"]) s = s.iloc[1:].copy(); s = s.sort_values("etendue") fig, ax = plt.subplots(figsize=(LARGEUR, 3.6)) y = np.arange(len(s)) ax.barh(y, s["haut"] - s["bas"], left=s["bas"], color=palette.cat[0], alpha=0.85, height=0.62) ax.axvline(ref, color=palette.ink, lw=1.0) for yi, (_, row) in zip(y, s.iterrows()): ax.text(row["bas"] - 1500, yi, fmt_dollars(row["bas"]), ha="right", va="center", fontsize=6.8, color=palette.ink2) ax.text(row["haut"] + 1500, yi, fmt_dollars(row["haut"]), ha="left", va="center", fontsize=6.8, color=palette.ink2) ax.set_yticks(y); ax.set_yticklabels(s["scenario"], fontsize=7.4) ax.set_xlim(530000, 615000); ax.xaxis.set_major_formatter(FuncFormatter(fmt_dollars)) ax.set_xlabel("Valeur pondérée (poids inverse de l'ajustement brut) ; trait vertical : référence " + fmt_dollars(ref)) savefig(fig, "ch17/ch17-fig-sensibilite.pdf") def main(): setup(); r = c17.tout() fig_cascade(r); fig_paires(r); fig_bruts_ajustes(r); fig_sensibilite(r) print("figures écrites dans assets/figures/ch17/") if __name__ == "__main__": main()