SPB Git
9.2 KB · 126 lines python
1#!/usr/bin/env python32"""Chapitre 16 — sélection des comparables pour le sujet du cas 1 (Gatineau, données synthétiques).3Imprime tous les chiffres cités dans le chapitre et produit les figures du chapitre 16.4Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch16_selection.py [--figures]"""5from __future__ import annotations6import sys, json, pathlib, datetime7import numpy as np, pandas as pd8sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))9from traite import comparables as cp10ROOT = pathlib.Path(__file__).resolve().parents[3]11D = ROOT / "data/synthetic"1213ECHELLES = {"superficie_habitable_m2": 25.0, "superficie_terrain_m2": 150.0, "age": 10.0, "qualite_1a5": 1.0, "garage_places": 1.0, "sous_sol_fini": 1.0, "dist_km": 1.5, "mois": 6.0}14POIDS = {"superficie_habitable_m2": 2.0, "superficie_terrain_m2": 0.7, "age": 1.0, "qualite_1a5": 1.0, "garage_places": 0.5, "sous_sol_fini": 0.3, "dist_km": 1.5, "mois": 1.0}1516def charger():17    v = pd.read_csv(D / "cas1_gatineau_ventes.csv", parse_dates=["date_vente"])18    s = json.loads((D / "cas1_gatineau_sujet.json").read_text())19    return v, s2021def preparer(v: pd.DataFrame, s: dict) -> pd.DataFrame:22    """Étape 1 : filtres qualitatifs (marché, délai, type) ; étape 2 : distance d'attributs."""23    date_eval = pd.Timestamp(s["date_evaluation"])24    v = v.copy()25    v["mois"] = (date_eval - v["date_vente"]).dt.days / 30.437526    v["dist_km"] = np.sqrt(((v["lat"] - s["lat"]) * 111) ** 2 + ((v["lon"] - s["lon"]) * 78) ** 2)27    v["prix_m2"] = v["prix_vente"] / v["superficie_habitable_m2"]28    suj = dict(s); suj["mois"] = 0.0; suj["dist_km"] = 0.029    v["distance"] = [cp.distance_attributs(suj, r, ECHELLES, POIDS) for r in v.to_dict("records")]30    return v3132def selectionner(v: pd.DataFrame, s: dict, k: int = 5, mois_max: float = 18, dist_max: float = 3.0, meme_type: bool = True) -> dict:33    f = v[(v["mois"] <= mois_max) & (v["mois"] >= 0) & (v["dist_km"] <= dist_max)]34    etape = {"candidats_total": len(v), "apres_delai": int((v["mois"].between(0, mois_max)).sum()), "apres_rayon": len(f)}35    if meme_type:36        f = f[f["type"] == s["type"]]; etape["apres_type"] = len(f)37    f = f[(f["superficie_habitable_m2"].between(0.75 * s["superficie_habitable_m2"], 1.25 * s["superficie_habitable_m2"]))]38    etape["apres_superficie"] = len(f)39    f = f.sort_values("distance")40    top = f.head(k)41    return {"etapes": etape, "candidats": f, "comparables": top}4243def main(figures: bool = False):44    v, s = charger(); v = preparer(v, s)45    sel = selectionner(v, s)46    top = sel["comparables"]47    print("== Sujet :", {k: s[k] for k in ["secteur", "type", "superficie_habitable_m2", "superficie_terrain_m2", "annee_construction", "qualite_1a5", "garage_places", "sous_sol_fini"]})48    print("== Entonnoir :", sel["etapes"])49    cols = ["id_vente", "date_vente", "secteur", "superficie_habitable_m2", "superficie_terrain_m2", "annee_construction", "qualite_1a5", "garage_places", "sous_sol_fini", "dist_km", "mois", "prix_vente", "prix_m2", "distance"]50    pd.set_option("display.width", 200)51    print("== Comparables retenus :\n", top[cols].round(2).to_string(index=False))52    # unités de comparaison53    cand = sel["candidats"]54    print(f"== $/m² : médiane candidats {cand['prix_m2'].median():.0f}, min {cand['prix_m2'].min():.0f}, max {cand['prix_m2'].max():.0f} ; comparables retenus médiane {top['prix_m2'].median():.0f}")55    ind_m2 = top["prix_m2"].median() * s["superficie_habitable_m2"]56    print(f"== Indication brute par $/m² médian × {s['superficie_habitable_m2']} m² = {ind_m2:,.0f} $")57    print(f"== Fourchette des prix bruts des comparables : {top['prix_vente'].min():,.0f} – {top['prix_vente'].max():,.0f} ; moyenne {top['prix_vente'].mean():,.0f} ; médiane {top['prix_vente'].median():,.0f}")58    # sensibilité : k et rayon59    sens = []60    for k in [3, 4, 5, 6, 8, 10]:61        for r in [1.5, 3.0, 5.0]:62            for m in [12, 18, 24]:63                t = selectionner(v, s, k, m, r)["comparables"]64                if len(t) >= 3:65                    sens.append({"k": k, "rayon_km": r, "mois_max": m, "n": len(t), "mediane_prix": t["prix_vente"].median(), "mediane_prix_m2": t["prix_m2"].median(), "indication_m2": t["prix_m2"].median() * s["superficie_habitable_m2"]})66    sens = pd.DataFrame(sens)67    print("== Sensibilité (indication par $/m²) : min %.0f, max %.0f, écart-type %.0f, médiane %.0f" % (sens.indication_m2.min(), sens.indication_m2.max(), sens.indication_m2.std(), sens.indication_m2.median()))68    print(sens.groupby("k").indication_m2.agg(["min", "median", "max"]).round(0))69    # rang des 5 retenus selon une autre pondération (robustesse)70    if figures:71        faire_figures(v, s, sel, sens)72    return {"sujet": s, "ventes": v, "selection": sel, "sensibilite": sens}7374def faire_figures(v, s, sel, sens):75    import matplotlib.pyplot as plt76    from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars, fmt_milliers77    from matplotlib.ticker import FuncFormatter78    setup()79    top = sel["comparables"]; cand = sel["candidats"]80    # Fig 1 : nuage prix × superficie, candidats vs retenus vs sujet81    fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))82    ax.scatter(v["superficie_habitable_m2"], v["prix_vente"], s=8, color=palette.grid, label="Toutes les ventes (n = %d)" % len(v), zorder=1)83    ax.scatter(cand["superficie_habitable_m2"], cand["prix_vente"], s=14, color=palette.cat[0], alpha=0.8, label="Candidats après filtres (n = %d)" % len(cand), zorder=2)84    ax.scatter(top["superficie_habitable_m2"], top["prix_vente"], s=46, facecolor="none", edgecolor=palette.cat[1], linewidth=1.6, label="Comparables retenus (n = %d)" % len(top), zorder=3)85    ax.axvline(s["superficie_habitable_m2"], color=palette.ink2, ls="--", lw=0.9)86    ax.annotate("Sujet : %d m²" % s["superficie_habitable_m2"], (s["superficie_habitable_m2"], ax.get_ylim()[1] * 0.97), xytext=(6, -2), textcoords="offset points", fontsize=8, color=palette.ink2, va="top")87    ax.set_xlabel("Superficie habitable (m²)"); ax.set_ylabel("Prix de vente")88    ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.legend(loc="lower right", fontsize=7.5)89    savefig(fig, "ch16/ch16-fig-nuage-selection.pdf")90    # Fig 2 : carte schématique des ventes (coordonnées synthétiques) avec rayon de recherche91    fig, ax = plt.subplots(figsize=(LARGEUR, 4.0))92    for i, sec in enumerate(["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]):93        w = v[v["secteur"] == sec]94        ax.scatter(w["lon"], w["lat"], s=7, color=palette.cat[i], alpha=0.55, label=sec)95    ax.scatter(top["lon"], top["lat"], s=50, facecolor="none", edgecolor=palette.ink, linewidth=1.4, label="Comparables retenus", zorder=4)96    ax.scatter([s["lon"]], [s["lat"]], marker="*", s=140, color=palette.ink, zorder=5, label="Sujet")97    th = np.linspace(0, 2 * np.pi, 200)98    ax.plot(s["lon"] + 3.0 / 78 * np.cos(th), s["lat"] + 3.0 / 111 * np.sin(th), color=palette.ink2, ls="--", lw=0.9)99    ax.text(s["lon"] + 3.0 / 78, s["lat"] + 0.002, "rayon 3 km", fontsize=7.5, color=palette.ink2)100    ax.set_xlabel("Longitude (coordonnées synthétiques)"); ax.set_ylabel("Latitude"); ax.set_aspect(111 / 78)101    ax.legend(fontsize=7, ncol=2, loc="upper left"); ax.grid(True, lw=0.4)102    savefig(fig, "ch16/ch16-fig-carte-comparables.pdf")103    # Fig 3 : distribution du $/m² : toutes ventes du secteur vs candidats vs retenus104    fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))105    groupes = [v.loc[v["secteur"] == s["secteur"], "prix_m2"], cand["prix_m2"], top["prix_m2"]]106    labels = ["Ventes du secteur %s\n(n = %d)" % (s["secteur"], len(groupes[0])), "Candidats filtrés\n(n = %d)" % len(cand), "Comparables retenus\n(n = %d)" % len(top)]107    bp = ax.boxplot(groupes, vert=False, widths=0.55, patch_artist=True, medianprops=dict(color=palette.ink, lw=1.4), flierprops=dict(marker="o", ms=3, alpha=0.5))108    for patch, c in zip(bp["boxes"], palette.cat[:3]): patch.set(facecolor=c, alpha=0.35, edgecolor=c)109    ax.set_yticks([1, 2, 3]); ax.set_yticklabels(labels, fontsize=7.5); ax.set_xlabel("Prix par mètre carré habitable ($/m²)")110    ax.xaxis.set_major_formatter(FuncFormatter(fmt_milliers))111    for i, g in enumerate(groupes, 1): ax.annotate("méd. %s" % fmt_milliers(g.median()), (g.median(), i + 0.33), fontsize=7.5, ha="center", color=palette.ink2)112    savefig(fig, "ch16/ch16-fig-boites-prix-m2.pdf")113    # Fig 4 : sensibilité de l'indication par $/m² au nombre de comparables et au rayon114    fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))115    for j, r in enumerate([1.5, 3.0, 5.0]):116        w = sens[(sens["rayon_km"] == r) & (sens["mois_max"] == 18)]117        ax.plot(w["k"], w["indication_m2"], marker="o", color=palette.cat[j], label="rayon %.1f km" % r)118        ax.annotate("%.1f km" % r, (w["k"].iloc[-1], w["indication_m2"].iloc[-1]), xytext=(5, 0), textcoords="offset points", fontsize=7.5, color=palette.cat[j], va="center")119    ax.set_xlabel("Nombre de comparables retenus (k)"); ax.set_ylabel("Indication brute ($/m² médian × superficie)")120    ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.legend(fontsize=7.5, loc="lower right")121    savefig(fig, "ch16/ch16-fig-sensibilite-selection.pdf")122    print("figures écrites dans assets/figures/ch16/")123124if __name__ == "__main__":125    main(figures="--figures" in sys.argv)126
Files · 16bdf3585d9654c975e1783947fa52c1869f76b7
Menu

Theme