spb/traite-evaluation-qc
Public
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