#!/usr/bin/env python3 """Chapitre 44 — rénovation et efficacité énergétique : analyse coût-bénéfice de deux mesures sur le sujet du cas 1 (cottage de Hull, Gatineau, DONNÉES SYNTHÉTIQUES) et lectures de la contribution à la valeur. Imprime tous les chiffres cités dans le chapitre 44 : * facture d'électricité du sujet au tarif D d'Hydro-Québec en vigueur le 1er avril 2026 (structure vérifiée : 46,154 ¢/jour ; 7,065 ¢/kWh jusqu'à 40 kWh/jour ; 11,142 ¢/kWh au-delà) pour une consommation DÉCLARÉE (hypothèse pédagogique) ; prix moyen et prix marginal du kilowattheure ; * deux mesures : (A) remplacement de la thermopompe centrale de 2016 par un appareil ENERGY STAR climat froid (subvention LogisVert 120 $ par 1 000 BTU/h, plafond 6 700 $) ; (B) isolation de l'entretoit (Rénoclimat, plafond « toit » 2 000 $ pour une maison) ; et leur combinaison (économies non additives) ; * flux annuels, VAN, TRI, délai de récupération simple et actualisé, annuité équivalente, coût de l'énergie économisée (traite.finance : van, tri, delai_recuperation, facteur_recouvrement) ; * sensibilité : grille VAN × (taux d'actualisation, croissance du prix de l'électricité), durée de vie, économie, coût, subvention ; seuils d'équilibre ; * lectures de la contribution à la valeur : coût, coût net, valeur actuelle des économies, prime verte de la littérature appliquée à la prédiction hédonique du cas 1 (traite.cas1.predire) ; * paires de ventes « qualité 4 vs qualité 3 » dans les 600 ventes du cas 1 (traite.comparables.ventes_appariees), comme indicateur de la contribution d'une rénovation non énergétique (cuisine, salles de bain) ; * composantes du sujet en fin de vie (fiche d'état du ch. 11 : analysis/python/cases/ch11_inspection.py) ; * chiffres des exercices. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch44_renovation.py Toutes les formules financières proviennent de analysis/python/traite/finance.py (aucune réécriture).""" from __future__ import annotations import sys, pathlib, math import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from traite import finance as fin, cas1 from traite import comparables as cp ROOT = pathlib.Path(__file__).resolve().parents[3] # --- Tarif D d'Hydro-Québec, en vigueur le 1er avril 2026 (hydrotarifs2026, section 2.5 ; vérifié le 2026-10-31) -------- FRAIS_ACCES_JOUR = 0.46154 # $ par jour PRIX_TRANCHE_1 = 0.07065 # $/kWh jusqu'à 40 kWh par jour SEUIL_KWH_JOUR = 40.0 # kWh par jour PRIX_TRANCHE_2 = 0.11142 # $/kWh au-delà JOURS = 365 # --- Consommation du sujet : HYPOTHÈSE PÉDAGOGIQUE (aucune facture réelle ; Hydro-Québec ne publie pas de facture # moyenne par type de maison — notes-E) -------------------------------------------------------------------------- CONSO_KWH = 22000.0 # kWh/an, maison tout électrique de 165 m² (1998) avec thermopompe centrale de 2016 PART_CHAUFFAGE = 0.50 # part du chauffage des locaux dans la consommation annuelle PART_EAU_CHAUDE = 0.20 PART_AUTRES = 0.30 # --- Paramètres financiers ---------------------------------------------------------------------------------------- TAUX_ACTUALISATION = 0.0458 # taux effectif des fonds avancés, prêts non assurés à taux fixe ≥ 5 ans, août 2026 (BdC, groupe A4) CROISSANCE_ENERGIE = 0.030 # hausse domestique 3,0 % en 2026 et 2027 (Régie, D-2026-033) ; maintenue par hypothèse au-delà VIE_RESTANTE_BATIMENT = 31 # ans (ch. 11 et 24 : vie économique 60 ans, âge effectif 29 ans) # --- Mesure A : thermopompe centrale ENERGY STAR climat froid ------------------------------------------------------- TP_CAPACITE_BTU = 36000 # BTU/h à −8 °C (3 tonnes ; hypothèse cohérente avec 165 m²) TP_COUT_STANDARD = 10481.32 * 1.33043 # poste « Thermopompe centrale et plinthes » du devis du cas 1, majoré (ch. 21) TP_COUT = 16500.0 # coût installé d'un appareil climat froid (hypothèse pédagogique, ≈ +18 % sur l'appareil standard) TP_LOGISVERT_STANDARD = 50.0 # $ par 1 000 BTU/h, appareil ENERGY STAR TP_LOGISVERT_FROID = 120.0 # $ par 1 000 BTU/h, appareil climat froid TP_LOGISVERT_MAX = 6700.0 TP_ECONOMIE_CHAUFFAGE = 0.30 # fraction de l'énergie de chauffage économisée par rapport à l'appareil de 2016 (hypothèse) TP_VIE = 15 # ans (fiche d'état du ch. 11 : vie utile 15 ans) TP_ECONOMIE_INCR = 0.20 # fraction du chauffage économisée par l'appareil climat froid PAR RAPPORT à un appareil standard neuf (hypothèse) # --- Mesure B : isolation de l'entretoit --------------------------------------------------------------------------- ISO_SURFACE_M2 = 95.0 # empreinte du bâtiment (ch. 22) ISO_COUT_M2 = 44.0 # $/m² installé, cellulose soufflée R-20 → R-60 (hypothèse pédagogique) ISO_COUT = ISO_SURFACE_M2 * ISO_COUT_M2 ISO_RENOCLIMAT_MAX = 2000.0 # plafond « toit », maison (Rénoclimat refondu le 1er octobre 2026) ISO_SUBVENTION = 2000.0 # hypothèse : travaux admissibles au plafond ISO_ECONOMIE_CHAUFFAGE = 0.08 # fraction de l'énergie de chauffage économisée (hypothèse) ISO_VIE = VIE_RESTANTE_BATIMENT # composante de longue durée : horizon = vie économique restante du bâtiment # --- Prime verte : résultats publiés (vérifiés le 2026-10-31, voir ch44-claims.csv) --------------------------------- PRIME_VERTE = [ # (clé, pays/marché, mesure, bas %, haut %, point %, type) ("brounenKok2011", "Pays-Bas, 2008-2009", "étiquette A, B ou C vs D-G", 3.6, 3.7, 3.7, "prix"), ("brounenKok2011", "Pays-Bas, 2008-2009", "étiquette A vs D ; G vs D", -5.0, 10.2, None, "prix"), ("kahnKok2014", "Californie, 2007-2012", "maison étiquetée verte vs non étiquetée", 4.5, 5.0, 5.0, "prix"), ("hylandLyonsLyons2013", "Irlande, 2008-2012", "cote A vs D (prix) ; 1 échelon BER", 1.3, 9.3, 9.3, "prix"), ("hylandLyonsLyons2013", "Irlande, 2008-2012", "cote A vs D (loyers)", 0.5, 1.8, 1.8, "loyer"), ("fuerstMcallisterNandaWyatt2015", "Angleterre, 1995-2012", "EPC A/B vs D ; C vs D ; G vs D", -6.0, 5.0, None, "prix"), ("aydinBrounenKok2020", "Pays-Bas", "+10 % d'efficacité énergétique mesurée", 2.2, 2.2, 2.2, "prix"), ("cespedeslopez2019", "Méta-analyse, 66 documents", "prix avec certificat : monde ; Europe ; Amérique", 2.32, 5.36, 4.20, "prix"), ("eichholtzKokQuigley2010", "É.-U., bureaux", "immeuble certifié : loyer ; loyer effectif ; prix", 3.0, 16.0, None, "bureaux"), ] HYPOTHESE_COUT_CUISINE_SDB = 65000.0 # coût d'une rénovation complète cuisine + 2 salles de bain (hypothèse pédagogique) # ------------------------------------------------------------------------------------------------------- tarif D def facture_tarif_d(kwh: float, jours: int = JOURS, indice: float = 1.0) -> dict: """Facture annuelle au tarif D (hors taxes) : frais d'accès + énergie en deux tranches ; `indice` multiplie les prix.""" seuil = SEUIL_KWH_JOUR * jours t1 = min(kwh, seuil); t2 = max(kwh - seuil, 0.0) acces = FRAIS_ACCES_JOUR * jours * indice e1 = t1 * PRIX_TRANCHE_1 * indice; e2 = t2 * PRIX_TRANCHE_2 * indice total = acces + e1 + e2 return {"kwh": kwh, "acces": acces, "tranche1_kwh": t1, "tranche2_kwh": t2, "energie_t1": e1, "energie_t2": e2, "total": total, "prix_moyen": total / kwh if kwh else float("nan"), "prix_marginal": (PRIX_TRANCHE_2 if kwh > seuil else PRIX_TRANCHE_1) * indice} def economie_annuelle(kwh_avant: float, kwh_apres: float, indice: float = 1.0) -> float: """Économie exacte = différence de factures (tient compte d'un éventuel franchissement du seuil).""" return facture_tarif_d(kwh_avant, indice=indice)["total"] - facture_tarif_d(kwh_apres, indice=indice)["total"] # ------------------------------------------------------------------------------------------------------- mesures def mesures() -> dict: chauffage = CONSO_KWH * PART_CHAUFFAGE eco_tp = chauffage * TP_ECONOMIE_CHAUFFAGE eco_iso = chauffage * ISO_ECONOMIE_CHAUFFAGE # combinaison : l'isolation réduit d'abord la charge ; la thermopompe économise une fraction du reste eco_combi = eco_iso + (chauffage - eco_iso) * TP_ECONOMIE_CHAUFFAGE sub_tp = min(TP_LOGISVERT_FROID * TP_CAPACITE_BTU / 1000.0, TP_LOGISVERT_MAX) sub_tp_std = min(TP_LOGISVERT_STANDARD * TP_CAPACITE_BTU / 1000.0, TP_LOGISVERT_MAX) return { "A": {"nom": "Thermopompe climat froid", "cout": TP_COUT, "subvention": sub_tp, "kwh": eco_tp, "vie": TP_VIE}, "B": {"nom": "Isolation de l'entretoit", "cout": ISO_COUT, "subvention": ISO_SUBVENTION, "kwh": eco_iso, "vie": ISO_VIE}, "AB": {"nom": "Les deux mesures", "cout": TP_COUT + ISO_COUT, "subvention": sub_tp + ISO_SUBVENTION, "kwh": eco_combi, "vie": TP_VIE}, # horizon commun = vie de la thermopompe (l'isolation continue au-delà : voir texte) # décision incrémentale à la fin de vie de l'appareil de 2016 : climat froid plutôt que standard "A_incr": {"nom": "Climat froid plutôt que standard (fin de vie)", "cout": TP_COUT - TP_COUT_STANDARD, "subvention": sub_tp - sub_tp_std, "kwh": chauffage * TP_ECONOMIE_INCR, "vie": TP_VIE}, "chauffage_kwh": chauffage, "eco_additive_kwh": eco_tp + eco_iso, "subvention_tp_standard": sub_tp_std, "cout_tp_standard": TP_COUT_STANDARD, "surcout_climat_froid": TP_COUT - TP_COUT_STANDARD, "surcout_net_climat_froid": (TP_COUT - TP_COUT_STANDARD) - (sub_tp - sub_tp_std), } def flux_mesure(m: dict, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE, vie: int | None = None, kwh_avant: float = CONSO_KWH, subvention: float | None = None, cout: float | None = None, kwh: float | None = None) -> dict: """Flux : t = 0 : −(coût − subvention) ; t = 1..n : économie_t = [F(kWh avant) − F(kWh après)] × (1+g)^t. Indicateurs : VAN, TRI, délai simple et actualisé, annuité équivalente, coût de l'énergie économisée.""" n = int(vie if vie is not None else m["vie"]); sub = m["subvention"] if subvention is None else subvention K = m["cout"] if cout is None else cout; dk = m["kwh"] if kwh is None else kwh eco0 = economie_annuelle(kwh_avant, kwh_avant - dk) flux = [-(K - sub)] + [eco0 * (1.0 + g) ** t for t in range(1, n + 1)] van = fin.van(flux, r) try: t_ri = fin.tri(flux) except ValueError: t_ri = float("nan") actualises = [cf / (1.0 + r) ** t for t, cf in enumerate(flux)] crf = fin.facteur_recouvrement(r, n) va_eco = sum(actualises[1:]) return {"flux": flux, "actualises": actualises, "cumul_actualise": np.cumsum(actualises).tolist(), "economie_an1": flux[1], "economie_courante": eco0, "cout_net": K - sub, "van": van, "tri": t_ri, "delai_simple": fin.delai_recuperation(flux), "delai_actualise": fin.delai_recuperation(actualises), "va_economies": va_eco, "indice_rentabilite": va_eco / (K - sub) if K > sub else float("inf"), "annuite_cout_net": (K - sub) * crf, "annuite_cout_brut": K * crf, "cee_net": (K - sub) * crf / dk, "cee_brut": K * crf / dk, # coût de l'énergie économisée, $/kWh "n": n, "r": r, "g": g} def seuils(m: dict, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE) -> dict: """Valeurs d'équilibre (VAN = 0) : croissance du prix de l'énergie, coût net, économie en kWh, taux (= TRI).""" f0 = flux_mesure(m, r=r, g=g) cout_net_eq = f0["va_economies"] # économie en kWh d'équilibre : VAN(kwh) = 0, recherche par bissection (économie linéaire si pas de changement de tranche) lo, hi = 0.0, CONSO_KWH * 0.9 for _ in range(200): mid = 0.5 * (lo + hi) if flux_mesure(m, r=r, g=g, kwh=mid)["van"] < 0: lo = mid else: hi = mid kwh_eq = 0.5 * (lo + hi) # croissance d'équilibre glo, ghi = -0.5, 0.5 for _ in range(200): gm = 0.5 * (glo + ghi) if flux_mesure(m, r=r, g=gm)["van"] < 0: glo = gm else: ghi = gm g_eq = 0.5 * (glo + ghi) return {"cout_net_equilibre": cout_net_eq, "subvention_equilibre": m["cout"] - cout_net_eq, "kwh_equilibre": kwh_eq, "fraction_chauffage_equilibre": kwh_eq / (CONSO_KWH * PART_CHAUFFAGE), "g_equilibre": g_eq, "tri": f0["tri"]} def 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: 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)) def sensibilite_tornade(m: dict) -> list[dict]: """Variation de la VAN de la mesure A pour des chocs déclarés sur chaque paramètre (toutes choses égales par ailleurs).""" base = flux_mesure(m)["van"] out = [] def add(nom, bas, haut): out.append({"param": nom, "base": base, "bas": bas, "haut": haut}) 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"]) add("Taux d'actualisation (8 % / 2 %)", flux_mesure(m, r=0.08)["van"], flux_mesure(m, r=0.02)["van"]) add("Croissance du prix (0 % / 6 %)", flux_mesure(m, g=0.0)["van"], flux_mesure(m, g=0.06)["van"]) add("Durée de vie (12 / 20 ans)", flux_mesure(m, vie=12)["van"], flux_mesure(m, vie=20)["van"]) add("Coût installé (+20 % / −20 %)", flux_mesure(m, cout=m["cout"] * 1.2)["van"], flux_mesure(m, cout=m["cout"] * 0.8)["van"]) add("Subvention (0 $ / plafond 6 700 $)", flux_mesure(m, subvention=0.0)["van"], flux_mesure(m, subvention=TP_LOGISVERT_MAX)["van"]) add("Consommation (16 000 / 28 000 kWh)", flux_mesure(m, kwh_avant=16000.0, kwh=16000.0 * PART_CHAUFFAGE * TP_ECONOMIE_CHAUFFAGE)["van"], flux_mesure(m, kwh_avant=28000.0, kwh=28000.0 * PART_CHAUFFAGE * TP_ECONOMIE_CHAUFFAGE)["van"]) return out def sensibilite_duree(m: dict, vies=(10, 12, 15, 18, 20, 25), taux=(0.02, 0.0458, 0.07, 0.10)) -> pd.DataFrame: 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)) # -------------------------------------------------------------------------------------------- contribution à la valeur def lectures_contribution(m: dict, f: dict, valeur_sujet: float) -> dict: """Quatre lectures de ce que la mesure « vaut » pour le bien : coût, coût net, VA des économies (acheteur rationnel), prime verte de la littérature appliquée à la valeur du sujet (bornes).""" primes = {"Europe (méta-analyse)": 0.0232, "Monde (méta-analyse)": 0.0420, "Amérique du Nord (méta-analyse)": 0.0536, "Pays-Bas, étiquette verte": 0.037, "Angleterre, EPC C vs D": 0.018, "Californie, étiquette verte": 0.05} return {"cout": m["cout"], "cout_net": f["cout_net"], "va_economies": f["va_economies"], "van": f["van"], "ratio_va_cout": f["va_economies"] / m["cout"], "ratio_va_cout_net": f["va_economies"] / f["cout_net"], "primes_dollars": {k: v * valeur_sujet for k, v in primes.items()}, "primes_pct": primes, "valeur_sujet": valeur_sujet} def va_economies_restantes(m: dict, annees_ecoulees: int, r: float = TAUX_ACTUALISATION, g: float = CROISSANCE_ENERGIE) -> float: """Valeur actuelle, à la date d'une revente future, des économies restantes (ce qu'un acheteur rationnel paierait au plus).""" f = flux_mesure(m, r=r, g=g) rest = f["flux"][1 + annees_ecoulees:] return sum(cf / (1.0 + r) ** (t + 1) for t, cf in enumerate(rest)) # -------------------------------------------------------------------------------------------- paires « qualité » def paires_qualite(v: pd.DataFrame, modele: dict, q_haut: int = 4, q_bas: int = 3, tol_s: float = 25.0, tol_age: int = 10, tol_mois: float = 18.0, tol_dist_km: float = 5.0, date_eval: str = "2026-09-01") -> dict: """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) : même secteur, même type, |ΔS| ≤ tol_s m², |Δâge| ≤ tol_age, mêmes garage / sous-sol / piscine, |Δdate| ≤ tol_mois, distance ≤ tol_dist_km. Le prix du membre « bas » est ramené à la date du membre « haut » par la tendance du modèle hédonique (croissance mensuelle composée). Chaque vente n'entre que dans une paire (appariement glouton par distance).""" g_m = modele["croissance_mensuelle"] hauts = v[v["qualite_1a5"] == q_haut].copy(); bas = v[v["qualite_1a5"] == q_bas].copy() coords = lambda df: np.radians(df[["lat", "lon"]].values) utilises = set(); rows = [] for _, h in hauts.iterrows(): cand = bas[(bas["secteur"] == h["secteur"]) & (bas["type"] == h["type"]) & (~bas["id_vente"].isin(utilises)) & ((bas["superficie_habitable_m2"] - h["superficie_habitable_m2"]).abs() <= tol_s) & ((bas["age"] - h["age"]).abs() <= tol_age) & (bas["garage_places"] == h["garage_places"]) & (bas["sous_sol_fini"] == h["sous_sol_fini"]) & (bas["piscine"] == h["piscine"])] if cand.empty: continue dmois = ((cand["date_vente"] - h["date_vente"]).dt.days / 30.4375) cand = cand[dmois.abs() <= tol_mois] if cand.empty: continue # distance haversine approximative (km) lat1, lon1 = math.radians(h["lat"]), math.radians(h["lon"]) lat2 = np.radians(cand["lat"].values); lon2 = np.radians(cand["lon"].values) a = np.sin((lat2 - lat1) / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin((lon2 - lon1) / 2) ** 2 dist = 6371.0 * 2 * np.arcsin(np.sqrt(a)) cand = cand.assign(dist_km=dist) cand = cand[cand["dist_km"] <= tol_dist_km] if cand.empty: continue # score de ressemblance : superficie, âge, date, distance normalisés score = ((cand["superficie_habitable_m2"] - h["superficie_habitable_m2"]).abs() / tol_s + (cand["age"] - h["age"]).abs() / tol_age + ((cand["date_vente"] - h["date_vente"]).dt.days / 30.4375).abs() / tol_mois + cand["dist_km"] / tol_dist_km) b = cand.loc[score.idxmin()] utilises.add(b["id_vente"]) mois = (h["date_vente"] - b["date_vente"]).days / 30.4375 prix_bas_aj = b["prix_vente"] * (1.0 + g_m) ** mois # ramené à la date de la vente « haute » rows.append({"id_haut": h["id_vente"], "id_bas": b["id_vente"], "secteur": h["secteur"], "prix_haut": h["prix_vente"], "prix_bas": b["prix_vente"], "prix_bas_ajuste": prix_bas_aj, "mois": mois, "dist_km": float(b["dist_km"]), "dS": h["superficie_habitable_m2"] - b["superficie_habitable_m2"], "dage": h["age"] - b["age"], "difference": h["prix_vente"] - prix_bas_aj, "difference_pct": h["prix_vente"] / prix_bas_aj - 1.0}) P = pd.DataFrame(rows) 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")} res["erreur_type"] = res["ecart_type"] / math.sqrt(res["n"]) if res["n"] > 1 else float("nan") beta_q = float(modele["coef"].loc["qualite", "coef"]) return {"paires": P, "resultat": res, "mediane_pct": float(P["difference_pct"].median()) if len(P) else float("nan"), "moyenne_pct": float(P["difference_pct"].mean()) if len(P) else float("nan"), "ecart_type_pct": float(P["difference_pct"].std(ddof=1)) if len(P) > 1 else float("nan"), "erreur_type_pct": float(P["difference_pct"].std(ddof=1) / math.sqrt(len(P))) if len(P) > 1 else float("nan"), "ic95_pct": (float(P["difference_pct"].mean() - 1.96 * P["difference_pct"].std(ddof=1) / math.sqrt(len(P))), 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")), "beta_qualite": beta_q, "effet_hedonique_pct": math.exp(beta_q) - 1.0} def contribution_renovation(paires: dict, valeur_sujet: float, cout: float = HYPOTHESE_COUT_CUISINE_SDB) -> dict: """Contribution d'un échelon de qualité au sujet (paires et hédonique) rapportée au coût hypothétique d'une rénovation cuisine + salles de bain qui ferait passer la note de 3 à 4.""" c_paires = paires["mediane_pct"] * valeur_sujet; c_hed = paires["effet_hedonique_pct"] * valeur_sujet return {"contribution_paires": c_paires, "contribution_hedonique": c_hed, "cout": cout, "ratio_paires": c_paires / cout, "ratio_hedonique": c_hed / cout, "van_renovation": c_paires - cout} # -------------------------------------------------------------------------------------------- fiche d'état (ch. 11) def composantes_fin_de_vie(seuil: float = 0.90) -> pd.DataFrame: import ch11_inspection as c11 devis, sujet, cn = c11.charger() f = c11.fiche(devis, cn) f = f.copy(); f["ratio"] = f["age_effectif"] / f["vie_utile"] cols = [c for c in ["composante", "annee", "age", "vie_utile", "etat", "age_effectif", "ratio", "cout_neuf"] if c in f.columns] return f[(~f["longue_duree"]) & (f["ratio"] >= seuil)][cols].sort_values("ratio", ascending=False).reset_index(drop=True) # -------------------------------------------------------------------------------------------- exercices def exercices() -> dict: ex = {} # Ex. 1 : factures à 18 000 et 26 000 kWh ; prix moyen et marginal ex["ex1"] = {k: facture_tarif_d(k) for k in (18000.0, 26000.0)} ex["ex1"]["economie_1000_kwh_a_26000"] = economie_annuelle(26000.0, 25000.0) ex["ex1"]["economie_1000_kwh_a_15000"] = economie_annuelle(15000.0, 14000.0) # Ex. 2 : LogisVert 24 000 BTU/h standard vs climat froid ; capacité où le plafond mord 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, "froid_60000": min(120.0 * 60, 6700.0)} # 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 $ m_fen = {"nom": "Fenêtres", "cout": 22311.0, "subvention": min(300.0 * 16, 4500.0), "kwh": 600.0, "vie": 30} ex["ex3"] = {"mesure": m_fen, "flux": flux_mesure(m_fen), "sans_subvention": flux_mesure(m_fen, subvention=0.0)} # Ex. 4 : coût de l'énergie économisée de l'isolation à 4,58 % et à 8 % mm = mesures() ex["ex4"] = {"r458": flux_mesure(mm["B"]), "r8": flux_mesure(mm["B"], r=0.08), "sans_sub": flux_mesure(mm["B"], subvention=0.0)} # Ex. 5 : primes transposées à 637 953 $ d = cas1.charger(); mod = cas1.hedonique(d["ventes"]); vs = float(cas1.predire(mod, cas1.sujet_df(d["sujet"]))[0]) ex["ex5"] = {"valeur": vs, "europe": 0.0232 * vs, "amerique": 0.0536 * vs, "va_economies_AB": flux_mesure(mm["AB"])["va_economies"], "va_economies_AB_31": flux_mesure(mm["AB"], vie=31)["va_economies"]} # Ex. 6 : seuils de la thermopompe ex["ex6"] = seuils(mm["A"]) ex["ex6"]["tornade"] = sensibilite_tornade(mm["A"]) # Ex. 7 : paires avec tolérances élargies / resserrées p_base = paires_qualite(d["ventes"], mod) p_large = paires_qualite(d["ventes"], mod, tol_s=30.0, tol_age=15, tol_mois=24.0, tol_dist_km=99.0) p_serre = paires_qualite(d["ventes"], mod, tol_s=15.0, tol_age=8, tol_mois=9.0, tol_dist_km=2.0) 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} return ex # -------------------------------------------------------------------------------------------- impression def main() -> dict: f = lambda x: f"{x:,.2f}".replace(",", " ") print("=== Facture du sujet au tarif D (hypothèse : 22 000 kWh/an) ===") F = facture_tarif_d(CONSO_KWH) for k, v in F.items(): print(f" {k:16s} {v:,.4f}") print(f" seuil annuel {SEUIL_KWH_JOUR * JOURS:,.0f} kWh ; conso/jour {CONSO_KWH / JOURS:.1f} kWh") print("\n=== Mesures ===") mm = mesures() for k in ("A", "B", "AB", "A_incr"): m = mm[k]; fl = flux_mesure(m) print(f"--- {k} {m['nom']} : coût {f(m['cout'])} ; subvention {f(m['subvention'])} ; net {f(fl['cout_net'])} ; " f"économie {m['kwh']:,.0f} kWh = {f(fl['economie_courante'])} $/an (an 1 : {f(fl['economie_an1'])}) ; vie {m['vie']} ans") print(f" VAN {f(fl['van'])} ; TRI {fl['tri']*100:.2f} % ; délai simple {fl['delai_simple']} ; actualisé {fl['delai_actualise']} ; " f"VA éco {f(fl['va_economies'])} ; IR {fl['indice_rentabilite']:.3f} ; annuité nette {f(fl['annuite_cout_net'])} ; " f"CEE net {fl['cee_net']*100:.2f} ¢/kWh ; brut {fl['cee_brut']*100:.2f} ¢/kWh") print(f" chauffage {mm['chauffage_kwh']:,.0f} kWh ; économie additive {mm['eco_additive_kwh']:,.0f} vs combinée {mm['AB']['kwh']:,.0f} kWh") 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'])}") print("\n=== Seuils d'équilibre (A) ===") for k, v in seuils(mm["A"]).items(): print(f" {k:30s} {v:,.4f}") print("\n=== Grille VAN (A) : taux × croissance ===") print(grille_van(mm["A"]).round(0).to_string()) print("\n=== Grille VAN (B) ===") print(grille_van(mm["B"]).round(0).to_string()) print("\n=== Sensibilité durée (A) ===") print(sensibilite_duree(mm["A"]).round(0).to_string()) print("\n=== Tornade (A) ===") for o in sensibilite_tornade(mm["A"]): print(f" {o['param']:40s} {f(o['bas'])} … {f(o['haut'])}") d = cas1.charger(); mod = cas1.hedonique(d["ventes"]); vs = float(cas1.predire(mod, cas1.sujet_df(d["sujet"]))[0]) print(f"\n=== Lectures de la contribution (valeur hédonique du sujet {f(vs)}) ===") for k in ("A", "B", "AB"): L = lectures_contribution(mm[k], flux_mesure(mm[k]), vs) 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}") for k, v in lectures_contribution(mm["A"], flux_mesure(mm["A"]), vs)["primes_dollars"].items(): print(f" prime {k:34s} {f(v)}") 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))}") print("\n=== Paires qualité 4 vs 3 (cas 1) ===") P = paires_qualite(d["ventes"], mod) 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'])}") 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})") print(P["paires"].round(2).to_string()) C = contribution_renovation(P, vs) for k, v in C.items(): print(f" {k:26s} {v:,.4f}") print("\n=== Composantes en fin de vie (fiche ch. 11, ratio ≥ 0,90) ===") print(composantes_fin_de_vie().round(3).to_string()) print("\n=== Exercices ===") ex = exercices() for k, v in ex["ex1"].items(): print(f" ex1 {k}: {v if not isinstance(v, dict) else {kk: round(vv, 4) for kk, vv in v.items()}}") print(f" ex2 {ex['ex2']}") 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'])}") 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'])}") print(" ex5 " + str({k: round(v, 2) for k, v in ex["ex5"].items()})) 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)") for k in ("base", "large", "serre"): 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} %") print(f" ex7 Kennedy {ex['ex7']['kennedy']*100:.3f} %") return {"facture": F, "mesures": mm, "valeur_sujet": vs, "paires": P, "contribution": C, "exercices": ex} if __name__ == "__main__": main()