SPB Git
25.3 KB · 329 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}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
Files · 031fae1bd368de12dc60ff542f1be9a8fd73e699
Menu

Theme