SPB Git
23.4 KB · 299 lines python
1"""Chapitre 32 — Duplex à quintuplex : calculs de l'exemple intégral (cas 2, triplex de Limoilou, données SYNTHÉTIQUES).23Tout chiffre cité dans manuscript/chapters/ch32.tex et solutions/ch32.tex provient de ce script, qui s'appuie sur les4fonctions partagées de traite.cas2 (revenus, RNE, ventes de plex), traite.finance (hypothèque canadienne, capitalisation)5et traite.comparables (distance d'attributs). Aucune formule financière n'est réécrite ici.67Exécution : python3 analysis/python/cases/ch32_plex.py   (depuis la racine du dépôt)8"""9from __future__ import annotations10import sys, pathlib11import numpy as np, pandas as pd1213ROOT = pathlib.Path(__file__).resolve().parents[3]14sys.path.insert(0, str(ROOT / "analysis/python"))15from traite import cas2, finance as fin, comparables as cmp  # noqa: E4021617# ----------------------------------------------------------------------------- paramètres déclarés du chapitre18TAUX_ASSURE = 0.0412        # BdC, fonds avancés, prêts assurés à taux fixe de 5 ans et plus, août 2026 (SRC-E-012)19TAUX_NON_ASSURE = 0.0458    # idem, prêts non assurés20AMORT_MOIS = 300            # 25 ans (30 ans possibles pour un premier acheteur ou une construction neuve, Finances Canada 2024)21PLANCHER_MQR, MARGE_MQR = 0.0525, 0.02   # BSIF, taux admissible minimal (prêts non assurés) : max(contrat + 2 %, 5,25 %)22SEUIL_5PCT, PLAFOND_ASSURABLE = 500_000.0, 1_500_000.0   # Finances Canada, en vigueur le 15 décembre 202423TAUX_SANS_RISQUE = 0.0358   # obligation du GdC 5 ans, 2026-10-29 (coût d'opportunité de la mise de fonds)24R_INVESTISSEUR_REF = 0.05   # taux global « d'investisseur » illustratif : borne basse CBRE Québec, basse hauteur B, T3 2026 (5,00-6,00 %)25POIDS_INDICATIONS = {"par_logement": 0.40, "mrb": 0.35, "capitalisation": 0.25}   # pondération raisonnée (sect. 32.4)26ECHELLES = {"superficie_habitable_m2": 30.0, "age": 15.0, "etat_1a5": 1.0, "stationnement": 1.0, "loyer_relatif": 0.05, "mois": 6.0}27POIDS_DIST = {"superficie_habitable_m2": 1.5, "age": 1.0, "etat_1a5": 1.0, "stationnement": 0.5, "loyer_relatif": 1.5, "mois": 1.0}28K_COMPARABLES = 5293031# ----------------------------------------------------------------------------- règles de financement (vérifiées, notes-C)32def mise_de_fonds_minimale_assuree(prix: float) -> float:33    """Mise de fonds minimale d'un prêt assuré (Finances Canada, 15 déc. 2024) : 5 % jusqu'à 500 000 $, 10 % de la tranche34    entre 500 000 $ et 1,5 M$ ; au-delà de 1,5 M$ le prêt n'est pas assurable (retourne NaN)."""35    if prix > PLAFOND_ASSURABLE:36        return float("nan")37    return 0.05 * min(prix, SEUIL_5PCT) + 0.10 * max(prix - SEUIL_5PCT, 0.0)383940def financement_occupant(prix: float, mise_de_fonds: float, taux: float, amort_mois: int = AMORT_MOIS) -> dict:41    """Prêt, paiement mensuel (composition semestrielle), service annuel, constante, paiement au taux admissible minimal."""42    pret = prix - mise_de_fonds43    h = fin.Hypotheque(pret, taux, amort_mois)44    tam = fin.taux_admissible_minimal(taux, PLANCHER_MQR, MARGE_MQR)45    h_tam = fin.Hypotheque(pret, tam, amort_mois)46    return {"prix": prix, "mise_de_fonds": mise_de_fonds, "rpv": fin.rpv(pret, prix), "pret": pret, "taux": taux, "amort_mois": amort_mois,47            "paiement_mensuel": h.paiement, "service_dette": h.service_dette_annuel, "constante": h.constante_hypothecaire,48            "taux_admissible": tam, "paiement_mensuel_tam": h_tam.paiement, "solde_60": h.solde(60), "capital_rembourse_5ans": pret - h.solde(60)}495051def cout_occupation(fi: dict, base: dict, loyers: pd.DataFrame, logement_occupe: str, taux_opportunite: float = TAUX_SANS_RISQUE) -> dict:52    """Coût net annuel et mensuel d'occupation pour un acheteur-occupant qui reprend `logement_occupe` :53    service de la dette + dépenses d'exploitation hors gestion imputée (il gère lui-même) + coût d'opportunité de la mise de fonds54    − loyers des autres logements (après inoccupation et pertes), comparé au loyer de marché du logement occupé."""55    d = base["detail"]56    dep_hors_gestion = float(d.loc[d["type"] != "imputée", "normalise"].sum())57    autres = loyers[loyers["logement"] != logement_occupe]58    loyers_percus = float(autres["loyer_mensuel"].sum() * 12) * (1 - cas2.TAUX_VACANCE_PERTES)59    occ = loyers[loyers["logement"] == logement_occupe].iloc[0]60    opp = fi["mise_de_fonds"] * taux_opportunite61    brut = fi["service_dette"] + dep_hors_gestion + opp62    net = brut - loyers_percus63    net_hors_capital = net - fi["capital_rembourse_5ans"] / 5.064    return {"logement": logement_occupe, "loyer_marche_logement": float(occ["loyer_marche_estime"]), "loyer_contrat_logement": float(occ["loyer_mensuel"]),65            "depenses_hors_gestion": dep_hors_gestion, "cout_opportunite": opp, "loyers_percus_nets": loyers_percus, "sortie_brute": brut,66            "cout_net_annuel": net, "cout_net_mensuel": net / 12, "cout_net_mensuel_hors_capital": net_hors_capital / 12,67            "capital_annuel_moyen_5ans": fi["capital_rembourse_5ans"] / 5.0}686970# ----------------------------------------------------------------------------- indicateurs de marché et décomposition du taux71def indicateurs_par_groupe(c: pd.DataFrame, cle: str) -> pd.DataFrame:72    g = c.assign(prix_m2=c["prix_vente"] / c["superficie_habitable_m2"], loyer_relatif=c["loyer_mensuel_moyen_contrat"] / c["loyer_mensuel_moyen_marche"])73    out = g.groupby(cle).agg(n=("prix_vente", "size"), prix_median=("prix_vente", "median"), prix_par_logement=("prix_par_logement", "median"),74                             prix_m2=("prix_m2", "median"), mrb=("mrb", "median"), taux=("taux_global", "median"), loyer_relatif=("loyer_relatif", "median"),75                             ratio_depenses=("ratio_depenses", "median"))76    return out777879def decomposition_taux(c: pd.DataFrame, inocc: float = 0.024) -> pd.DataFrame:80    """Pour chaque vente : loyer relatif ρ = contrat/marché, taux extrait au contrat R_c = RNE_c/P, taux « au marché »81    R_m = RBP_m (1 − v)(1 − d)/P avec le ratio de dépenses d de la vente, MRB au marché, et identité R_c = ρ R_m (dépenses proportionnelles)."""82    g = c.copy()83    g["loyer_relatif"] = g["loyer_mensuel_moyen_contrat"] / g["loyer_mensuel_moyen_marche"]84    g["rbp_marche"] = g["loyer_mensuel_moyen_marche"] * g["nb_logements"] * 1285    g["rne_marche"] = g["rbp_marche"] * (1 - inocc) * (1 - g["ratio_depenses"])86    g["taux_marche"] = g["rne_marche"] / g["prix_vente"]87    g["mrb_marche"] = g["prix_vente"] / g["rbp_marche"]88    g["taux_contrat_reconstitue"] = g["loyer_relatif"] * g["taux_marche"]89    return g909192def regression_taux_loyer_relatif(g: pd.DataFrame) -> dict:93    """OLS : taux_global = a + b (ρ − 1) + ε, puis variante avec l'état (centré sur 3) et le nombre de logements (centré sur 3)."""94    y = g["taux_global"].to_numpy()95    x1 = np.column_stack([np.ones(len(g)), g["loyer_relatif"] - 1.0])96    b1, *_ = np.linalg.lstsq(x1, y, rcond=None)97    res1 = y - x1 @ b198    x2 = np.column_stack([np.ones(len(g)), g["loyer_relatif"] - 1.0, g["etat_1a5"] - 3, g["nb_logements"] - 3])99    b2, *_ = np.linalg.lstsq(x2, y, rcond=None)100    res2 = y - x2 @ b2101    sst = ((y - y.mean()) ** 2).sum()102    # MRB (log) sur ρ103    ym = np.log(g["mrb"].to_numpy())104    bm, *_ = np.linalg.lstsq(x1, ym, rcond=None)105    resm = ym - x1 @ bm106    return {"a": b1[0], "b_rho": b1[1], "r2": 1 - (res1 ** 2).sum() / sst, "sigma": float(np.sqrt((res1 ** 2).sum() / (len(y) - 2))),107            "a2": b2[0], "b2_rho": b2[1], "b2_etat": b2[2], "b2_nb": b2[3], "r2_2": 1 - (res2 ** 2).sum() / sst,108            "mrb_a": bm[0], "mrb_b_rho": bm[1], "mrb_r2": 1 - (resm ** 2).sum() / ((ym - ym.mean()) ** 2).sum(),109            "rho_median": float(g["loyer_relatif"].median()), "n": len(g)}110111112def prime_occupant(prix: float, rne_contrat: float, rne_marche: float, r_inv: float = R_INVESTISSEUR_REF) -> dict:113    """Décomposition pédagogique P = RNE_c/R* + (RNE_m − RNE_c)/R* + π : valeur d'investisseur aux loyers en place, valeur du rattrapage114    (si les loyers étaient au marché), et résidu π attribué à la demande d'occupants (prime d'occupation)."""115    v_inv = fin.capitalisation_directe(rne_contrat, r_inv); v_m = fin.capitalisation_directe(rne_marche, r_inv)116    return {"r_inv": r_inv, "v_investisseur_contrat": v_inv, "v_rattrapage": v_m - v_inv, "v_investisseur_marche": v_m, "prime": prix - v_m,117            "prime_pct": (prix - v_m) / prix, "taux_implicite": rne_contrat / prix}118119120# ----------------------------------------------------------------------------- sélection des comparables triplex de Limoilou121def selection_comparables(c: pd.DataFrame, sujet: dict, base: dict, k: int = K_COMPARABLES, quartier: str = "Limoilou") -> pd.DataFrame:122    cand = c[(c["nb_logements"] == sujet["nb_logements"]) & (c["quartier"] == quartier)].copy()123    cand["loyer_relatif"] = cand["loyer_mensuel_moyen_contrat"] / cand["loyer_mensuel_moyen_marche"]124    cand["prix_m2"] = cand["prix_vente"] / cand["superficie_habitable_m2"]125    s = {"superficie_habitable_m2": sujet["superficie_habitable_m2"], "age": sujet["age"], "etat_1a5": sujet["etat_1a5"], "stationnement": sujet["stationnement"],126         "loyer_relatif": base["rbp_contrat"] / base["rbp_marche"], "mois": 0.0}127    cand["distance"] = [cmp.distance_attributs(s, {kk: float(row[kk]) for kk in ECHELLES}, ECHELLES, POIDS_DIST) for _, row in cand.iterrows()]128    cand = cand.sort_values("distance")129    cand["retenu"] = False; cand.iloc[:k, cand.columns.get_loc("retenu")] = True130    return cand131132133G_ROLE_ANNUEL = (1 + 0.235) ** (1 / 3) - 1   # croissance annuelle composée implicite du rôle 2025-2027 de Québec (+23,5 % en 3 ans), hypothèse du jeu134135136def indications(base: dict, sujet: dict, ind: dict, comp5: pd.DataFrame, reg: dict, c: pd.DataFrame | None = None) -> dict:137    rbp, rne, n = base["rbp_contrat"], base["rne"], sujet["nb_logements"]138    rho_s = base["rbp_contrat"] / base["rbp_marche"]139    taux_ajuste = reg["a"] + reg["b_rho"] * (rho_s - 1.0)140    mrb_ajuste = float(np.exp(reg["mrb_a"] + reg["mrb_b_rho"] * (rho_s - 1.0)))141    out = {142        "rho_sujet": rho_s,143        # échantillon des 127 ventes (traite.cas2.tout)144        "v_mrb_127": ind["mrb_median_triplex"] * rbp, "v_cap_127": fin.capitalisation_directe(rne, ind["taux_median"]),145        "v_log_127": ind["prix_par_logement_triplex"] * n, "v_m2_127": ind["prix_m2_median"] * sujet["superficie_habitable_m2"],146        # ajustements par régression sur le loyer relatif147        "taux_ajuste": taux_ajuste, "v_cap_ajuste": fin.capitalisation_directe(rne, taux_ajuste), "mrb_ajuste": mrb_ajuste, "v_mrb_ajuste": mrb_ajuste * rbp,148        # cinq comparables triplex de Limoilou149        "mrb_comp": float(comp5["mrb"].median()), "taux_comp": float(comp5["taux_global"].median()), "log_comp": float(comp5["prix_par_logement"].median()),150        "m2_comp": float(comp5["prix_m2"].median()),151    }152    # explication de l'écart capitalisation vs comparaison : (i) structure de dépenses du sujet vs ratio médian des ventes ;153    # (ii) ancienneté médiane des 127 ventes et indexation au rythme du rôle (+23,5 % en 3 ans, hypothèse du jeu synthétique)154    out["v_mrb_comp"] = out["mrb_comp"] * rbp; out["v_cap_comp"] = fin.capitalisation_directe(rne, out["taux_comp"])155    out["v_log_comp"] = out["log_comp"] * n; out["v_m2_comp"] = out["m2_comp"] * sujet["superficie_habitable_m2"]156    # indications retenues pour la pondération : comparables (par logement), MRB des comparables, capitalisation ajustée au loyer relatif157    if c is not None:158        d_med = float(c["ratio_depenses"].median()); mois_med = float(c["mois"].median())159        rbe = base["rbe"]; rne_ratio_ventes = rbe * (1 - d_med)160        out.update({"ratio_dep_median_ventes": d_med, "rne_au_ratio_des_ventes": rne_ratio_ventes,161                    "v_cap_ratio_ventes": fin.capitalisation_directe(rne_ratio_ventes, taux_ajuste),162                    "mois_median_127": mois_med, "g_role_annuel": G_ROLE_ANNUEL, "v_log_127_indexe": out["v_log_127"] * (1 + G_ROLE_ANNUEL) ** (mois_med / 12),163                    "mois_median_comp": float(comp5["mois"].median())})164    retenues = {"par_logement": out["v_log_comp"], "mrb": out["v_mrb_comp"], "capitalisation": out["v_cap_ajuste"]}165    out["retenues"] = retenues166    out["valeur_ponderee"] = sum(POIDS_INDICATIONS[kk] * v for kk, v in retenues.items())167    out["valeur_arrondie"] = round(out["valeur_ponderee"] / 5000) * 5000168    out["etendue_pct"] = (max(retenues.values()) - min(retenues.values())) / out["valeur_ponderee"]169    out["taux_implicite_final"] = rne / out["valeur_ponderee"]; out["mrb_implicite_final"] = out["valeur_ponderee"] / rbp170    out["prix_log_final"] = out["valeur_ponderee"] / n171    out["ecart_role"] = out["valeur_ponderee"] / sujet["valeur_role_2025"] - 1172    return out173174175# ----------------------------------------------------------------------------- sensibilité176def sensibilite(base: dict, sujet: dict, ind: dict, reg: dict, indic: dict, loyers: pd.DataFrame) -> dict:177    rbp_c, rbp_m, rne_c = base["rbp_contrat"], base["rbp_marche"], base["rne"]178    d = base["detail"]; dep_fixes = float(d.loc[~d["type"].isin(["imputée"]), "normalise"].sum())179    rows = []180    for rho in (0.85, 0.90, 0.95, 1.00):181        rbp = rbp_m * rho; rbe = fin.revenu_brut_effectif(rbp, cas2.TAUX_VACANCE_PERTES)182        rne = fin.rne(rbe, dep_fixes - base["reserve"] + cas2.GESTION_IMPUTEE * rbe, base["reserve"])183        taux = reg["a"] + reg["b_rho"] * (rho - 1.0)184        rows.append({"rho": rho, "rbp": rbp, "rne": rne, "taux_ajuste": taux, "v_cap_ajuste": fin.capitalisation_directe(rne, taux),185                     "v_cap_taux_median": fin.capitalisation_directe(rne, ind["taux_median"]), "v_mrb": indic["mrb_comp"] * rbp})186    loyers_tab = pd.DataFrame(rows)187    taux_tab = pd.DataFrame([{"taux": t, "valeur": fin.capitalisation_directe(rne_c, t)} for t in np.arange(0.0250, 0.0401, 0.0025)])188    prix = indic["valeur_arrondie"]189    fin_rows = []190    for libelle, mf, taux, amort in (("assuré, mise de fonds minimale, 25 ans", mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE, 300),191                                     ("assuré, mise de fonds minimale, 30 ans (premier acheteur)", mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE, 360),192                                     ("assuré, mise de fonds de 10 %", 0.10 * prix, TAUX_ASSURE, 300),193                                     ("non assuré, mise de fonds de 20 %", 0.20 * prix, TAUX_NON_ASSURE, 300),194                                     ("assuré, mise de fonds minimale, taux +100 pb", mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE + 0.01, 300),195                                     ("assuré, mise de fonds minimale, taux +200 pb", mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE + 0.02, 300)):196        f = financement_occupant(prix, mf, taux, amort); co = cout_occupation(f, base, loyers, "RC")197        fin_rows.append({"scenario": libelle, **{kk: f[kk] for kk in ("mise_de_fonds", "rpv", "pret", "taux", "amort_mois", "paiement_mensuel", "service_dette", "paiement_mensuel_tam")},198                         "cout_net_mensuel": co["cout_net_mensuel"], "cout_net_mensuel_hors_capital": co["cout_net_mensuel_hors_capital"]})199    return {"loyers": loyers_tab, "taux": taux_tab, "financement": pd.DataFrame(fin_rows)}200201202# ----------------------------------------------------------------------------- exercices (solutions/ch32.tex)203def exercices() -> dict:204    ex = {}205    # 32.1 — duplex : MRB, RNE, taux, identité R = (1 − v)(1 − d)/MRB206    P, rbp, v, d = 585_000.0, 31_200.0, 0.03, 0.38207    rbe = fin.revenu_brut_effectif(rbp, v); rne = rbe * (1 - d)208    ex["1"] = {"prix": P, "rbp": rbp, "rbe": rbe, "rne": rne, "mrb": fin.multiplicateur_revenu_brut(P, rbp), "taux": fin.taux_global_extrait(rne, P),209               "identite": (1 - v) * (1 - d) / fin.multiplicateur_revenu_brut(P, rbp), "prix_par_logement": P / 2}210    # 32.2 — mise de fonds minimale assurée pour trois prix ; prêt ; au-delà du plafond211    ex["2"] = {str(p): {"mise": mise_de_fonds_minimale_assuree(p), "pret": p - mise_de_fonds_minimale_assuree(p) if p <= PLAFOND_ASSURABLE else float("nan"),212                        "mise_pct": mise_de_fonds_minimale_assuree(p) / p} for p in (450_000.0, 843_000.0, 1_200_000.0, 1_600_000.0)}213    ex["2"]["mf20_1600000"] = 0.20 * 1_600_000214    # 32.3 — décomposition du taux : R_c = R_m − (1 − ρ)(1 − v)/MRB_m (dépenses fixes) et R_c = ρ R_m (dépenses proportionnelles)215    P3, lm, lc, n3, v3, dfix = 717_000.0, 1_430.0, 1_215.0, 3, 0.024, 17_500.0216    rbp_m, rbp_c = lm * n3 * 12, lc * n3 * 12; rho = rbp_c / rbp_m217    rne_m, rne_c = rbp_m * (1 - v3) - dfix, rbp_c * (1 - v3) - dfix218    ex["3"] = {"rho": rho, "rbp_m": rbp_m, "rbp_c": rbp_c, "rne_m": rne_m, "rne_c": rne_c, "R_m": rne_m / P3, "R_c": rne_c / P3, "mrb_m": P3 / rbp_m,219               "terme": (1 - rho) * (1 - v3) / (P3 / rbp_m), "R_c_prop": rho * rne_m / P3, "ecart_pb": (rne_m - rne_c) / P3 * 1e4}220    # 32.4 — coût d'occupation si l'acheteur reprend le 2e plutôt que le RC (cas 2, prix retenu, financement assuré minimal)221    r = cas2.tout(); base, loyers, sujet = r["base"], r["loyers"], r["sujet"]222    c = r["comparables"]; comp = selection_comparables(c, sujet, base); comp5 = comp[comp["retenu"]]223    g = decomposition_taux(c); reg = regression_taux_loyer_relatif(g); indic = indications(base, sujet, r["indicateurs"], comp5, reg, c)224    prix = indic["valeur_arrondie"]225    f = financement_occupant(prix, mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE)226    ex["4"] = {"prix": prix, "financement": f, "RC": cout_occupation(f, base, loyers, "RC"), "2e": cout_occupation(f, base, loyers, "2e")}227    # 32.5 — régression taux ~ ρ sur les 127 ventes et application au sujet228    ex["5"] = {"reg": reg, "taux_ajuste": indic["taux_ajuste"], "v_cap_ajuste": indic["v_cap_ajuste"], "v_cap_127": indic["v_cap_127"],229               "taux_a_rho_1": reg["a"], "rho_sujet": indic["rho_sujet"], "taux_median": r["indicateurs"]["taux_median"]}230    # 32.6 — pondérations alternatives231    ret = indic["retenues"]232    alt = {"retenue (40/35/25)": POIDS_INDICATIONS, "égale (1/3 chacune)": {k: 1 / 3 for k in ret}, "revenu dominant (20/20/60)": {"par_logement": 0.2, "mrb": 0.2, "capitalisation": 0.6},233           "comparaison seule (50/50/0)": {"par_logement": 0.5, "mrb": 0.5, "capitalisation": 0.0}}234    ex["6"] = {"indications": ret, "ponderations": {lib: sum(w[k] * ret[k] for k in ret) for lib, w in alt.items()}}235    # 32.7 — valeur actualisée de l'écart de loyer du RC selon la date de reprise (r = 6 %, croissance TAL 3,1 %, marché 3,5 %)236    lc0, lm0, g_tal, g_m, rr = 1_047.0, 1_430.0, 0.031, 0.035, 0.06237    def va_ecart(annees_avant_reprise: int, horizon: int = 10) -> float:238        va = 0.0239        for t in range(1, horizon + 1):240            if t <= annees_avant_reprise:241                ecart = (lm0 * (1 + g_m) ** (t - 1) - lc0 * (1 + g_tal) ** (t - 1)) * 12242                va += ecart / (1 + rr) ** t243        return va244    ex["7"] = {"va_1an": va_ecart(1), "va_3ans": va_ecart(3), "va_10ans": va_ecart(10), "ecart_annee1": (lm0 - lc0) * 12}245    return ex246247248# ----------------------------------------------------------------------------- tout249def calculer() -> dict:250    r = cas2.tout()251    base, sujet, loyers, ind, c = r["base"], r["sujet"], r["loyers"], r["indicateurs"], r["comparables"]252    par_taille = indicateurs_par_groupe(c, "nb_logements"); par_quartier = indicateurs_par_groupe(c, "quartier")253    g = decomposition_taux(c); reg = regression_taux_loyer_relatif(g)254    comp = selection_comparables(c, sujet, base); comp5 = comp[comp["retenu"]]255    indic = indications(base, sujet, ind, comp5, reg, c)256    rbe_m = base["rbe_marche"]; d = base["detail"]257    dep_m = float(d.loc[~d["type"].isin(["imputée", "réserve"]), "normalise"].sum()) + cas2.GESTION_IMPUTEE * rbe_m258    rne_marche = fin.rne(rbe_m, dep_m, base["reserve"])259    prime = prime_occupant(indic["valeur_ponderee"], base["rne"], rne_marche)260    prix = indic["valeur_arrondie"]261    fi = financement_occupant(prix, mise_de_fonds_minimale_assuree(prix), TAUX_ASSURE)262    occ = cout_occupation(fi, base, loyers, "RC")263    sens = sensibilite(base, sujet, ind, reg, indic, loyers)264    return {**r, "par_taille": par_taille, "par_quartier": par_quartier, "decomposition": g, "regression": reg, "candidats": comp, "comparables5": comp5,265            "indications": indic, "rne_marche": rne_marche, "prime": prime, "financement": fi, "occupation": occ, "sensibilite": sens}266267268def _d(x): return f"{x:,.0f} $".replace(",", " ")269def _p(x, k=2): return f"{100 * x:.{k}f} %"270271272if __name__ == "__main__":273    R = calculer(); b = R["base"]; ind = R["indicateurs"]; I = R["indications"]; reg = R["regression"]274    print("== Cas 2 — revenus et RNE (traite.cas2)")275    print(f"RBP contrat {_d(b['rbp_contrat'])} ; RBP marché {_d(b['rbp_marche'])} ; écart {_d(b['ecart_marche'])} ({_p(b['ecart_marche']/b['rbp_marche'],1)} du marché) ; ρ sujet = {I['rho_sujet']:.4f}")276    print(f"RBE {b['rbe']:.2f} ; dépenses hors réserve {b['depenses_hors_reserve']:.2f} ; réserve {b['reserve']:.0f} ; RNE {b['rne']:.4f} ; ratio {_p(b['ratio_depenses'],1)} ; RNE aux loyers de marché {R['rne_marche']:.2f}")277    print(R["base"]["detail"][["poste", "montant_annuel", "normalise"]].to_string(index=False))278    print("\n== 127 ventes de 2-3 logements (4 quartiers, 24 mois)")279    print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in ind.items()})280    print(R["par_taille"].round(4).to_string()); print(R["par_quartier"].round(4).to_string())281    g = R["decomposition"]282    print(f"ρ médian {g['loyer_relatif'].median():.3f} ; taux contrat médian {_p(g['taux_global'].median())} ; taux au marché médian {_p(g['taux_marche'].median())} ; MRB marché médian {g['mrb_marche'].median():.2f}")283    print(f"Régression taux = a + b(ρ−1) : a = {_p(reg['a'],3)}, b = {reg['b_rho']:.4f} (R² {reg['r2']:.3f}, σ {_p(reg['sigma'],3)}) ; avec état et taille : a = {_p(reg['a2'],3)}, b_ρ = {reg['b2_rho']:.4f}, b_état = {reg['b2_etat']:.5f}, b_n = {reg['b2_nb']:.5f}, R² {reg['r2_2']:.3f}")284    print(f"ln MRB = {reg['mrb_a']:.4f} + {reg['mrb_b_rho']:.4f}(ρ−1), R² {reg['mrb_r2']:.3f} → MRB à ρ=1 : {np.exp(reg['mrb_a']):.2f}")285    print("\n== Comparables triplex Limoilou (distance d'attributs)")286    cols = ["id_vente", "date_vente", "superficie_habitable_m2", "annee_construction", "etat_1a5", "stationnement", "loyer_mensuel_moyen_contrat", "loyer_mensuel_moyen_marche", "loyer_relatif", "revenu_brut_potentiel", "prix_vente", "mrb", "taux_global", "prix_par_logement", "prix_m2", "mois", "distance", "retenu"]287    print(f"candidats : {len(R['candidats'])}"); print(R["candidats"][cols].assign(date_vente=lambda x: x["date_vente"].dt.date).head(8).round(3).to_string(index=False))288    print("\n== Indications")289    for k, v in I.items():290        if k != "retenues": print(f"{k:22s} {v:,.4f}" if isinstance(v, float) else f"{k:22s} {v}")291    print("retenues", {k: round(v) for k, v in I["retenues"].items()}, "poids", POIDS_INDICATIONS)292    print("\n== Prime d'occupation (R* = 5 %)"); print({k: round(v, 4) for k, v in R["prime"].items()})293    print("\n== Financement de l'acheteur-occupant (prix arrondi)"); print({k: round(v, 4) for k, v in R["financement"].items()})294    print({k: (round(v, 2) if isinstance(v, float) else v) for k, v in R["occupation"].items()})295    print("\n== Sensibilité"); S = R["sensibilite"]296    print(S["loyers"].round(4).to_string(index=False)); print(S["taux"].round(2).to_string(index=False)); print(S["financement"].round(2).to_string(index=False))297    print("\n== Exercices"); E = exercices()298    for k, v in E.items(): print(k, v if k not in ("4", "5") else {kk: (vv if not isinstance(vv, dict) else {a: (round(bb, 4) if isinstance(bb, float) else bb) for a, bb in vv.items()}) for kk, vv in v.items()})299
Files · 4958fcec3502ce9b6da151998f7eaebf95b47bcc
Menu

Theme