spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Figures du chapitre 1 (DONNÉES SYNTHÉTIQUES du cas 1, graine 20261031 ; simulations étiquetées).3Sorties (assets/figures/ch01/) :4 ch01-fig-prix-valeur.pdf nuage prix observé vs valeur estimée (modèle hédonique de référence), comparables du ch. 16 en évidence5 ch01-fig-residus.pdf distribution des écarts prix/valeur (résidus) et loi normale ajustée6 ch01-fig-recherche.pdf simulation du modèle de recherche : offres, prix acceptés, délais de vente7 ch01-fig-rosen.pdf schéma de l'équilibre hédonique de Rosen : enchères, offres et enveloppe des prix8 ch01-fig-sensibilite-sigma.pdf sensibilité : bornes de l'intervalle à 95 % et probabilité d'un écart > 10 % selon sigma9Exécuter depuis la racine : python3 analysis/python/figures/ch01_figures.py"""10import sys, pathlib, math11ROOT = pathlib.Path(__file__).resolve().parents[3]12sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))13import numpy as np14import matplotlib.pyplot as plt15from scipy.stats import norm16from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars, fmt_pct17import ch01_valeur_prix as c11819def fig_prix_valeur(t):20 m = t["modele"]; v = t["ventes"]21 pred = np.exp(m["X"].values @ m["beta"]); obs = v["prix_vente"].values22 comps = t["comparables"]23 fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))24 ax.scatter(pred, obs, s=9, color=palette.cat[0], alpha=0.45, linewidths=0, label="600 ventes synthétiques")25 lo, hi = 150_000, 1_000_00026 ax.plot([lo, hi], [lo, hi], color=palette.ink2, lw=1.0, ls="--", label="Prix = valeur estimée")27 ax.plot([lo, hi], [lo * 1.1, hi * 1.1], color=palette.grid, lw=0.8, ls=":"); ax.plot([lo, hi], [lo * 0.9, hi * 0.9], color=palette.grid, lw=0.8, ls=":")28 ax.text(hi / 1.1 * 0.97, hi * 0.985, "+10 %", ha="right", va="top", fontsize=7.5, color=palette.ink2)29 ax.text(hi * 0.985, hi * 0.9 * 0.985, "−10 %", ha="right", va="top", fontsize=7.5, color=palette.ink2)30 ax.scatter(comps["valeur_estimee"], comps["prix_vente"], s=34, facecolors="none", edgecolors=palette.cat[1], linewidths=1.3, label="Comparables du chapitre 16", zorder=5)31 for _, r in comps.iterrows():32 ax.annotate(r["id_vente"], (r["valeur_estimee"], r["prix_vente"]), xytext=(5, -9), textcoords="offset points", fontsize=7, color=palette.ink)33 p = t["pred"]34 ax.axvline(p["mediane"], color=palette.cat[2], lw=0.9, ls="-.")35 ax.text(p["mediane"] * 1.01, lo * 1.15, "valeur estimée du sujet\n" + fmt_dollars(p["mediane"]), fontsize=7.5, color=palette.ink, ha="left")36 ax.set_xlim(lo, hi); ax.set_ylim(lo, hi)37 ax.xaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars)); ax.yaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars))38 ax.set_xlabel("Valeur estimée $\\hat V = \\exp(\\mathbf{x}'\\hat{\\beta})$ (médiane conditionnelle)"); ax.set_ylabel("Prix de vente observé $P$")39 ax.legend(loc="upper left", fontsize=7.5)40 return savefig(fig, "ch01/ch01-fig-prix-valeur.pdf")4142def fig_residus(t):43 res = t["modele"]["residus"]; sigma = t["modele"]["sigma"]44 ecarts = (np.exp(res) - 1) * 10045 fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))46 bins = np.arange(-30, 32, 2.5)47 ax.hist(ecarts, bins=bins, density=True, color=palette.cat[0], alpha=0.75, edgecolor=palette.surface, linewidth=0.5, label="Écarts observés (600 ventes)")48 x = np.linspace(-30, 30, 400)49 # densité de (exp(e) − 1)·100 avec e ~ N(0, σ²)50 dens = norm.pdf(np.log(1 + x / 100), 0, sigma) / (1 + x / 100) / 10051 ax.plot(x, dens, color=palette.cat[1], lw=1.6, label=f"Loi log-normale ajustée ($\\sigma$ = {sigma:.3f})")52 for q in (-10, 10):53 ax.axvline(q, color=palette.ink2, lw=0.8, ls="--")54 d = t["dispersion"]55 ax.text(-29, ax.get_ylim()[1] * 0.80, f"{d['part_dans_pm10pct']*100:.1f} % des ventes\nà ±10 % de la\nvaleur estimée", ha="left", fontsize=8, color=palette.ink)56 ax.set_xlabel("Écart entre le prix observé et la valeur estimée, $P/\\hat V - 1$ (%)"); ax.set_ylabel("Densité")57 ax.legend(loc="upper right", fontsize=7.5)58 return savefig(fig, "ch01/ch01-fig-residus.pdf")5960def fig_recherche(t):61 s = t["recherche"]; centre = t["pred"]["mediane"]; P = c1.PARAMS_RECHERCHE62 prix_rel = (s["prix"] / centre - 1) * 100; r_rel = (s["prix_reserve"] / centre - 1) * 10063 fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), gridspec_kw={"width_ratios": [1.5, 1]})64 x = np.linspace(-25, 25, 500)65 a1.plot(x, norm.pdf(np.log(1 + x / 100), 0, P["sigma_o"]) / (1 + x / 100) / 100, color=palette.cat[0], lw=1.5, label="Offres reçues (toutes)")66 a1.hist(prix_rel, bins=np.arange(-5, 25, 1.0), density=True, color=palette.cat[1], alpha=0.7, edgecolor=palette.surface, linewidth=0.4, label="Prix acceptés (simulation)")67 a1.axvline(r_rel, color=palette.ink2, lw=0.9, ls="--")68 a1.text(r_rel + 7.5, a1.get_ylim()[1] * 0.62, f"prix de réserve\n+{r_rel:.1f} %", fontsize=7.5, color=palette.ink)69 a1.set_xlabel("Écart au centre des enchères $m$ (%)"); a1.set_ylabel("Densité"); a1.legend(loc="upper left", fontsize=7)70 delais = s["delai"]; mx = int(min(delais.max(), 24))71 vals = [np.mean(delais == k) for k in range(1, mx + 1)]72 a2.bar(range(1, mx + 1), vals, color=palette.cat[0], width=0.8)73 a2.axvline(s["delai_moyen"], color=palette.ink2, lw=0.9, ls="--")74 a2.text(s["delai_moyen"] + 0.4, max(vals) * 0.9, f"moyenne\n{s['delai_moyen']:.1f} mois", fontsize=7.5, color=palette.ink)75 a2.set_xlabel("Délai de vente (mois)"); a2.set_ylabel("Fréquence"); a2.set_xlim(0.3, mx + 0.7)76 fig.tight_layout(w_pad=1.5)77 return savefig(fig, "ch01/ch01-fig-recherche.pdf")7879def fig_rosen():80 """Schéma : fonctions d'enchère θ(z; u) concaves de deux acheteurs, fonctions d'offre φ(z; π) convexes de deux vendeurs,81 enveloppe p(z) tangente aux deux familles. Formes fonctionnelles déclarées (purement illustratives)."""82 z = np.linspace(0.5, 4.5, 400)83 p = 100 + 60 * np.log(z) # enveloppe (fonction de prix hédonique)84 dp = 60 / z85 fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))86 ax.plot(z, p, color=palette.ink, lw=2.0, label="Fonction de prix $p(z)$ (enveloppe)")87 for i, z0 in enumerate((1.3, 3.2)):88 p0, s0 = 100 + 60 * math.log(z0), 60 / z089 theta = p0 + s0 * (z - z0) - 18 * (z - z0) ** 2 # enchère d'un acheteur : concave, tangente en z090 phi = p0 + s0 * (z - z0) + 18 * (z - z0) ** 2 # offre d'un vendeur : convexe, tangente en z091 mask = np.abs(z - z0) < 0.992 ax.plot(z[mask], theta[mask], color=palette.cat[0], lw=1.3, ls="--", label="Enchères des acheteurs $\\theta(z; u)$" if i == 0 else None)93 ax.plot(z[mask], phi[mask], color=palette.cat[1], lw=1.3, ls=":", label="Offres des vendeurs $\\phi(z; \\pi)$" if i == 0 else None)94 ax.scatter([z0], [p0], color=palette.ink, s=22, zorder=5)95 ax.annotate(f"appariement {i+1}", (z0, p0), xytext=(8, -14), textcoords="offset points", fontsize=7.5, color=palette.ink)96 ax.set_xlabel("Quantité de l'attribut $z$ (p. ex. superficie)"); ax.set_ylabel("Prix")97 ax.set_yticks([]); ax.set_xticks([])98 ax.text(4.45, 100 + 60 * math.log(4.45) - 6, "pente = prix implicite $\\partial p/\\partial z$", ha="right", fontsize=7.5, color=palette.ink2)99 ax.legend(loc="lower right", fontsize=7.5)100 return savefig(fig, "ch01/ch01-fig-rosen.pdf")101102def fig_sensibilite(t):103 sig = np.linspace(0.02, 0.20, 100)104 fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.0))105 a1.plot(sig, (np.exp(1.96 * sig) - 1) * 100, color=palette.cat[0], lw=1.6, label="Borne haute (+)")106 a1.plot(sig, (np.exp(-1.96 * sig) - 1) * 100, color=palette.cat[1], lw=1.6, label="Borne basse (−)")107 a1.fill_between(sig, (np.exp(-1.96 * sig) - 1) * 100, (np.exp(1.96 * sig) - 1) * 100, color=palette.cat[0], alpha=0.08)108 s0 = t["modele"]["sigma"]109 a1.axvline(s0, color=palette.ink2, lw=0.8, ls="--"); a1.text(s0 + 0.004, -35, f"cas 1 : $\\sigma$ = {s0:.3f}", fontsize=7.5, color=palette.ink)110 a1.set_xlabel("Dispersion $\\sigma$ (écart-type de $\\ln P$ à $\\mathbf{x}$ donné)"); a1.set_ylabel("Bornes de l'intervalle à 95 % (% de la valeur)")111 a1.legend(loc="upper left", fontsize=7.5)112 for d, col, lab in ((0.05, palette.cat[2], "> 5 %"), (0.10, palette.cat[0], "> 10 %"), (0.20, palette.cat[1], "> 20 %")):113 a2.plot(sig, [c1.proba_ecart(s, d) * 100 for s in sig], color=col, lw=1.6, label=f"écart {lab}")114 a2.axvline(s0, color=palette.ink2, lw=0.8, ls="--")115 a2.set_xlabel("Dispersion $\\sigma$"); a2.set_ylabel("Probabilité qu'un prix s'écarte de la valeur (%)")116 a2.legend(loc="upper left", fontsize=7.5)117 fig.tight_layout(w_pad=1.5)118 return savefig(fig, "ch01/ch01-fig-sensibilite-sigma.pdf")119120def main():121 setup()122 t = c1.tout()123 for f in (fig_prix_valeur(t), fig_residus(t), fig_recherche(t), fig_rosen(), fig_sensibilite(t)):124 print("OK", f)125126if __name__ == "__main__":127 main()128