SPB Git
26.5 KB · 339 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,148            "mrb_cap": et["cas3_valeur_cap"] / rbp,149            # proposition 33.1 : MRB = (1 - v)(1 - rho) / R, autres revenus négligés150            "mrb_theorique": (1 - base["taux_vacance_pertes"]) * (1 - base["ratio_depenses"]) / et["cas3_mediane"],151            "mrb_theorique_rho55": (1 - base["taux_vacance_pertes"]) * (1 - 0.55) / et["cas3_mediane"]}152153154# ----------------------------------------------------------------------------- P45 : 36 logements à Gatineau155156def _X_ligne(rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0) -> np.ndarray:157    x = [1.0, etat - 3, np.log(n / 12), inond, sol, (age - 50) / 10]158    x += [1.0 if rmr == r else 0.0 for r in ["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]]159    return np.array(x)160161162def _regression_cas4(pf: pd.DataFrame) -> dict:163    """Même spécification que traite.cas4.effets_regionaux, mais on conserve les résidus pour le facteur de Duan."""164    X = pd.DataFrame({"const": 1.0, "etat": pf["etat_1a5"] - 3, "ln_logements": np.log(pf["nb_logements"] / 12), "inondable": pf["zone_inondable"],165                      "sol": pf["contamination_sol"], "age": (pf["age"] - 50) / 10})166    for r in ["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]:167        X[f"rmr_{r}"] = (pf["rmr"] == r).astype(float)168    res = h.mco(X.values, np.log(pf["taux_global"].values))169    res["duan"] = float(np.mean(np.exp(res["residus"])))170    res["colonnes"] = list(X.columns)171    return res172173174def taux_predit(reg: dict, rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0, duan: bool = True) -> float:175    x = _X_ligne(rmr, etat, n, age, inond, sol)176    return float(np.exp(x @ reg["beta"]) * (reg["duan"] if duan else 1.0))177178179def p45(som: dict, et: dict, amortissement_mois: int = 480) -> dict:180    pf = som["portefeuille"]; row = pf.set_index("id_immeuble").loc[ID_36]181    reg = _regression_cas4(pf)182    rne = float(row["rne_2026"]); n = int(row["nb_logements"]); rmr = str(row["rmr"]); etat = int(row["etat_1a5"]); age = float(row["age"])183    g = som["par_rmr"]184    taux = {185        "montreal_cas3_mediane": et["cas3_mediane"],                                  # erreur : taux d'un autre marché186        "gatineau_pondere": float(g.loc[rmr, "taux_pondere"]),                       # RNE/valeur agrégés, Gatineau (cas 4)187        "gatineau_median": float(g.loc[rmr, "taux_median"]),                         # médiane des 8 immeubles de Gatineau188        "regression_sujet": taux_predit(reg, rmr, etat, n, age),                     # régression semi-log, caractéristiques de P45189        "regression_si_montreal": taux_predit(reg, "Montréal", etat, n, age),        # même immeuble à Montréal (effet RMR)190        "regression_si_12_logements": taux_predit(reg, rmr, etat, 12, age),          # même immeuble à 12 logements (effet taille)191        "regression_si_etat_3": taux_predit(reg, rmr, 3, n, age),                    # même immeuble en état moyen192        "fichier": float(row["taux_global"]),                                        # taux inscrit au dossier synthétique193    }194    valeurs = {k: fin.capitalisation_directe(rne, R) for k, R in taux.items()}195    par_log = {k: v / n for k, v in valeurs.items()}196    hyp = fin.Hypotheque(float(row["dette"]), float(row["taux_dette"]), amortissement_mois)197    fin_ = {"dette": float(row["dette"]), "taux_dette": float(row["taux_dette"]), "echeance": int(row["echeance_dette"]), "service_dette": hyp.service_dette_annuel,198            "rcd": fin.rcd(rne, hyp.service_dette_annuel), "rpv_fichier": float(row["dette"]) / float(row["valeur_2026"]),199            "rpv_regression": float(row["dette"]) / valeurs["regression_sujet"], "constante": hyp.constante_hypothecaire}200    # décomposition de l'écart Montréal cas 3 → régression sujet (en points de base, cumul multiplicatif)201    r_mtl12 = taux_predit(reg, "Montréal", etat, 12, age)202    decomposition = {"regression_montreal_12_log_etat2_age72": r_mtl12,203                     "effet_taille_pb": (taux_predit(reg, "Montréal", etat, n, age) - r_mtl12) * 1e4,204                     "effet_rmr_pb": (taux["regression_sujet"] - taux_predit(reg, "Montréal", etat, n, age)) * 1e4}205    return {"id": ID_36, "rmr": rmr, "n": n, "annee": int(row["annee_construction"]), "age": age, "etat": etat, "loyer_moyen": float(row["loyer_mensuel_moyen"]),206            "rbp": float(row["revenu_brut_potentiel"]), "ratio_depenses": float(row["ratio_depenses"]), "rne": rne, "rne_par_logement": rne / n,207            "inocc_marche": float(row["taux_inoccupation_marche"]), "valeur_fichier": float(row["valeur_2026"]), "valeur_fichier_par_logement": float(row["valeur_2026"]) / n,208            "mrb_fichier": float(row["valeur_2026"]) / float(row["revenu_brut_potentiel"]), "taux": taux, "valeurs": valeurs, "par_logement": par_log,209            "regression": reg, "financement": fin_, "decomposition": decomposition,210            "ecart_montreal_vs_sujet": valeurs["montreal_cas3_mediane"] / valeurs["regression_sujet"] - 1,211            "gatineau_valeur_par_logement": float(g.loc[rmr, "valeur_par_logement"]), "montreal_valeur_par_logement": float(g.loc["Montréal", "valeur_par_logement"])}212213214# ----------------------------------------------------------------------------- sensibilité215216def sensibilite_p45(p: dict) -> dict:217    reg, n, age, rne = p["regression"], p["n"], p["age"], p["rne"]218    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)])219    grille_valeur_log = grille.map(lambda R: rne / R / n)220    tailles = [6, 12, 24, 36, 48, 72]221    courbe = pd.DataFrame({"n": tailles, "taux_gatineau": [taux_predit(reg, "Gatineau", p["etat"], k, age) for k in tailles],222                           "taux_montreal": [taux_predit(reg, "Montréal", p["etat"], k, age) for k in tailles]})223    courbe["valeur_par_logement_gatineau"] = rne / n / courbe["taux_gatineau"]   # RNE par logement constant, taille variable224    R0 = p["taux"]["regression_sujet"]225    pb = pd.DataFrame({"delta_pb": [-50, -25, 0, 25, 50]})226    pb["taux"] = R0 + pb["delta_pb"] / 1e4; pb["valeur"] = rne / pb["taux"]; pb["variation_pct"] = pb["valeur"] / (rne / R0) - 1227    # élasticité exacte : dV/V = -dR/R228    return {"grille_taux": grille, "grille_valeur_par_logement": grille_valeur_log, "courbe_taille": courbe, "taux_pb": pb,229            "elasticite": -1.0, "variation_25pb_pct": float(R0 / (R0 + 0.0025) - 1),230            "valeur_un_pb": rne / R0 * 1e-4 / R0, "pb_par_million_p45": 1e6 * 1e-4 / R0, "pb_par_million_cas3": 1e6 * 1e-4 / p["taux"]["montreal_cas3_mediane"]}231232233# ----------------------------------------------------------------------------- exercices234235def exercices(som: dict, et: dict, u: dict, p: dict) -> dict:236    ex = {}237    # ex1 — unités de comparaison : un 16 logements fictif (données de l'énoncé) évalué par les trois unités du cas 3238    n16, m2_16, ch16 = 16, 1040.0, 26239    ex["ex1"] = {"par_logement": u["par_logement"] * n16, "par_m2": u["par_m2"] * m2_16, "par_chambre": u["par_chambre"] * ch16,240                 "m2_par_logement_sujet": u["m2_par_logement"], "m2_par_logement_16": m2_16 / n16, "chambres_par_logement_16": ch16 / n16}241    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"]) - 1242    # ex2 — taxes de la Ville de Québec : 5 logements (1-5) vs 6 logements (6+) pour une même valeur au rôle243    role = 1_800_000244    taxe5 = role * TAXE_QUEBEC_1A5 + 5 * (TARIF_QUEBEC_EAU + TARIF_QUEBEC_MATIERES)245    taxe6 = role * TAXE_QUEBEC_6PLUS + 6 * (TARIF_QUEBEC_EAU + TARIF_QUEBEC_MATIERES)246    ex["ex2"] = {"role": role, "taxe_5": taxe5, "taxe_6": taxe6, "ecart_taux_seul": role * (TAXE_QUEBEC_6PLUS - TAXE_QUEBEC_1A5),247                 "ecart_total": taxe6 - taxe5, "effet_valeur_a_5pct": role * (TAXE_QUEBEC_6PLUS - TAXE_QUEBEC_1A5) / 0.055}248    # ex3 — effet de taille (cas 3) : 36 logements au lieu de 12249    R12 = et["cas3_constante"]; R36 = et["cas3_constante"] + (et["cas3_pente_pb_par_logement"] / 1e4) * (36 - 12)250    rne_log = et["cas3_rne"] / 12251    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,252                 "prime_pct": rne_log / R36 / (rne_log / R12) - 1, "cas4_doublement_pct": et["cas4_effet_doublement_pct"]}253    # ex4 — erreur de marché : P45 au taux montréalais254    ex["ex4"] = {"valeur_montreal": p["valeurs"]["montreal_cas3_mediane"], "valeur_regression": p["valeurs"]["regression_sujet"],255                 "ecart": p["valeurs"]["montreal_cas3_mediane"] - p["valeurs"]["regression_sujet"], "ecart_pct": p["ecart_montreal_vs_sujet"],256                 "valeur_gatineau_pondere": p["valeurs"]["gatineau_pondere"], "effet_rmr_pb": p["decomposition"]["effet_rmr_pb"], "effet_taille_pb": p["decomposition"]["effet_taille_pb"]}257    # 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égression258    delta_loyer = 4 * 120 * 12259    R = p["taux"]["regression_sujet"]; v = p["inocc_marche"]260    delta_rne = delta_loyer * (1 - v) * (1 - 0.04)261    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"],262                 "R": R, "v": v}263    # ex6 — régression : 24 logements, état 3, 45 ans, dans chaque RMR ; IC à 95 % approximatif sur l'effet RMR (HC1)264    reg = p["regression"]; cols = reg["colonnes"]265    ex6 = {}266    for r in RMR_LISTE:267        R = taux_predit(reg, r, 3, 24, 45)268        if r == "Montréal":269            lo = hi = None270        else:271            j = cols.index(f"rmr_{r}"); b, s = reg["beta"][j], reg["se_hc1"][j]272            lo, hi = np.exp(b - 1.96 * s) - 1, np.exp(b + 1.96 * s) - 1273        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}274    ex["ex6"] = {"par_rmr": ex6, "r2": reg["r2"], "n": reg["n"], "duan": reg["duan"]}275    # 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,276    # 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 %)277    cout_2025, terrain, n_neuf, loyer_neuf, v_neuf, ratio_neuf = 6_400_000, 1_100_000, 24, 1975, 0.056, 0.32278    cout_2026 = cout_2025 * (1 + IPCB_MTL_BASSE_HAUTEUR_T3_2026)279    rbp = n_neuf * loyer_neuf * 12; rbe = fin.revenu_brut_effectif(rbp, v_neuf); rne_neuf = fin.rne(rbe, ratio_neuf * rbe)280    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,281                 "V_450": fin.capitalisation_directe(rne_neuf, 0.045), "V_475": fin.capitalisation_directe(rne_neuf, 0.0475),282                 "taux_implicite_cout": rne_neuf / (cout_2026 + terrain), "valeur_assurable_hors_terrain": cout_2026}283    return ex284285286# ----------------------------------------------------------------------------- assemblage et impression287288def calculer() -> dict:289    som = sommaire_cas4(); et = effet_taille(som["portefeuille"]); u = unites_cas3(et); p = p45(som, et); s = sensibilite_p45(p)290    schl = schl_ile_montreal()291    return {"sommaire": som, "effet_taille": et, "unites": u, "p45": p, "sensibilite": s, "schl": schl, "exercices": exercices(som, et, u, p)}292293294def _d(x): return f"{x:,.0f}".replace(",", " ") + " $"295def _p(x, d=2): return f"{100*x:.{d}f}".replace(".", ",") + " %"296297298def imprimer(R: dict) -> None:299    pd.set_option("display.width", 200); pd.set_option("display.max_columns", 20)300    print("=== SCHL, île de Montréal, octobre 2025 (données réelles) ===")301    print(R["schl"]["taille"].to_string(index=False)); print(R["schl"]["annee"].to_string(index=False))302    g = R["sommaire"]["par_rmr"]; t = R["sommaire"]["total"]303    print("\n=== Cas 4 : sommaire par RMR ===")304    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())305    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'])}")306    et = R["effet_taille"]307    print("\n=== Effet de taille ===")308    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")309    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")310    print(et["par_classe"].to_string()); print(et["cas4_effets"].to_string())311    u = R["unites"]312    print("\n=== Unités (cas 3, valeur finale ch. 47) ===")313    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 ; "314          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'])} ; "315          f"MRB théorique (prop. 33.1) {u['mrb_theorique']:.2f} (ρ = 55 % : {u['mrb_theorique_rho55']:.2f}) ; MRB de V_cap {u['mrb_cap']:.2f} ; 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.)")316    p = R["p45"]317    print(f"\n=== {p['id']} : {p['n']} logements, {p['rmr']}, {p['annee']} ({p['age']:.0f} ans), état {p['etat']} ===")318    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.)")319    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'])})")320    for k in p["taux"]:321        print(f"  {k:30s} R = {_p(p['taux'][k], 3)}  V = {_d(p['valeurs'][k])}  ({_d(p['par_logement'][k])}/log.)")322    d = p["decomposition"]323    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")324    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'])}")325    f = p["financement"]326    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'])}")327    reg = p["regression"]; print(f"Régression cas 4 : n = {reg['n']}, R² = {reg['r2']:.3f}, facteur de Duan {reg['duan']:.4f}")328    s = R["sensibilite"]329    print("\n=== Sensibilité P45 ===")330    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))331    print(f"Variation de la valeur pour +25 pb : {_p(s['variation_25pb_pct'])} ; 1 pb = {_d(s['valeur_un_pb'])} pour P45 ; par million : {_d(s['pb_par_million_p45'])} à R P45, {_d(s['pb_par_million_cas3'])} à 4,415 %")332    print("\n=== Exercices ===")333    for k, v in R["exercices"].items():334        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)})335336337if __name__ == "__main__":338    imprimer(calculer())339
Files · main
Menu

Theme