SPB Git
9.9 KB · 144 lines python
1#!/usr/bin/env python32"""Figures du chapitre 19 — modèles spatiaux et temporels (cas 1, Gatineau, DONNÉES SYNTHÉTIQUES, graine 20261031).3Toutes les cartes sont tracées sur les coordonnées synthétiques (km est/nord autour du centroïde des ventes), sans fond de carte.4Sorties (assets/figures/ch19/) :5  ch19-fig-carte-residus.pdf      résidus MCO sans indicatrices de secteur vs modèle de référence6  ch19-fig-correlogramme.pdf      I de Moran par bande de distance7  ch19-fig-gwr-coefficients.pdf   coefficients locaux de la GWR (localisation, superficie, âge)8  ch19-fig-gwr-bande.pdf          score de validation croisée selon la bande passante9  ch19-fig-variogramme.pdf        variogramme empirique et modèle exponentiel10  ch19-fig-krigeage.pdf           surface krigée des résidus11  ch19-fig-sensibilite-W.pdf      I, ρ et λ selon la matrice de poids12Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch19_figures.py"""13import sys, pathlib14ROOT = pathlib.Path(__file__).resolve().parents[3]15sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))16import numpy as np17import matplotlib.pyplot as plt18from matplotlib.ticker import FuncFormatter19from traite.style import setup, palette, savefig, LARGEUR, DEMI20from traite import hedonique as h21import ch19_spatial as c192223SECTEURS = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]2425def _axes_km(ax, xy):26    ax.set_aspect("equal"); ax.set_xlim(xy[:, 0].min() - 1.5, xy[:, 0].max() + 1.5); ax.set_ylim(xy[:, 1].min() - 1.5, xy[:, 1].max() + 1.5)27    ax.set_xlabel("Est (km, coordonnées synthétiques)"); ax.grid(True, lw=0.4)2829def _etiquettes_secteurs(ax, v, xy, dy=0.0):30    for s in SECTEURS:31        m = (v["secteur"] == s).values32        cx, cy = np.median(xy[m, 0]), np.median(xy[m, 1])33        ax.text(cx, cy + dy, s, fontsize=7, color=palette.ink2, ha="center", va="center",34                bbox=dict(boxstyle="round,pad=0.15", fc=palette.surface, ec="none", alpha=0.75))3536def fig_carte_residus(res):37    b = res["base"]; v, xy, cs = b["ventes"], b["xy"], b["cs"]38    series = [("Sans indicatrices de secteur (I = %.2f)" % res["moran"].loc[2, "I_knn"], b["mco_r"]["residus"]),39              ("Modèle de référence (I = %.2f)" % res["moran"].loc[3, "I_knn"], b["modele"]["residus"])]40    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.6), sharey=True)41    vmax = 0.25; cmap = palette.cmap_div()42    for ax, (titre, e) in zip(axes, series):43        sc = ax.scatter(xy[:, 0], xy[:, 1], c=np.clip(e, -vmax, vmax), cmap=cmap, vmin=-vmax, vmax=vmax, s=9, lw=0)44        ax.scatter(cs[:, 0], cs[:, 1], marker="*", s=110, color=palette.ink, zorder=5)45        _axes_km(ax, xy); ax.set_title(titre, fontsize=8.5); _etiquettes_secteurs(ax, v, xy, dy=4.5)46    axes[0].set_ylabel("Nord (km)")47    cb = fig.colorbar(sc, ax=axes, orientation="horizontal", fraction=0.05, pad=0.16, aspect=40)48    cb.set_label("Résidu du modèle semi-logarithmique (ln P observé − ln P ajusté)", fontsize=8)49    cb.ax.tick_params(labelsize=7.5)50    savefig(fig, "ch19/ch19-fig-carte-residus.pdf")5152def fig_correlogramme(res):53    cg = res["correlogramme"]54    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))55    for i, (cle, nom) in enumerate([("sans_secteur", "Résidus sans indicatrices de secteur"), ("reference", "Résidus du modèle de référence")]):56        pts = cg[cle]; x = np.array([(p["d_min"] + p["d_max"]) / 2 for p in pts]); I = np.array([p["I"] for p in pts])57        sd = np.sqrt(np.array([p["Var_I"] for p in pts]))58        ax.errorbar(x + (i - 0.5) * 0.12, I, yerr=1.96 * sd, fmt="o-", color=palette.cat[i], ms=4.5, lw=1.4, capsize=2, label=nom)59    ax.axhline(cg["reference"][0]["E_I"], color=palette.ink2, lw=0.9, ls="--")60    ax.text(14.6, cg["reference"][0]["E_I"] + 0.012, "E[I] = −1/(n−1)", fontsize=7, color=palette.ink2, ha="right")61    ax.set_xlabel("Distance entre paires de ventes (km, centre de la bande)"); ax.set_ylabel("I de Moran de la bande")62    ax.legend(loc="upper right", fontsize=7.5)63    savefig(fig, "ch19/ch19-fig-correlogramme.pdf")6465def fig_gwr_coefficients(res):66    b = res["base"]; v, xy, cs = b["ventes"], b["xy"], b["cs"]; g = res["gwr"]["gwr"]; cols = list(b["Xr"].columns)67    j = cols.index("dist_centre"); dist = b["Xr"]["dist_centre"].values68    sect = v["secteur"].values69    loc = g["beta"][:, 0] + g["beta"][:, j] * dist - np.median(g["beta"][sect == "Hull", 0])70    panneaux = [("Effet de localisation local\n(écart à Hull, en log)", loc, palette.cmap_div(), -0.35, 0.35),71                ("Coefficient local de ln(superficie)\n(élasticité)", g["beta"][:, cols.index("ln_S")], palette.cmap_seq(), None, None),72                ("Coefficient local de l'âge\n(× 100, % par année)", 100 * g["beta"][:, cols.index("age")], palette.cmap_seq(), None, None)]73    fig, axes = plt.subplots(1, 3, figsize=(LARGEUR, 2.75))74    for ax, (titre, z, cmap, vmin, vmax) in zip(axes, panneaux):75        sc = ax.scatter(xy[:, 0], xy[:, 1], c=z, cmap=cmap, vmin=vmin, vmax=vmax, s=6, lw=0)76        ax.scatter(cs[:, 0], cs[:, 1], marker="*", s=70, color=palette.ink, zorder=5)77        _axes_km(ax, xy); ax.set_title(titre, fontsize=7.5); ax.tick_params(labelsize=6.5); ax.set_xlabel("Est (km)", fontsize=7)78        cb = fig.colorbar(sc, ax=ax, orientation="horizontal", fraction=0.06, pad=0.22, aspect=28); cb.ax.tick_params(labelsize=6.5)79    axes[0].set_ylabel("Nord (km)", fontsize=7)80    savefig(fig, "ch19/ch19-fig-gwr-coefficients.pdf")8182def fig_gwr_bande(res):83    sel = res["gwr"]["selection"]84    fig, ax = plt.subplots(figsize=(DEMI, 2.5))85    ax.plot(sel["grille"], sel["cv_grille"], marker="o", ms=3.5, color=palette.cat[0])86    ax.axvline(sel["bande"], color=palette.ink2, ls="--", lw=0.9)87    ax.text(sel["bande"] * 1.1, sel["cv"] * 1.6, "h* = %.2f km" % sel["bande"], fontsize=7, color=palette.ink2, va="bottom")88    ax.set_xscale("log"); ax.set_yscale("log"); ax.set_xlabel("Bande passante h (km, échelle log)"); ax.set_ylabel("Score de validation croisée\n(somme des carrés des erreurs, échelle log)")89    ax.set_xticks([0.3, 0.5, 1, 2, 5, 10]); ax.get_xaxis().set_major_formatter(FuncFormatter(lambda x, p: ("%g" % x)))90    savefig(fig, "ch19/ch19-fig-gwr-bande.pdf")9192def fig_variogramme(res):93    vk = res["variogramme"]; vg, vr, p = vk["variogramme"], vk["variogramme_reference"], vk["parametres"]94    fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))95    taille = 12 + 60 * vg["n"] / vg["n"].max()96    ax.scatter(vg["h"], vg["gamma"], s=taille, color=palette.cat[0], alpha=0.85, lw=0, label="Résidus sans indicatrices de secteur (taille selon le nombre de paires)", zorder=3)97    hh = np.linspace(0.05, 16, 200)98    ax.plot(hh, h.variogramme_exponentiel(hh, p["pepite"], p["palier_partiel"], p["portee"]), color=palette.cat[0], lw=1.6,99            label="Modèle exponentiel ajusté (pépite %.4f, palier %.4f, portée pratique %.0f km)" % (p["pepite"], p["palier"], p["portee_pratique"]))100    ax.scatter(vr["h"], vr["gamma"], s=14, marker="s", color=palette.cat[1], lw=0, label="Résidus du modèle de référence", zorder=3)101    ax.axhline(vk["sigma2_reference"], color=palette.cat[1], lw=0.9, ls=":")102    ax.axhline(vk["variance_residus"], color=palette.cat[0], lw=0.9, ls=":")103    ax.text(15.8, vk["variance_residus"] + 0.0004, "variance des résidus sans secteur", fontsize=7, color=palette.ink2, ha="right")104    ax.text(15.8, vk["sigma2_reference"] - 0.0012, "σ² du modèle de référence", fontsize=7, color=palette.ink2, ha="right")105    ax.set_xlabel("Distance h entre paires de ventes (km)"); ax.set_ylabel("Semi-variance γ(h) (log²)")106    ax.set_ylim(0, 0.02); ax.legend(loc="upper left", fontsize=7)107    savefig(fig, "ch19/ch19-fig-variogramme.pdf")108109def fig_krigeage(res):110    b = res["base"]; v, xy, cs = b["ventes"], b["xy"], b["cs"]; gr = res["variogramme"]["grille"]111    GX, GY = np.meshgrid(gr["x"], gr["y"]); pts = np.column_stack([GX.ravel(), GY.ravel()])112    from scipy.spatial import cKDTree113    dmin = cKDTree(xy).query(pts)[0].reshape(GX.shape)114    Z = np.ma.masked_where(dmin > 2.5, gr["z"])115    fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))116    vmax = 0.25117    cf = ax.contourf(GX, GY, np.clip(Z, -vmax, vmax), levels=np.linspace(-vmax, vmax, 21), cmap=palette.cmap_div(), vmin=-vmax, vmax=vmax)118    ax.scatter(xy[:, 0], xy[:, 1], s=3, color=palette.ink, alpha=0.45, lw=0)119    ax.scatter(cs[:, 0], cs[:, 1], marker="*", s=120, color=palette.ink, zorder=5)120    ax.annotate("sujet : résidu krigé %.3f" % res["variogramme"]["sujet"]["prediction"][0], (cs[0, 0], cs[0, 1]), xytext=(14, -34), textcoords="offset points",121                fontsize=7, color=palette.ink, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))122    _axes_km(ax, xy); ax.set_ylabel("Nord (km)"); _etiquettes_secteurs(ax, v, xy, dy=5.0)123    cb = fig.colorbar(cf, ax=ax, fraction=0.035, pad=0.02); cb.set_label("Résidu krigé (log)", fontsize=8); cb.ax.tick_params(labelsize=7.5)124    savefig(fig, "ch19/ch19-fig-krigeage.pdf")125126def fig_sensibilite(res):127    s = res["sensibilite"]; k = s[s["famille"].str.startswith("k")]128    fig, ax = plt.subplots(figsize=(LARGEUR, 2.9))129    ax.errorbar(k["parametre"], k["rho"], yerr=1.96 * k["se_rho"], fmt="o-", color=palette.cat[0], ms=4.5, capsize=2, label="ρ du SAR (± 1,96 é.-t.)")130    ax.errorbar(k["parametre"], k["lambda"], yerr=1.96 * k["se_lambda"], fmt="s-", color=palette.cat[1], ms=4.5, capsize=2, label="λ du SEM (± 1,96 é.-t.)")131    ax.plot(k["parametre"], k["I"], "^-", color=palette.cat[2], ms=4.5, label="I de Moran des résidus MCO")132    ax.set_xlabel("Nombre k de plus proches voisins dans W"); ax.set_ylabel("Coefficient"); ax.set_xticks(k["parametre"].astype(int))133    ax.set_ylim(0, 0.95); ax.legend(loc="upper left", fontsize=7.5)134    savefig(fig, "ch19/ch19-fig-sensibilite-W.pdf")135136def main():137    setup()138    res = c19.main(verbose=False)139    for f in [fig_carte_residus, fig_correlogramme, fig_gwr_coefficients, fig_gwr_bande, fig_variogramme, fig_krigeage, fig_sensibilite]:140        f(res); print("OK", f.__name__)141142if __name__ == "__main__":143    main()144
Files · faf80546624105e8fab6f9b3544b7f1ede4bc34a
Menu

Theme