SPB Git
9.9 KB · 133 lines python
1#!/usr/bin/env python32"""Figures du chapitre 15 — Études de marché et segmentation. Produit dans assets/figures/ch15/ :3  ch15-fig-mois-inventaire.pdf     mois d'inventaire par RMR, T3 2026 (chiffres APCIQ cités ; calculs de l'auteur) — données réelles4  ch15-fig-dendrogramme.pdf        dendrogramme de Ward des 600 ventes du cas 1 (attributs + coordonnées) — SYNTHÉTIQUE5  ch15-fig-silhouette.pdf          silhouette moyenne selon k et jeu de variables ; profil de silhouette à k = 5 — SYNTHÉTIQUE6  ch15-fig-carte-segments.pdf      carte fictive des segments k-moyennes (k = 2 et k = 5), petits multiples — SYNTHÉTIQUE7  ch15-fig-coefficients-secteurs.pdf coefficients hédoniques par secteur avec IC à 95 % (HC1) et coefficient commun — SYNTHÉTIQUE8  ch15-fig-loyers-rosemont.pdf     structure des loyers et de l'univers locatif, zone SCHL 9 Rosemont vs RMR, oct. 2025 — données réelles9Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch15_figures.py"""10from __future__ import annotations11import sys, pathlib12ROOT = pathlib.Path(__file__).resolve().parents[3]13sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))14import numpy as np, pandas as pd15import matplotlib.pyplot as plt16from matplotlib.ticker import FuncFormatter17from scipy.cluster.hierarchy import dendrogram18from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers19from traite import hedonique as h20import ch15_segmentation as c152122def fig_mois_inventaire(r):23    ind = r["indicateurs"].copy()24    ind["court"] = ind["territoire"].str.replace("RMR de ", "", regex=False).str.replace("Province de Québec", "Province")25    ind = ind.sort_values("mois_inventaire", ascending=True)26    fig, ax = plt.subplots(figsize=(LARGEUR, 3.3))27    ax.axvspan(0, 4, color=palette.cat[1], alpha=0.08, lw=0); ax.axvspan(4, 6, color=palette.grid, alpha=0.35, lw=0); ax.axvspan(6, 8, color=palette.cat[2], alpha=0.08, lw=0)28    ax.barh(ind["court"], ind["mois_inventaire"], color=palette.brand, height=0.6)29    for y, (mi, dj) in enumerate(zip(ind["mois_inventaire"], ind["delai_unif_j"])):30        lab = f"{mi:.1f} mois" + (f"  (délai unifam. {int(dj)} j)" if not np.isnan(dj) else "")31        ax.text(mi + 0.08, y, lab, va="center", ha="left", fontsize=8, color=palette.ink)32    ax.set_xlim(0, 8.6); ax.set_xlabel("Mois d'inventaire = inscriptions en vigueur / ventes mensuelles du trimestre")33    yt = len(ind) + 0.5534    ax.text(2, yt, "favorable aux vendeurs (< 4 mois, usage)", ha="center", va="center", fontsize=7.5, color=palette.ink2)35    ax.text(5, yt, "équilibré (4 à 6)", ha="center", va="center", fontsize=7.5, color=palette.ink2)36    ax.text(7.2, yt, "favorable aux acheteurs (> 6)", ha="center", va="center", fontsize=7.5, color=palette.ink2)37    ax.set_ylim(-0.6, len(ind) + 1.0); ax.grid(axis="y", visible=False)38    savefig(fig, "ch15/ch15-fig-mois-inventaire.pdf")3940def fig_dendrogramme(r):41    L = r["classification"]["linkage"]42    fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))43    d = dendrogram(L, truncate_mode="lastp", p=30, ax=ax, color_threshold=0, above_threshold_color=palette.brand, leaf_rotation=90, leaf_font_size=7, no_labels=False)44    # hauteurs de coupe pour k = 2 et k = 5 (distance de fusion des dernières agrégations)45    hauteurs = L[:, 2]; n = L.shape[0] + 146    for k, col, lab in [(2, palette.cat[2], "k = 2"), (5, palette.cat[1], "k = 5")]:47        y = 0.5 * (hauteurs[n - k - 1] + hauteurs[n - k])48        ax.axhline(y, color=col, ls="--", lw=1.1); ax.text(ax.get_xlim()[1] * 0.995, y, lab, ha="right", va="bottom", fontsize=8, color=col)49    ax.set_ylabel("Distance de fusion (critère de Ward)"); ax.set_xlabel("Grappes terminales (effectif entre parenthèses) — 600 ventes synthétiques")50    ax.grid(axis="x", visible=False)51    savefig(fig, "ch15/ch15-fig-dendrogramme.pdf")5253def fig_silhouette(r):54    v = r["ventes"]55    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2), gridspec_kw={"width_ratios": [1.1, 1]})56    ax = axes[0]57    for i, (jeu, lab) in enumerate([("attributs", "attributs seuls"), ("coordonnees", "coordonnées seules"), ("attributs+coordonnees", "attributs + coordonnées")]):58        Z = c15.caracteristiques_segmentation(v, jeu); t = c15.classifier(Z)["tableau"]59        ax.plot(t["k"], t["silhouette_kmeans"], marker="o", color=palette.cat[i], label=lab)60    ax.set_xlabel("Nombre de segments $k$ (k-moyennes)"); ax.set_ylabel("Silhouette moyenne $\\bar s$"); ax.set_ylim(0, 0.8); ax.legend(loc="lower left")61    ax.set_title("(a) silhouette moyenne selon $k$", loc="left")62    ax = axes[1]63    Z = r["Z"].values; lab5 = r["labels_kmeans5"]64    from sklearn.metrics import silhouette_samples65    s = silhouette_samples(Z, lab5); y0 = 066    for g in range(5):67        sg = np.sort(s[lab5 == g])[::-1]; yy = np.arange(y0, y0 + len(sg))68        ax.barh(yy, sg, height=1.0, color=palette.cat[g], lw=0)69        ax.text(-0.02, y0 + len(sg) / 2, f"{g + 1}", ha="right", va="center", fontsize=8); y0 += len(sg) + 870    ax.axvline(s.mean(), color=palette.ink2, ls="--", lw=1); ax.text(s.mean() + 0.01, y0, f"$\\bar s$ = {s.mean():.2f}", fontsize=8, va="top", color=palette.ink2)71    ax.set_xlim(-0.25, 0.75); ax.set_yticks([]); ax.set_xlabel("Silhouette $s(i)$ de chaque vente"); ax.set_title("(b) profil à $k$ = 5 (attributs + coordonnées)", loc="left")72    ax.grid(axis="y", visible=False)73    savefig(fig, "ch15/ch15-fig-silhouette.pdf")7475def fig_carte_segments(r):76    v = r["ventes"]; s = r["sujet"]77    xy = h.coordonnees_km(v["lat"].values, v["lon"].values); sxy = h.coordonnees_km(np.array([s["lat"]]), np.array([s["lon"]]), lat0=float(v["lat"].mean()), lon0=float(v["lon"].mean()))78    lab2 = r["labels_kmeans"]; lab5 = r["labels_kmeans5"]79    panneaux = [("(a) $k$ = 2, « Est »", lab2 == (np.bincount(lab2).argmin()))] + [(f"({c}) $k$ = 5, seg. {g + 1}", lab5 == g) for c, g in zip("bcdef", range(5))]80    fig, axes = plt.subplots(2, 3, figsize=(LARGEUR, 4.0), sharex=True, sharey=True)81    for ax, (titre, m) in zip(axes.ravel(), panneaux):82        ax.scatter(xy[~m, 0], xy[~m, 1], s=5, color=palette.grid, lw=0)83        ax.scatter(xy[m, 0], xy[m, 1], s=7, color=palette.brand, lw=0)84        ax.scatter(sxy[0, 0], sxy[0, 1], marker="*", s=70, color=palette.cat[2], edgecolor=palette.ink, lw=0.4, zorder=5)85        ax.set_title(f"{titre} ($n$ = {int(m.sum())})", loc="left", fontsize=8); ax.set_aspect("equal"); ax.grid(visible=False)86    fig.subplots_adjust(hspace=0.35, wspace=0.08)87    for ax in axes[1]: ax.set_xlabel("km (est)")88    for ax in axes[:, 0]: ax.set_ylabel("km (nord)")89    savefig(fig, "ch15/ch15-fig-carte-segments.pdf")9091def fig_coefficients_secteurs(r):92    ch = r["chow"]["secteur"]; ref = r["reference"]["coef"]93    vars_ = [("ln_S", "ln(superficie)"), ("qualite", "qualité (échelon)"), ("age", "âge (année)"), ("mois", "tendance (mois)")]94    ordre = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]95    fig, axes = plt.subplots(1, 4, figsize=(LARGEUR, 2.9), sharey=True)96    for ax, (vv, titre) in zip(axes, vars_):97        for i, sct in enumerate(ordre):98            d = ch["par_segment"][sct]; b = d["coef"][vv]; se = d["se_hc1"][vv]99            ax.errorbar(b, i, xerr=1.96 * se, fmt="o", color=palette.brand, ecolor=palette.brand, capsize=2, ms=4, lw=1.1)100        ax.axvline(ref.loc[vv, "coef"], color=palette.cat[2], ls="--", lw=1.1)101        ax.set_title(titre, loc="left", fontsize=8.5); ax.grid(axis="y", visible=False)102        ax.ticklabel_format(axis="x", style="plain", useOffset=False)103    axes[0].set_yticks(range(len(ordre))); axes[0].set_yticklabels([f"{s} ($n$ = {ch['par_segment'][s]['n']})" for s in ordre]); axes[0].invert_yaxis()104    fig.text(0.5, -0.04, "Coefficient estimé par secteur (point) avec intervalle à 95 % (HC1) ; tireté : coefficient commun du modèle de référence", ha="center", fontsize=8, color=palette.ink2)105    savefig(fig, "ch15/ch15-fig-coefficients-secteurs.pdf")106107def fig_loyers_rosemont(r):108    Z9 = c15.SCHL_ZONE9; types = ["studio", "1 ch.", "2 ch.", "3 ch. +", "tous"]109    rmr = {"studio": 1005, "1 ch.": 1131, "2 ch.": 1346, "3 ch. +": 1626, "tous": 1290}   # SCHL tableau 1.1.2, Montréal (RMR), 2025110    fig, axes = plt.subplots(1, 2, figsize=(LARGEUR, 3.2), gridspec_kw={"width_ratios": [1.25, 1]})111    ax = axes[0]; x = np.arange(len(types)); w = 0.38112    b1 = ax.bar(x - w / 2, [Z9["loyer_2025"][t] for t in types], w, color=palette.brand, label="zone 9 Rosemont/La Petite-Patrie")113    b2 = ax.bar(x + w / 2, [rmr[t] for t in types], w, color=palette.cat[1], label="RMR de Montréal")114    for bars in (b1, b2):115        for b in bars: ax.text(b.get_x() + b.get_width() / 2, b.get_height() + 25, fmt_milliers(b.get_height()), ha="center", va="bottom", fontsize=6.5, rotation=90, color=palette.ink)116    ax.set_xticks(x); ax.set_xticklabels(types); ax.set_ylabel("Loyer moyen, octobre 2025 ($/mois)"); ax.set_ylim(0, 2300); ax.legend(loc="upper left", fontsize=7.5)117    ax.set_title("(a) loyer moyen par taille de logement", loc="left"); ax.grid(axis="x", visible=False)118    ax = axes[1]; t4 = types[:4]; u = [Z9["univers_2025"][t] for t in t4]; tot = Z9["univers_2025"]["tous"]119    bars = ax.bar(t4, u, color=palette.brand, width=0.6)120    for b, val in zip(bars, u): ax.text(b.get_x() + b.get_width() / 2, val + 300, f"{fmt_milliers(val)}\n({100 * val / tot:.0f} %)", ha="center", va="bottom", fontsize=7.5)121    ax.set_ylim(0, 27000); ax.set_ylabel("Logements (univers ELL)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_milliers))122    ax.set_title(f"(b) univers zone 9 : {fmt_milliers(tot)} log.", loc="left"); ax.grid(axis="x", visible=False)123    fig.subplots_adjust(wspace=0.35)124    savefig(fig, "ch15/ch15-fig-loyers-rosemont.pdf")125126def main():127    setup(); r = c15.tout()128    fig_mois_inventaire(r); fig_dendrogramme(r); fig_silhouette(r); fig_carte_segments(r); fig_coefficients_secteurs(r); fig_loyers_rosemont(r)129    print("Figures du chapitre 15 écrites dans", ROOT / "assets/figures/ch15")130131if __name__ == "__main__":132    main()133
Files · ae9e02e7cd803a16ec66687feb5b5de2a15d673f
Menu

Theme