spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 12 — Mesurage, superficies et plans : calculs du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES).34Imprime tous les chiffres cités dans le chapitre 12 :5 * plan coté du sujet (cottage à étages, empreinte ≈ 95 m², 165 m² habitables) : superficies selon six conventions6 (aire au sol et aire des étages du MEFQ, superficie habitable, nette, habitable avec sous-sol fini, type ANSI Z765) ;7 * conversions exactes m² ↔ pi² ;8 * prix unitaires implicites du sujet selon la convention (valeur hédonique traite.cas1.predire ≈ 637 953 $) ;9 * propagation d'une erreur de mesure sur la valeur : ∂V/∂S = β V/S, méthode delta et effet exact d'une erreur de ±5 % ;10 * biais d'un mélange de conventions (sujet mesuré brut, comparables mesurés habitables) ;11 * erreur sur la superficie d'un comparable et variance combinée des erreurs ;12 * atténuation du coefficient hédonique de la superficie par une erreur de mesure (simulation, graine 20261031).1314Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch12_mesurage.py15Le modèle hédonique provient de analysis/python/traite/cas1.py (aucune réécriture)."""16from __future__ import annotations17import sys, pathlib, math18import numpy as np, pandas as pd19sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))20from traite import cas12122ROOT = pathlib.Path(__file__).resolve().parents[3]23PI2_PAR_M2 = 10.7639 # 1 m² = 10,7639 pi² (convention de l'ouvrage, annexe des conventions)24M2_PAR_PI2 = 0.09290304 # 1 pi² = 0,09290304 m² exactement (1 pi = 0,3048 m)25GRAINE = 202610312627# ----------------------------------------------------------------------------- plan coté du sujet (hypothèses déclarées)28PLAN = {29 "long_ext_m": 12.00, # longueur hors tout (face externe des murs), ≈ 39 pi 4 po30 "larg_ext_m": 7.92, # largeur hors tout, ≈ 26 pi31 "epaisseur_mur_m": 0.28, # épaisseur moyenne des murs extérieurs (ossature 2×6, isolant, gypse, revêtement mixte)32 "tremie_m2": 1.00 * 3.40, # ouverture de la cage d'escalier à l'étage (1,00 × 3,40 m)33 "cloisons_m_lin": 44.0, # longueur cumulée des cloisons intérieures (RDC 18 m + étage 26 m)34 "epaisseur_cloison_m": 0.115,35 "n_etages": 2,36 "sous_sol_fini_m2": 70.0, # cohérent avec le poste « Planchers flottants (sous-sol) : 70 m² » du devis du cas 137 "sous_sol_plancher_sous_sol_m": 1.90, # plancher du sous-sol à 1,90 m sous le niveau moyen du sol (> 0,4 m : pas un étage MEFQ)38 "garage_long_m": 6.10, "garage_larg_m": 3.66, # garage simple attaché (20 pi × 12 pi), hors empreinte principale39 "lot_facade_m": 15.24, "lot_prof_m": 40.68, # lot rectangulaire 50 pi × 133,5 pi ≈ 620 m²40 "marge_avant_m": 7.50, "marge_lat_gauche_m": 2.00, "marge_lat_droite_m": 1.66, # 15,24 − 7,92 − 3,66 − 2,0041}424344def superficies(plan: dict = PLAN) -> dict:45 """Superficies du sujet selon les conventions comparées au tableau 12.3."""46 L, W, t = plan["long_ext_m"], plan["larg_ext_m"], plan["epaisseur_mur_m"]47 aire_sol = L * W # MEFQ R0701A : projection au sol (tous étages et sous-sol : même empreinte)48 perim_sol = 2 * (L + W) # MEFQ R0701B49 aire_etages = plan["n_etages"] * aire_sol # MEFQ R0702A : somme des aires brutes des étages (sous-sol exclu)50 perim_etages = plan["n_etages"] * perim_sol # MEFQ R0702B51 aire_int_niveau = (L - 2 * t) * (W - 2 * t) # intérieur des murs extérieurs, un niveau52 habitable = plan["n_etages"] * aire_int_niveau - plan["tremie_m2"] # convention « habitable » de l'ouvrage (hors sous-sol, hors trémie)53 cloisons = plan["cloisons_m_lin"] * plan["epaisseur_cloison_m"]54 nette = habitable - cloisons # surface utile : cloisons déduites55 habitable_ss = habitable + plan["sous_sol_fini_m2"] # « superficie habitable totale » de certaines fiches (sous-sol fini inclus)56 ansi_hors_sol = aire_etages # type ANSI Z765 : face externe, niveaux entièrement hors sol ; trémie remplacée par les marches57 garage = plan["garage_long_m"] * plan["garage_larg_m"]58 lot = plan["lot_facade_m"] * plan["lot_prof_m"]59 ratio_murs = aire_int_niveau / aire_sol # = 1 − tP/A + 4t²/A60 return {"aire_sol_m2": aire_sol, "perimetre_sol_m": perim_sol, "aire_etages_m2": aire_etages, "perimetre_etages_m": perim_etages,61 "aire_int_niveau_m2": aire_int_niveau, "habitable_m2": habitable, "cloisons_m2": cloisons, "nette_m2": nette,62 "habitable_sous_sol_m2": habitable_ss, "ansi_hors_sol_m2": ansi_hors_sol, "ansi_sous_sol_m2": plan["sous_sol_fini_m2"],63 "garage_m2": garage, "lot_m2": lot, "ratio_int_ext": ratio_murs,64 "ratio_murs_formule": 1 - t * perim_sol / aire_sol + 4 * t ** 2 / aire_sol}656667CONVENTIONS = [("Aire au sol (MEFQ R0701A)", "aire_sol_m2"), ("Superficie nette (utile)", "nette_m2"),68 ("Superficie habitable (convention de l'ouvrage)", "habitable_m2"),69 ("Aire des étages, brute (MEFQ R0702A)", "aire_etages_m2"),70 ("Habitable + sous-sol fini (fiches de courtage)", "habitable_sous_sol_m2")]717273def modele() -> dict:74 d = cas1.charger(); m = cas1.hedonique(d["ventes"])75 V = float(cas1.predire(m, cas1.sujet_df(d["sujet"]))[0])76 beta = float(m["coef"].loc["ln_S", "coef"]); se = float(m["coef"].loc["ln_S", "se_hc1"])77 return {"data": d, "modele": m, "V": V, "beta": beta, "se_beta": se, "S": float(d["sujet"]["superficie_habitable_m2"])}787980def prix_unitaires(V: float, sup: dict) -> pd.DataFrame:81 rows = []82 for nom, cle in CONVENTIONS:83 s = sup[cle]84 rows.append({"convention": nom, "m2": s, "pi2": s * PI2_PAR_M2, "dollars_m2": V / s, "dollars_pi2": V / (s * PI2_PAR_M2)})85 return pd.DataFrame(rows)868788def propagation(V: float, S: float, beta: float, erreurs=(-0.10, -0.05, -0.02, 0.02, 0.05, 0.10)) -> pd.DataFrame:89 """Effet d'une erreur relative e sur S : approximation linéaire β e et effet exact (1+e)^β − 1 ; unitaire naïf (β = 1)."""90 dVdS = beta * V / S91 rows = []92 for e in erreurs:93 rows.append({"erreur_S_pct": 100 * e, "dS_m2": e * S, "lineaire_pct": 100 * beta * e, "lineaire_dollars": dVdS * e * S,94 "exact_pct": 100 * ((1 + e) ** beta - 1), "exact_dollars": V * ((1 + e) ** beta - 1),95 "naif_dollars": (V / S) * e * S})96 return pd.DataFrame(rows), dVdS979899def melange_conventions(V: float, beta: float, s_hab: float, s_brute: float) -> dict:100 """Sujet mesuré selon une convention, modèle (ou comparables) selon une autre : Δ ln V = β ln(S_1/S_2)."""101 d1 = beta * math.log(s_brute / s_hab) # sujet brut, référence habitable → surévaluation102 d2 = beta * math.log(s_hab / s_brute) # sujet habitable, référence brute → sous-évaluation103 return {"ratio": s_brute / s_hab, "ecart_pct": 100 * (s_brute / s_hab - 1), "sujet_brut_pct": 100 * (math.exp(d1) - 1),104 "sujet_brut_dollars": V * (math.exp(d1) - 1), "sujet_hab_pct": 100 * (math.exp(d2) - 1), "sujet_hab_dollars": V * (math.exp(d2) - 1)}105106107def erreur_comparable(P_c: float, S_c: float, S_s: float, beta: float, S_c_obs: float) -> dict:108 """Ajustement de superficie A_S = P_c[(S_s/S_c)^β − 1] avec la vraie superficie et avec une superficie erronée du comparable."""109 A_vrai = P_c * ((S_s / S_c) ** beta - 1); A_obs = P_c * ((S_s / S_c_obs) ** beta - 1)110 return {"A_vrai": A_vrai, "A_obs": A_obs, "erreur": A_obs - A_vrai, "erreur_approx": -beta * P_c * (S_c_obs - S_c) / S_c}111112113def variance_combinee(V: float, beta: float, sigma_s: float, sigma_c: float, n: int) -> dict:114 """Écart-type relatif de l'indication par comparaison : β sqrt(σ_s² + σ_c²/n) (erreurs relatives indépendantes, poids égaux)."""115 rel = beta * math.sqrt(sigma_s ** 2 + sigma_c ** 2 / n)116 return {"relatif": rel, "dollars": rel * V, "part_sujet": sigma_s ** 2 / (sigma_s ** 2 + sigma_c ** 2 / n)}117118119def attenuation(m: dict, d: dict, sigmas=(0.0, 0.02, 0.05, 0.10, 0.15, 0.20), rep: int = 200, graine: int = GRAINE) -> pd.DataFrame:120 """Erreur de mesure classique sur ln S (u ~ N(0, σ_u²)) : réestimation de β par MCO (traite.hedonique.mco) et prédiction du sujet.121 λ théorique = Var(ẽ)/(Var(ẽ) + σ_u²), ẽ = résidu de ln S sur les autres régresseurs."""122 from traite import hedonique as h123 v = d["ventes"].copy(); X = cas1.matrice(v); y = np.log(v["prix_vente"].astype(float)).values124 cols = list(X.columns); j = cols.index("ln_S")125 Xo = X.drop(columns="ln_S").values126 lnS = X["ln_S"].values127 e = lnS - Xo @ np.linalg.lstsq(Xo, lnS, rcond=None)[0]128 var_e = float(np.var(e, ddof=0)); var_lnS = float(np.var(lnS, ddof=0))129 rng = np.random.default_rng(graine)130 s_sujet = cas1.sujet_df(d["sujet"]); Xs = cas1.matrice(s_sujet)[cols].values131 petite = dict(d["sujet"]); petite["superficie_habitable_m2"] = 120; Xp = cas1.matrice(cas1.sujet_df(petite))[cols].values132 rows = []133 for s in sigmas:134 b, Vs, Vp = [], [], []135 for _ in range(rep if s > 0 else 1):136 Xn = X.values.copy(); Xn[:, j] = lnS + rng.normal(0, s, len(lnS))137 r = h.mco(Xn, y); b.append(r["beta"][j])138 fac = float(np.mean(np.exp(r["residus"])))139 Vs.append(float(np.exp(Xs @ r["beta"])[0]) * fac); Vp.append(float(np.exp(Xp @ r["beta"])[0]) * fac)140 lam = var_e / (var_e + s ** 2)141 rows.append({"sigma_u": s, "lambda_theorique": lam, "beta_attendu": lam * m["beta"][j], "beta_moyen": float(np.mean(b)),142 "beta_ecart_type": float(np.std(b, ddof=1)) if len(b) > 1 else 0.0,143 "V_sujet_165": float(np.mean(Vs)), "V_maison_120": float(np.mean(Vp))})144 out = pd.DataFrame(rows)145 out.attrs["var_e"] = var_e; out.attrs["var_lnS"] = var_lnS; out.attrs["sd_lnS"] = math.sqrt(var_lnS)146 return out147148149def tout() -> dict:150 sup = superficies(); M = modele()151 pu = prix_unitaires(M["V"], sup)152 prop, dVdS = propagation(M["V"], M["S"], M["beta"])153 mel = melange_conventions(M["V"], M["beta"], sup["habitable_m2"], sup["aire_etages_m2"])154 sel = cas1.selection(); comps = sel["comparables"]155 c = comps[comps["id_vente"] == "G0404"].iloc[0]156 ec = erreur_comparable(float(c["prix_vente"]), float(c["superficie_habitable_m2"]), M["S"], M["beta"], float(c["superficie_habitable_m2"]) + 10)157 vc = {k: variance_combinee(M["V"], M["beta"], 0.02, 0.02, 5) for k in ["base"]}158 vc["sujet_seul"] = variance_combinee(M["V"], M["beta"], 0.02, 0.0, 5)159 vc["comparables_seuls"] = variance_combinee(M["V"], M["beta"], 0.0, 0.02, 5)160 vc["cinq_pct"] = variance_combinee(M["V"], M["beta"], 0.05, 0.02, 5)161 att = attenuation(M["modele"], M["data"])162 return {"sup": sup, "M": M, "pu": pu, "prop": prop, "dVdS": dVdS, "mel": mel, "comp": c, "ec": ec, "vc": vc, "att": att}163164165def main():166 r = tout(); sup, M = r["sup"], r["M"]167 print("=== Plan coté du sujet (hypothèses déclarées) ===")168 for k, v in PLAN.items(): print(f" {k:32s} {v}")169 print("\n=== Superficies du sujet (m²) ===")170 for k, v in sup.items(): print(f" {k:28s} {v:10.4f}" if isinstance(v, float) else f" {k:28s} {v}")171 print(f" (vérif. ratio murs : 1 − tP/A + 4t²/A = {sup['ratio_murs_formule']:.4f} ; direct {sup['ratio_int_ext']:.4f})")172 print(f"\n=== Conversions : 1 m² = {PI2_PAR_M2} pi² ; 1 pi² = {M2_PAR_PI2} m² ; produit = {PI2_PAR_M2 * M2_PAR_PI2:.7f}")173 print(f" habitable {sup['habitable_m2']:.2f} m² = {sup['habitable_m2']*PI2_PAR_M2:.1f} pi² ; aire des étages {sup['aire_etages_m2']:.2f} m² = {sup['aire_etages_m2']*PI2_PAR_M2:.1f} pi²")174 print(f"\n=== Modèle hédonique (traite.cas1) : β_S = {M['beta']:.5f} (ET HC1 {M['se_beta']:.5f}) ; V sujet (Duan) = {M['V']:,.2f} $ ; S = {M['S']} m²")175 print("\n=== Prix unitaires implicites du sujet selon la convention ===")176 print(r["pu"].to_string(index=False, float_format=lambda x: f"{x:,.2f}"))177 print(f"\n=== Propagation : ∂V/∂S = β V / S = {r['dVdS']:,.2f} $/m² ; prix unitaire naïf V/S = {M['V']/M['S']:,.2f} $/m² ; rapport = β = {r['dVdS']/(M['V']/M['S']):.4f}")178 print(f" σ_S = 5 % = {0.05*M['S']:.2f} m² → σ_V ≈ {r['dVdS']*0.05*M['S']:,.2f} $ ({100*M['beta']*0.05:.3f} % de V)")179 print(r["prop"].to_string(index=False, float_format=lambda x: f"{x:,.3f}"))180 print("\n=== Mélange de conventions (habitable vs aire brute des étages) ===")181 for k, v in r["mel"].items(): print(f" {k:22s} {v:,.4f}")182 c = r["comp"]; ec = r["ec"]183 print(f"\n=== Erreur sur la superficie d'un comparable : {c['id_vente']} P = {c['prix_vente']:,.0f} $, S = {c['superficie_habitable_m2']} m² (sujet {M['S']} m²), S observée +10 m² ===")184 for k, v in ec.items(): print(f" {k:14s} {v:,.2f} $")185 print("\n=== Variance combinée (erreurs relatives indépendantes, 5 comparables, poids égaux) ===")186 for k, v in r["vc"].items(): print(f" {k:18s} relatif {100*v['relatif']:.3f} % ; {v['dollars']:,.0f} $ ; part du sujet {100*v['part_sujet']:.1f} %")187 att = r["att"]188 print(f"\n=== Atténuation (erreur de mesure sur ln S ; Var(ẽ) = {att.attrs['var_e']:.5f}, sd(ln S) = {att.attrs['sd_lnS']:.4f}) ===")189 print(att.to_string(index=False, float_format=lambda x: f"{x:,.4f}"))190191192if __name__ == "__main__":193 main()194