SPB Git
25.7 KB · 334 lines python
1#!/usr/bin/env python32"""Chapitre 33 — Immeubles de six logements et plus : tous les chiffres cités dans manuscript/chapters/ch33.tex3et solutions/ch33.tex (DONNÉES SYNTHÉTIQUES des cas 3 et 4, plus des données RÉELLES de la SCHL lues dans data/raw).45Contenu :6  - schl_ile_montreal() : lecture des tableaux 1.3.1/1.3.2/1.3.4 (taille d'immeuble) et 1.2.1/1.2.2 (année de7    construction) du classeur SCHL « rmr-montreal-2025-fr.xlsx » (Enquête sur les logements locatifs, octobre 2025),8    bloc « Île de Montréal (zones 1-18) » — données réelles, citées avec attribution (conditions d'utilisation SCHL).9  - sommaire_cas4() : portefeuille du cas 4 par RMR (traite.cas4.sommaire) + statistiques de taux et de taille.10  - effet_taille() : effet de la taille sur le taux global dans le cas 3 (régression linéaire de traite.cas3) et dans11    le cas 4 (régression semi-log de traite.cas4.effets_regionaux), et taux médians par classe de taille.12  - unites_cas3() : analyse par logement, par m², par chambre et multiplicateurs pour le sujet du cas 3.13  - p45() : évaluation sommaire par capitalisation d'un immeuble de 36 logements du cas 4 (P45, Gatineau) avec14    plusieurs taux candidats (Montréal cas 3, Gatineau cas 4, régression cas 4) pour isoler l'effet de RMR et de taille.15  - sensibilite_p45() : grilles RMR × état et courbe taille → taux prédit et valeur.16  - exercices() : chiffres des solutions.1718Aucune formule financière n'est réécrite : traite.finance, traite.cas3, traite.cas4, traite.hedonique.19Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch33_six_logements.py20"""21from __future__ import annotations22import sys, pathlib23import numpy as np, pandas as pd24sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))25from traite import cas3, cas4, finance as fin, hedonique as h2627ROOT = pathlib.Path(__file__).resolve().parents[3]28SCHL_XLSX = ROOT / "data/raw/schl_rml_2025/rmr-montreal-2025-fr.xlsx"2930# ----------------------------------------------------------------------------- constantes citées (sources : notes-B, notes-E, ch. 47)31V_FINALE_CH47 = 2_060_000          # valeur marchande finale du cas 3 au ch. 47 (réconciliation 45/35/20) — vérifiée par test contre ch47_dossier32TAXE_QUEBEC_1A5 = 0.7464 / 100     # Ville de Québec 2026, résidentiel 1 à 5 logements, $ par 100 $ (R.V.Q. 3492)33TAXE_QUEBEC_6PLUS = 0.7697 / 100   # Ville de Québec 2026, résidentiel 6 logements et plus34TARIF_QUEBEC_EAU = 386.0           # $ par logement (aqueduc-égout) ; + 129 $ par chambre (non utilisé ici)35TARIF_QUEBEC_MATIERES = 195.0      # $ par logement (matières résiduelles)36IPCB_MTL_BASSE_HAUTEUR_T3_2026 = 0.092   # IPCB Montréal, immeuble < 5 étages, variation annuelle T3 2026 (StatCan 18-10-0289-01)37IPCB_MTL_TOUR_T3_2026 = 0.079            # idem, tour ≥ 5 étages38ID_36 = "P45"                      # immeuble de 36 logements retenu dans le cas 4 (Gatineau, 1954, état 2)39RMR_LISTE = ["Montréal", "Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]404142# ----------------------------------------------------------------------------- SCHL (données réelles)4344def _lire_bloc(ws, titre_bloc: str, n_groupes: int, largeur_groupe: int, labels_arret=("Tous les immeubles",)) -> pd.DataFrame:45    """Lit les lignes du bloc `titre_bloc` d'une feuille SCHL. Chaque ligne = [libellé, g1..gk] ; chaque groupe de46    `largeur_groupe` cellules = (oct. 2024, qualité, oct. 2025, qualité[, flèche]). Retourne, par ligne, les valeurs47    d'octobre 2025 des groupes 2 chambres (3e) et tous (5e), avec leur lettre de qualité."""48    rows = list(ws.iter_rows(values_only=True))49    out, dans_bloc = [], False50    for r in rows:51        lab = r[0]52        if isinstance(lab, str) and lab.startswith(titre_bloc):53            dans_bloc = True; continue54        if not dans_bloc or lab is None:55            continue56        def val(g, off):57            v = r[1 + g * largeur_groupe + off]58            if v is None or str(v).strip() in ("**", "-", ""):59                return np.nan60            return float(str(v).replace(" ", "").replace(" ", "").replace(",", "."))61        def qual(g, off):62            q = r[1 + g * largeur_groupe + off]63            return str(q).strip() if isinstance(q, str) else ""64        out.append({"categorie": str(lab).strip(), "deux_ch_2025": val(2, 2), "deux_ch_qual": qual(2, 3),65                    "tous_2025": val(4, 2), "tous_qual": qual(4, 3), "tous_2024": val(4, 0)})66        if str(lab).strip() in labels_arret:67            break68    return pd.DataFrame(out)697071def schl_ile_montreal() -> dict:72    """Tableaux SCHL (octobre 2025), île de Montréal : inoccupation, loyer moyen et rotation par taille d'immeuble ;73    inoccupation et loyer moyen par année de construction. Valeurs « tous les appartements » et « 2 chambres »."""74    import openpyxl75    wb = openpyxl.load_workbook(SCHL_XLSX, read_only=True, data_only=True)76    bloc = "Île de Montréal"77    taille_inocc = _lire_bloc(wb["Tableau 1.3.1"], bloc, 5, 5)78    taille_loyer = _lire_bloc(wb["Tableau 1.3.2"], bloc, 5, 4)79    taille_rot = _lire_bloc(wb["Tableau 1.3.4"], bloc, 5, 5)80    annee_inocc = _lire_bloc(wb["Tableau 1.2.1"], bloc, 5, 5)81    annee_loyer = _lire_bloc(wb["Tableau 1.2.2"], bloc, 5, 4)82    annee_rot = _lire_bloc(wb["Tableau 1.2.3"], bloc, 5, 5)83    taille = taille_inocc[["categorie"]].copy()84    taille["inoccupation_tous"] = taille_inocc["tous_2025"].values85    taille["inoccupation_2024"] = taille_inocc["tous_2024"].values86    taille["loyer_tous"] = taille_loyer.set_index("categorie").loc[taille["categorie"], "tous_2025"].values87    taille["loyer_2ch"] = taille_loyer.set_index("categorie").loc[taille["categorie"], "deux_ch_2025"].values88    taille["rotation_tous"] = taille_rot.set_index("categorie").loc[taille["categorie"], "tous_2025"].values89    annee = annee_inocc[["categorie"]].copy()90    annee["inoccupation_tous"] = annee_inocc["tous_2025"].values91    annee["loyer_tous"] = annee_loyer.set_index("categorie").loc[annee["categorie"], "tous_2025"].values92    annee["loyer_2ch"] = annee_loyer.set_index("categorie").loc[annee["categorie"], "deux_ch_2025"].values93    annee["rotation_tous"] = annee_rot.set_index("categorie").loc[annee["categorie"], "tous_2025"].values94    annee = annee[annee["categorie"] != "Inconnue"].reset_index(drop=True)95    return {"taille": taille, "annee": annee}969798# ----------------------------------------------------------------------------- cas 4 : sommaire et effets99100def sommaire_cas4() -> dict:101    d = cas4.charger(); pf = d["portefeuille"]102    s = cas4.sommaire(pf)103    g = s["par_rmr"].copy()104    stats = pf.groupby("rmr").agg(taux_median=("taux_global", "median"), taux_min=("taux_global", "min"), taux_max=("taux_global", "max"),105                                  logements_moyen=("nb_logements", "mean"), loyer_moyen=("loyer_mensuel_moyen", "mean"),106                                  inocc_marche=("taux_inoccupation_marche", "first"), age_moyen=("age", "mean"))107    g = g.join(stats)108    g["rne_par_logement"] = g["rne"] / g["logements"]109    g = g.loc[RMR_LISTE]110    pf = pf.assign(valeur_par_logement=pf["valeur_2026"] / pf["nb_logements"], rne_par_logement=pf["rne_2026"] / pf["nb_logements"],111                   mrb=pf["valeur_2026"] / pf["revenu_brut_potentiel"], rpv=pf["dette"] / pf["valeur_2026"])112    return {"par_rmr": g, "total": s["total"], "portefeuille": pf, "series": d["series"]}113114115def effet_taille(pf: pd.DataFrame) -> dict:116    """Effet de la taille sur le taux global : (i) cas 3, pente linéaire par logement (traite.cas3.taux_extraits) ;117    (ii) cas 4, élasticité semi-log (traite.cas4.effets_regionaux) ; (iii) taux médians par classe de taille (cas 4)."""118    t3 = cas3.tout()119    reg3 = t3["taux"]["regression"]120    eff4 = cas4.effets_regionaux(pf)121    b_ln = float(eff4.loc["ln_logements", "coef"]); se_ln = float(eff4.loc["ln_logements", "se_hc1"])122    classes = pd.cut(pf["nb_logements"], bins=[5, 8, 16, 24, 48, 72], labels=["6–8", "12–16", "24", "36–48", "72"])123    par_classe = pf.groupby(classes, observed=True).agg(n=("id_immeuble", "count"), taux_median=("taux_global", "median"),124                                                        valeur_par_logement=("valeur_2026", lambda v: float(v.sum()) / float(pf.loc[v.index, "nb_logements"].sum())))125    # effet, en points de base, de passer de 12 à 36 logements, toutes choses égales par ailleurs, à un taux de 5,5 % (cas 4) et à la médiane du cas 3126    r_ref4 = float(np.exp(eff4.loc["const", "coef"]))  # Montréal, état 3, 12 logements, 50 ans, sans risque127    d_pb_cas4 = (np.exp(b_ln * np.log(36 / 12)) - 1) * r_ref4 * 1e4128    d_pb_cas3 = reg3["taille"] * (36 - 12) * 1e4129    return {"cas3_pente_pb_par_logement": reg3["taille"] * 1e4, "cas3_constante": reg3["constante"], "cas3_mediane": t3["taux"]["mediane"],130            "cas3_n": t3["taux"]["n"], "cas3_comparables": t3["taux"]["comparables"], "cas3_valeur_cap": t3["valeur_capitalisation"],131            "cas3_rne": t3["base"]["rne"], "cas3_base": t3["base"], "cas3_etat": t3["etat"], "cas3_sujet": t3["sujet"], "cas3_dcf": t3["dcf"],132            "cas4_coef_ln": b_ln, "cas4_se_ln": se_ln, "cas4_effet_doublement_pct": float(np.exp(b_ln * np.log(2)) - 1),133            "cas4_effets": eff4, "cas4_r_ref": r_ref4, "d_pb_12_36_cas4": float(d_pb_cas4), "d_pb_12_36_cas3": float(d_pb_cas3), "par_classe": par_classe}134135136# ----------------------------------------------------------------------------- unités d'analyse (cas 3)137138def unites_cas3(et: dict, valeur: float = V_FINALE_CH47) -> dict:139    etat, sujet, base = et["cas3_etat"], et["cas3_sujet"], et["cas3_base"]140    n = int(sujet["nb_logements"]); m2 = float(sujet["superficie_locative_m2"])141    chambres = int(etat["chambres"].sum())142    rbp = base["rbp_contrat"]; rne = base["rne"]143    return {"valeur": valeur, "n": n, "m2": m2, "chambres": chambres, "par_logement": valeur / n, "par_m2": valeur / m2, "par_chambre": valeur / chambres,144            "m2_par_logement": m2 / n, "chambres_par_logement": chambres / n, "mrb": fin.multiplicateur_revenu_brut(valeur, rbp),145            "mrb_marche": fin.multiplicateur_revenu_brut(valeur, base["rbp_marche"]), "taux_implicite": fin.taux_global_extrait(rne, valeur),146            "rne_par_logement": rne / n, "loyer_moyen": float(etat["loyer_mensuel_contrat"].mean()), "loyer_moyen_m2": float((etat["loyer_mensuel_contrat"].sum()) / m2),147            "valeur_cap": et["cas3_valeur_cap"], "valeur_cap_par_logement": et["cas3_valeur_cap"] / n}148149150# ----------------------------------------------------------------------------- P45 : 36 logements à Gatineau151152def _X_ligne(rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0) -> np.ndarray:153    x = [1.0, etat - 3, np.log(n / 12), inond, sol, (age - 50) / 10]154    x += [1.0 if rmr == r else 0.0 for r in ["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]]155    return np.array(x)156157158def _regression_cas4(pf: pd.DataFrame) -> dict:159    """Même spécification que traite.cas4.effets_regionaux, mais on conserve les résidus pour le facteur de Duan."""160    X = pd.DataFrame({"const": 1.0, "etat": pf["etat_1a5"] - 3, "ln_logements": np.log(pf["nb_logements"] / 12), "inondable": pf["zone_inondable"],161                      "sol": pf["contamination_sol"], "age": (pf["age"] - 50) / 10})162    for r in ["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]:163        X[f"rmr_{r}"] = (pf["rmr"] == r).astype(float)164    res = h.mco(X.values, np.log(pf["taux_global"].values))165    res["duan"] = float(np.mean(np.exp(res["residus"])))166    res["colonnes"] = list(X.columns)167    return res168169170def taux_predit(reg: dict, rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0, duan: bool = True) -> float:171    x = _X_ligne(rmr, etat, n, age, inond, sol)172    return float(np.exp(x @ reg["beta"]) * (reg["duan"] if duan else 1.0))173174175def p45(som: dict, et: dict, amortissement_mois: int = 480) -> dict:176    pf = som["portefeuille"]; row = pf.set_index("id_immeuble").loc[ID_36]177    reg = _regression_cas4(pf)178    rne = float(row["rne_2026"]); n = int(row["nb_logements"]); rmr = str(row["rmr"]); etat = int(row["etat_1a5"]); age = float(row["age"])179    g = som["par_rmr"]180    taux = {181        "montreal_cas3_mediane": et["cas3_mediane"],                                  # erreur : taux d'un autre marché182        "gatineau_pondere": float(g.loc[rmr, "taux_pondere"]),                       # RNE/valeur agrégés, Gatineau (cas 4)183        "gatineau_median": float(g.loc[rmr, "taux_median"]),                         # médiane des 8 immeubles de Gatineau184        "regression_sujet": taux_predit(reg, rmr, etat, n, age),                     # régression semi-log, caractéristiques de P45185        "regression_si_montreal": taux_predit(reg, "Montréal", etat, n, age),        # même immeuble à Montréal (effet RMR)186        "regression_si_12_logements": taux_predit(reg, rmr, etat, 12, age),          # même immeuble à 12 logements (effet taille)187        "regression_si_etat_3": taux_predit(reg, rmr, 3, n, age),                    # même immeuble en état moyen188        "fichier": float(row["taux_global"]),                                        # taux inscrit au dossier synthétique189    }190    valeurs = {k: fin.capitalisation_directe(rne, R) for k, R in taux.items()}191    par_log = {k: v / n for k, v in valeurs.items()}192    hyp = fin.Hypotheque(float(row["dette"]), float(row["taux_dette"]), amortissement_mois)193    fin_ = {"dette": float(row["dette"]), "taux_dette": float(row["taux_dette"]), "echeance": int(row["echeance_dette"]), "service_dette": hyp.service_dette_annuel,194            "rcd": fin.rcd(rne, hyp.service_dette_annuel), "rpv_fichier": float(row["dette"]) / float(row["valeur_2026"]),195            "rpv_regression": float(row["dette"]) / valeurs["regression_sujet"], "constante": hyp.constante_hypothecaire}196    # décomposition de l'écart Montréal cas 3 → régression sujet (en points de base, cumul multiplicatif)197    r_mtl12 = taux_predit(reg, "Montréal", etat, 12, age)198    decomposition = {"regression_montreal_12_log_etat2_age72": r_mtl12,199                     "effet_taille_pb": (taux_predit(reg, "Montréal", etat, n, age) - r_mtl12) * 1e4,200                     "effet_rmr_pb": (taux["regression_sujet"] - taux_predit(reg, "Montréal", etat, n, age)) * 1e4}201    return {"id": ID_36, "rmr": rmr, "n": n, "annee": int(row["annee_construction"]), "age": age, "etat": etat, "loyer_moyen": float(row["loyer_mensuel_moyen"]),202            "rbp": float(row["revenu_brut_potentiel"]), "ratio_depenses": float(row["ratio_depenses"]), "rne": rne, "rne_par_logement": rne / n,203            "inocc_marche": float(row["taux_inoccupation_marche"]), "valeur_fichier": float(row["valeur_2026"]), "valeur_fichier_par_logement": float(row["valeur_2026"]) / n,204            "mrb_fichier": float(row["valeur_2026"]) / float(row["revenu_brut_potentiel"]), "taux": taux, "valeurs": valeurs, "par_logement": par_log,205            "regression": reg, "financement": fin_, "decomposition": decomposition,206            "ecart_montreal_vs_sujet": valeurs["montreal_cas3_mediane"] / valeurs["regression_sujet"] - 1,207            "gatineau_valeur_par_logement": float(g.loc[rmr, "valeur_par_logement"]), "montreal_valeur_par_logement": float(g.loc["Montréal", "valeur_par_logement"])}208209210# ----------------------------------------------------------------------------- sensibilité211212def sensibilite_p45(p: dict) -> dict:213    reg, n, age, rne = p["regression"], p["n"], p["age"], p["rne"]214    grille = pd.DataFrame({r: [taux_predit(reg, r, e, n, age) for e in range(1, 6)] for r in RMR_LISTE}, index=[f"état {e}" for e in range(1, 6)])215    grille_valeur_log = grille.map(lambda R: rne / R / n)216    tailles = [6, 12, 24, 36, 48, 72]217    courbe = pd.DataFrame({"n": tailles, "taux_gatineau": [taux_predit(reg, "Gatineau", p["etat"], k, age) for k in tailles],218                           "taux_montreal": [taux_predit(reg, "Montréal", p["etat"], k, age) for k in tailles]})219    courbe["valeur_par_logement_gatineau"] = rne / n / courbe["taux_gatineau"]   # RNE par logement constant, taille variable220    R0 = p["taux"]["regression_sujet"]221    pb = pd.DataFrame({"delta_pb": [-50, -25, 0, 25, 50]})222    pb["taux"] = R0 + pb["delta_pb"] / 1e4; pb["valeur"] = rne / pb["taux"]; pb["variation_pct"] = pb["valeur"] / (rne / R0) - 1223    # élasticité exacte : dV/V = -dR/R224    return {"grille_taux": grille, "grille_valeur_par_logement": grille_valeur_log, "courbe_taille": courbe, "taux_pb": pb,225            "elasticite": -1.0, "variation_25pb_pct": float(R0 / (R0 + 0.0025) - 1)}226227228# ----------------------------------------------------------------------------- exercices229230def exercices(som: dict, et: dict, u: dict, p: dict) -> dict:231    ex = {}232    # ex1 — unités de comparaison : un 16 logements fictif (données de l'énoncé) évalué par les trois unités du cas 3233    n16, m2_16, ch16 = 16, 1040.0, 26234    ex["ex1"] = {"par_logement": u["par_logement"] * n16, "par_m2": u["par_m2"] * m2_16, "par_chambre": u["par_chambre"] * ch16,235                 "m2_par_logement_sujet": u["m2_par_logement"], "m2_par_logement_16": m2_16 / n16, "chambres_par_logement_16": ch16 / n16}236    ex["ex1"]["ecart_max_pct"] = max(ex["ex1"]["par_logement"], ex["ex1"]["par_m2"], ex["ex1"]["par_chambre"]) / min(ex["ex1"]["par_logement"], ex["ex1"]["par_m2"], ex["ex1"]["par_chambre"]) - 1237    # ex2 — taxes de la Ville de Québec : 5 logements (1-5) vs 6 logements (6+) pour une même valeur au rôle238    role = 1_800_000239    taxe5 = role * TAXE_QUEBEC_1A5 + 5 * (TARIF_QUEBEC_EAU + TARIF_QUEBEC_MATIERES)240    taxe6 = role * TAXE_QUEBEC_6PLUS + 6 * (TARIF_QUEBEC_EAU + TARIF_QUEBEC_MATIERES)241    ex["ex2"] = {"role": role, "taxe_5": taxe5, "taxe_6": taxe6, "ecart_taux_seul": role * (TAXE_QUEBEC_6PLUS - TAXE_QUEBEC_1A5),242                 "ecart_total": taxe6 - taxe5, "effet_valeur_a_5pct": role * (TAXE_QUEBEC_6PLUS - TAXE_QUEBEC_1A5) / 0.055}243    # ex3 — effet de taille (cas 3) : 36 logements au lieu de 12244    R12 = et["cas3_constante"]; R36 = et["cas3_constante"] + (et["cas3_pente_pb_par_logement"] / 1e4) * (36 - 12)245    rne_log = et["cas3_rne"] / 12246    ex["ex3"] = {"R12": R12, "R36": R36, "ecart_pb": (R36 - R12) * 1e4, "valeur_par_logement_12": rne_log / R12, "valeur_par_logement_36": rne_log / R36,247                 "prime_pct": rne_log / R36 / (rne_log / R12) - 1, "cas4_doublement_pct": et["cas4_effet_doublement_pct"]}248    # ex4 — erreur de marché : P45 au taux montréalais249    ex["ex4"] = {"valeur_montreal": p["valeurs"]["montreal_cas3_mediane"], "valeur_regression": p["valeurs"]["regression_sujet"],250                 "ecart": p["valeurs"]["montreal_cas3_mediane"] - p["valeurs"]["regression_sujet"], "ecart_pct": p["ecart_montreal_vs_sujet"],251                 "valeur_gatineau_pondere": p["valeurs"]["gatineau_pondere"], "effet_rmr_pb": p["decomposition"]["effet_rmr_pb"], "effet_taille_pb": p["decomposition"]["effet_taille_pb"]}252    # ex5 — diligence : 4 baux surévalués de 120 $/mois dans l'état locatif de P45 ; vacance 3,8 %, gestion 4 % du RBE, R = régression253    delta_loyer = 4 * 120 * 12254    R = p["taux"]["regression_sujet"]; v = p["inocc_marche"]255    delta_rne = delta_loyer * (1 - v) * (1 - 0.04)256    ex["ex5"] = {"delta_rbp": delta_loyer, "delta_rne": delta_rne, "delta_valeur": delta_rne / R, "delta_valeur_pct": delta_rne / R / p["valeurs"]["regression_sujet"],257                 "R": R, "v": v}258    # ex6 — régression : 24 logements, état 3, 45 ans, dans chaque RMR ; IC à 95 % approximatif sur l'effet RMR (HC1)259    reg = p["regression"]; cols = reg["colonnes"]260    ex6 = {}261    for r in RMR_LISTE:262        R = taux_predit(reg, r, 3, 24, 45)263        if r == "Montréal":264            lo = hi = None265        else:266            j = cols.index(f"rmr_{r}"); b, s = reg["beta"][j], reg["se_hc1"][j]267            lo, hi = np.exp(b - 1.96 * s) - 1, np.exp(b + 1.96 * s) - 1268        ex6[r] = {"taux": R, "effet_pct": (np.exp(reg["beta"][cols.index(f"rmr_{r}")]) - 1) if r != "Montréal" else 0.0, "ic_lo": lo, "ic_hi": hi}269    ex["ex6"] = {"par_rmr": ex6, "r2": reg["r2"], "n": reg["n"], "duan": reg["duan"]}270    # ex7 — immeuble neuf : coût indexé vs revenu (données de l'énoncé : coût 2025 hors terrain 6 400 000 $, terrain 1 100 000 $, 24 logements,271    # loyer 1 975 $/mois, inoccupation 5,6 % (SCHL, île, immeubles 2015+), dépenses 32 % du RBE, taux neuf CBRE Montréal 4,50-4,75 %)272    cout_2025, terrain, n_neuf, loyer_neuf, v_neuf, ratio_neuf = 6_400_000, 1_100_000, 24, 1975, 0.056, 0.32273    cout_2026 = cout_2025 * (1 + IPCB_MTL_BASSE_HAUTEUR_T3_2026)274    rbp = n_neuf * loyer_neuf * 12; rbe = fin.revenu_brut_effectif(rbp, v_neuf); rne_neuf = fin.rne(rbe, ratio_neuf * rbe)275    ex["ex7"] = {"cout_2026": cout_2026, "cout_total": cout_2026 + terrain, "cout_par_logement": (cout_2026 + terrain) / n_neuf, "rbp": rbp, "rbe": rbe, "rne": rne_neuf,276                 "V_450": fin.capitalisation_directe(rne_neuf, 0.045), "V_475": fin.capitalisation_directe(rne_neuf, 0.0475),277                 "taux_implicite_cout": rne_neuf / (cout_2026 + terrain), "valeur_assurable_hors_terrain": cout_2026}278    return ex279280281# ----------------------------------------------------------------------------- assemblage et impression282283def calculer() -> dict:284    som = sommaire_cas4(); et = effet_taille(som["portefeuille"]); u = unites_cas3(et); p = p45(som, et); s = sensibilite_p45(p)285    schl = schl_ile_montreal()286    return {"sommaire": som, "effet_taille": et, "unites": u, "p45": p, "sensibilite": s, "schl": schl, "exercices": exercices(som, et, u, p)}287288289def _d(x): return f"{x:,.0f}".replace(",", " ") + " $"290def _p(x, d=2): return f"{100*x:.{d}f}".replace(".", ",") + " %"291292293def imprimer(R: dict) -> None:294    pd.set_option("display.width", 200); pd.set_option("display.max_columns", 20)295    print("=== SCHL, île de Montréal, octobre 2025 (données réelles) ===")296    print(R["schl"]["taille"].to_string(index=False)); print(R["schl"]["annee"].to_string(index=False))297    g = R["sommaire"]["par_rmr"]; t = R["sommaire"]["total"]298    print("\n=== Cas 4 : sommaire par RMR ===")299    print(g[["n", "logements", "valeur", "rne", "taux_pondere", "taux_median", "taux_min", "taux_max", "rpv", "valeur_par_logement", "rne_par_logement", "logements_moyen", "loyer_moyen"]].to_string())300    print(f"Total : {t['n']} immeubles, {t['logements']} logements, valeur {_d(t['valeur'])}, RNE {_d(t['rne'])}, taux pondéré {_p(t['taux_pondere'])}, RPV {_p(t['rpv'], 1)}, valeur/logement {_d(t['valeur']/t['logements'])}")301    et = R["effet_taille"]302    print("\n=== Effet de taille ===")303    print(f"Cas 3 : pente {et['cas3_pente_pb_par_logement']:.2f} pb/logement ; constante {_p(et['cas3_constante'], 3)} ; médiane {_p(et['cas3_mediane'], 3)} ; 12→36 : {et['d_pb_12_36_cas3']:.1f} pb")304    print(f"Cas 4 : coef ln(n/12) = {et['cas4_coef_ln']:.4f} (ET HC1 {et['cas4_se_ln']:.4f}) ; doublement → {_p(et['cas4_effet_doublement_pct'])} du taux ; 12→36 à R réf. {_p(et['cas4_r_ref'], 2)} : {et['d_pb_12_36_cas4']:.1f} pb")305    print(et["par_classe"].to_string()); print(et["cas4_effets"].to_string())306    u = R["unites"]307    print("\n=== Unités (cas 3, valeur finale ch. 47) ===")308    print(f"V = {_d(u['valeur'])} ; {u['n']} log., {u['m2']:.0f} m², {u['chambres']} chambres → {_d(u['par_logement'])}/log., {_d(u['par_m2'])}/m², {_d(u['par_chambre'])}/chambre ; "309          f"{u['m2_par_logement']:.1f} m²/log. ; {u['chambres_par_logement']:.2f} ch./log. ; MRB contrat {u['mrb']:.2f}, marché {u['mrb_marche']:.2f} ; taux implicite {_p(u['taux_implicite'])} ; "310          f"RNE/log. {_d(u['rne_par_logement'])} ; loyer moyen {_d(u['loyer_moyen'])}/mois ; loyer {u['loyer_moyen_m2']:.2f} $/m²/mois ; V_cap {_d(u['valeur_cap'])} ({_d(u['valeur_cap_par_logement'])}/log.)")311    p = R["p45"]312    print(f"\n=== {p['id']} : {p['n']} logements, {p['rmr']}, {p['annee']} ({p['age']:.0f} ans), état {p['etat']} ===")313    print(f"Loyer moyen {_d(p['loyer_moyen'])}/mois ; RBP {_d(p['rbp'])} ; ratio dépenses {_p(p['ratio_depenses'], 1)} ; inoccupation de marché {_p(p['inocc_marche'], 1)} ; RNE {_d(p['rne'])} ({_d(p['rne_par_logement'])}/log.)")314    print(f"Valeur au dossier {_d(p['valeur_fichier'])} ({_d(p['valeur_fichier_par_logement'])}/log., MRB {p['mrb_fichier']:.2f}, taux {_p(p['taux']['fichier'])})")315    for k in p["taux"]:316        print(f"  {k:30s} R = {_p(p['taux'][k], 3)}  V = {_d(p['valeurs'][k])}  ({_d(p['par_logement'][k])}/log.)")317    d = p["decomposition"]318    print(f"Décomposition : Montréal 12 log. état 2 âge 72 → {_p(d['regression_montreal_12_log_etat2_age72'], 3)} ; effet taille {d['effet_taille_pb']:+.1f} pb ; effet RMR {d['effet_rmr_pb']:+.1f} pb")319    print(f"Écart Montréal cas 3 vs régression : {_p(p['ecart_montreal_vs_sujet'], 1)} ; valeur/log. Gatineau cas 4 {_d(p['gatineau_valeur_par_logement'])}, Montréal {_d(p['montreal_valeur_par_logement'])}")320    f = p["financement"]321    print(f"Dette {_d(f['dette'])} à {_p(f['taux_dette'])} (échéance {f['echeance']}) ; service (480 mois, hyp. prêt assuré) {_d(f['service_dette'])} ; RCD {f['rcd']:.2f} ; RPV dossier {_p(f['rpv_fichier'], 1)} ; RPV sur V régression {_p(f['rpv_regression'], 1)} ; constante {_p(f['constante'])}")322    reg = p["regression"]; print(f"Régression cas 4 : n = {reg['n']}, R² = {reg['r2']:.3f}, facteur de Duan {reg['duan']:.4f}")323    s = R["sensibilite"]324    print("\n=== Sensibilité P45 ===")325    print((s["grille_taux"] * 100).round(2).to_string()); print(s["grille_valeur_par_logement"].round(0).to_string()); print(s["courbe_taille"].to_string(index=False)); print(s["taux_pb"].to_string(index=False))326    print(f"Variation de la valeur pour +25 pb : {_p(s['variation_25pb_pct'])}")327    print("\n=== Exercices ===")328    for k, v in R["exercices"].items():329        print(k, {kk: (round(vv, 4) if isinstance(vv, float) else vv) for kk, vv in v.items()} if k != "ex6" else {r: {kk: (round(vv, 4) if isinstance(vv, float) else vv) for kk, vv in d.items()} for r, d in v["par_rmr"].items()} | {"r2": round(v["r2"], 3), "duan": round(v["duan"], 4)})330331332if __name__ == "__main__":333    imprimer(calculer())334
Files · 52713f1726a682cc02b48561b6b82a2be2ec84ba
Menu

Theme