SPB Git
28.7 KB · 383 lines python
1#!/usr/bin/env python32"""Chapitre 44 — rénovation et efficacité énergétique : analyse coût-bénéfice de deux mesures sur le sujet du cas 13(cottage de Hull, Gatineau, DONNÉES SYNTHÉTIQUES) et lectures de la contribution à la valeur.45Imprime tous les chiffres cités dans le chapitre 44 :6  * facture d'électricité du sujet au tarif D d'Hydro-Québec en vigueur le 1er avril 2026 (structure vérifiée :7    46,154 ¢/jour ; 7,065 ¢/kWh jusqu'à 40 kWh/jour ; 11,142 ¢/kWh au-delà) pour une consommation DÉCLARÉE8    (hypothèse pédagogique) ; prix moyen et prix marginal du kilowattheure ;9  * deux mesures : (A) remplacement de la thermopompe centrale de 2016 par un appareil ENERGY STAR climat froid10    (subvention LogisVert 120 $ par 1 000 BTU/h, plafond 6 700 $) ; (B) isolation de l'entretoit (Rénoclimat, plafond11    « toit » 2 000 $ pour une maison) ; et leur combinaison (économies non additives) ;12  * flux annuels, VAN, TRI, délai de récupération simple et actualisé, annuité équivalente, coût de l'énergie13    économisée (traite.finance : van, tri, delai_recuperation, facteur_recouvrement) ;14  * sensibilité : grille VAN × (taux d'actualisation, croissance du prix de l'électricité), durée de vie, économie,15    coût, subvention ; seuils d'équilibre ;16  * lectures de la contribution à la valeur : coût, coût net, valeur actuelle des économies, prime verte de la17    littérature appliquée à la prédiction hédonique du cas 1 (traite.cas1.predire) ;18  * paires de ventes « qualité 4 vs qualité 3 » dans les 600 ventes du cas 1 (traite.comparables.ventes_appariees),19    comme indicateur de la contribution d'une rénovation non énergétique (cuisine, salles de bain) ;20  * composantes du sujet en fin de vie (fiche d'état du ch. 11 : analysis/python/cases/ch11_inspection.py) ;21  * chiffres des exercices.2223Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch44_renovation.py24Toutes les formules financières proviennent de analysis/python/traite/finance.py (aucune réécriture)."""25from __future__ import annotations26import sys, pathlib, math27import numpy as np, pandas as pd28sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))29sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))30from traite import finance as fin, cas131from traite import comparables as cp3233ROOT = pathlib.Path(__file__).resolve().parents[3]3435# --- Tarif D d'Hydro-Québec, en vigueur le 1er avril 2026 (hydrotarifs2026, section 2.5 ; vérifié le 2026-10-31) --------36FRAIS_ACCES_JOUR = 0.46154      # $ par jour37PRIX_TRANCHE_1 = 0.07065        # $/kWh jusqu'à 40 kWh par jour38SEUIL_KWH_JOUR = 40.0           # kWh par jour39PRIX_TRANCHE_2 = 0.11142        # $/kWh au-delà40JOURS = 3654142# --- Consommation du sujet : HYPOTHÈSE PÉDAGOGIQUE (aucune facture réelle ; Hydro-Québec ne publie pas de facture43#     moyenne par type de maison — notes-E) --------------------------------------------------------------------------44CONSO_KWH = 22000.0             # kWh/an, maison tout électrique de 165 m² (1998) avec thermopompe centrale de 201645PART_CHAUFFAGE = 0.50           # part du chauffage des locaux dans la consommation annuelle46PART_EAU_CHAUDE = 0.2047PART_AUTRES = 0.304849# --- Paramètres financiers ----------------------------------------------------------------------------------------50TAUX_ACTUALISATION = 0.0458     # taux effectif des fonds avancés, prêts non assurés à taux fixe ≥ 5 ans, août 2026 (BdC, groupe A4)51CROISSANCE_ENERGIE = 0.030      # hausse domestique 3,0 % en 2026 et 2027 (Régie, D-2026-033) ; maintenue par hypothèse au-delà52VIE_RESTANTE_BATIMENT = 31      # ans (ch. 11 et 24 : vie économique 60 ans, âge effectif 29 ans)5354# --- Mesure A : thermopompe centrale ENERGY STAR climat froid -------------------------------------------------------55TP_CAPACITE_BTU = 36000         # BTU/h à −8 °C (3 tonnes ; hypothèse cohérente avec 165 m²)56TP_COUT_STANDARD = 10481.32 * 1.33043   # poste « Thermopompe centrale et plinthes » du devis du cas 1, majoré (ch. 21)57TP_COUT = 16500.0               # coût installé d'un appareil climat froid (hypothèse pédagogique, ≈ +18 % sur l'appareil standard)58TP_LOGISVERT_STANDARD = 50.0    # $ par 1 000 BTU/h, appareil ENERGY STAR59TP_LOGISVERT_FROID = 120.0      # $ par 1 000 BTU/h, appareil climat froid60TP_LOGISVERT_MAX = 6700.061TP_ECONOMIE_CHAUFFAGE = 0.30    # fraction de l'énergie de chauffage économisée par rapport à l'appareil de 2016 (hypothèse)62TP_VIE = 15                     # ans (fiche d'état du ch. 11 : vie utile 15 ans)63TP_ECONOMIE_INCR = 0.20         # fraction du chauffage économisée par l'appareil climat froid PAR RAPPORT à un appareil standard neuf (hypothèse)6465# --- Mesure B : isolation de l'entretoit ---------------------------------------------------------------------------66ISO_SURFACE_M2 = 95.0           # empreinte du bâtiment (ch. 22)67ISO_COUT_M2 = 44.0              # $/m² installé, cellulose soufflée R-20 → R-60 (hypothèse pédagogique)68ISO_COUT = ISO_SURFACE_M2 * ISO_COUT_M269ISO_RENOCLIMAT_MAX = 2000.0     # plafond « toit », maison (Rénoclimat refondu le 1er octobre 2026)70ISO_SUBVENTION = 2000.0         # hypothèse : travaux admissibles au plafond71ISO_ECONOMIE_CHAUFFAGE = 0.08   # fraction de l'énergie de chauffage économisée (hypothèse)72ISO_VIE = VIE_RESTANTE_BATIMENT # composante de longue durée : horizon = vie économique restante du bâtiment7374# --- Prime verte : résultats publiés (vérifiés le 2026-10-31, voir ch44-claims.csv) ---------------------------------75PRIME_VERTE = [76    # (clé, pays/marché, mesure, bas %, haut %, point %, type)77    ("brounenKok2011", "Pays-Bas, 2008-2009", "étiquette A, B ou C vs D-G", 3.6, 3.7, 3.7, "prix"),78    ("brounenKok2011", "Pays-Bas, 2008-2009", "étiquette A vs D ; G vs D", -5.0, 10.2, None, "prix"),79    ("kahnKok2014", "Californie, 2007-2012", "maison étiquetée verte vs non étiquetée", 4.5, 5.0, 5.0, "prix"),80    ("hylandLyonsLyons2013", "Irlande, 2008-2012", "cote A vs D (prix) ; 1 échelon BER", 1.3, 9.3, 9.3, "prix"),81    ("hylandLyonsLyons2013", "Irlande, 2008-2012", "cote A vs D (loyers)", 0.5, 1.8, 1.8, "loyer"),82    ("fuerstMcallisterNandaWyatt2015", "Angleterre, 1995-2012", "EPC A/B vs D ; C vs D ; G vs D", -6.0, 5.0, None, "prix"),83    ("aydinBrounenKok2020", "Pays-Bas", "+10 % d'efficacité énergétique mesurée", 2.2, 2.2, 2.2, "prix"),84    ("cespedeslopez2019", "Méta-analyse, 66 documents", "prix avec certificat : monde ; Europe ; Amérique", 2.32, 5.36, 4.20, "prix"),85    ("eichholtzKokQuigley2010", "É.-U., bureaux", "immeuble certifié : loyer ; loyer effectif ; prix", 3.0, 16.0, None, "bureaux"),86]87HYPOTHESE_COUT_CUISINE_SDB = 65000.0   # coût d'une rénovation complète cuisine + 2 salles de bain (hypothèse pédagogique)888990# ------------------------------------------------------------------------------------------------------- tarif D91def facture_tarif_d(kwh: float, jours: int = JOURS, indice: float = 1.0) -> dict:92    """Facture annuelle au tarif D (hors taxes) : frais d'accès + énergie en deux tranches ; `indice` multiplie les prix."""93    seuil = SEUIL_KWH_JOUR * jours94    t1 = min(kwh, seuil); t2 = max(kwh - seuil, 0.0)95    acces = FRAIS_ACCES_JOUR * jours * indice96    e1 = t1 * PRIX_TRANCHE_1 * indice; e2 = t2 * PRIX_TRANCHE_2 * indice97    total = acces + e1 + e298    return {"kwh": kwh, "acces": acces, "tranche1_kwh": t1, "tranche2_kwh": t2, "energie_t1": e1, "energie_t2": e2,99            "total": total, "prix_moyen": total / kwh if kwh else float("nan"),100            "prix_marginal": (PRIX_TRANCHE_2 if kwh > seuil else PRIX_TRANCHE_1) * indice}101102103def economie_annuelle(kwh_avant: float, kwh_apres: float, indice: float = 1.0) -> float:104    """Économie exacte = différence de factures (tient compte d'un éventuel franchissement du seuil)."""105    return facture_tarif_d(kwh_avant, indice=indice)["total"] - facture_tarif_d(kwh_apres, indice=indice)["total"]106107108# ------------------------------------------------------------------------------------------------------- mesures109def mesures() -> dict:110    chauffage = CONSO_KWH * PART_CHAUFFAGE111    eco_tp = chauffage * TP_ECONOMIE_CHAUFFAGE112    eco_iso = chauffage * ISO_ECONOMIE_CHAUFFAGE113    # combinaison : l'isolation réduit d'abord la charge ; la thermopompe économise une fraction du reste114    eco_combi = eco_iso + (chauffage - eco_iso) * TP_ECONOMIE_CHAUFFAGE115    sub_tp = min(TP_LOGISVERT_FROID * TP_CAPACITE_BTU / 1000.0, TP_LOGISVERT_MAX)116    sub_tp_std = min(TP_LOGISVERT_STANDARD * TP_CAPACITE_BTU / 1000.0, TP_LOGISVERT_MAX)117    return {118        "A": {"nom": "Thermopompe climat froid", "cout": TP_COUT, "subvention": sub_tp, "kwh": eco_tp, "vie": TP_VIE},119        "B": {"nom": "Isolation de l'entretoit", "cout": ISO_COUT, "subvention": ISO_SUBVENTION, "kwh": eco_iso, "vie": ISO_VIE},120        "AB": {"nom": "Les deux mesures", "cout": TP_COUT + ISO_COUT, "subvention": sub_tp + ISO_SUBVENTION, "kwh": eco_combi,121               "vie": TP_VIE},   # horizon commun = vie de la thermopompe (l'isolation continue au-delà : voir texte)122        # décision incrémentale à la fin de vie de l'appareil de 2016 : climat froid plutôt que standard123        "A_incr": {"nom": "Climat froid plutôt que standard (fin de vie)", "cout": TP_COUT - TP_COUT_STANDARD,124                   "subvention": sub_tp - sub_tp_std, "kwh": chauffage * TP_ECONOMIE_INCR, "vie": TP_VIE},125        "chauffage_kwh": chauffage, "eco_additive_kwh": eco_tp + eco_iso,126        "subvention_tp_standard": sub_tp_std, "cout_tp_standard": TP_COUT_STANDARD,127        "surcout_climat_froid": TP_COUT - TP_COUT_STANDARD, "surcout_net_climat_froid": (TP_COUT - TP_COUT_STANDARD) - (sub_tp - sub_tp_std),128    }129130131def flux_mesure(m: dict, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE, vie: int | None = None,132                kwh_avant: float = CONSO_KWH, subvention: float | None = None, cout: float | None = None,133                kwh: float | None = None) -> dict:134    """Flux : t = 0 : −(coût − subvention) ; t = 1..n : économie_t = [F(kWh avant) − F(kWh après)] × (1+g)^t.135    Indicateurs : VAN, TRI, délai simple et actualisé, annuité équivalente, coût de l'énergie économisée."""136    n = int(vie if vie is not None else m["vie"]); sub = m["subvention"] if subvention is None else subvention137    K = m["cout"] if cout is None else cout; dk = m["kwh"] if kwh is None else kwh138    eco0 = economie_annuelle(kwh_avant, kwh_avant - dk)139    flux = [-(K - sub)] + [eco0 * (1.0 + g) ** t for t in range(1, n + 1)]140    van = fin.van(flux, r)141    try:142        t_ri = fin.tri(flux)143    except ValueError:144        t_ri = float("nan")145    actualises = [cf / (1.0 + r) ** t for t, cf in enumerate(flux)]146    crf = fin.facteur_recouvrement(r, n)147    va_eco = sum(actualises[1:])148    return {"flux": flux, "actualises": actualises, "cumul_actualise": np.cumsum(actualises).tolist(),149            "economie_an1": flux[1], "economie_courante": eco0, "cout_net": K - sub, "van": van, "tri": t_ri,150            "delai_simple": fin.delai_recuperation(flux), "delai_actualise": fin.delai_recuperation(actualises),151            "va_economies": va_eco, "indice_rentabilite": va_eco / (K - sub) if K > sub else float("inf"),152            "annuite_cout_net": (K - sub) * crf, "annuite_cout_brut": K * crf,153            "cee_net": (K - sub) * crf / dk, "cee_brut": K * crf / dk,     # coût de l'énergie économisée, $/kWh154            "n": n, "r": r, "g": g}155156157def seuils(m: dict, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE) -> dict:158    """Valeurs d'équilibre (VAN = 0) : croissance du prix de l'énergie, coût net, économie en kWh, taux (= TRI)."""159    f0 = flux_mesure(m, r=r, g=g)160    cout_net_eq = f0["va_economies"]161    # économie en kWh d'équilibre : VAN(kwh) = 0, recherche par bissection (économie linéaire si pas de changement de tranche)162    lo, hi = 0.0, CONSO_KWH * 0.9163    for _ in range(200):164        mid = 0.5 * (lo + hi)165        if flux_mesure(m, r=r, g=g, kwh=mid)["van"] < 0: lo = mid166        else: hi = mid167    kwh_eq = 0.5 * (lo + hi)168    # croissance d'équilibre169    glo, ghi = -0.5, 0.5170    for _ in range(200):171        gm = 0.5 * (glo + ghi)172        if flux_mesure(m, r=r, g=gm)["van"] < 0: glo = gm173        else: ghi = gm174    g_eq = 0.5 * (glo + ghi)175    return {"cout_net_equilibre": cout_net_eq, "subvention_equilibre": m["cout"] - cout_net_eq, "kwh_equilibre": kwh_eq,176            "fraction_chauffage_equilibre": kwh_eq / (CONSO_KWH * PART_CHAUFFAGE), "g_equilibre": g_eq, "tri": f0["tri"]}177178179def grille_van(m: dict, taux=(0.02, 0.03, 0.04, 0.0458, 0.05, 0.06, 0.07, 0.08, 0.10), croissances=(0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06)) -> pd.DataFrame:180    return pd.DataFrame([[flux_mesure(m, r=r, g=g)["van"] for g in croissances] for r in taux], index=list(taux), columns=list(croissances))181182183def sensibilite_tornade(m: dict) -> list[dict]:184    """Variation de la VAN de la mesure A pour des chocs déclarés sur chaque paramètre (toutes choses égales par ailleurs)."""185    base = flux_mesure(m)["van"]186    out = []187    def add(nom, bas, haut): out.append({"param": nom, "base": base, "bas": bas, "haut": haut})188    add("Économie (20 % / 40 % du chauffage)", flux_mesure(m, kwh=CONSO_KWH * PART_CHAUFFAGE * 0.20)["van"], flux_mesure(m, kwh=CONSO_KWH * PART_CHAUFFAGE * 0.40)["van"])189    add("Taux d'actualisation (8 % / 2 %)", flux_mesure(m, r=0.08)["van"], flux_mesure(m, r=0.02)["van"])190    add("Croissance du prix (0 % / 6 %)", flux_mesure(m, g=0.0)["van"], flux_mesure(m, g=0.06)["van"])191    add("Durée de vie (12 / 20 ans)", flux_mesure(m, vie=12)["van"], flux_mesure(m, vie=20)["van"])192    add("Coût installé (+20 % / −20 %)", flux_mesure(m, cout=m["cout"] * 1.2)["van"], flux_mesure(m, cout=m["cout"] * 0.8)["van"])193    add("Subvention (0 $ / plafond 6 700 $)", flux_mesure(m, subvention=0.0)["van"], flux_mesure(m, subvention=TP_LOGISVERT_MAX)["van"])194    add("Consommation (16 000 / 28 000 kWh)", flux_mesure(m, kwh_avant=16000.0, kwh=16000.0 * PART_CHAUFFAGE * TP_ECONOMIE_CHAUFFAGE)["van"],195        flux_mesure(m, kwh_avant=28000.0, kwh=28000.0 * PART_CHAUFFAGE * TP_ECONOMIE_CHAUFFAGE)["van"])196    return out197198199def sensibilite_duree(m: dict, vies=(10, 12, 15, 18, 20, 25), taux=(0.02, 0.0458, 0.07, 0.10)) -> pd.DataFrame:200    return pd.DataFrame([[flux_mesure(m, r=r, vie=v)["van"] for r in taux] for v in vies], index=list(vies), columns=list(taux))201202203# -------------------------------------------------------------------------------------------- contribution à la valeur204def lectures_contribution(m: dict, f: dict, valeur_sujet: float) -> dict:205    """Quatre lectures de ce que la mesure « vaut » pour le bien : coût, coût net, VA des économies (acheteur rationnel),206    prime verte de la littérature appliquée à la valeur du sujet (bornes)."""207    primes = {"Europe (méta-analyse)": 0.0232, "Monde (méta-analyse)": 0.0420, "Amérique du Nord (méta-analyse)": 0.0536,208              "Pays-Bas, étiquette verte": 0.037, "Angleterre, EPC C vs D": 0.018, "Californie, étiquette verte": 0.05}209    return {"cout": m["cout"], "cout_net": f["cout_net"], "va_economies": f["va_economies"], "van": f["van"],210            "ratio_va_cout": f["va_economies"] / m["cout"], "ratio_va_cout_net": f["va_economies"] / f["cout_net"],211            "primes_dollars": {k: v * valeur_sujet for k, v in primes.items()}, "primes_pct": primes, "valeur_sujet": valeur_sujet}212213214def va_economies_restantes(m: dict, annees_ecoulees: int, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE) -> float:215    """Valeur actuelle, à la date d'une revente future, des économies restantes (ce qu'un acheteur rationnel paierait au plus)."""216    f = flux_mesure(m, r=r, g=g)217    rest = f["flux"][1 + annees_ecoulees:]218    return sum(cf / (1.0 + r) ** (t + 1) for t, cf in enumerate(rest))219220221# -------------------------------------------------------------------------------------------- paires « qualité »222def paires_qualite(v: pd.DataFrame, modele: dict, q_haut: int = 4, q_bas: int = 3, tol_s: float = 25.0, tol_age: int = 10,223                   tol_mois: float = 18.0, tol_dist_km: float = 5.0, date_eval: str = "2026-09-01") -> dict:224    """Paires de ventes du cas 1 ne différant (à des tolérances déclarées près) que par la note de qualité (q_haut vs q_bas) :225    même secteur, même type, |ΔS| ≤ tol_s m², |Δâge| ≤ tol_age, mêmes garage / sous-sol / piscine, |Δdate| ≤ tol_mois,226    distance ≤ tol_dist_km. Le prix du membre « bas » est ramené à la date du membre « haut » par la tendance du modèle227    hédonique (croissance mensuelle composée). Chaque vente n'entre que dans une paire (appariement glouton par distance)."""228    g_m = modele["croissance_mensuelle"]229    hauts = v[v["qualite_1a5"] == q_haut].copy(); bas = v[v["qualite_1a5"] == q_bas].copy()230    coords = lambda df: np.radians(df[["lat", "lon"]].values)231    utilises = set(); rows = []232    for _, h in hauts.iterrows():233        cand = bas[(bas["secteur"] == h["secteur"]) & (bas["type"] == h["type"]) & (~bas["id_vente"].isin(utilises))234                   & ((bas["superficie_habitable_m2"] - h["superficie_habitable_m2"]).abs() <= tol_s)235                   & ((bas["age"] - h["age"]).abs() <= tol_age) & (bas["garage_places"] == h["garage_places"])236                   & (bas["sous_sol_fini"] == h["sous_sol_fini"]) & (bas["piscine"] == h["piscine"])]237        if cand.empty: continue238        dmois = ((cand["date_vente"] - h["date_vente"]).dt.days / 30.4375)239        cand = cand[dmois.abs() <= tol_mois]240        if cand.empty: continue241        # distance haversine approximative (km)242        lat1, lon1 = math.radians(h["lat"]), math.radians(h["lon"])243        lat2 = np.radians(cand["lat"].values); lon2 = np.radians(cand["lon"].values)244        a = np.sin((lat2 - lat1) / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin((lon2 - lon1) / 2) ** 2245        dist = 6371.0 * 2 * np.arcsin(np.sqrt(a))246        cand = cand.assign(dist_km=dist)247        cand = cand[cand["dist_km"] <= tol_dist_km]248        if cand.empty: continue249        # score de ressemblance : superficie, âge, date, distance normalisés250        score = ((cand["superficie_habitable_m2"] - h["superficie_habitable_m2"]).abs() / tol_s + (cand["age"] - h["age"]).abs() / tol_age251                 + ((cand["date_vente"] - h["date_vente"]).dt.days / 30.4375).abs() / tol_mois + cand["dist_km"] / tol_dist_km)252        b = cand.loc[score.idxmin()]253        utilises.add(b["id_vente"])254        mois = (h["date_vente"] - b["date_vente"]).days / 30.4375255        prix_bas_aj = b["prix_vente"] * (1.0 + g_m) ** mois     # ramené à la date de la vente « haute »256        rows.append({"id_haut": h["id_vente"], "id_bas": b["id_vente"], "secteur": h["secteur"], "prix_haut": h["prix_vente"],257                     "prix_bas": b["prix_vente"], "prix_bas_ajuste": prix_bas_aj, "mois": mois, "dist_km": float(b["dist_km"]),258                     "dS": h["superficie_habitable_m2"] - b["superficie_habitable_m2"], "dage": h["age"] - b["age"],259                     "difference": h["prix_vente"] - prix_bas_aj, "difference_pct": h["prix_vente"] / prix_bas_aj - 1.0})260    P = pd.DataFrame(rows)261    res = cp.ventes_appariees(P["prix_haut"].tolist(), P["prix_bas_ajuste"].tolist()) if len(P) else {"n": 0, "moyenne": float("nan"), "mediane": float("nan"), "ecart_type": float("nan")}262    res["erreur_type"] = res["ecart_type"] / math.sqrt(res["n"]) if res["n"] > 1 else float("nan")263    beta_q = float(modele["coef"].loc["qualite", "coef"])264    return {"paires": P, "resultat": res, "mediane_pct": float(P["difference_pct"].median()) if len(P) else float("nan"),265            "moyenne_pct": float(P["difference_pct"].mean()) if len(P) else float("nan"),266            "ecart_type_pct": float(P["difference_pct"].std(ddof=1)) if len(P) > 1 else float("nan"),267            "erreur_type_pct": float(P["difference_pct"].std(ddof=1) / math.sqrt(len(P))) if len(P) > 1 else float("nan"),268            "ic95_pct": (float(P["difference_pct"].mean() - 1.96 * P["difference_pct"].std(ddof=1) / math.sqrt(len(P))),269                         float(P["difference_pct"].mean() + 1.96 * P["difference_pct"].std(ddof=1) / math.sqrt(len(P)))) if len(P) > 1 else (float("nan"), float("nan")),270            "beta_qualite": beta_q, "effet_hedonique_pct": math.exp(beta_q) - 1.0}271272273def contribution_renovation(paires: dict, valeur_sujet: float, cout: float = HYPOTHESE_COUT_CUISINE_SDB) -> dict:274    """Contribution d'un échelon de qualité au sujet (paires et hédonique) rapportée au coût hypothétique d'une rénovation275    cuisine + salles de bain qui ferait passer la note de 3 à 4."""276    c_paires = paires["mediane_pct"] * valeur_sujet; c_hed = paires["effet_hedonique_pct"] * valeur_sujet277    return {"contribution_paires": c_paires, "contribution_hedonique": c_hed, "cout": cout,278            "ratio_paires": c_paires / cout, "ratio_hedonique": c_hed / cout, "van_renovation": c_paires - cout}279280281# -------------------------------------------------------------------------------------------- fiche d'état (ch. 11)282def composantes_fin_de_vie(seuil: float = 0.90) -> pd.DataFrame:283    import ch11_inspection as c11284    devis, sujet, cn = c11.charger()285    f = c11.fiche(devis, cn)286    f = f.copy(); f["ratio"] = f["age_effectif"] / f["vie_utile"]287    cols = [c for c in ["composante", "annee", "age", "vie_utile", "etat", "age_effectif", "ratio", "cout_neuf"] if c in f.columns]288    return f[(~f["longue_duree"]) & (f["ratio"] >= seuil)][cols].sort_values("ratio", ascending=False).reset_index(drop=True)289290291# -------------------------------------------------------------------------------------------- exercices292def exercices() -> dict:293    ex = {}294    # Ex. 1 : factures à 18 000 et 26 000 kWh ; prix moyen et marginal295    ex["ex1"] = {k: facture_tarif_d(k) for k in (18000.0, 26000.0)}296    ex["ex1"]["economie_1000_kwh_a_26000"] = economie_annuelle(26000.0, 25000.0)297    ex["ex1"]["economie_1000_kwh_a_15000"] = economie_annuelle(15000.0, 14000.0)298    # Ex. 2 : LogisVert 24 000 BTU/h standard vs climat froid ; capacité où le plafond mord299    ex["ex2"] = {"standard": min(50.0 * 24, 6700.0), "froid": min(120.0 * 24, 6700.0), "capacite_plafond_btu": 6700.0 / 120.0 * 1000.0,300                 "froid_60000": min(120.0 * 60, 6700.0)}301    # Ex. 3 : remplacement des 16 fenêtres (fin de vie 0,717) : coût neuf 22 311 $ (fiche ch. 11), économie 600 kWh, Rénoclimat 300 $/fenêtre max 4 500 $302    m_fen = {"nom": "Fenêtres", "cout": 22311.0, "subvention": min(300.0 * 16, 4500.0), "kwh": 600.0, "vie": 30}303    ex["ex3"] = {"mesure": m_fen, "flux": flux_mesure(m_fen), "sans_subvention": flux_mesure(m_fen, subvention=0.0)}304    # Ex. 4 : coût de l'énergie économisée de l'isolation à 4,58 % et à 8 %305    mm = mesures()306    ex["ex4"] = {"r458": flux_mesure(mm["B"]), "r8": flux_mesure(mm["B"], r=0.08), "sans_sub": flux_mesure(mm["B"], subvention=0.0)}307    # Ex. 5 : primes transposées à 637 953 $308    d = cas1.charger(); mod = cas1.hedonique(d["ventes"]); vs = float(cas1.predire(mod, cas1.sujet_df(d["sujet"]))[0])309    ex["ex5"] = {"valeur": vs, "europe": 0.0232 * vs, "amerique": 0.0536 * vs, "va_economies_AB": flux_mesure(mm["AB"])["va_economies"],310                 "va_economies_AB_31": flux_mesure(mm["AB"], vie=31)["va_economies"]}311    # Ex. 6 : seuils de la thermopompe312    ex["ex6"] = seuils(mm["A"])313    ex["ex6"]["tornade"] = sensibilite_tornade(mm["A"])314    # Ex. 7 : paires avec tolérances élargies / resserrées315    p_base = paires_qualite(d["ventes"], mod)316    p_large = paires_qualite(d["ventes"], mod, tol_s=30.0, tol_age=15, tol_mois=24.0, tol_dist_km=99.0)317    p_serre = paires_qualite(d["ventes"], mod, tol_s=15.0, tol_age=8, tol_mois=9.0, tol_dist_km=2.0)318    ex["ex7"] = {"base": p_base, "large": p_large, "serre": p_serre, "kennedy": math.exp(mod["coef"].loc["qualite", "coef"] - 0.5 * mod["coef"].loc["qualite", "se_hc1"] ** 2) - 1.0}319    return ex320321322# -------------------------------------------------------------------------------------------- impression323def main() -> dict:324    f = lambda x: f"{x:,.2f}".replace(",", " ")325    print("=== Facture du sujet au tarif D (hypothèse : 22 000 kWh/an) ===")326    F = facture_tarif_d(CONSO_KWH)327    for k, v in F.items(): print(f"  {k:16s} {v:,.4f}")328    print(f"  seuil annuel {SEUIL_KWH_JOUR * JOURS:,.0f} kWh ; conso/jour {CONSO_KWH / JOURS:.1f} kWh")329    print("\n=== Mesures ===")330    mm = mesures()331    for k in ("A", "B", "AB", "A_incr"):332        m = mm[k]; fl = flux_mesure(m)333        print(f"--- {k} {m['nom']} : coût {f(m['cout'])} ; subvention {f(m['subvention'])} ; net {f(fl['cout_net'])} ; "334              f"économie {m['kwh']:,.0f} kWh = {f(fl['economie_courante'])} $/an (an 1 : {f(fl['economie_an1'])}) ; vie {m['vie']} ans")335        print(f"    VAN {f(fl['van'])} ; TRI {fl['tri']*100:.2f} % ; délai simple {fl['delai_simple']} ; actualisé {fl['delai_actualise']} ; "336              f"VA éco {f(fl['va_economies'])} ; IR {fl['indice_rentabilite']:.3f} ; annuité nette {f(fl['annuite_cout_net'])} ; "337              f"CEE net {fl['cee_net']*100:.2f} ¢/kWh ; brut {fl['cee_brut']*100:.2f} ¢/kWh")338    print(f"  chauffage {mm['chauffage_kwh']:,.0f} kWh ; économie additive {mm['eco_additive_kwh']:,.0f} vs combinée {mm['AB']['kwh']:,.0f} kWh")339    print(f"  thermopompe standard : coût {f(mm['cout_tp_standard'])} ; LogisVert {f(mm['subvention_tp_standard'])} ; surcoût climat froid {f(mm['surcout_climat_froid'])} ; net {f(mm['surcout_net_climat_froid'])}")340    print("\n=== Seuils d'équilibre (A) ===")341    for k, v in seuils(mm["A"]).items(): print(f"  {k:30s} {v:,.4f}")342    print("\n=== Grille VAN (A) : taux × croissance ===")343    print(grille_van(mm["A"]).round(0).to_string())344    print("\n=== Grille VAN (B) ===")345    print(grille_van(mm["B"]).round(0).to_string())346    print("\n=== Sensibilité durée (A) ===")347    print(sensibilite_duree(mm["A"]).round(0).to_string())348    print("\n=== Tornade (A) ===")349    for o in sensibilite_tornade(mm["A"]): print(f"  {o['param']:40s} {f(o['bas'])} … {f(o['haut'])}")350    d = cas1.charger(); mod = cas1.hedonique(d["ventes"]); vs = float(cas1.predire(mod, cas1.sujet_df(d["sujet"]))[0])351    print(f"\n=== Lectures de la contribution (valeur hédonique du sujet {f(vs)}) ===")352    for k in ("A", "B", "AB"):353        L = lectures_contribution(mm[k], flux_mesure(mm[k]), vs)354        print(f"  {k}: coût {f(L['cout'])} ; net {f(L['cout_net'])} ; VA éco {f(L['va_economies'])} ; VA/coût {L['ratio_va_cout']:.3f} ; VA/net {L['ratio_va_cout_net']:.3f}")355    for k, v in lectures_contribution(mm["A"], flux_mesure(mm["A"]), vs)["primes_dollars"].items(): print(f"    prime {k:34s} {f(v)}")356    for a in (0, 5, 10): print(f"  VA des économies restantes de A après {a} ans : {f(va_economies_restantes(mm['A'], a))}")357    print("\n=== Paires qualité 4 vs 3 (cas 1) ===")358    P = paires_qualite(d["ventes"], mod)359    print(f"  n = {P['resultat']['n']} ; différence moyenne {f(P['resultat']['moyenne'])} ; médiane {f(P['resultat']['mediane'])} ; écart-type {f(P['resultat']['ecart_type'])} ; erreur-type {f(P['resultat']['erreur_type'])}")360    print(f"  en % : moyenne {P['moyenne_pct']*100:.2f} ; médiane {P['mediane_pct']*100:.2f} ; écart-type {P['ecart_type_pct']*100:.2f} ; erreur-type {P['erreur_type_pct']*100:.2f} ; IC95 {P['ic95_pct'][0]*100:.2f}..{P['ic95_pct'][1]*100:.2f} ; hédonique {P['effet_hedonique_pct']*100:.2f} (β = {P['beta_qualite']:.5f})")361    print(P["paires"].round(2).to_string())362    C = contribution_renovation(P, vs)363    for k, v in C.items(): print(f"  {k:26s} {v:,.4f}")364    print("\n=== Composantes en fin de vie (fiche ch. 11, ratio ≥ 0,90) ===")365    print(composantes_fin_de_vie().round(3).to_string())366    print("\n=== Exercices ===")367    ex = exercices()368    for k, v in ex["ex1"].items():369        print(f"  ex1 {k}: {v if not isinstance(v, dict) else {kk: round(vv, 4) for kk, vv in v.items()}}")370    print(f"  ex2 {ex['ex2']}")371    print(f"  ex3 fenêtres : VAN {f(ex['ex3']['flux']['van'])} ; TRI {ex['ex3']['flux']['tri']} ; CEE net {ex['ex3']['flux']['cee_net']*100:.2f} ¢ ; VA éco {f(ex['ex3']['flux']['va_economies'])} ; sans sub VAN {f(ex['ex3']['sans_subvention']['van'])}")372    print(f"  ex4 CEE iso : 4,58 % net {ex['ex4']['r458']['cee_net']*100:.2f} ¢ brut {ex['ex4']['r458']['cee_brut']*100:.2f} ¢ ; 8 % net {ex['ex4']['r8']['cee_net']*100:.2f} ¢ ; VAN sans sub {f(ex['ex4']['sans_sub']['van'])}")373    print("  ex5 " + str({k: round(v, 2) for k, v in ex["ex5"].items()}))374    print(f"  ex6 g_eq {ex['ex6']['g_equilibre']*100:.2f} % ; coût net eq {f(ex['ex6']['cout_net_equilibre'])} ; kWh eq {ex['ex6']['kwh_equilibre']:,.0f} ({ex['ex6']['fraction_chauffage_equilibre']*100:.1f} % du chauffage)")375    for k in ("base", "large", "serre"):376        p = ex["ex7"][k]; print(f"  ex7 {k}: n {p['resultat']['n']} ; médiane {p['mediane_pct']*100:.2f} % ; moyenne {p['moyenne_pct']*100:.2f} % ; σ {p['ecart_type_pct']*100:.2f} %")377    print(f"  ex7 Kennedy {ex['ex7']['kennedy']*100:.3f} %")378    return {"facture": F, "mesures": mm, "valeur_sujet": vs, "paires": P, "contribution": C, "exercices": ex}379380381if __name__ == "__main__":382    main()383
Files · cd76781f94cbc7113926bd8a4bd920d14c17c982
Menu

Theme