"""Cas 2 — triplex à Québec, Limoilou (données synthétiques) : calculs PARTAGÉS par les chapitres 26, 27, 32 et 46. Revenus, dépenses, RNE, multiplicateurs et taux extraits des ventes de plex, comparaison par logement.""" from __future__ import annotations import json, pathlib import numpy as np, pandas as pd from . import finance as fin ROOT = pathlib.Path(__file__).resolve().parents[3] D = ROOT / "data/synthetic" TAUX_VACANCE_PERTES = 0.03 # convention du dossier : inoccupation 2,4 % (SCHL Québec) + mauvaises créances ≈ 0,6 % GESTION_IMPUTEE = 0.04 # propriétaire-occupant : gestion imputée à 4 % du RBE def charger() -> dict: return {"ventes": pd.read_csv(D / "cas2_quebec_ventes_plex.csv", parse_dates=["date_vente"]), "sujet": json.loads((D / "cas2_quebec_sujet.json").read_text()), "loyers": pd.read_csv(D / "cas2_quebec_loyers.csv"), "depenses": pd.read_csv(D / "cas2_quebec_depenses.csv")} def revenus(loyers: pd.DataFrame) -> dict: rbp_c = float(loyers["loyer_mensuel"].sum() * 12); rbp_m = float(loyers["loyer_marche_estime"].sum() * 12) rbe = fin.revenu_brut_effectif(rbp_c, TAUX_VACANCE_PERTES) return {"rbp_contrat": rbp_c, "rbp_marche": rbp_m, "ecart_marche": rbp_m - rbp_c, "taux_vacance_pertes": TAUX_VACANCE_PERTES, "rbe": rbe, "rbe_marche": fin.revenu_brut_effectif(rbp_m, TAUX_VACANCE_PERTES)} def depenses_normalisees(dep: pd.DataFrame, rbe: float) -> pd.DataFrame: d = dep.copy(); d["normalise"] = d["montant_annuel"].astype(float) d.loc[d["type"] == "imputée", "normalise"] = GESTION_IMPUTEE * rbe return d def rne(loyers, dep) -> dict: r = revenus(loyers); dn = depenses_normalisees(dep, r["rbe"]) reserve = float(dn.loc[dn["type"] == "réserve", "normalise"].sum()); autres = float(dn.loc[dn["type"] != "réserve", "normalise"].sum()) noi = fin.rne(r["rbe"], autres, reserve) return {**r, "depenses_hors_reserve": autres, "reserve": reserve, "depenses_totales": autres + reserve, "rne": noi, "ratio_depenses": (autres + reserve) / r["rbe"], "detail": dn} def comparables_plex(v: pd.DataFrame, s: dict, nb_log=(2, 3), fenetre_mois: int = 24, quartiers=("Limoilou", "Saint-Sauveur", "Saint-Roch", "Maizerets"), date_eval="2026-09-01") -> pd.DataFrame: c = v[(v["nb_logements"].isin(nb_log)) & (v["quartier"].isin(quartiers)) & (v["date_vente"] >= pd.Timestamp(date_eval) - pd.DateOffset(months=fenetre_mois))].copy() c["mois"] = (pd.Timestamp(date_eval) - c["date_vente"]).dt.days / 30.4375 return c def indicateurs_marche(c: pd.DataFrame) -> dict: tri = c[c["nb_logements"] == 3] return {"n": len(c), "n_triplex": len(tri), "mrb_median": float(c["mrb"].median()), "mrb_median_triplex": float(tri["mrb"].median()) if len(tri) else np.nan, "taux_median": float(c["taux_global"].median()), "taux_q1": float(c["taux_global"].quantile(0.25)), "taux_q3": float(c["taux_global"].quantile(0.75)), "prix_par_logement_median": float(c["prix_par_logement"].median()), "prix_par_logement_triplex": float(tri["prix_par_logement"].median()) if len(tri) else np.nan, "prix_m2_median": float((c["prix_vente"] / c["superficie_habitable_m2"]).median())} def tout() -> dict: d = charger(); base = rne(d["loyers"], d["depenses"]); c = comparables_plex(d["ventes"], d["sujet"]); ind = indicateurs_marche(c) v_mrb = ind["mrb_median_triplex"] * base["rbp_contrat"]; v_cap = fin.capitalisation_directe(base["rne"], ind["taux_median"]) v_log = ind["prix_par_logement_triplex"] * d["sujet"]["nb_logements"] return {**d, "base": base, "comparables": c, "indicateurs": ind, "valeur_mrb": v_mrb, "valeur_capitalisation": v_cap, "valeur_par_logement": v_log}