SPB Git
24.1 KB · 314 lines python
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}119120121def classement(detail: pd.DataFrame, rbe: float, nb_log: int, surface_m2: float) -> dict:122    d = detail.copy()123    fixes = float(d.loc[d["type"] == "fixe", "normalise"].sum()); variables = float(d.loc[d["type"] == "variable", "normalise"].sum())124    reserve = float(d.loc[d["type"] == "réserve", "normalise"].sum()); total = fixes + variables + reserve125    gestion = float(d.loc[d["poste"].str.startswith("Gestion"), "normalise"].sum())126    return {"fixes": fixes, "variables": variables, "variables_hors_gestion": variables - gestion, "gestion": gestion, "reserve": reserve, "total": total,127            "part_fixes": fixes / total, "part_variables": variables / total, "part_reserve": reserve / total,128            "fixes_pct_rbe": fixes / rbe, "variables_pct_rbe": variables / rbe, "reserve_pct_rbe": reserve / rbe, "total_pct_rbe": total / rbe,129            "total_par_logement": total / nb_log, "total_par_m2": total / surface_m2, "groupes": _groupes(d),130            "groupes_pct_rbe": _groupes(d) / rbe, "groupes_par_logement": _groupes(d) / nb_log}131132133def rbe_fonction_loyer(loyer_moyen: float, nb_log: int, v: float, autres: float) -> float:134    return fin.revenu_brut_effectif(loyer_moyen * nb_log * 12, v, autres)135136137def ratio_iso_depenses(loyer_moyen, base: dict, hyp: dict, nb_log: int = 12, gestion: float = 0.04):138    """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)."""139    L = np.asarray(loyer_moyen, dtype=float)140    rbe = rbe_fonction_loyer(L, nb_log, hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"])141    de_hors_gestion = base["depenses_hors_reserve"] - gestion * base["rbe"]142    return (de_hors_gestion + gestion * rbe + base["reserve"]) / rbe143144145def sensibilites(base: dict, hyp: dict, R: float, nb_log: int = 12) -> dict:146    """Sensibilité du RNE (et de V = RNE/R à taux inchangé) à la réserve par logement, au taux de taxe et au taux de gestion."""147    rbe = base["rbe"]; de_hors_res = base["depenses_hors_reserve"]; res0 = base["reserve"]; noi0 = base["rne"]; V0 = noi0 / R148    role = hyp["valeur_role_2026"]; taxe0 = role * hyp["taux_taxe_fonciere"]; gestion0 = 0.04 * rbe149    # réserve par logement150    rows_r = []151    for rpl in [0, 100, 300, 500, 675.25, 900, 1200]:152        res = rpl * nb_log; noi = fin.rne(rbe, de_hors_res, res)153        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})154    # taux de taxe municipale ($/100 $), rôle inchangé155    rows_t = []156    scen = [("Montréal, taxe générale 2026 (base)", hyp["taux_taxe_fonciere"]),157            ("Compte 2025 indexé de 5,3 % (Rosemont)", 10631 * 1.053 / role),158            ("Gatineau, six logements ou plus (6,964 $/1 000 $)", 0.006964),159            ("Québec, six logements et plus (0,7697 $/100 $)", 0.007697),160            ("Taux + 10 points de base", hyp["taux_taxe_fonciere"] + 0.001)]161    for nom, tau in scen:162        taxe = role * tau; noi = fin.rne(rbe, de_hors_res - taxe0 + taxe, res0)163        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})164    # taux de gestion165    rows_g = []166    for m in [0.0, 0.03, 0.04, 0.05, 0.06, 0.10]:167        noi = fin.rne(rbe, de_hors_res - gestion0 + m * rbe, res0)168        rows_g.append({"gestion_pct": m, "gestion": m * rbe, "rne": noi, "V": noi / R, "dV_pct": noi / noi0 - 1})169    # dérivée : un dollar de dépense = 1/R dollars de valeur170    return {"reserve": pd.DataFrame(rows_r), "taxe": pd.DataFrame(rows_t), "gestion": pd.DataFrame(rows_g), "dV_dDE": -1.0 / R, "V0": V0,171            "elasticite_rne_depenses": -(de_hors_res + res0) / noi0}172173174def cas2_detail() -> dict:175    d = cas2.charger(); b = cas2.rne(d["loyers"], d["depenses"]); s = d["sujet"]176    det = b["detail"].copy(); det["type_livre"] = det["type"].replace({"imputée": "variable"})177    det_cl = pd.DataFrame({"poste": det["poste"], "normalise": det["normalise"], "type": det["type_livre"]})178    cl = classement(det_cl, b["rbe"], s["nb_logements"], s["superficie_habitable_m2"])179    # vérification des taxes de Québec 2026 : taux 1-5 logements + tarifs par logement ; taxe scolaire sur (rôle − 25 000 $)180    role = s["valeur_role_2025"]; n = s["nb_logements"]181    taxe_gen = role * 0.007464; tarifs = n * (386 + 195); taxe_mun_recalc = taxe_gen + tarifs182    scol_recalc = max(role - 25000, 0) * 0.0007899183    ind = cas2.indicateurs_marche(cas2.comparables_plex(d["ventes"], s))184    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"],185            "taxes_quebec_recalc": {"taxe_generale": taxe_gen, "tarifs": tarifs, "total": taxe_mun_recalc, "declaree": 5900.0, "ecart": 5900.0 - taxe_mun_recalc,186                                     "scolaire_recalc": scol_recalc, "scolaire_declaree": 470.0},187            "gestion_imputee": float(det.loc[det["type"] == "imputée", "normalise"].sum()), "reserve_par_logement": b["reserve"] / n}188189190def calculer() -> dict:191    d = cas3.charger(); etat, dep, hyp, sujet, comps = d["etat"], d["dep"], d["hyp"], d["sujet"], d["comps"]192    base = cas3.rne(etat, dep, hyp); tx = cas3.taux_extraits(comps); R = tx["mediane"]193    nb = sujet["nb_logements"]; surf = sujet["superficie_locative_m2"]194    hist = historique_cas3(dep); alt = normalisation_alternative(dep, hyp["croissance_depenses"])195    cl = classement(base["detail"], base["rbe"], nb, surf)196    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")))197    det["pct_rbe"] = det["normalise"] / base["rbe"]; det["par_logement"] = det["normalise"] / nb198    demo = demonstration_reserve(base["rne"], base["reserve"], R)199    res_comp = reserve_par_composantes(nb_logements=nb, taux=hyp["croissance_depenses"])200    eq_capex = equivalence_capex(hyp)201    sens = sensibilites(base, hyp, R, nb)202    # revenu net et ratio si les loyers étaient au marché (même convention que l'exercice 29.7)203    rbe_m = fin.revenu_brut_effectif(base["rbp_marche"], hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"])204    de_m = base["depenses_hors_reserve"] - 0.04 * base["rbe"] + 0.04 * rbe_m205    noi_m = fin.rne(rbe_m, de_m, base["reserve"])206    marche = {"rbe": rbe_m, "depenses_hors_reserve": de_m, "rne": noi_m, "ratio": (de_m + base["reserve"]) / rbe_m,207              "loyer_moyen": base["rbp_marche"] / 12 / nb, "loyer_moyen_contrat": base["rbp_contrat"] / 12 / nb}208    comps_stats = {"n": len(comps), "ratio_median": float(comps["ratio_depenses"].median()), "ratio_q1": float(comps["ratio_depenses"].quantile(0.25)),209                   "ratio_q3": float(comps["ratio_depenses"].quantile(0.75)), "ratio_min": float(comps["ratio_depenses"].min()), "ratio_max": float(comps["ratio_depenses"].max()),210                   "corr_ratio_loyer": float(comps[["ratio_depenses", "loyer_mensuel_moyen"]].corr().iloc[0, 1]),211                   "loyer_median": float(comps["loyer_mensuel_moyen"].median())}212    # revenu net historique 2025 « tel que déclaré » (sans gestion, sans indexation) pour l'erreur du RNE historique213    dep_2025 = float(hist.loc["Total", 2025]); noi_hist = base["rbe"] - dep_2025214    return {**d, "base": base, "R": R, "taux": tx, "nb_logements": nb, "surface_m2": surf, "historique": hist, "alternative": alt, "classement": cl, "detail": det,215            "demo_reserve": demo, "reserve_composantes": res_comp, "equivalence_capex": eq_capex, "sensibilites": sens, "marche": marche, "comps_stats": comps_stats,216            "rne_par_logement": base["rne"] / nb, "rne_par_m2": base["rne"] / surf, "rne_pct_rbe": base["rne"] / base["rbe"],217            "depenses_2025_declarees": dep_2025, "rne_historique_2025": noi_hist, "ecart_normalisation": base["depenses_totales"] - dep_2025,218            "V_historique": noi_hist / R, "V": base["rne"] / R, "cas2": cas2_detail()}219220221def exercices(res: dict) -> dict:222    """Chiffres des solutions (solutions/ch28.tex)."""223    base, hyp, R = res["base"], res["hyp"], res["R"]224    ex = {}225    # 28.1 — classement et ratios d'un huit logements (données de l'énoncé)226    rbe1 = 8 * 1250 * 12 * (1 - 0.04) + 3600227    postes1 = {"Taxes municipales": 1450000 * 0.004631, "Taxe scolaire": 1450000 * 0.0007899, "Assurances": 7400.0, "Électricité des aires communes": 2100.0,228               "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}229    fixes1 = postes1["Taxes municipales"] + postes1["Taxe scolaire"] + postes1["Assurances"]; res1 = postes1["Réserve (600 $/log.)"]230    var1 = sum(postes1.values()) - fixes1 - res1231    ex["ex1"] = {"rbe": rbe1, "postes": postes1, "fixes": fixes1, "variables": var1, "reserve": res1, "total": fixes1 + var1 + res1,232                 "rne": rbe1 - fixes1 - var1 - res1, "ratio": (fixes1 + var1 + res1) / rbe1, "rne_par_logement": (rbe1 - fixes1 - var1 - res1) / 8,233                 "part_fixes": fixes1 / (fixes1 + var1 + res1)}234    # 28.2 — normalisation : dernière année indexée vs moyenne indexée pour trois postes du cas 3235    alt = res["alternative"]236    ex["ex2"] = {p: {"derniere": alt.loc[p, "derniere_indexee"], "moyenne": alt.loc[p, "moyenne_indexee"], "ecart": alt.loc[p, "ecart"]}237                 for p in ["Assurances", "Entretien et réparations", "Conciergerie"]}238    ex["ex2"]["ecart_total"] = sum(v["ecart"] for k, v in ex["ex2"].items() if isinstance(v, dict))239    ex["ex2"]["dV"] = ex["ex2"]["ecart_total"] / R240    # 28.3 — taxes : rôle × taux vs compte indexé ; effet sur RNE et V241    role = hyp["valeur_role_2026"]; taxe_base = role * hyp["taux_taxe_fonciere"]; taxe_idx = 10631 * 1.053242    ex["ex3"] = {"taxe_base": taxe_base, "taxe_indexee": taxe_idx, "ecart": taxe_idx - taxe_base, "rne_alt": base["rne"] - (taxe_idx - taxe_base),243                 "V_base": base["rne"] / R, "V_alt": (base["rne"] - (taxe_idx - taxe_base)) / R, "dV": -(taxe_idx - taxe_base) / R,244                 "scolaire_sans_exemption": role * hyp["taux_taxe_scolaire"], "scolaire_avec_exemption": (role - 25000) * hyp["taux_taxe_scolaire"],245                 "ecart_exemption": 25000 * hyp["taux_taxe_scolaire"]}246    # 28.4 — réserve : linéaire vs fonds d'amortissement à 3 % et à 5 %247    rc3 = reserve_par_composantes(taux=0.03); rc5 = reserve_par_composantes(taux=0.05)248    ex["ex4"] = {"lineaire": rc3["lineaire_par_logement"], "fonds_3": rc3["fonds_par_logement"], "fonds_5": rc5["fonds_par_logement"],249                 "lineaire_immeuble": rc3["lineaire_immeuble"], "fonds_3_immeuble": rc3["fonds_immeuble"], "convention": base["reserve"] / 12,250                 "detail3": rc3["detail"]}251    # 28.5 — cohérence réserve/taux sur un comparable de 20 logements252    noi_c, rr_c, P_c = 182000.0, 20 * 675.0, 4100000.0253    R_c_apres = noi_c / P_c; R_c_avant = (noi_c + rr_c) / P_c254    ex["ex5"] = {"R_apres": R_c_apres, "R_avant": R_c_avant, "ecart_pb": (R_c_avant - R_c_apres) * 1e4,255                 "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,256                 "delta_V": base["rne"] / R_c_apres - base["rne"] / R_c_avant, "delta_formule_sujet": base["reserve"] / R_c_avant,257                 "ratio_reserve_comparable": rr_c / (noi_c + rr_c), "ratio_reserve_sujet": base["reserve"] / (base["rne"] + base["reserve"]),258                 "V_avant_sur_avant": (base["rne"] + base["reserve"]) / R_c_avant}259    # 28.6 — gestion imputée et propriétaire-occupant (cas 2)260    c2 = res["cas2"]; b2 = c2["base"]261    noi_sans_gestion = b2["rne"] + c2["gestion_imputee"]262    ex["ex6"] = {"gestion": c2["gestion_imputee"], "rne_avec": b2["rne"], "rne_sans": noi_sans_gestion, "ratio_avec": b2["ratio_depenses"],263                 "ratio_sans": (b2["depenses_totales"] - c2["gestion_imputee"]) / b2["rbe"], "gestion_5": 0.05 * b2["rbe"], "rne_5": b2["rne"] - 0.01 * b2["rbe"],264                 "taux_median_plex": c2["indicateurs"]["taux_median"], "V_avec": b2["rne"] / c2["indicateurs"]["taux_median"],265                 "V_sans": noi_sans_gestion / c2["indicateurs"]["taux_median"], "surevaluation": noi_sans_gestion / b2["rne"] - 1}266    # 28.7 — CAPEX vs réserve : annuité équivalente et double comptage267    eq = res["equivalence_capex"]268    ex["ex7"] = {**eq, "rne_double": base["rne"] - eq["annuite_equivalente"], "V_double": (base["rne"] - eq["annuite_equivalente"]) / R,269                 "erreur_rel_double": -eq["annuite_equivalente"] / base["rne"], "va_a_R": None}270    # valeur actuelle de la réserve perpétuelle vs CAPEX perpétuels : composante de 2 200 $/log. tous les 12 ans à 3 %271    C = 2200.0 * 12; N = 12; r = 0.03272    ex["ex7"]["va_capex_perpetuel"] = C / ((1 + r) ** N - 1); ex["ex7"]["reserve_equiv"] = C * fin.facteur_fonds_amortissement(r, N)273    ex["ex7"]["va_reserve_perpetuelle"] = ex["ex7"]["reserve_equiv"] / r274    return ex275276277def _f(x): return f"{x:,.2f}".replace(",", " ").replace(".", ",")278279280if __name__ == "__main__":281    pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30)282    res = calculer(); b = res["base"]283    print("=== Cas 3 — historique 2023-2025 ($)"); print(res["historique"].round(3).to_string())284    print("\n=== Cas 3 — dépenses normalisées (première année)"); print(res["detail"].round(2).to_string())285    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} %")286    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} %")287    cl = res["classement"]288    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} %)")289    print(f"Dépenses par logement {_f(cl['total_par_logement'])} | par m² {_f(cl['total_par_m2'])}")290    print("Groupes (% du RBE) :"); print((cl["groupes_pct_rbe"] * 100).round(2).to_string())291    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'])}")292    print("\n=== Normalisation alternative (moyenne indexée vs dernière indexée)"); print(res["alternative"].round(2).to_string())293    print("\n=== Démonstration réserve / taux"); print({k: round(v, 6) for k, v in res["demo_reserve"].items()})294    print("\n=== Réserve par composantes (hypothèses pédagogiques)"); print(res["reserve_composantes"]["detail"].round(2).to_string())295    print({k: round(v, 2) for k, v in res["reserve_composantes"].items() if k != "detail"})296    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()})297    print("\n=== Loyers au marché"); print({k: round(v, 4) for k, v in res["marche"].items()})298    print("\n=== Comparables : ratios"); print({k: round(v, 4) for k, v in res["comps_stats"].items()})299    s = res["sensibilites"]300    print("\n=== Sensibilité réserve"); print(s["reserve"].round(4).to_string())301    print("\n=== Sensibilité taxes"); print(s["taxe"].round(4).to_string())302    print("\n=== Sensibilité gestion"); print(s["gestion"].round(4).to_string())303    print(f"dV/dDE = {s['dV_dDE']:.3f} ; élasticité RNE/dépenses = {s['elasticite_rne_depenses']:.3f}")304    c2 = res["cas2"]305    print("\n=== Cas 2"); print(c2["detail"].round(2).to_string())306    print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in c2["base"].items() if k != "detail"})307    print("classement :", {k: (round(v, 4) if isinstance(v, float) else None) for k, v in c2["classement"].items() if isinstance(v, float)})308    print("taxes Québec recalculées :", {k: round(v, 2) for k, v in c2["taxes_quebec_recalc"].items()})309    print(f"RNE/log {_f(c2['rne_par_logement'])} ; RNE/m² {_f(c2['rne_par_m2'])} ; réserve/log {_f(c2['reserve_par_logement'])}")310    print("\n=== Exercices")311    ex = exercices(res)312    for k, v in ex.items():313        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)314
Files · d49e547b9b0535e034d80fed31849741adb255e2
Menu

Theme