spb/traite-evaluation-qc
Public
1"""Cas 1 — maison unifamiliale à Gatineau (données synthétiques) : calculs PARTAGÉS par les chapitres 9, 14, 16-20, 24, 25, 35, 37, 39 et 45.2Sélection des comparables (identique au chapitre 16), modèle hédonique de référence, prix implicites et ajustements dérivés."""3from __future__ import annotations4import json, pathlib, sys5import numpy as np, pandas as pd67ROOT = pathlib.Path(__file__).resolve().parents[3]8D = ROOT / "data/synthetic"9sys.path.insert(0, str(ROOT / "analysis/python/cases"))1011def charger() -> dict:12 v = pd.read_csv(D / "cas1_gatineau_ventes.csv", parse_dates=["date_vente"])13 s = json.loads((D / "cas1_gatineau_sujet.json").read_text())14 c = pd.read_csv(D / "cas1_gatineau_couts.csv")15 return {"ventes": v, "sujet": s, "couts": c}1617def selection(k: int = 5) -> dict:18 """Les comparables du chapitre 16 (mêmes filtres, mêmes échelles et poids)."""19 import ch16_selection as c1620 v, s = c16.charger(); v = c16.preparer(v, s)21 sel = c16.selectionner(v, s, k=k)22 return {"ventes": v, "sujet": s, **sel}2324SECTEURS = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]25REF_DATE = pd.Timestamp("2024-07-01") # référence temporelle du jeu (date de référence du rôle fictif)2627def matrice(df: pd.DataFrame, date_eval: pd.Timestamp | None = None) -> pd.DataFrame:28 """Régresseurs du modèle hédonique de référence (semi-log).29 mois = mois écoulés depuis REF_DATE (tendance linéaire du marché) ; secteur de référence : Hull ; type de référence : plain-pied."""30 X = pd.DataFrame(index=df.index)31 X["const"] = 1.032 X["ln_S"] = np.log(df["superficie_habitable_m2"] / 140.0)33 X["ln_T"] = np.log(df["superficie_terrain_m2"] / 560.0)34 X["age"] = df["age"].astype(float)35 X["age2_100"] = df["age"].astype(float) ** 2 / 100.036 X["qualite"] = df["qualite_1a5"].astype(float) - 3.037 X["garage"] = df["garage_places"].astype(float)38 X["sous_sol"] = df["sous_sol_fini"].astype(float)39 X["piscine"] = df["piscine"].astype(float)40 X["etages"] = (df["type"] == "à étages").astype(float)41 X["dist_centre"] = np.clip(df["dist_centre_km"].astype(float) - 3.0, 0, None)42 for sct in SECTEURS[1:]:43 X[f"sect_{sct}"] = (df["secteur"] == sct).astype(float)44 d = df["date_vente"] if "date_vente" in df else pd.Series([date_eval] * len(df), index=df.index)45 X["mois"] = ((pd.to_datetime(d) - REF_DATE).dt.days / 30.4375).astype(float)46 return X4748def hedonique(v: pd.DataFrame) -> dict:49 """MCO semi-log avec erreurs-types HC1 (traite.hedonique.mco)."""50 from . import hedonique as h51 X = matrice(v); y = np.log(v["prix_vente"].astype(float))52 r = h.mco(X.values, y.values)53 coef = pd.DataFrame({"coef": r["beta"], "se_hc1": r["se_hc1"]}, index=X.columns)54 coef["t"] = coef["coef"] / coef["se_hc1"]55 coef["effet_pct"] = np.exp(coef["coef"]) - 156 return {"coef": coef, "r2": r["r2"], "r2_ajuste": r["r2_ajuste"], "sigma": float(np.sqrt(r["sigma2"])), "n": r["n"], "k": r["k"],57 "residus": r["residus"], "X": X, "y": y.values, "beta": r["beta"], "V_hc1": r["V_hc1"], "croissance_mensuelle": float(np.exp(r["beta"][list(X.columns).index("mois")]) - 1)}5859def sujet_df(s: dict, date_eval: str | None = None) -> pd.DataFrame:60 d = dict(s); d["date_vente"] = pd.Timestamp(date_eval or s["date_evaluation"])61 return pd.DataFrame([d])6263def predire(modele: dict, df: pd.DataFrame, duan: bool = True) -> np.ndarray:64 """Prédiction en niveau ; correction de retransformation de Duan (smearing) si demandée."""65 X = matrice(df)[modele["X"].columns]66 lnp = X.values @ modele["beta"]67 facteur = float(np.mean(np.exp(modele["residus"]))) if duan else 1.068 return np.exp(lnp) * facteur6970def intervalle_prediction(modele: dict, df: pd.DataFrame, niveau: float = 0.95) -> pd.DataFrame:71 """Intervalle de prédiction (log-normal) : ln P ± z·sqrt(σ² + x'Vx), retransformé."""72 from scipy.stats import norm73 X = matrice(df)[modele["X"].columns].values74 lnp = X @ modele["beta"]75 var = modele["sigma"] ** 2 + np.einsum("ij,jk,ik->i", X, modele["V_hc1"], X)76 z = norm.ppf(0.5 + niveau / 2)77 return pd.DataFrame({"ln_pred": lnp, "bas": np.exp(lnp - z * np.sqrt(var)), "point": np.exp(lnp), "haut": np.exp(lnp + z * np.sqrt(var))})7879CARACTERISTIQUES_AJUSTEMENT = ["mois", "dist", "ln_S", "ln_T", "age", "qualite", "garage", "sous_sol", "piscine", "etages"]8081def ajustements_hedoniques(modele: dict, comps: pd.DataFrame, s: dict, date_eval: str | None = None) -> pd.DataFrame:82 """Ajustements monétaires de chaque comparable vers le sujet, dérivés du modèle hédonique :83 pour la caractéristique k, A_k = P_comp × [exp(β_k (x_s,k − x_c,k)) − 1] appliqué en cascade dans l'ordre déclaré84 (conditions du marché d'abord, puis localisation, puis caractéristiques physiques). Signe du point de vue du sujet."""85 sd = sujet_df(s, date_eval); Xs = matrice(sd).iloc[0]86 Xc = matrice(comps); beta = pd.Series(modele["beta"], index=modele["X"].columns)87 groupes = {"conditions_marche": ["mois"], "localisation": ["dist_centre"] + [f"sect_{x}" for x in SECTEURS[1:]],88 "superficie": ["ln_S"], "terrain": ["ln_T"], "age": ["age", "age2_100"], "qualite": ["qualite"],89 "garage": ["garage"], "sous_sol": ["sous_sol"], "piscine": ["piscine"], "type": ["etages"]}90 rows = []91 for i, c in comps.iterrows():92 base = float(c["prix_vente"]); row = {"id_vente": c["id_vente"], "prix_vente": base}93 for g, cols in groupes.items():94 dlog = float(sum(beta[col] * (Xs[col] - Xc.loc[i, col]) for col in cols))95 adj = base * (np.exp(dlog) - 1.0)96 row[g] = adj; base += adj97 row["prix_ajuste"] = base98 row["ajust_brut_pct"] = sum(abs(row[g]) for g in groupes) / row["prix_vente"]99 row["ajust_net_pct"] = (base - row["prix_vente"]) / row["prix_vente"]100 rows.append(row)101 return pd.DataFrame(rows)102103def tout() -> dict:104 d = charger(); sel = selection(); m = hedonique(d["ventes"])105 adj = ajustements_hedoniques(m, sel["comparables"], d["sujet"])106 pred = predire(m, sujet_df(d["sujet"]))[0]; ip = intervalle_prediction(m, sujet_df(d["sujet"])).iloc[0]107 return {**d, "selection": sel, "modele": m, "ajustements": adj, "prediction_hedonique": float(pred), "intervalle": ip}108