SPB Git
8.0 KB · 102 lines python
1"""Cas 3 — immeuble de 12 logements à Montréal (données synthétiques) : calculs PARTAGÉS par les chapitres 26-31, 33, 34, 40 et 47.2Toute valeur citée dans ces chapitres provient d'ici (ou d'un script qui appelle ces fonctions), pour que les chiffres se répondent."""3from __future__ import annotations4import json, pathlib5import numpy as np, pandas as pd6from . import finance as fin78ROOT = pathlib.Path(__file__).resolve().parents[3]9D = ROOT / "data/synthetic"1011def charger() -> dict:12    return {"etat": pd.read_csv(D / "cas3_montreal_etat_locatif.csv"), "dep": pd.read_csv(D / "cas3_montreal_depenses.csv"),13            "comps": pd.read_csv(D / "cas3_montreal_comparables.csv", parse_dates=["date_vente"]),14            "hyp": json.loads((D / "cas3_montreal_hypotheses_dcf.json").read_text()), "sujet": json.loads((D / "cas3_montreal_sujet.json").read_text())}1516def revenus(etat: pd.DataFrame, hyp: dict) -> dict:17    """RBP contractuel, RBP au marché, autres revenus, RBE (convention : vacance/pertes appliquée au RBP seulement)."""18    rbp_c = float(etat["loyer_mensuel_contrat"].sum() * 12); rbp_m = float(etat["loyer_mensuel_marche"].sum() * 12)19    autres = float(hyp["autres_revenus_annuels"]); v = float(hyp["taux_inoccupation_pertes"])20    rbe = fin.revenu_brut_effectif(rbp_c, v, autres)21    return {"rbp_contrat": rbp_c, "rbp_marche": rbp_m, "ecart_marche": rbp_m - rbp_c, "autres_revenus": autres, "taux_vacance_pertes": v,22            "vacance_pertes": rbp_c * v, "rbe": rbe}2324def depenses_normalisees(dep: pd.DataFrame, hyp: dict, rbe: float, annee_base: int = 2025, inflation: float | None = None) -> pd.DataFrame:25    """Normalisation : dernière année observée indexée d'un an (dépenses +g), taxes recalculées sur la valeur au rôle × taux,26    gestion = 4 % du RBE (imputée, même si propriétaire gère), réserve = 650 $/logement (convention du livre)."""27    g = hyp["croissance_depenses"] if inflation is None else inflation28    d = dep[dep["annee"] == annee_base].set_index("poste")["montant"].astype(float)29    norm = {}30    for poste, m in d.items():31        if poste.startswith("Taxes municipales"): norm[poste] = hyp["valeur_role_2026"] * hyp["taux_taxe_fonciere"]32        elif poste.startswith("Taxe scolaire"): norm[poste] = hyp["valeur_role_2026"] * hyp["taux_taxe_scolaire"]33        elif poste.startswith("Réserve"): norm[poste] = m  # fixée par convention, non indexée34        else: norm[poste] = m * (1 + g)35    norm["Gestion (4 % du RBE)"] = 0.04 * rbe36    out = pd.DataFrame({"poste": list(norm), "historique_2025": [d.get(k, np.nan) for k in norm], "normalise": list(norm.values())})37    out["type"] = ["réserve" if k.startswith("Réserve") else ("fixe" if any(k.startswith(x) for x in ["Taxes", "Taxe", "Assurances"]) else "variable") for k in norm]38    return out3940def rne(etat, dep, hyp) -> dict:41    r = revenus(etat, hyp); dn = depenses_normalisees(dep, hyp, r["rbe"])42    de_hors_reserve = float(dn.loc[dn["type"] != "réserve", "normalise"].sum()); reserve = float(dn.loc[dn["type"] == "réserve", "normalise"].sum())43    noi = fin.rne(r["rbe"], de_hors_reserve, reserve)44    return {**r, "depenses_hors_reserve": de_hors_reserve, "reserve": reserve, "depenses_totales": de_hors_reserve + reserve,45            "rne": noi, "ratio_depenses": (de_hors_reserve + reserve) / r["rbe"], "rne_par_logement": noi / len(etat), "detail": dn}4647def taux_extraits(comps: pd.DataFrame, n_min: int = 6, n_max: int = 30, fenetre_mois: int = 24, date_eval="2026-09-01") -> dict:48    """Extraction du taux global : médiane, moyenne, moyenne pondérée par le RNE, et régression simple sur l'état et la taille."""49    c = comps.copy()50    c = c[(c["nb_logements"].between(n_min, n_max)) & (c["date_vente"] >= pd.Timestamp(date_eval) - pd.DateOffset(months=fenetre_mois))]51    w = c["rne"] / c["rne"].sum()52    X = np.column_stack([np.ones(len(c)), c["etat_1a5"] - 3, c["nb_logements"] - 12])53    beta, *_ = np.linalg.lstsq(X, c["taux_global"], rcond=None)54    return {"n": len(c), "mediane": float(c["taux_global"].median()), "moyenne": float(c["taux_global"].mean()), "ponderee_rne": float((w * c["taux_global"]).sum()),55            "q1": float(c["taux_global"].quantile(0.25)), "q3": float(c["taux_global"].quantile(0.75)), "min": float(c["taux_global"].min()), "max": float(c["taux_global"].max()),56            "regression": {"constante": float(beta[0]), "etat": float(beta[1]), "taille": float(beta[2])}, "comparables": c}5758def projection_dcf(etat, dep, hyp, horizon: int | None = None) -> pd.DataFrame:59    """Projection annuelle : loyers contractuels indexés au taux TAL ; chaque année une fraction `rotation_annuelle` des60    logements est relouée au loyer de marché (qui croît à `croissance_loyers_marche`) ; dépenses +g ; CAPEX aux années prévues."""61    T = horizon or hyp["horizon_ans"]62    base = rne(etat, dep, hyp)63    loyers = etat["loyer_mensuel_contrat"].astype(float).values.copy(); marche = etat["loyer_mensuel_marche"].astype(float).values.copy()64    rot = hyp["rotation_annuelle"]; g_c = hyp["croissance_loyers_contrat"]; g_m = hyp["croissance_loyers_marche"]; g_d = hyp["croissance_depenses"]65    v = hyp["taux_inoccupation_pertes"]; autres = hyp["autres_revenus_annuels"]66    de0 = base["depenses_hors_reserve"]; res0 = base["reserve"]67    rows = []; annee0 = int(hyp["date_evaluation"][:4])68    rng = np.random.default_rng(3)  # ordre déterministe de relocation : les plus sous le marché d'abord69    for t in range(1, T + 2):70        if t > 1:71            loyers *= (1 + g_c); marche *= (1 + g_m)72            k = int(round(rot * len(loyers)))73            idx = np.argsort(loyers / marche)[:k]   # logements les plus sous-évalués se libèrent en premier (hypothèse)74            loyers[idx] = marche[idx]75        rbp = loyers.sum() * 12; rbe = fin.revenu_brut_effectif(rbp, v, autres * (1 + g_d) ** (t - 1))76        de = de0 * (1 + g_d) ** (t - 1); res = res077        noi = rbe - de - res78        capex = float(hyp["capex"].get(str(annee0 + t - 1), 0.0)) if t <= T else 0.079        rows.append({"annee": t, "exercice": annee0 + t - 1, "rbp": rbp, "rbp_marche": marche.sum() * 12, "rbe": rbe, "depenses": de, "reserve": res, "rne": noi, "capex": capex, "flux_avant_dette": noi - capex})80    return pd.DataFrame(rows)8182def valeur_dcf(proj: pd.DataFrame, hyp: dict) -> dict:83    T = hyp["horizon_ans"]; r = hyp["taux_actualisation"]; Rt = hyp["taux_terminal"]; cv = hyp["couts_vente"]84    flux = proj.loc[proj["annee"] <= T, "flux_avant_dette"].tolist(); noi_T1 = float(proj.loc[proj["annee"] == T + 1, "rne"].iloc[0])85    res = fin.dcf(flux, r, noi_suivant=noi_T1, taux_terminal=Rt, couts_vente=cv)86    return {"valeur": res.valeur, "va_flux": res.va_flux, "va_terminale": res.va_terminale, "vt_brute": res.valeur_terminale_brute, "vt_nette": res.valeur_terminale_nette,87            "rne_T1": noi_T1, "part_terminale": res.va_terminale / res.valeur, "flux": flux}8889def financement(valeur: float, noi_1: float, hyp: dict) -> dict:90    f = hyp["financement"]91    pret_rpv = fin.pret_max_par_rpv(valeur, f["rpv"]); pret_rcd = fin.pret_max_par_rcd(noi_1, f["rcd_min"], f["taux"], f["amortissement_mois"])92    pret = min(pret_rpv, pret_rcd); h = fin.Hypotheque(pret, f["taux"], f["amortissement_mois"])93    return {"pret_rpv": pret_rpv, "pret_rcd": pret_rcd, "pret": pret, "contrainte": "RPV" if pret_rpv <= pret_rcd else "RCD", "hypotheque": h,94            "service_dette": h.service_dette_annuel, "rcd": fin.rcd(noi_1, h.service_dette_annuel), "rpv": pret / valeur, "mise_de_fonds": valeur - pret,95            "constante": h.constante_hypothecaire, "taux_mensuel": h.taux_periodique}9697def tout() -> dict:98    d = charger(); base = rne(d["etat"], d["dep"], d["hyp"]); tx = taux_extraits(d["comps"]); proj = projection_dcf(d["etat"], d["dep"], d["hyp"])99    V_cap = fin.capitalisation_directe(float(proj.loc[proj["annee"] == 1, "rne"].iloc[0]), tx["mediane"])100    V_dcf = valeur_dcf(proj, d["hyp"]); fi = financement(V_dcf["valeur"], base["rne"], d["hyp"])101    return {**d, "base": base, "taux": tx, "projection": proj, "valeur_capitalisation": V_cap, "dcf": V_dcf, "financement": fi}102
Files · 66a30b7f12cdbf4da142dea3e146d765bc5bf70e
Menu

Theme