SPB Git
11.0 KB · 147 lines python
1#!/usr/bin/env python32"""Chapitre 20 — figures : densités bootstrap et énumération exhaustive ; résidus des candidats et élargissement ;3fourchettes par méthode ; sensibilité de la conclusion aux poids et variance de la combinaison selon la corrélation.4Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch20_figures.py"""5from __future__ import annotations6import sys, pathlib7import numpy as np8import matplotlib.pyplot as plt9from matplotlib.ticker import FuncFormatter10from scipy.stats import gaussian_kde11sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))12sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))13from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars14import ch20_reconciliation as c201516def fmt_k(x, pos=None):17    return f"{x/1000:,.0f}".replace(",", " ") + " k$"181920def fig_bootstrap(r):21    b, bs, ex = r["base"], r["bootstrap"], r["exhaustif"]22    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.3))23    ax = axes[0]24    grille = np.linspace(520000, 700000, 400)25    for i, (k, lab) in enumerate([("VA", "candidats"), ("VB", "ajustements"), ("VC", "les deux")]):26        d = gaussian_kde(bs[k])(grille)27        ax.plot(grille, d / d.max(), color=palette.cat[i], lw=1.6 if k != "VB" else 1.2, ls="-" if k != "VB" else "--", label=lab)28    ax.axvline(b["V_comp"], color=palette.ink2, lw=0.8); ax.text(b["V_comp"] - 2000, 1.04, "comparaison\n(5 retenus)", ha="right", va="bottom", fontsize=7, color=palette.ink2)29    ax.axvline(b["V_hed"], color=palette.ink2, lw=0.8, ls=":"); ax.text(b["V_hed"] + 2000, 0.04, "hédonique", ha="left", va="bottom", fontsize=7, color=palette.ink2)30    ax.set_ylim(0, 1.22); ax.set_yticks([]); ax.set_xlabel("Valeur pondérée par comparaison"); ax.set_ylabel("Densité (échelle relative)")31    ax.xaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.legend(loc="upper right", fontsize=7, frameon=False, title="rééchantillonnage", title_fontsize=7)32    ax.set_title("(a) Bootstrap, B = 2 000", loc="left", fontsize=8.5)33    ax = axes[1]34    ax.hist(ex["valeurs"], bins=36, color=palette.cat[0], alpha=0.85, edgecolor="white", lw=0.3)35    ax.axvline(b["V_comp"], color=palette.cat[1], lw=1.4); ax.text(b["V_comp"] - 1500, ax.get_ylim()[1] * 0.95, "sélection\ndu ch. 16", ha="right", va="top", fontsize=7, color=palette.cat[1])36    ax.axvline(b["V_hed"], color=palette.ink2, lw=0.8, ls=":"); ax.text(b["V_hed"] + 1500, ax.get_ylim()[1] * 0.95, "hédonique", ha="left", va="top", fontsize=7, color=palette.ink2)37    ax.axvline(ex["moyenne"], color=palette.ink, lw=0.8, ls="--"); ax.text(ex["moyenne"] + 1500, ax.get_ylim()[1] * 0.55, "moyenne des\n4 368 sous-ensembles", ha="left", va="top", fontsize=7)38    ax.set_xlabel("Valeur pondérée du sous-ensemble de 5 candidats"); ax.set_ylabel("Nombre de sous-ensembles")39    ax.xaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.set_title("(b) Tous les sous-ensembles de 5 parmi 16", loc="left", fontsize=8.5)40    fig.tight_layout(w_pad=2.0)41    savefig(fig, "ch20/ch20-fig-bootstrap-densite.pdf")424344def fig_residus_elargissement(r):45    b, el = r["base"], r["elargissement"]46    a = b["adj16"]47    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2))48    ax = axes[0]49    rang = np.arange(1, len(a) + 1); res = (np.exp(a["residu_log"].values) - 1) * 10050    ret = a["retenu"].values51    ax.axhline(0, color=palette.ink2, lw=0.8)52    ax.bar(rang[ret], res[ret], color=palette.cat[1], width=0.7, label="cinq comparables retenus")53    ax.bar(rang[~ret], res[~ret], color=palette.cat[0], width=0.7, alpha=0.75, label="onze autres candidats")54    ax.axhline(res[ret].mean(), color=palette.cat[1], lw=1.0, ls="--"); ax.text(16.4, res[ret].mean(), f"moy. {res[ret].mean():.1f} %".replace(".", ","), fontsize=7, va="center", ha="left", color=palette.cat[1])55    ax.axhline(res[~ret].mean(), color=palette.cat[0], lw=1.0, ls="--"); ax.text(16.4, res[~ret].mean() + 1.0, f"moy. {res[~ret].mean():.1f} %".replace(".", ","), fontsize=7, va="bottom", ha="left", color=palette.cat[0])56    ax.set_xticks(rang); ax.set_xticklabels(a["id_vente"].values, rotation=90, fontsize=6.5)57    ax.set_xlim(0.3, 18.5); ax.set_ylabel("Résidu du modèle à la vente (%)"); ax.set_xlabel("Candidats, par distance d'attributs croissante")58    ax.set_ylim(-22, 14); ax.legend(loc="upper left", fontsize=7, frameon=False); ax.set_title("(a) Résidus des 16 candidats", loc="left", fontsize=8.5)59    ax = axes[1]60    ax.plot(el["k"], el["valeur_ponderee"], marker="o", color=palette.cat[0], lw=1.6, label="valeur pondérée (inverse du brut)")61    ax.plot(el["k"], el["moyenne"], marker="s", ms=3.5, color=palette.cat[2], lw=1.0, ls="--", label="moyenne simple")62    ax.axhline(b["V_hed"], color=palette.ink2, lw=0.8, ls=":"); ax.text(16.2, b["V_hed"] - 1500, "hédonique", fontsize=7, ha="right", va="top", color=palette.ink2)63    ax.set_xlabel("Nombre k de comparables retenus (par distance croissante)"); ax.set_ylabel("Indication par comparaison")64    ax.yaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.set_xticks(el["k"]); ax.set_ylim(560000, 650000); ax.legend(loc="lower right", fontsize=7, frameon=False)65    ax.set_title("(b) Élargissement de l'échantillon", loc="left", fontsize=8.5)66    fig.tight_layout(w_pad=2.0)67    savefig(fig, "ch20/ch20-fig-residus-elargissement.pdf")686970def fig_fourchettes(r):71    b, bs, co, cb, vt, bk = r["base"], r["bootstrap"], r["cout"], r["combinaison"], r["verite"], r["bootstrap_cout"]72    ip = b["ip_hed"]73    z90 = 1.644974    lignes = [75        ("Prix de vente probable\n(intervalle de prédiction à 95 %, ch. 18)", b["V_hed"], ip["bas"], ip["haut"], palette.cat[3], 0.5),76        ("Coût, lecture ventilée (ch. 11), écartée\n(point)", co["ventilee"], np.nan, np.nan, palette.cat[2], 0.6),77        ("Coût, dépréciation extraite (ch. 24)\n(bootstrap, 90 %)", co["valeur"], bk["resume"]["q05"], bk["resume"]["q95"], palette.cat[2], 1.0),78        ("Comparaison, 5 comparables\n(bootstrap conjoint, 90 %)", b["V_comp"], bs["resume"]["conjoint"]["q05"], bs["resume"]["conjoint"]["q95"], palette.cat[1], 1.0),79        ("Comparaison élargie, 16 candidats\n(point)", r["elargissement"]["valeur_ponderee"].iloc[-1], np.nan, np.nan, palette.cat[1], 1.0),80        ("Hédonique (estimation + spécification,\n± 1,645 écart-type)", b["V_hed"], b["V_hed"] - z90 * cb["s"][1], b["V_hed"] + z90 * cb["s"][1], palette.cat[0], 1.0),81        ("Conclusion : valeur la plus probable,\nfourchette conclue (bande : bootstrap 90 %)", cb["conclusion"], cb["fourchette_conclue"][0], cb["fourchette_conclue"][1], palette.brand, 1.0),82    ]83    fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))84    y = np.arange(len(lignes))[::-1]85    for yi, (lab, pt, lo, hi, col, alpha) in zip(y, lignes):86        if not np.isnan(lo):87            ax.plot([lo, hi], [yi, yi], color=col, lw=2.2 if alpha == 1.0 else 3.5, alpha=alpha, solid_capstyle="butt")88            ax.plot([lo, lo], [yi - 0.12, yi + 0.12], color=col, lw=1.0, alpha=alpha); ax.plot([hi, hi], [yi - 0.12, yi + 0.12], color=col, lw=1.0, alpha=alpha)89        ax.plot(pt, yi, "o", color=col, ms=6, zorder=5, mec="white", mew=0.8)90        ax.text(pt, yi + 0.2, fmt_dollars(pt), ha="center", va="bottom", fontsize=7)91    # bande bootstrap 90 % de la conclusion (plus épaisse, sous la fourchette conclue)92    yc = y[-1]93    ax.plot([cb["fourchette90"][0], cb["fourchette90"][1]], [yc, yc], color=palette.brand, lw=6, alpha=0.35, solid_capstyle="butt", zorder=1)94    ax.axvline(cb["conclusion"], color=palette.brand, lw=0.8, ls="--", zorder=0)95    ax.axvline(vt["V_true"], color=palette.ink2, lw=0.8, ls=":", zorder=0)96    ax.text(vt["V_true"] + 3000, -0.55, "espérance de la loi\nde génération", fontsize=7, color=palette.ink2, ha="left", va="center")97    ax.set_ylim(-0.9, len(lignes) - 0.3); ax.set_yticks(y); ax.set_yticklabels([l[0] for l in lignes], fontsize=7.2)98    ax.set_xlim(420000, 770000); ax.xaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.set_xlabel("Dollars (synthétiques), 1er septembre 2026")99    ax.grid(axis="y", visible=False)100    fig.tight_layout()101    savefig(fig, "ch20/ch20-fig-fourchettes-methodes.pdf")102103104def fig_sensibilite_poids(r):105    cb = r["combinaison"]106    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2))107    ax = axes[0]108    for i, (wc, courbe) in enumerate(cb["courbes"].items()):109        lab = "poids du coût = " + (f"{wc*100:.0f} %")110        ax.plot(cb["wh"], courbe, color=palette.cat[i], lw=1.6 if wc == 0.15 else 1.1, ls="-" if wc == 0.15 else "--", label=lab)111    tab = cb["tableau"]112    marqueurs = {"raisonnée (retenue)": ("o", palette.brand), "inverse de la variance (indépendance)": ("s", palette.cat[0]), "variance minimale avec corrélation (MCG)": ("^", palette.cat[0]), "égale (un tiers chacune)": ("D", palette.cat[2])}113    for _, row in tab.iterrows():114        if row["schema"] in marqueurs:115            m, c = marqueurs[row["schema"]]116            ax.plot(row["w_hed"], row["valeur"], m, color=c, ms=6, mec="white", mew=0.6, zorder=5)117    ax.annotate("raisonnée\n(0,40 / 0,45 / 0,15)", (0.45, tab.loc[tab["schema"] == "raisonnée (retenue)", "valeur"].iloc[0]), xytext=(0.62, 588000), fontsize=7, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))118    ax.annotate("inverse de la variance\net MCG", (0.616, tab.loc[tab["schema"] == "inverse de la variance (indépendance)", "valeur"].iloc[0]), xytext=(0.30, 630000), fontsize=7, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))119    ax.annotate("égale (un tiers au coût)", (1 / 3, tab.loc[tab["schema"] == "égale (un tiers chacune)", "valeur"].iloc[0]), xytext=(0.03, 622000), fontsize=7, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))120    ax.set_xlabel("Poids de l'indication hédonique $w_h$ (le reste à la comparaison)"); ax.set_ylabel("Valeur combinée")121    ax.yaxis.set_major_formatter(FuncFormatter(fmt_k)); ax.set_xlim(0, 1); ax.legend(loc="upper left", fontsize=7, frameon=False)122    ax.set_title("(a) Conclusion selon les poids", loc="left", fontsize=8.5)123    ax = axes[1]124    s = cb["s"]125    for i, (rho, var) in enumerate(cb["var_deux"].items()):126        sd = np.sqrt(np.maximum(var, 0)) / 1000127        ax.plot(cb["wh"], sd, color=palette.seq[min(2 + i, len(palette.seq) - 1)], lw=1.4, label=f"ρ = {rho:.1f}".replace(".", ","))128        w1 = c20.poids_optimaux_deux(s[0], s[1], rho)129        if 0 <= 1 - w1 <= 1:130            ax.plot(1 - w1, np.sqrt(w1 ** 2 * s[0] ** 2 + (1 - w1) ** 2 * s[1] ** 2 + 2 * w1 * (1 - w1) * rho * s[0] * s[1]) / 1000, "o", color=palette.seq[min(2 + i, len(palette.seq) - 1)], ms=5, mec="white", mew=0.6)131    ax.set_xlabel("Poids de l'indication hédonique $w_h$ ($w_c = 1 - w_h$)"); ax.set_ylabel("Écart-type de la combinaison (k$)")132    ax.set_xlim(0, 1); ax.legend(loc="upper center", fontsize=7, frameon=False, ncol=2)133    ax.set_title("(b) Variance de deux indications corrélées", loc="left", fontsize=8.5)134    fig.tight_layout(w_pad=2.0)135    savefig(fig, "ch20/ch20-fig-sensibilite-poids.pdf")136137138def main():139    setup()140    r = c20.tout()141    fig_bootstrap(r); fig_residus_elargissement(r); fig_fourchettes(r); fig_sensibilite_poids(r)142    print("figures ch20 écrites dans assets/figures/ch20/")143144145if __name__ == "__main__":146    main()147
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme