#!/usr/bin/env python3 """Figures du chapitre 1 (DONNÉES SYNTHÉTIQUES du cas 1, graine 20261031 ; simulations étiquetées). Sorties (assets/figures/ch01/) : 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 évidence ch01-fig-residus.pdf distribution des écarts prix/valeur (résidus) et loi normale ajustée ch01-fig-recherche.pdf simulation du modèle de recherche : offres, prix acceptés, délais de vente ch01-fig-rosen.pdf schéma de l'équilibre hédonique de Rosen : enchères, offres et enveloppe des prix ch01-fig-sensibilite-sigma.pdf sensibilité : bornes de l'intervalle à 95 % et probabilité d'un écart > 10 % selon sigma Exécuter depuis la racine : python3 analysis/python/figures/ch01_figures.py""" import sys, pathlib, math ROOT = pathlib.Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases")) import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars, fmt_pct import ch01_valeur_prix as c1 def fig_prix_valeur(t): m = t["modele"]; v = t["ventes"] pred = np.exp(m["X"].values @ m["beta"]); obs = v["prix_vente"].values comps = t["comparables"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.9)) ax.scatter(pred, obs, s=9, color=palette.cat[0], alpha=0.45, linewidths=0, label="600 ventes synthétiques") lo, hi = 150_000, 1_000_000 ax.plot([lo, hi], [lo, hi], color=palette.ink2, lw=1.0, ls="--", label="Prix = valeur estimée") 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=":") ax.text(hi / 1.1 * 0.97, hi * 0.985, "+10 %", ha="right", va="top", fontsize=7.5, color=palette.ink2) ax.text(hi * 0.985, hi * 0.9 * 0.985, "−10 %", ha="right", va="top", fontsize=7.5, color=palette.ink2) 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) for _, r in comps.iterrows(): ax.annotate(r["id_vente"], (r["valeur_estimee"], r["prix_vente"]), xytext=(5, -9), textcoords="offset points", fontsize=7, color=palette.ink) p = t["pred"] ax.axvline(p["mediane"], color=palette.cat[2], lw=0.9, ls="-.") 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") ax.set_xlim(lo, hi); ax.set_ylim(lo, hi) ax.xaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars)); ax.yaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars)) ax.set_xlabel("Valeur estimée $\\hat V = \\exp(\\mathbf{x}'\\hat{\\beta})$ (médiane conditionnelle)"); ax.set_ylabel("Prix de vente observé $P$") ax.legend(loc="upper left", fontsize=7.5) return savefig(fig, "ch01/ch01-fig-prix-valeur.pdf") def fig_residus(t): res = t["modele"]["residus"]; sigma = t["modele"]["sigma"] ecarts = (np.exp(res) - 1) * 100 fig, ax = plt.subplots(figsize=(LARGEUR, 3.2)) bins = np.arange(-30, 32, 2.5) 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)") x = np.linspace(-30, 30, 400) # densité de (exp(e) − 1)·100 avec e ~ N(0, σ²) dens = norm.pdf(np.log(1 + x / 100), 0, sigma) / (1 + x / 100) / 100 ax.plot(x, dens, color=palette.cat[1], lw=1.6, label=f"Loi log-normale ajustée ($\\sigma$ = {sigma:.3f})") for q in (-10, 10): ax.axvline(q, color=palette.ink2, lw=0.8, ls="--") d = t["dispersion"] 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) ax.set_xlabel("Écart entre le prix observé et la valeur estimée, $P/\\hat V - 1$ (%)"); ax.set_ylabel("Densité") ax.legend(loc="upper right", fontsize=7.5) return savefig(fig, "ch01/ch01-fig-residus.pdf") def fig_recherche(t): s = t["recherche"]; centre = t["pred"]["mediane"]; P = c1.PARAMS_RECHERCHE prix_rel = (s["prix"] / centre - 1) * 100; r_rel = (s["prix_reserve"] / centre - 1) * 100 fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.0), gridspec_kw={"width_ratios": [1.5, 1]}) x = np.linspace(-25, 25, 500) 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)") 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)") a1.axvline(r_rel, color=palette.ink2, lw=0.9, ls="--") 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) a1.set_xlabel("Écart au centre des enchères $m$ (%)"); a1.set_ylabel("Densité"); a1.legend(loc="upper left", fontsize=7) delais = s["delai"]; mx = int(min(delais.max(), 24)) vals = [np.mean(delais == k) for k in range(1, mx + 1)] a2.bar(range(1, mx + 1), vals, color=palette.cat[0], width=0.8) a2.axvline(s["delai_moyen"], color=palette.ink2, lw=0.9, ls="--") 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) a2.set_xlabel("Délai de vente (mois)"); a2.set_ylabel("Fréquence"); a2.set_xlim(0.3, mx + 0.7) fig.tight_layout(w_pad=1.5) return savefig(fig, "ch01/ch01-fig-recherche.pdf") def fig_rosen(): """Schéma : fonctions d'enchère θ(z; u) concaves de deux acheteurs, fonctions d'offre φ(z; π) convexes de deux vendeurs, enveloppe p(z) tangente aux deux familles. Formes fonctionnelles déclarées (purement illustratives).""" z = np.linspace(0.5, 4.5, 400) p = 100 + 60 * np.log(z) # enveloppe (fonction de prix hédonique) dp = 60 / z fig, ax = plt.subplots(figsize=(LARGEUR, 3.4)) ax.plot(z, p, color=palette.ink, lw=2.0, label="Fonction de prix $p(z)$ (enveloppe)") for i, z0 in enumerate((1.3, 3.2)): p0, s0 = 100 + 60 * math.log(z0), 60 / z0 theta = p0 + s0 * (z - z0) - 18 * (z - z0) ** 2 # enchère d'un acheteur : concave, tangente en z0 phi = p0 + s0 * (z - z0) + 18 * (z - z0) ** 2 # offre d'un vendeur : convexe, tangente en z0 mask = np.abs(z - z0) < 0.9 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) 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) ax.scatter([z0], [p0], color=palette.ink, s=22, zorder=5) ax.annotate(f"appariement {i+1}", (z0, p0), xytext=(8, -14), textcoords="offset points", fontsize=7.5, color=palette.ink) ax.set_xlabel("Quantité de l'attribut $z$ (p. ex. superficie)"); ax.set_ylabel("Prix") ax.set_yticks([]); ax.set_xticks([]) 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) ax.legend(loc="lower right", fontsize=7.5) return savefig(fig, "ch01/ch01-fig-rosen.pdf") def fig_sensibilite(t): sig = np.linspace(0.02, 0.20, 100) fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.0)) a1.plot(sig, (np.exp(1.96 * sig) - 1) * 100, color=palette.cat[0], lw=1.6, label="Borne haute (+)") a1.plot(sig, (np.exp(-1.96 * sig) - 1) * 100, color=palette.cat[1], lw=1.6, label="Borne basse (−)") 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) s0 = t["modele"]["sigma"] 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) a1.set_xlabel("Dispersion $\\sigma$ (écart-type de $\\ln P$ à $\\mathbf{x}$ donné)"); a1.set_ylabel("Bornes de l'intervalle à 95 % (% de la valeur)") a1.legend(loc="upper left", fontsize=7.5) for d, col, lab in ((0.05, palette.cat[2], "> 5 %"), (0.10, palette.cat[0], "> 10 %"), (0.20, palette.cat[1], "> 20 %")): a2.plot(sig, [c1.proba_ecart(s, d) * 100 for s in sig], color=col, lw=1.6, label=f"écart {lab}") a2.axvline(s0, color=palette.ink2, lw=0.8, ls="--") a2.set_xlabel("Dispersion $\\sigma$"); a2.set_ylabel("Probabilité qu'un prix s'écarte de la valeur (%)") a2.legend(loc="upper left", fontsize=7.5) fig.tight_layout(w_pad=1.5) return savefig(fig, "ch01/ch01-fig-sensibilite-sigma.pdf") def main(): setup() t = c1.tout() for f in (fig_prix_valeur(t), fig_residus(t), fig_recherche(t), fig_rosen(), fig_sensibilite(t)): print("OK", f) if __name__ == "__main__": main()