SPB Git
13.2 KB · 194 lines python
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
Files · 739a7ae283cf11e4f280ccc5836e47f331cb784a
Menu

Theme