spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 28 — Dépenses, réserves, CAPEX et RNE : calculs du cas 3 (12 logements, Rosemont) et du cas 2 (triplex, Limoilou).3DONNÉES SYNTHÉTIQUES (graine 20261031).45Tous les chiffres cités dans manuscript/chapters/ch28.tex et solutions/ch28.tex sortent d'ici. Les calculs partagés6(revenus, dépenses normalisées, RNE) viennent de traite.cas3 et traite.cas2, pour coïncider avec les chapitres 29, 30,732, 46 et 47 ; les formules financières viennent de traite.finance. Aucune formule partagée n'est réécrite ; les8calculs propres au chapitre (classement, normalisation alternative, équivalence réserve/CAPEX, sensibilités) sont ici.910Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch28_depenses.py11"""12from __future__ import annotations13import sys, pathlib14import numpy as np, pandas as pd15sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))16from traite import cas3, cas2, finance as fin1718ROOT = pathlib.Path(__file__).resolve().parents[3]1920# Taux global médian extrait au chapitre 29 (traite.cas3.taux_extraits ; revenu net APRÈS réserve). Repris ici pour les21# démonstrations de cohérence ; recalculé par calculer() et vérifié par le test.22GROUPES = { # regroupement des postes pour les figures et les ratios de référence23 "Taxes municipales": "Taxes foncières", "Taxe scolaire": "Taxes foncières", "Assurances": "Assurances",24 "Électricité (aires communes)": "Énergie des aires communes", "Chauffage (aires communes, gaz)": "Énergie des aires communes",25 "Eau et égouts (taxe)": "Taxes foncières", "Entretien et réparations": "Entretien et réparations",26 "Conciergerie": "Conciergerie et services", "Déneigement et paysagement": "Conciergerie et services",27 "Déneigement et entretien extérieur": "Conciergerie et services",28 "Frais juridiques et comptables": "Honoraires et publicité", "Publicité et location": "Honoraires et publicité",29 "Gestion (4 % du RBE)": "Gestion", "Gestion (propriétaire-occupant : imputée 4 %)": "Gestion",30 "Réserve de remplacement (par logement)": "Réserve de remplacement", "Réserve de remplacement": "Réserve de remplacement",31}32ORDRE_GROUPES = ["Taxes foncières", "Assurances", "Énergie des aires communes", "Entretien et réparations",33 "Conciergerie et services", "Honoraires et publicité", "Gestion", "Réserve de remplacement"]3435# Composantes à vie courte couvertes par la réserve — HYPOTHÈSES PÉDAGOGIQUES déclarées (coût unitaire par logement, durée36# de vie en années). Elles servent uniquement à illustrer les deux méthodes de calcul d'une réserve (linéaire et fonds37# d'amortissement) ; elles ne proviennent d'aucun barème publié.38COMPOSANTES_RESERVE = [39 ("Cuisinière et réfrigérateur", 2200.0, 12),40 ("Chauffe-eau", 1100.0, 10),41 ("Revêtements de sol", 3600.0, 20),42 ("Peinture et finition intérieure", 1500.0, 7),43]444546def _groupes(detail: pd.DataFrame, col: str = "normalise") -> pd.Series:47 g = detail.assign(groupe=detail["poste"].map(GROUPES)).groupby("groupe")[col].sum()48 return g.reindex(ORDRE_GROUPES).fillna(0.0)495051def historique_cas3(dep: pd.DataFrame) -> pd.DataFrame:52 """Pivot 2023-2025 par poste, avec variation totale 2025/2023 et taux de croissance annuel composé."""53 piv = dep.pivot(index="poste", columns="annee", values="montant").astype(float)54 ordre = ["Taxes municipales", "Taxe scolaire", "Assurances", "Électricité (aires communes)", "Chauffage (aires communes, gaz)",55 "Eau et égouts (taxe)", "Entretien et réparations", "Conciergerie", "Déneigement et paysagement",56 "Frais juridiques et comptables", "Publicité et location", "Réserve de remplacement (par logement)"]57 piv = piv.reindex(ordre)58 piv.loc["Total"] = piv.sum()59 with np.errstate(divide="ignore", invalid="ignore"):60 piv["var_2025_2023"] = piv[2025] / piv[2023] - 161 piv["tcac"] = (piv[2025] / piv[2023]) ** 0.5 - 162 return piv636465def normalisation_alternative(dep: pd.DataFrame, g: float = 0.03, annee_cible: int = 2026) -> pd.DataFrame:66 """Pour les postes indexés : (a) dernière année × (1+g) [règle du livre] ; (b) moyenne des trois exercices, chacun67 ramené en dollars de l'année cible au taux g (moyenne indexée). Permet de comparer les deux conventions."""68 piv = dep.pivot(index="poste", columns="annee", values="montant").astype(float)69 rows = []70 for poste, r in piv.iterrows():71 indexes = {an: r[an] * (1 + g) ** (annee_cible - an) for an in (2023, 2024, 2025)}72 rows.append({"poste": poste, "derniere_indexee": indexes[2025], "moyenne_indexee": float(np.mean(list(indexes.values()))),73 "min_indexe": min(indexes.values()), "max_indexe": max(indexes.values())})74 out = pd.DataFrame(rows).set_index("poste")75 out["ecart"] = out["moyenne_indexee"] - out["derniere_indexee"]76 return out777879def reserve_par_composantes(composantes=COMPOSANTES_RESERVE, nb_logements: int = 12, taux: float = 0.03) -> dict:80 """Réserve annuelle par la méthode linéaire (coût/durée) et par le fonds d'amortissement (coût × SFF au taux donné)."""81 rows = []82 for nom, cout, vie in composantes:83 lin = cout / vie84 sff = cout * fin.facteur_fonds_amortissement(taux, vie)85 rows.append({"composante": nom, "cout_par_logement": cout, "duree_vie": vie, "lineaire_par_logement": lin, "fonds_par_logement": sff,86 "lineaire_immeuble": lin * nb_logements, "fonds_immeuble": sff * nb_logements})87 df = pd.DataFrame(rows)88 return {"detail": df, "lineaire_par_logement": float(df["lineaire_par_logement"].sum()), "fonds_par_logement": float(df["fonds_par_logement"].sum()),89 "lineaire_immeuble": float(df["lineaire_immeuble"].sum()), "fonds_immeuble": float(df["fonds_immeuble"].sum()), "taux": taux}909192def demonstration_reserve(noi_apres: float, reserve: float, R_apres: float) -> dict:93 """Cohérence réserve/taux : V cohérent ; taux « avant réserve » équivalent R' = R + RR/V ; valeur erronée si R' est94 appliqué au revenu après réserve ; écart exact ΔV = RR/R' ; erreur relative −RR/(RNE+RR)."""95 V = fin.capitalisation_directe(noi_apres, R_apres)96 R_avant = fin.taux_global_extrait(noi_apres + reserve, V)97 V_err = fin.capitalisation_directe(noi_apres, R_avant)98 V_err_inverse = fin.capitalisation_directe(noi_apres + reserve, R_apres) # taux après réserve sur revenu avant réserve99 return {"V": V, "R_apres": R_apres, "R_avant": R_avant, "ecart_taux_pb": (R_avant - R_apres) * 1e4, "V_err": V_err,100 "delta_V": V - V_err, "delta_V_formule": reserve / R_avant, "erreur_relative": V_err / V - 1,101 "erreur_relative_formule": -reserve / (noi_apres + reserve), "approx_reserve_sur_R": reserve / R_apres,102 "V_err_inverse": V_err_inverse, "erreur_relative_inverse": V_err_inverse / V - 1}103104105def equivalence_capex(hyp: dict, r: float | None = None, T: int | None = None) -> dict:106 """Les CAPEX programmés du dossier (flux actualisés) convertis en charge annuelle équivalente sur l'horizon : montre107 ce qu'une réserve « pour travaux lourds » représenterait si on la comptait en plus des CAPEX (double comptage)."""108 r = hyp["taux_actualisation"] if r is None else r; T = hyp["horizon_ans"] if T is None else T109 an0 = int(hyp["date_evaluation"][:4])110 flux = [0.0] * (T + 1)111 for an, m in hyp["capex"].items():112 t = int(an) - an0 + 1113 if 1 <= t <= T: flux[t] = float(m)114 va = fin.van(flux, r) # flux[0] = 0 à t = 0115 annuite = va * fin.facteur_recouvrement(r, T)116 total = float(sum(hyp["capex"].values()))117 return {"capex": {int(k): float(v) for k, v in hyp["capex"].items()}, "total": total, "va": va, "annuite_equivalente": annuite,118 "moyenne_simple": total / T, "r": r, "T": T}119120121# IPC Québec, ensemble, non désaisonnalisé, 2002 = 100 (Statistique Canada, tableau 18-10-0004-01, vecteur v41691783,122# data/raw/statcan_18100004/18100004.csv, diffusion du 2026-10-19) : avril 2015 = 124,7 ; septembre 2026 = 167,7.123IPC_QC_2015_04 = 124.7124IPC_QC_2026_09 = 167.7125126127def indexation_ipc(base: float = IPC_QC_2015_04, cible: float = IPC_QC_2026_09) -> dict:128 """Facteur d'indexation du barème SCHL d'avril 2015 à septembre 2026 et barème indexé (entretien 500 $, salaires 300 $, réserve 100 $/log.)."""129 f = cible / base130 return {"facteur": f, "variation": f - 1, "entretien_500": 500 * f, "salaires_300": 300 * f, "salaires_125": 125 * f, "reserve_100": 100 * f}131132133def classement(detail: pd.DataFrame, rbe: float, nb_log: int, surface_m2: float) -> dict:134 d = detail.copy()135 fixes = float(d.loc[d["type"] == "fixe", "normalise"].sum()); variables = float(d.loc[d["type"] == "variable", "normalise"].sum())136 reserve = float(d.loc[d["type"] == "réserve", "normalise"].sum()); total = fixes + variables + reserve137 gestion = float(d.loc[d["poste"].str.startswith("Gestion"), "normalise"].sum())138 return {"fixes": fixes, "variables": variables, "variables_hors_gestion": variables - gestion, "gestion": gestion, "reserve": reserve, "total": total,139 "part_fixes": fixes / total, "part_variables": variables / total, "part_reserve": reserve / total,140 "fixes_pct_rbe": fixes / rbe, "variables_pct_rbe": variables / rbe, "reserve_pct_rbe": reserve / rbe, "total_pct_rbe": total / rbe,141 "total_par_logement": total / nb_log, "total_par_m2": total / surface_m2, "groupes": _groupes(d),142 "groupes_pct_rbe": _groupes(d) / rbe, "groupes_par_logement": _groupes(d) / nb_log}143144145def rbe_fonction_loyer(loyer_moyen: float, nb_log: int, v: float, autres: float) -> float:146 return fin.revenu_brut_effectif(loyer_moyen * nb_log * 12, v, autres)147148149def ratio_iso_depenses(loyer_moyen, base: dict, hyp: dict, nb_log: int = 12, gestion: float = 0.04):150 """Ratio de dépenses du sujet si son loyer moyen était L (dépenses hors gestion et réserve inchangées, gestion en % du RBE)."""151 L = np.asarray(loyer_moyen, dtype=float)152 rbe = rbe_fonction_loyer(L, nb_log, hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"])153 de_hors_gestion = base["depenses_hors_reserve"] - gestion * base["rbe"]154 return (de_hors_gestion + gestion * rbe + base["reserve"]) / rbe155156157def sensibilites(base: dict, hyp: dict, R: float, nb_log: int = 12) -> dict:158 """Sensibilité du RNE (et de V = RNE/R à taux inchangé) à la réserve par logement, au taux de taxe et au taux de gestion."""159 rbe = base["rbe"]; de_hors_res = base["depenses_hors_reserve"]; res0 = base["reserve"]; noi0 = base["rne"]; V0 = noi0 / R160 role = hyp["valeur_role_2026"]; taxe0 = role * hyp["taux_taxe_fonciere"]; gestion0 = 0.04 * rbe161 # réserve par logement162 rows_r = []163 for rpl in [0, 100, 300, 500, 675.25, 900, 1200]:164 res = rpl * nb_log; noi = fin.rne(rbe, de_hors_res, res)165 rows_r.append({"reserve_par_logement": rpl, "reserve": res, "rne": noi, "V": noi / R, "dV_pct": noi / noi0 - 1, "ratio": (de_hors_res + res) / rbe})166 # taux de taxe municipale ($/100 $), rôle inchangé167 rows_t = []168 scen = [("Montréal, taxe générale 2026 (base)", hyp["taux_taxe_fonciere"]),169 ("Compte 2025 indexé de 5,3 % (Rosemont)", 10631 * 1.053 / role),170 ("Gatineau, six logements ou plus (6,964 $/1 000 $)", 0.006964),171 ("Québec, six logements et plus (0,7697 $/100 $)", 0.007697),172 ("Taux + 10 points de base", hyp["taux_taxe_fonciere"] + 0.001)]173 for nom, tau in scen:174 taxe = role * tau; noi = fin.rne(rbe, de_hors_res - taxe0 + taxe, res0)175 rows_t.append({"scenario": nom, "taux_par_100": tau * 100, "taxe": taxe, "delta_taxe": taxe - taxe0, "rne": noi, "V": noi / R, "dV_pct": noi / noi0 - 1})176 # taux de gestion177 rows_g = []178 for m in [0.0, 0.03, 0.04, 0.05, 0.06, 0.10]:179 noi = fin.rne(rbe, de_hors_res - gestion0 + m * rbe, res0)180 rows_g.append({"gestion_pct": m, "gestion": m * rbe, "rne": noi, "V": noi / R, "dV_pct": noi / noi0 - 1})181 # dérivée : un dollar de dépense = 1/R dollars de valeur182 return {"reserve": pd.DataFrame(rows_r), "taxe": pd.DataFrame(rows_t), "gestion": pd.DataFrame(rows_g), "dV_dDE": -1.0 / R, "V0": V0,183 "elasticite_rne_depenses": -(de_hors_res + res0) / noi0,184 # élasticité du RNE au RBE lorsque seule la gestion (4 % du RBE) s'ajuste : d RNE / d RBE = 1 − m ; élasticité = (1 − m) RBE / RNE185 "elasticite_rne_rbe": 0.96 * rbe / noi0, "depenses_equiv_25pb": noi0 / R - noi0 / (R + 0.0025)}186187188def cas2_detail() -> dict:189 d = cas2.charger(); b = cas2.rne(d["loyers"], d["depenses"]); s = d["sujet"]190 det = b["detail"].copy(); det["type_livre"] = det["type"].replace({"imputée": "variable"})191 det_cl = pd.DataFrame({"poste": det["poste"], "normalise": det["normalise"], "type": det["type_livre"]})192 cl = classement(det_cl, b["rbe"], s["nb_logements"], s["superficie_habitable_m2"])193 # vérification des taxes de Québec 2026 : taux 1-5 logements + tarifs par logement ; taxe scolaire sur (rôle − 25 000 $)194 role = s["valeur_role_2025"]; n = s["nb_logements"]195 taxe_gen = role * 0.007464; tarifs = n * (386 + 195); taxe_mun_recalc = taxe_gen + tarifs196 scol_recalc = max(role - 25000, 0) * 0.0007899197 ind = cas2.indicateurs_marche(cas2.comparables_plex(d["ventes"], s))198 return {**d, "base": b, "indicateurs": ind, "classement": cl, "detail": det, "rne_par_logement": b["rne"] / n, "rne_par_m2": b["rne"] / s["superficie_habitable_m2"],199 "taxes_quebec_recalc": {"taxe_generale": taxe_gen, "tarifs": tarifs, "total": taxe_mun_recalc, "declaree": 5900.0, "ecart": 5900.0 - taxe_mun_recalc,200 "scolaire_recalc": scol_recalc, "scolaire_declaree": 470.0},201 "gestion_imputee": float(det.loc[det["type"] == "imputée", "normalise"].sum()), "reserve_par_logement": b["reserve"] / n}202203204def calculer() -> dict:205 d = cas3.charger(); etat, dep, hyp, sujet, comps = d["etat"], d["dep"], d["hyp"], d["sujet"], d["comps"]206 base = cas3.rne(etat, dep, hyp); tx = cas3.taux_extraits(comps); R = tx["mediane"]207 nb = sujet["nb_logements"]; surf = sujet["superficie_locative_m2"]208 hist = historique_cas3(dep); alt = normalisation_alternative(dep, hyp["croissance_depenses"])209 cl = classement(base["detail"], base["rbe"], nb, surf)210 det = base["detail"].copy(); det["regle"] = det["poste"].map(lambda p: "rôle × taux" if p.startswith("Tax") else ("maintenue" if p.startswith("Réserve") else ("% du RBE" if p.startswith("Gestion") else "× 1,03")))211 det["pct_rbe"] = det["normalise"] / base["rbe"]; det["par_logement"] = det["normalise"] / nb212 demo = demonstration_reserve(base["rne"], base["reserve"], R)213 res_comp = reserve_par_composantes(nb_logements=nb, taux=hyp["croissance_depenses"])214 eq_capex = equivalence_capex(hyp)215 sens = sensibilites(base, hyp, R, nb)216 # revenu net et ratio si les loyers étaient au marché (même convention que l'exercice 29.7)217 rbe_m = fin.revenu_brut_effectif(base["rbp_marche"], hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"])218 de_m = base["depenses_hors_reserve"] - 0.04 * base["rbe"] + 0.04 * rbe_m219 noi_m = fin.rne(rbe_m, de_m, base["reserve"])220 marche = {"rbe": rbe_m, "depenses_hors_reserve": de_m, "rne": noi_m, "ratio": (de_m + base["reserve"]) / rbe_m,221 "loyer_moyen": base["rbp_marche"] / 12 / nb, "loyer_moyen_contrat": base["rbp_contrat"] / 12 / nb}222 comps_stats = {"n": len(comps), "ratio_median": float(comps["ratio_depenses"].median()), "ratio_q1": float(comps["ratio_depenses"].quantile(0.25)),223 "ratio_q3": float(comps["ratio_depenses"].quantile(0.75)), "ratio_min": float(comps["ratio_depenses"].min()), "ratio_max": float(comps["ratio_depenses"].max()),224 "corr_ratio_loyer": float(comps[["ratio_depenses", "loyer_mensuel_moyen"]].corr().iloc[0, 1]),225 "loyer_median": float(comps["loyer_mensuel_moyen"].median())}226 # revenu net historique 2025 « tel que déclaré » (sans gestion, sans indexation) pour l'erreur du RNE historique227 dep_2025 = float(hist.loc["Total", 2025]); noi_hist = base["rbe"] - dep_2025228 return {**d, "base": base, "R": R, "taux": tx, "ipc": indexation_ipc(), "nb_logements": nb, "surface_m2": surf, "historique": hist, "alternative": alt, "classement": cl, "detail": det,229 "demo_reserve": demo, "reserve_composantes": res_comp, "equivalence_capex": eq_capex, "sensibilites": sens, "marche": marche, "comps_stats": comps_stats,230 "rne_par_logement": base["rne"] / nb, "rne_par_m2": base["rne"] / surf, "rne_pct_rbe": base["rne"] / base["rbe"],231 "depenses_2025_declarees": dep_2025, "rne_historique_2025": noi_hist, "ecart_normalisation": base["depenses_totales"] - dep_2025,232 "V_historique": noi_hist / R, "V": base["rne"] / R, "cas2": cas2_detail()}233234235def exercices(res: dict) -> dict:236 """Chiffres des solutions (solutions/ch28.tex)."""237 base, hyp, R = res["base"], res["hyp"], res["R"]238 ex = {}239 # 28.1 — classement et ratios d'un huit logements (données de l'énoncé)240 rbe1 = 8 * 1250 * 12 * (1 - 0.04) + 3600241 postes1 = {"Taxes municipales": 1450000 * 0.004631, "Taxe scolaire": 1450000 * 0.0007899, "Assurances": 7400.0, "Électricité des aires communes": 2100.0,242 "Entretien et réparations": 6800.0, "Déneigement et paysagement": 3900.0, "Gestion (5 % du RBE)": 0.05 * rbe1, "Réserve (600 $/log.)": 8 * 600.0}243 fixes1 = postes1["Taxes municipales"] + postes1["Taxe scolaire"] + postes1["Assurances"]; res1 = postes1["Réserve (600 $/log.)"]244 var1 = sum(postes1.values()) - fixes1 - res1245 ex["ex1"] = {"rbe": rbe1, "postes": postes1, "fixes": fixes1, "variables": var1, "reserve": res1, "total": fixes1 + var1 + res1,246 "rne": rbe1 - fixes1 - var1 - res1, "ratio": (fixes1 + var1 + res1) / rbe1, "rne_par_logement": (rbe1 - fixes1 - var1 - res1) / 8,247 "part_fixes": fixes1 / (fixes1 + var1 + res1)}248 # 28.2 — normalisation : dernière année indexée vs moyenne indexée pour trois postes du cas 3249 alt = res["alternative"]250 ex["ex2"] = {p: {"derniere": alt.loc[p, "derniere_indexee"], "moyenne": alt.loc[p, "moyenne_indexee"], "ecart": alt.loc[p, "ecart"]}251 for p in ["Assurances", "Entretien et réparations", "Conciergerie"]}252 ex["ex2"]["ecart_total"] = sum(v["ecart"] for k, v in ex["ex2"].items() if isinstance(v, dict))253 ex["ex2"]["dV"] = ex["ex2"]["ecart_total"] / R254 # 28.3 — taxes : rôle × taux vs compte indexé ; effet sur RNE et V255 role = hyp["valeur_role_2026"]; taxe_base = role * hyp["taux_taxe_fonciere"]; taxe_idx = 10631 * 1.053256 ex["ex3"] = {"taxe_base": taxe_base, "taxe_indexee": taxe_idx, "ecart": taxe_idx - taxe_base, "rne_alt": base["rne"] - (taxe_idx - taxe_base),257 "V_base": base["rne"] / R, "V_alt": (base["rne"] - (taxe_idx - taxe_base)) / R, "dV": -(taxe_idx - taxe_base) / R,258 "scolaire_sans_exemption": role * hyp["taux_taxe_scolaire"], "scolaire_avec_exemption": (role - 25000) * hyp["taux_taxe_scolaire"],259 "ecart_exemption": 25000 * hyp["taux_taxe_scolaire"]}260 # 28.4 — réserve : linéaire vs fonds d'amortissement à 3 % et à 5 %261 rc3 = reserve_par_composantes(taux=0.03); rc5 = reserve_par_composantes(taux=0.05)262 ex["ex4"] = {"lineaire": rc3["lineaire_par_logement"], "fonds_3": rc3["fonds_par_logement"], "fonds_5": rc5["fonds_par_logement"],263 "lineaire_immeuble": rc3["lineaire_immeuble"], "fonds_3_immeuble": rc3["fonds_immeuble"], "convention": base["reserve"] / 12,264 "detail3": rc3["detail"]}265 # 28.5 — cohérence réserve/taux sur un comparable de 20 logements266 noi_c, rr_c, P_c = 182000.0, 20 * 675.0, 4100000.0267 R_c_apres = noi_c / P_c; R_c_avant = (noi_c + rr_c) / P_c268 ex["ex5"] = {"R_apres": R_c_apres, "R_avant": R_c_avant, "ecart_pb": (R_c_avant - R_c_apres) * 1e4,269 "V_coherent": base["rne"] / R_c_apres, "V_err": base["rne"] / R_c_avant, "erreur_rel": (base["rne"] / R_c_avant) / (base["rne"] / R_c_apres) - 1,270 "delta_V": base["rne"] / R_c_apres - base["rne"] / R_c_avant, "delta_formule_sujet": base["reserve"] / R_c_avant,271 "ratio_reserve_comparable": rr_c / (noi_c + rr_c), "ratio_reserve_sujet": base["reserve"] / (base["rne"] + base["reserve"]),272 "V_avant_sur_avant": (base["rne"] + base["reserve"]) / R_c_avant}273 # 28.6 — gestion imputée et propriétaire-occupant (cas 2)274 c2 = res["cas2"]; b2 = c2["base"]275 noi_sans_gestion = b2["rne"] + c2["gestion_imputee"]276 ex["ex6"] = {"gestion": c2["gestion_imputee"], "rne_avec": b2["rne"], "rne_sans": noi_sans_gestion, "ratio_avec": b2["ratio_depenses"],277 "ratio_sans": (b2["depenses_totales"] - c2["gestion_imputee"]) / b2["rbe"], "gestion_5": 0.05 * b2["rbe"], "rne_5": b2["rne"] - 0.01 * b2["rbe"],278 "taux_median_plex": c2["indicateurs"]["taux_median"], "V_avec": b2["rne"] / c2["indicateurs"]["taux_median"],279 "V_sans": noi_sans_gestion / c2["indicateurs"]["taux_median"], "surevaluation": noi_sans_gestion / b2["rne"] - 1}280 # 28.7 — CAPEX vs réserve : annuité équivalente et double comptage281 eq = res["equivalence_capex"]282 ex["ex7"] = {**eq, "rne_double": base["rne"] - eq["annuite_equivalente"], "V_double": (base["rne"] - eq["annuite_equivalente"]) / R,283 "erreur_rel_double": -eq["annuite_equivalente"] / base["rne"], "va_a_R": None}284 # valeur actuelle de la réserve perpétuelle vs CAPEX perpétuels : composante de 2 200 $/log. tous les 12 ans à 3 %285 C = 2200.0 * 12; N = 12; r = 0.03286 ex["ex7"]["va_capex_perpetuel"] = C / ((1 + r) ** N - 1); ex["ex7"]["reserve_equiv"] = C * fin.facteur_fonds_amortissement(r, N)287 ex["ex7"]["va_reserve_perpetuelle"] = ex["ex7"]["reserve_equiv"] / r288 return ex289290291def _f(x): return f"{x:,.2f}".replace(",", " ").replace(".", ",")292293294if __name__ == "__main__":295 pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30)296 res = calculer(); b = res["base"]297 print("=== Cas 3 — historique 2023-2025 ($)"); print(res["historique"].round(3).to_string())298 print("\n=== Cas 3 — dépenses normalisées (première année)"); print(res["detail"].round(2).to_string())299 print(f"\nRBE {_f(b['rbe'])} | DE hors réserve {_f(b['depenses_hors_reserve'])} | réserve {_f(b['reserve'])} | total {_f(b['depenses_totales'])} | ratio {b['ratio_depenses']*100:.2f} %")300 print(f"RNE {_f(b['rne'])} | par logement {_f(res['rne_par_logement'])} | par m² {_f(res['rne_par_m2'])} | RNE/RBE {res['rne_pct_rbe']*100:.1f} %")301 cl = res["classement"]302 print(f"Fixes {_f(cl['fixes'])} ({cl['part_fixes']*100:.1f} %) | variables {_f(cl['variables'])} ({cl['part_variables']*100:.1f} %) | réserve {_f(cl['reserve'])} ({cl['part_reserve']*100:.1f} %)")303 print(f"Dépenses par logement {_f(cl['total_par_logement'])} | par m² {_f(cl['total_par_m2'])}")304 print("Groupes (% du RBE) :"); print((cl["groupes_pct_rbe"] * 100).round(2).to_string())305 print(f"\nDépenses déclarées 2025 {_f(res['depenses_2025_declarees'])} ; écart de normalisation {_f(res['ecart_normalisation'])} ; RNE historique {_f(res['rne_historique_2025'])} ; V hist {_f(res['V_historique'])} vs V {_f(res['V'])}")306 print("\n=== Normalisation alternative (moyenne indexée vs dernière indexée)"); print(res["alternative"].round(2).to_string())307 print("\n=== Démonstration réserve / taux"); print({k: round(v, 6) for k, v in res["demo_reserve"].items()})308 print("\n=== Réserve par composantes (hypothèses pédagogiques)"); print(res["reserve_composantes"]["detail"].round(2).to_string())309 print({k: round(v, 2) for k, v in res["reserve_composantes"].items() if k != "detail"})310 print("\n=== CAPEX programmés → annuité équivalente"); print({k: (round(v, 2) if isinstance(v, float) else v) for k, v in res["equivalence_capex"].items()})311 print("\n=== Loyers au marché"); print({k: round(v, 4) for k, v in res["marche"].items()})312 print("\n=== Comparables : ratios"); print({k: round(v, 4) for k, v in res["comps_stats"].items()})313 s = res["sensibilites"]314 print("\n=== Sensibilité réserve"); print(s["reserve"].round(4).to_string())315 print("\n=== Sensibilité taxes"); print(s["taxe"].round(4).to_string())316 print("\n=== Sensibilité gestion"); print(s["gestion"].round(4).to_string())317 print(f"dV/dDE = {s['dV_dDE']:.3f} ; élasticité RNE/dépenses = {s['elasticite_rne_depenses']:.3f} ; élasticité RNE/RBE = {s['elasticite_rne_rbe']:.3f} ; dépenses équivalentes à 25 pb = {s['depenses_equiv_25pb']*res['R']:.2f}")318 print("IPC 2015-04 → 2026-09 :", {k: round(v, 4) for k, v in res["ipc"].items()})319 c2 = res["cas2"]320 print("\n=== Cas 2"); print(c2["detail"].round(2).to_string())321 print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in c2["base"].items() if k != "detail"})322 print("classement :", {k: (round(v, 4) if isinstance(v, float) else None) for k, v in c2["classement"].items() if isinstance(v, float)})323 print("taxes Québec recalculées :", {k: round(v, 2) for k, v in c2["taxes_quebec_recalc"].items()})324 print(f"RNE/log {_f(c2['rne_par_logement'])} ; RNE/m² {_f(c2['rne_par_m2'])} ; réserve/log {_f(c2['reserve_par_logement'])}")325 print("\n=== Exercices")326 ex = exercices(res)327 for k, v in ex.items():328 print(k, {kk: (round(vv, 4) if isinstance(vv, float) else ("<df>" if isinstance(vv, pd.DataFrame) else vv)) for kk, vv in v.items()} if isinstance(v, dict) else v)329