#!/usr/bin/env python3 """Chapitre 28 — Dépenses, réserves, CAPEX et RNE : calculs du cas 3 (12 logements, Rosemont) et du cas 2 (triplex, Limoilou). DONNÉES SYNTHÉTIQUES (graine 20261031). Tous les chiffres cités dans manuscript/chapters/ch28.tex et solutions/ch28.tex sortent d'ici. Les calculs partagés (revenus, dépenses normalisées, RNE) viennent de traite.cas3 et traite.cas2, pour coïncider avec les chapitres 29, 30, 32, 46 et 47 ; les formules financières viennent de traite.finance. Aucune formule partagée n'est réécrite ; les calculs propres au chapitre (classement, normalisation alternative, équivalence réserve/CAPEX, sensibilités) sont ici. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch28_depenses.py """ from __future__ import annotations import sys, pathlib import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from traite import cas3, cas2, finance as fin ROOT = pathlib.Path(__file__).resolve().parents[3] # Taux global médian extrait au chapitre 29 (traite.cas3.taux_extraits ; revenu net APRÈS réserve). Repris ici pour les # démonstrations de cohérence ; recalculé par calculer() et vérifié par le test. GROUPES = { # regroupement des postes pour les figures et les ratios de référence "Taxes municipales": "Taxes foncières", "Taxe scolaire": "Taxes foncières", "Assurances": "Assurances", "Électricité (aires communes)": "Énergie des aires communes", "Chauffage (aires communes, gaz)": "Énergie des aires communes", "Eau et égouts (taxe)": "Taxes foncières", "Entretien et réparations": "Entretien et réparations", "Conciergerie": "Conciergerie et services", "Déneigement et paysagement": "Conciergerie et services", "Déneigement et entretien extérieur": "Conciergerie et services", "Frais juridiques et comptables": "Honoraires et publicité", "Publicité et location": "Honoraires et publicité", "Gestion (4 % du RBE)": "Gestion", "Gestion (propriétaire-occupant : imputée 4 %)": "Gestion", "Réserve de remplacement (par logement)": "Réserve de remplacement", "Réserve de remplacement": "Réserve de remplacement", } ORDRE_GROUPES = ["Taxes foncières", "Assurances", "Énergie des aires communes", "Entretien et réparations", "Conciergerie et services", "Honoraires et publicité", "Gestion", "Réserve de remplacement"] # Composantes à vie courte couvertes par la réserve — HYPOTHÈSES PÉDAGOGIQUES déclarées (coût unitaire par logement, durée # de vie en années). Elles servent uniquement à illustrer les deux méthodes de calcul d'une réserve (linéaire et fonds # d'amortissement) ; elles ne proviennent d'aucun barème publié. COMPOSANTES_RESERVE = [ ("Cuisinière et réfrigérateur", 2200.0, 12), ("Chauffe-eau", 1100.0, 10), ("Revêtements de sol", 3600.0, 20), ("Peinture et finition intérieure", 1500.0, 7), ] def _groupes(detail: pd.DataFrame, col: str = "normalise") -> pd.Series: g = detail.assign(groupe=detail["poste"].map(GROUPES)).groupby("groupe")[col].sum() return g.reindex(ORDRE_GROUPES).fillna(0.0) def historique_cas3(dep: pd.DataFrame) -> pd.DataFrame: """Pivot 2023-2025 par poste, avec variation totale 2025/2023 et taux de croissance annuel composé.""" piv = dep.pivot(index="poste", columns="annee", values="montant").astype(float) ordre = ["Taxes municipales", "Taxe scolaire", "Assurances", "Électricité (aires communes)", "Chauffage (aires communes, gaz)", "Eau et égouts (taxe)", "Entretien et réparations", "Conciergerie", "Déneigement et paysagement", "Frais juridiques et comptables", "Publicité et location", "Réserve de remplacement (par logement)"] piv = piv.reindex(ordre) piv.loc["Total"] = piv.sum() with np.errstate(divide="ignore", invalid="ignore"): piv["var_2025_2023"] = piv[2025] / piv[2023] - 1 piv["tcac"] = (piv[2025] / piv[2023]) ** 0.5 - 1 return piv def normalisation_alternative(dep: pd.DataFrame, g: float = 0.03, annee_cible: int = 2026) -> pd.DataFrame: """Pour les postes indexés : (a) dernière année × (1+g) [règle du livre] ; (b) moyenne des trois exercices, chacun ramené en dollars de l'année cible au taux g (moyenne indexée). Permet de comparer les deux conventions.""" piv = dep.pivot(index="poste", columns="annee", values="montant").astype(float) rows = [] for poste, r in piv.iterrows(): indexes = {an: r[an] * (1 + g) ** (annee_cible - an) for an in (2023, 2024, 2025)} rows.append({"poste": poste, "derniere_indexee": indexes[2025], "moyenne_indexee": float(np.mean(list(indexes.values()))), "min_indexe": min(indexes.values()), "max_indexe": max(indexes.values())}) out = pd.DataFrame(rows).set_index("poste") out["ecart"] = out["moyenne_indexee"] - out["derniere_indexee"] return out def reserve_par_composantes(composantes=COMPOSANTES_RESERVE, nb_logements: int = 12, taux: float = 0.03) -> dict: """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é).""" rows = [] for nom, cout, vie in composantes: lin = cout / vie sff = cout * fin.facteur_fonds_amortissement(taux, vie) rows.append({"composante": nom, "cout_par_logement": cout, "duree_vie": vie, "lineaire_par_logement": lin, "fonds_par_logement": sff, "lineaire_immeuble": lin * nb_logements, "fonds_immeuble": sff * nb_logements}) df = pd.DataFrame(rows) return {"detail": df, "lineaire_par_logement": float(df["lineaire_par_logement"].sum()), "fonds_par_logement": float(df["fonds_par_logement"].sum()), "lineaire_immeuble": float(df["lineaire_immeuble"].sum()), "fonds_immeuble": float(df["fonds_immeuble"].sum()), "taux": taux} def demonstration_reserve(noi_apres: float, reserve: float, R_apres: float) -> dict: """Cohérence réserve/taux : V cohérent ; taux « avant réserve » équivalent R' = R + RR/V ; valeur erronée si R' est appliqué au revenu après réserve ; écart exact ΔV = RR/R' ; erreur relative −RR/(RNE+RR).""" V = fin.capitalisation_directe(noi_apres, R_apres) R_avant = fin.taux_global_extrait(noi_apres + reserve, V) V_err = fin.capitalisation_directe(noi_apres, R_avant) V_err_inverse = fin.capitalisation_directe(noi_apres + reserve, R_apres) # taux après réserve sur revenu avant réserve return {"V": V, "R_apres": R_apres, "R_avant": R_avant, "ecart_taux_pb": (R_avant - R_apres) * 1e4, "V_err": V_err, "delta_V": V - V_err, "delta_V_formule": reserve / R_avant, "erreur_relative": V_err / V - 1, "erreur_relative_formule": -reserve / (noi_apres + reserve), "approx_reserve_sur_R": reserve / R_apres, "V_err_inverse": V_err_inverse, "erreur_relative_inverse": V_err_inverse / V - 1} def equivalence_capex(hyp: dict, r: float | None = None, T: int | None = None) -> dict: """Les CAPEX programmés du dossier (flux actualisés) convertis en charge annuelle équivalente sur l'horizon : montre ce qu'une réserve « pour travaux lourds » représenterait si on la comptait en plus des CAPEX (double comptage).""" r = hyp["taux_actualisation"] if r is None else r; T = hyp["horizon_ans"] if T is None else T an0 = int(hyp["date_evaluation"][:4]) flux = [0.0] * (T + 1) for an, m in hyp["capex"].items(): t = int(an) - an0 + 1 if 1 <= t <= T: flux[t] = float(m) va = fin.van(flux, r) # flux[0] = 0 à t = 0 annuite = va * fin.facteur_recouvrement(r, T) total = float(sum(hyp["capex"].values())) return {"capex": {int(k): float(v) for k, v in hyp["capex"].items()}, "total": total, "va": va, "annuite_equivalente": annuite, "moyenne_simple": total / T, "r": r, "T": T} # IPC Québec, ensemble, non désaisonnalisé, 2002 = 100 (Statistique Canada, tableau 18-10-0004-01, vecteur v41691783, # data/raw/statcan_18100004/18100004.csv, diffusion du 2026-10-19) : avril 2015 = 124,7 ; septembre 2026 = 167,7. IPC_QC_2015_04 = 124.7 IPC_QC_2026_09 = 167.7 def indexation_ipc(base: float = IPC_QC_2015_04, cible: float = IPC_QC_2026_09) -> dict: """Facteur d'indexation du barème SCHL d'avril 2015 à septembre 2026 et barème indexé (entretien 500 $, salaires 300 $, réserve 100 $/log.).""" f = cible / base return {"facteur": f, "variation": f - 1, "entretien_500": 500 * f, "salaires_300": 300 * f, "salaires_125": 125 * f, "reserve_100": 100 * f} def classement(detail: pd.DataFrame, rbe: float, nb_log: int, surface_m2: float) -> dict: d = detail.copy() fixes = float(d.loc[d["type"] == "fixe", "normalise"].sum()); variables = float(d.loc[d["type"] == "variable", "normalise"].sum()) reserve = float(d.loc[d["type"] == "réserve", "normalise"].sum()); total = fixes + variables + reserve gestion = float(d.loc[d["poste"].str.startswith("Gestion"), "normalise"].sum()) return {"fixes": fixes, "variables": variables, "variables_hors_gestion": variables - gestion, "gestion": gestion, "reserve": reserve, "total": total, "part_fixes": fixes / total, "part_variables": variables / total, "part_reserve": reserve / total, "fixes_pct_rbe": fixes / rbe, "variables_pct_rbe": variables / rbe, "reserve_pct_rbe": reserve / rbe, "total_pct_rbe": total / rbe, "total_par_logement": total / nb_log, "total_par_m2": total / surface_m2, "groupes": _groupes(d), "groupes_pct_rbe": _groupes(d) / rbe, "groupes_par_logement": _groupes(d) / nb_log} def rbe_fonction_loyer(loyer_moyen: float, nb_log: int, v: float, autres: float) -> float: return fin.revenu_brut_effectif(loyer_moyen * nb_log * 12, v, autres) def ratio_iso_depenses(loyer_moyen, base: dict, hyp: dict, nb_log: int = 12, gestion: float = 0.04): """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).""" L = np.asarray(loyer_moyen, dtype=float) rbe = rbe_fonction_loyer(L, nb_log, hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"]) de_hors_gestion = base["depenses_hors_reserve"] - gestion * base["rbe"] return (de_hors_gestion + gestion * rbe + base["reserve"]) / rbe def sensibilites(base: dict, hyp: dict, R: float, nb_log: int = 12) -> dict: """Sensibilité du RNE (et de V = RNE/R à taux inchangé) à la réserve par logement, au taux de taxe et au taux de gestion.""" rbe = base["rbe"]; de_hors_res = base["depenses_hors_reserve"]; res0 = base["reserve"]; noi0 = base["rne"]; V0 = noi0 / R role = hyp["valeur_role_2026"]; taxe0 = role * hyp["taux_taxe_fonciere"]; gestion0 = 0.04 * rbe # réserve par logement rows_r = [] for rpl in [0, 100, 300, 500, 675.25, 900, 1200]: res = rpl * nb_log; noi = fin.rne(rbe, de_hors_res, res) 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}) # taux de taxe municipale ($/100 $), rôle inchangé rows_t = [] scen = [("Montréal, taxe générale 2026 (base)", hyp["taux_taxe_fonciere"]), ("Compte 2025 indexé de 5,3 % (Rosemont)", 10631 * 1.053 / role), ("Gatineau, six logements ou plus (6,964 $/1 000 $)", 0.006964), ("Québec, six logements et plus (0,7697 $/100 $)", 0.007697), ("Taux + 10 points de base", hyp["taux_taxe_fonciere"] + 0.001)] for nom, tau in scen: taxe = role * tau; noi = fin.rne(rbe, de_hors_res - taxe0 + taxe, res0) 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}) # taux de gestion rows_g = [] for m in [0.0, 0.03, 0.04, 0.05, 0.06, 0.10]: noi = fin.rne(rbe, de_hors_res - gestion0 + m * rbe, res0) rows_g.append({"gestion_pct": m, "gestion": m * rbe, "rne": noi, "V": noi / R, "dV_pct": noi / noi0 - 1}) # dérivée : un dollar de dépense = 1/R dollars de valeur return {"reserve": pd.DataFrame(rows_r), "taxe": pd.DataFrame(rows_t), "gestion": pd.DataFrame(rows_g), "dV_dDE": -1.0 / R, "V0": V0, "elasticite_rne_depenses": -(de_hors_res + res0) / noi0, # élasticité du RNE au RBE lorsque seule la gestion (4 % du RBE) s'ajuste : d RNE / d RBE = 1 − m ; élasticité = (1 − m) RBE / RNE "elasticite_rne_rbe": 0.96 * rbe / noi0, "depenses_equiv_25pb": noi0 / R - noi0 / (R + 0.0025)} def cas2_detail() -> dict: d = cas2.charger(); b = cas2.rne(d["loyers"], d["depenses"]); s = d["sujet"] det = b["detail"].copy(); det["type_livre"] = det["type"].replace({"imputée": "variable"}) det_cl = pd.DataFrame({"poste": det["poste"], "normalise": det["normalise"], "type": det["type_livre"]}) cl = classement(det_cl, b["rbe"], s["nb_logements"], s["superficie_habitable_m2"]) # vérification des taxes de Québec 2026 : taux 1-5 logements + tarifs par logement ; taxe scolaire sur (rôle − 25 000 $) role = s["valeur_role_2025"]; n = s["nb_logements"] taxe_gen = role * 0.007464; tarifs = n * (386 + 195); taxe_mun_recalc = taxe_gen + tarifs scol_recalc = max(role - 25000, 0) * 0.0007899 ind = cas2.indicateurs_marche(cas2.comparables_plex(d["ventes"], s)) 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"], "taxes_quebec_recalc": {"taxe_generale": taxe_gen, "tarifs": tarifs, "total": taxe_mun_recalc, "declaree": 5900.0, "ecart": 5900.0 - taxe_mun_recalc, "scolaire_recalc": scol_recalc, "scolaire_declaree": 470.0}, "gestion_imputee": float(det.loc[det["type"] == "imputée", "normalise"].sum()), "reserve_par_logement": b["reserve"] / n} def calculer() -> dict: d = cas3.charger(); etat, dep, hyp, sujet, comps = d["etat"], d["dep"], d["hyp"], d["sujet"], d["comps"] base = cas3.rne(etat, dep, hyp); tx = cas3.taux_extraits(comps); R = tx["mediane"] nb = sujet["nb_logements"]; surf = sujet["superficie_locative_m2"] hist = historique_cas3(dep); alt = normalisation_alternative(dep, hyp["croissance_depenses"]) cl = classement(base["detail"], base["rbe"], nb, surf) 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"))) det["pct_rbe"] = det["normalise"] / base["rbe"]; det["par_logement"] = det["normalise"] / nb demo = demonstration_reserve(base["rne"], base["reserve"], R) res_comp = reserve_par_composantes(nb_logements=nb, taux=hyp["croissance_depenses"]) eq_capex = equivalence_capex(hyp) sens = sensibilites(base, hyp, R, nb) # revenu net et ratio si les loyers étaient au marché (même convention que l'exercice 29.7) rbe_m = fin.revenu_brut_effectif(base["rbp_marche"], hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"]) de_m = base["depenses_hors_reserve"] - 0.04 * base["rbe"] + 0.04 * rbe_m noi_m = fin.rne(rbe_m, de_m, base["reserve"]) marche = {"rbe": rbe_m, "depenses_hors_reserve": de_m, "rne": noi_m, "ratio": (de_m + base["reserve"]) / rbe_m, "loyer_moyen": base["rbp_marche"] / 12 / nb, "loyer_moyen_contrat": base["rbp_contrat"] / 12 / nb} comps_stats = {"n": len(comps), "ratio_median": float(comps["ratio_depenses"].median()), "ratio_q1": float(comps["ratio_depenses"].quantile(0.25)), "ratio_q3": float(comps["ratio_depenses"].quantile(0.75)), "ratio_min": float(comps["ratio_depenses"].min()), "ratio_max": float(comps["ratio_depenses"].max()), "corr_ratio_loyer": float(comps[["ratio_depenses", "loyer_mensuel_moyen"]].corr().iloc[0, 1]), "loyer_median": float(comps["loyer_mensuel_moyen"].median())} # revenu net historique 2025 « tel que déclaré » (sans gestion, sans indexation) pour l'erreur du RNE historique dep_2025 = float(hist.loc["Total", 2025]); noi_hist = base["rbe"] - dep_2025 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, "demo_reserve": demo, "reserve_composantes": res_comp, "equivalence_capex": eq_capex, "sensibilites": sens, "marche": marche, "comps_stats": comps_stats, "rne_par_logement": base["rne"] / nb, "rne_par_m2": base["rne"] / surf, "rne_pct_rbe": base["rne"] / base["rbe"], "depenses_2025_declarees": dep_2025, "rne_historique_2025": noi_hist, "ecart_normalisation": base["depenses_totales"] - dep_2025, "V_historique": noi_hist / R, "V": base["rne"] / R, "cas2": cas2_detail()} def exercices(res: dict) -> dict: """Chiffres des solutions (solutions/ch28.tex).""" base, hyp, R = res["base"], res["hyp"], res["R"] ex = {} # 28.1 — classement et ratios d'un huit logements (données de l'énoncé) rbe1 = 8 * 1250 * 12 * (1 - 0.04) + 3600 postes1 = {"Taxes municipales": 1450000 * 0.004631, "Taxe scolaire": 1450000 * 0.0007899, "Assurances": 7400.0, "Électricité des aires communes": 2100.0, "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} fixes1 = postes1["Taxes municipales"] + postes1["Taxe scolaire"] + postes1["Assurances"]; res1 = postes1["Réserve (600 $/log.)"] var1 = sum(postes1.values()) - fixes1 - res1 ex["ex1"] = {"rbe": rbe1, "postes": postes1, "fixes": fixes1, "variables": var1, "reserve": res1, "total": fixes1 + var1 + res1, "rne": rbe1 - fixes1 - var1 - res1, "ratio": (fixes1 + var1 + res1) / rbe1, "rne_par_logement": (rbe1 - fixes1 - var1 - res1) / 8, "part_fixes": fixes1 / (fixes1 + var1 + res1)} # 28.2 — normalisation : dernière année indexée vs moyenne indexée pour trois postes du cas 3 alt = res["alternative"] ex["ex2"] = {p: {"derniere": alt.loc[p, "derniere_indexee"], "moyenne": alt.loc[p, "moyenne_indexee"], "ecart": alt.loc[p, "ecart"]} for p in ["Assurances", "Entretien et réparations", "Conciergerie"]} ex["ex2"]["ecart_total"] = sum(v["ecart"] for k, v in ex["ex2"].items() if isinstance(v, dict)) ex["ex2"]["dV"] = ex["ex2"]["ecart_total"] / R # 28.3 — taxes : rôle × taux vs compte indexé ; effet sur RNE et V role = hyp["valeur_role_2026"]; taxe_base = role * hyp["taux_taxe_fonciere"]; taxe_idx = 10631 * 1.053 ex["ex3"] = {"taxe_base": taxe_base, "taxe_indexee": taxe_idx, "ecart": taxe_idx - taxe_base, "rne_alt": base["rne"] - (taxe_idx - taxe_base), "V_base": base["rne"] / R, "V_alt": (base["rne"] - (taxe_idx - taxe_base)) / R, "dV": -(taxe_idx - taxe_base) / R, "scolaire_sans_exemption": role * hyp["taux_taxe_scolaire"], "scolaire_avec_exemption": (role - 25000) * hyp["taux_taxe_scolaire"], "ecart_exemption": 25000 * hyp["taux_taxe_scolaire"]} # 28.4 — réserve : linéaire vs fonds d'amortissement à 3 % et à 5 % rc3 = reserve_par_composantes(taux=0.03); rc5 = reserve_par_composantes(taux=0.05) ex["ex4"] = {"lineaire": rc3["lineaire_par_logement"], "fonds_3": rc3["fonds_par_logement"], "fonds_5": rc5["fonds_par_logement"], "lineaire_immeuble": rc3["lineaire_immeuble"], "fonds_3_immeuble": rc3["fonds_immeuble"], "convention": base["reserve"] / 12, "detail3": rc3["detail"]} # 28.5 — cohérence réserve/taux sur un comparable de 20 logements noi_c, rr_c, P_c = 182000.0, 20 * 675.0, 4100000.0 R_c_apres = noi_c / P_c; R_c_avant = (noi_c + rr_c) / P_c ex["ex5"] = {"R_apres": R_c_apres, "R_avant": R_c_avant, "ecart_pb": (R_c_avant - R_c_apres) * 1e4, "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, "delta_V": base["rne"] / R_c_apres - base["rne"] / R_c_avant, "delta_formule_sujet": base["reserve"] / R_c_avant, "ratio_reserve_comparable": rr_c / (noi_c + rr_c), "ratio_reserve_sujet": base["reserve"] / (base["rne"] + base["reserve"]), "V_avant_sur_avant": (base["rne"] + base["reserve"]) / R_c_avant} # 28.6 — gestion imputée et propriétaire-occupant (cas 2) c2 = res["cas2"]; b2 = c2["base"] noi_sans_gestion = b2["rne"] + c2["gestion_imputee"] ex["ex6"] = {"gestion": c2["gestion_imputee"], "rne_avec": b2["rne"], "rne_sans": noi_sans_gestion, "ratio_avec": b2["ratio_depenses"], "ratio_sans": (b2["depenses_totales"] - c2["gestion_imputee"]) / b2["rbe"], "gestion_5": 0.05 * b2["rbe"], "rne_5": b2["rne"] - 0.01 * b2["rbe"], "taux_median_plex": c2["indicateurs"]["taux_median"], "V_avec": b2["rne"] / c2["indicateurs"]["taux_median"], "V_sans": noi_sans_gestion / c2["indicateurs"]["taux_median"], "surevaluation": noi_sans_gestion / b2["rne"] - 1} # 28.7 — CAPEX vs réserve : annuité équivalente et double comptage eq = res["equivalence_capex"] ex["ex7"] = {**eq, "rne_double": base["rne"] - eq["annuite_equivalente"], "V_double": (base["rne"] - eq["annuite_equivalente"]) / R, "erreur_rel_double": -eq["annuite_equivalente"] / base["rne"], "va_a_R": None} # valeur actuelle de la réserve perpétuelle vs CAPEX perpétuels : composante de 2 200 $/log. tous les 12 ans à 3 % C = 2200.0 * 12; N = 12; r = 0.03 ex["ex7"]["va_capex_perpetuel"] = C / ((1 + r) ** N - 1); ex["ex7"]["reserve_equiv"] = C * fin.facteur_fonds_amortissement(r, N) ex["ex7"]["va_reserve_perpetuelle"] = ex["ex7"]["reserve_equiv"] / r return ex def _f(x): return f"{x:,.2f}".replace(",", " ").replace(".", ",") if __name__ == "__main__": pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30) res = calculer(); b = res["base"] print("=== Cas 3 — historique 2023-2025 ($)"); print(res["historique"].round(3).to_string()) print("\n=== Cas 3 — dépenses normalisées (première année)"); print(res["detail"].round(2).to_string()) 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} %") 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} %") cl = res["classement"] 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} %)") print(f"Dépenses par logement {_f(cl['total_par_logement'])} | par m² {_f(cl['total_par_m2'])}") print("Groupes (% du RBE) :"); print((cl["groupes_pct_rbe"] * 100).round(2).to_string()) 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'])}") print("\n=== Normalisation alternative (moyenne indexée vs dernière indexée)"); print(res["alternative"].round(2).to_string()) print("\n=== Démonstration réserve / taux"); print({k: round(v, 6) for k, v in res["demo_reserve"].items()}) print("\n=== Réserve par composantes (hypothèses pédagogiques)"); print(res["reserve_composantes"]["detail"].round(2).to_string()) print({k: round(v, 2) for k, v in res["reserve_composantes"].items() if k != "detail"}) 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()}) print("\n=== Loyers au marché"); print({k: round(v, 4) for k, v in res["marche"].items()}) print("\n=== Comparables : ratios"); print({k: round(v, 4) for k, v in res["comps_stats"].items()}) s = res["sensibilites"] print("\n=== Sensibilité réserve"); print(s["reserve"].round(4).to_string()) print("\n=== Sensibilité taxes"); print(s["taxe"].round(4).to_string()) print("\n=== Sensibilité gestion"); print(s["gestion"].round(4).to_string()) 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}") print("IPC 2015-04 → 2026-09 :", {k: round(v, 4) for k, v in res["ipc"].items()}) c2 = res["cas2"] print("\n=== Cas 2"); print(c2["detail"].round(2).to_string()) print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in c2["base"].items() if k != "detail"}) print("classement :", {k: (round(v, 4) if isinstance(v, float) else None) for k, v in c2["classement"].items() if isinstance(v, float)}) print("taxes Québec recalculées :", {k: round(v, 2) for k, v in c2["taxes_quebec_recalc"].items()}) print(f"RNE/log {_f(c2['rne_par_logement'])} ; RNE/m² {_f(c2['rne_par_m2'])} ; réserve/log {_f(c2['reserve_par_logement'])}") print("\n=== Exercices") ex = exercices(res) for k, v in ex.items(): print(k, {kk: (round(vv, 4) if isinstance(vv, float) else ("" if isinstance(vv, pd.DataFrame) else vv)) for kk, vv in v.items()} if isinstance(v, dict) else v)