SPB Git
42.5 KB · 458 lines python
1#!/usr/bin/env python32"""Chapitre 48 — Cas 4 : portefeuille multirésidentiel interrégional (DONNÉES SYNTHÉTIQUES, 48 immeubles fictifs, 6 RMR).34Dossier complet d'évaluation de portefeuille : sommaire par RMR, distribution des tailles/âges/états, modèle d'évaluation de5masse (régression semi-logarithmique du taux global : traite.cas4.effets_regionaux, facteur de Duan, valeur prédite par immeuble,6statistiques de ratio IAAO), dynamique des RNE 2016-2026 (croissance, volatilité, choc pandémique, corrélations intra- et7inter-régionales), risques (inondation, sol, concentration, dette : échéancier, RCD, renouvellement à +100/+200 pb, capacité de8refinancement), agrégation de l'incertitude par Monte-Carlo avec corrélation régionale (copule gaussienne, décomposition de Cholesky,910 000 tirages, graine 20261031 ; démonstration de la diversification : VaR du portefeuille vs somme des VaR), allocation10(score rendement/croissance/risque, frontière rendement-risque par RMR), réconciliation et exercices.1112Les calculs partagés viennent de `traite.cas4` (sommaire, croissance_rne, effets_regionaux), `traite.finance` (hypothèques,13RCD, capitalisation) et `traite.hedonique` (MCO HC1, statistiques de ratio) ; ce script n'y ajoute que les calculs propres au14dossier et IMPRIME tous les chiffres cités dans manuscript/chapters/ch48.tex.1516Hypothèses déclarées (voir §48.1 et §48.6 du chapitre) :17  - date d'évaluation 2026-06-30 (fin de la série de RNE) ; valeurs et RNE « 2026 » du fichier = indications individuelles ;18  - dette du fichier = solde au 2026-06-30 ; service de la dette calculé sur un amortissement de 480 mois (prêts assurés19    multilogement, même hypothèse que le ch. 33 pour P45) ; sensibilité à 300 mois ;20  - renouvellement : à l'échéance, le solde (formule prospective) est refinancé sur l'amortissement résiduel au taux + Δ.2122Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch48_portefeuille.py [--n 10000]23"""24from __future__ import annotations25import sys, pathlib, argparse, math26import numpy as np, pandas as pd27sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))28from traite import cas4, finance as fin, hedonique as h2930ROOT = pathlib.Path(__file__).resolve().parents[3]31GRAINE = 2026103132DATE_EVAL = "2026-06-30"33RMR = ["Montréal", "Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]34RMR_REG = ["Québec", "Gatineau", "Sherbrooke", "Trois-Rivières", "Saguenay"]      # indicatrices (référence Montréal)3536# ----------------------------------------------------------------------------- hypothèses propres au chapitre 4837AMORT_BASE = 480            # mois (prêts assurés multilogement ; cohérent avec le ch. 33, P45)38AMORT_ALT = 300             # sensibilité39RCD_MIN = 1.20              # covenant conventionnel (ch. 31, 47)40RPV_MAX = 0.7541CHOCS_PB = (0, 100, 200)42N_MC = 10_00043# corrélations déclarées du choc de TAUX (facteur commun des marchés de capitaux) ; celles du choc de RNE sont estimées sur les séries44RHO_TAUX = {"global": 0.50, "regional": 0.70}45OBLIG_10_ANS = 0.0384       # Banque du Canada, 2026-10-29 (notes-C §4.1) — repère pour la frontière46ARRONDI_PORTEFEUILLE = 100_0004748# ----------------------------------------------------------------------------- utilitaires4950def _d(x): return f"{x:,.0f}".replace(",", " ") + " $"51def _p(x, d=2): return f"{100*x:.{d}f}".replace(".", ",") + " %"5253def arrondir(x: float, pas: int = ARRONDI_PORTEFEUILLE) -> float:54    return round(x / pas) * pas5556# ----------------------------------------------------------------------------- 48.2 portefeuille5758def sommaire_etendu(pf: pd.DataFrame) -> dict:59    """Sommaire de traite.cas4.sommaire complété : médiane et étendue des taux, taille moyenne, âge, état, loyer, RNE/logement, part de la valeur."""60    s = cas4.sommaire(pf); g = s["par_rmr"].copy()61    agg = pf.groupby("rmr").agg(taux_median=("taux_global", "median"), taux_min=("taux_global", "min"), taux_max=("taux_global", "max"),62                                taille_moy=("nb_logements", "mean"), age_moy=("age", "mean"), etat_moy=("etat_1a5", "mean"),63                                loyer_moy=("loyer_mensuel_moyen", "mean"), inondables=("zone_inondable", "sum"), sols=("contamination_sol", "sum"))64    g = g.join(agg); g["rne_par_logement"] = g["rne"] / g["logements"]; g["part_valeur"] = g["valeur"] / s["total"]["valeur"]65    g["taux_dette_pondere"] = pf.groupby("rmr").apply(lambda d: (d["dette"] * d["taux_dette"]).sum() / d["dette"].sum(), include_groups=False)66    g = g.loc[RMR]67    tot = s["total"]; tot["valeur_par_logement"] = tot["valeur"] / tot["logements"]; tot["rne_par_logement"] = tot["rne"] / tot["logements"]68    tot["taux_dette_pondere"] = float((pf["dette"] * pf["taux_dette"]).sum() / pf["dette"].sum())69    tot["taux_median"] = float(pf["taux_global"].median()); tot["taux_moyen"] = float(pf["taux_global"].mean()); tot["taux_sd"] = float(pf["taux_global"].std(ddof=1))70    tot["part_sol"] = float(pf.loc[pf["contamination_sol"] == 1, "valeur_2026"].sum() / tot["valeur"])71    # distributions72    tailles = pf.groupby("nb_logements").agg(n=("id_immeuble", "count"), logements=("nb_logements", "sum"), valeur=("valeur_2026", "sum"), taux_median=("taux_global", "median"))73    bornes_age = [0, 30, 50, 65, 200]; etiq_age = ["< 30 ans", "30-49 ans", "50-64 ans", "65 ans et +"]74    ages = pf.assign(classe=pd.cut(pf["age"], bornes_age, labels=etiq_age, right=False)).groupby("classe", observed=False).agg(n=("id_immeuble", "count"), valeur=("valeur_2026", "sum"), taux_median=("taux_global", "median"), etat_moy=("etat_1a5", "mean"))75    etats = pf.groupby("etat_1a5").agg(n=("id_immeuble", "count"), valeur=("valeur_2026", "sum"), taux_median=("taux_global", "median"), loyer_moy=("loyer_mensuel_moyen", "mean"))76    # concentration (Herfindahl) par RMR et par immeuble, sur la valeur77    w_rmr = g["part_valeur"].values; w_imm = (pf["valeur_2026"] / tot["valeur"]).values78    conc = {"hhi_rmr": float((w_rmr ** 2).sum()), "hhi_immeuble": float((w_imm ** 2).sum()), "n_equiv_rmr": float(1 / (w_rmr ** 2).sum()), "n_equiv_immeuble": float(1 / (w_imm ** 2).sum()),79            "top5_part": float(np.sort(w_imm)[-5:].sum()), "top5_ids": pf.nlargest(5, "valeur_2026")["id_immeuble"].tolist()}80    return {"par_rmr": g, "total": tot, "tailles": tailles, "ages": ages, "etats": etats, "concentration": conc}8182# ----------------------------------------------------------------------------- 48.4 modèle d'évaluation de masse8384def regression_masse(pf: pd.DataFrame) -> dict:85    """Même spécification que traite.cas4.effets_regionaux (coefficients identiques) ; conserve résidus, R², facteur de Duan et la matrice X."""86    eff = cas4.effets_regionaux(pf)87    X = pd.DataFrame({"const": 1.0, "etat": pf["etat_1a5"] - 3, "ln_logements": np.log(pf["nb_logements"] / 12), "inondable": pf["zone_inondable"],88                      "sol": pf["contamination_sol"], "age": (pf["age"] - 50) / 10})89    for r in RMR_REG: X[f"rmr_{r}"] = (pf["rmr"] == r).astype(float)90    res = h.mco(X.values, np.log(pf["taux_global"].values))91    assert np.allclose(res["beta"], eff["coef"].values)92    duan = float(np.mean(np.exp(res["residus"])))93    tab = eff.copy(); tab["t_hc1"] = tab["coef"] / tab["se_hc1"]94    tab["effet_pb_a_520"] = tab["effet_pct"] * 0.0520 * 1e4          # effet en points de base au taux pondéré du portefeuille95    tab["ic_bas_pct"] = np.exp(tab["coef"] - 1.96 * tab["se_hc1"]) - 1; tab["ic_haut_pct"] = np.exp(tab["coef"] + 1.96 * tab["se_hc1"]) - 196    return {"table": tab, "beta": res["beta"], "colonnes": list(X.columns), "X": X.values, "residus": res["residus"], "sigma": float(np.sqrt(res["sigma2"])),97            "r2": float(res["r2"]), "r2_ajuste": float(res["r2_ajuste"]), "n": int(res["n"]), "k": int(res["k"]), "duan": duan}9899def x_ligne(rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0) -> np.ndarray:100    x = [1.0, etat - 3, math.log(n / 12), inond, sol, (age - 50) / 10] + [1.0 if rmr == r else 0.0 for r in RMR_REG]101    return np.array(x)102103def taux_predit(reg: dict, rmr: str, etat: int, n: int, age: float, inond: int = 0, sol: int = 0, duan: bool = True) -> float:104    return float(np.exp(x_ligne(rmr, etat, n, age, inond, sol) @ reg["beta"]) * (reg["duan"] if duan else 1.0))105106def valeurs_predites(pf: pd.DataFrame, reg: dict) -> pd.DataFrame:107    """Taux prédit et valeur prédite (RNE / taux prédit) de chaque immeuble ; ratio valeur prédite / indication individuelle."""108    t = pf.copy()109    t["taux_predit"] = np.exp(reg["X"] @ reg["beta"]) * reg["duan"]110    t["valeur_predite"] = t["rne_2026"] / t["taux_predit"]111    t["ratio"] = t["valeur_predite"] / t["valeur_2026"]112    t["ecart_pb"] = (t["taux_predit"] - t["taux_global"]) * 1e4113    t["rpv"] = t["dette"] / t["valeur_2026"]; t["valeur_par_logement"] = t["valeur_2026"] / t["nb_logements"]114    return t115116def ratios_iaao(t: pd.DataFrame) -> dict:117    """Statistiques de ratio (IAAO) : modèle de masse vs indications individuelles, portefeuille et par RMR."""118    out = {"portefeuille": h.statistiques_ratio(t["valeur_predite"].values, t["valeur_2026"].values)}119    for r in RMR:120        s = t[t["rmr"] == r]121        out[r] = h.statistiques_ratio(s["valeur_predite"].values, s["valeur_2026"].values) if len(s) >= 4 else {"n": len(s), "ratio_median": float((s["valeur_predite"] / s["valeur_2026"]).median()), "COD": float("nan"), "PRD": float("nan"), "PRB": float("nan")}122    out["somme_predite"] = float(t["valeur_predite"].sum()); out["somme_individuelle"] = float(t["valeur_2026"].sum())123    out["ecart_agrege_pct"] = out["somme_predite"] / out["somme_individuelle"] - 1124    out["extremes"] = t.loc[(t["ratio"] - 1).abs().nlargest(5).index, ["id_immeuble", "rmr", "nb_logements", "etat_1a5", "taux_global", "taux_predit", "valeur_2026", "valeur_predite", "ratio"]]125    return out126127# ----------------------------------------------------------------------------- 48.5 dynamique des RNE128129def dynamique_rne(se: pd.DataFrame, pf: pd.DataFrame) -> dict:130    cr = cas4.croissance_rne(se).merge(pf[["id_immeuble", "rmr", "valeur_2026", "zone_inondable"]], on="id_immeuble")131    piv = se.pivot(index="trimestre", columns="id_immeuble", values="rne_trimestriel").sort_index()132    rmr_de = pf.set_index("id_immeuble")["rmr"]133    agg_rmr = piv.T.groupby(rmr_de).sum().T[RMR]; agg_tot = piv.sum(axis=1)134    t = np.arange(len(piv)) / 4.0135    def croiss(y):136        b = np.polyfit(t, np.log(y.values), 1)[0]; return float(np.exp(b) - 1)137    def vol(y): return float(np.std(np.diff(np.log(y.values))) * 2)138    reg_tab = pd.DataFrame({"croissance_agregee": {r: croiss(agg_rmr[r]) for r in RMR}, "volatilite_agregee": {r: vol(agg_rmr[r]) for r in RMR},139                            "croissance_mediane_immeubles": cr.groupby("rmr")["croissance_annuelle"].median(), "croissance_min": cr.groupby("rmr")["croissance_annuelle"].min(),140                            "croissance_max": cr.groupby("rmr")["croissance_annuelle"].max(), "volatilite_mediane_immeubles": cr.groupby("rmr")["volatilite"].median(),141                            "n": cr.groupby("rmr")["id_immeuble"].count()}).loc[RMR]142    tot = {"croissance_agregee": croiss(agg_tot), "volatilite_agregee": vol(agg_tot), "croissance_mediane_immeubles": float(cr["croissance_annuelle"].median()),143           "volatilite_mediane_immeubles": float(cr["volatilite"].median()), "rne_2016T1_annualise": float(agg_tot.iloc[0] * 4), "rne_2026T2_annualise": float(agg_tot.iloc[-1] * 4),144           "rne_fichier_2026": float(pf["rne_2026"].sum())}145    # choc pandémique : écart moyen (log) à la tendance pendant 2020T2-2021T2, par RMR et total146    def choc(y):147        ly = np.log(y.values); b = np.polyfit(t, ly, 1); trend = np.polyval(b, t)148        m = (piv.index >= pd.Period("2020Q2", "Q")) & (piv.index <= pd.Period("2021Q2", "Q"))149        return float(np.exp(np.mean(ly[m] - trend[m])) - 1)150    pand = {r: choc(agg_rmr[r]) for r in RMR}; pand["Portefeuille"] = choc(agg_tot)151    # variation glissante sur 4 trimestres de l'agrégat (point bas)152    g4 = agg_tot / agg_tot.shift(4) - 1153    # corrélations des variations trimestrielles (log) entre immeubles154    dl = np.log(piv).diff().dropna()155    C = dl.corr(); ids = list(piv.columns); r_of = rmr_de.loc[ids].values156    same = np.equal.outer(r_of, r_of); iu = np.triu_indices(len(ids), 1)157    cvals = C.values[iu]; svals = same[iu]158    corr = {"intra_rmr_moyenne": float(cvals[svals].mean()), "inter_rmr_moyenne": float(cvals[~svals].mean()), "toutes_moyenne": float(cvals.mean()),159            "intra_rmr_mediane": float(np.median(cvals[svals])), "inter_rmr_mediane": float(np.median(cvals[~svals])), "n_paires_intra": int(svals.sum()), "n_paires_inter": int((~svals).sum())}160    # corrélations hors période pandémique (le choc commun crée de la corrélation)161    m_hors = ~((dl.index >= pd.Period("2020Q1", "Q")) & (dl.index <= pd.Period("2021Q3", "Q")))162    C2 = dl[m_hors].corr().values[iu]163    corr["intra_hors_pandemie"] = float(C2[svals].mean()); corr["inter_hors_pandemie"] = float(C2[~svals].mean())164    corr_rmr = np.log(agg_rmr).diff().dropna().corr()165    return {"par_immeuble": cr, "par_rmr": reg_tab, "total": tot, "agregats_rmr": agg_rmr, "agregat_total": agg_tot, "pandemie": pand, "glissant4": g4,166            "correlations": corr, "corr_rmr": corr_rmr, "matrice_corr_immeubles": C, "diff_log": dl}167168# ----------------------------------------------------------------------------- 48.6 risques et dette169170def risques(pf: pd.DataFrame, reg: dict, t: pd.DataFrame) -> dict:171    tot_v = float(pf["valeur_2026"].sum())172    inond = pf[pf["zone_inondable"] == 1].copy(); sol = pf[pf["contamination_sol"] == 1].copy()173    b_in = float(reg["table"].loc["inondable", "coef"]); b_sol = float(reg["table"].loc["sol", "coef"])174    # valeur « sans le risque » : même RNE, taux divisé par exp(β) (effet proportionnel du modèle)175    inond["taux_sans_risque"] = inond["taux_global"] / np.exp(b_in); inond["valeur_sans_risque"] = inond["valeur_2026"] * np.exp(b_in)   # même RNE, taux ÷ exp(β)176    inond["decote"] = inond["valeur_2026"] / inond["valeur_sans_risque"] - 1; inond["pb"] = (inond["taux_global"] - inond["taux_sans_risque"]) * 1e4177    sol["taux_sans_risque"] = sol["taux_global"] / np.exp(b_sol); sol["valeur_sans_risque"] = sol["valeur_2026"] * np.exp(b_sol)178    sol["decote"] = sol["valeur_2026"] / sol["valeur_sans_risque"] - 1; sol["pb"] = (sol["taux_global"] - sol["taux_sans_risque"]) * 1e4179    return {"inondables": inond, "part_inondable": float(inond["valeur_2026"].sum() / tot_v), "valeur_inondable": float(inond["valeur_2026"].sum()),180            "decote_inondable_agregee": float(inond["valeur_2026"].sum() / inond["valeur_sans_risque"].sum() - 1), "effet_inondable_pct_taux": float(np.exp(b_in) - 1),181            "sols": sol, "part_sol": float(sol["valeur_2026"].sum() / tot_v), "valeur_sol": float(sol["valeur_2026"].sum()), "effet_sol_pct_taux": float(np.exp(b_sol) - 1),182            "decote_sol_agregee": float(sol["valeur_2026"].sum() / sol["valeur_sans_risque"].sum() - 1),183            "dette_inondable": float(inond["dette"].sum()), "rpv_inondable": float(inond["dette"].sum() / inond["valeur_2026"].sum())}184185def dette(pf: pd.DataFrame, amort: int = AMORT_BASE, chocs=CHOCS_PB, croissance: pd.Series | None = None) -> dict:186    """Service de la dette par immeuble (amortissement `amort`), RCD, échéancier, renouvellement à +Δ pb, capacité de refinancement."""187    rows = []188    for r in pf.to_dict("records"):189        hyp = fin.Hypotheque(float(r["dette"]), float(r["taux_dette"]), amort)190        sd = hyp.service_dette_annuel; rne = float(r["rne_2026"])191        annees = int(r["echeance_dette"]) - 2026; mois = annees * 12192        solde = hyp.solde(mois); amort_res = amort - mois193        g = float(croissance.loc[r["id_immeuble"]]) if croissance is not None else 0.0194        rne_ech = rne * (1 + g) ** annees195        row = {"id_immeuble": r["id_immeuble"], "rmr": r["rmr"], "valeur": r["valeur_2026"], "dette": r["dette"], "taux_dette": r["taux_dette"], "echeance": r["echeance_dette"],196               "rpv": r["dette"] / r["valeur_2026"], "service_dette": sd, "constante": hyp.constante_hypothecaire, "rcd": rne / sd, "solde_echeance": solde, "rne_echeance": rne_ech,197               "levier": "négatif" if hyp.constante_hypothecaire > r["taux_global"] else "positif"}198        for pb in chocs:199            h2 = fin.Hypotheque(solde, float(r["taux_dette"]) + pb / 1e4, amort_res)200            sd2 = h2.service_dette_annuel201            cap_rcd = fin.pret_max_par_rcd(rne_ech, RCD_MIN, float(r["taux_dette"]) + pb / 1e4, amort_res)202            cap_rpv = fin.pret_max_par_rpv(float(r["valeur_2026"]), RPV_MAX)203            cap = min(cap_rcd, cap_rpv)204            row[f"sd_+{pb}"] = sd2; row[f"rcd_+{pb}"] = rne_ech / sd2; row[f"capacite_+{pb}"] = cap; row[f"injection_+{pb}"] = max(solde - cap, 0.0)205        rows.append(row)206    d = pd.DataFrame(rows)207    tot = {"dette": float(d["dette"].sum()), "service_dette": float(d["service_dette"].sum()), "rcd_agrege": float(pf["rne_2026"].sum() / d["service_dette"].sum()),208           "rcd_median": float(d["rcd"].median()), "rcd_min": float(d["rcd"].min()), "id_rcd_min": d.loc[d["rcd"].idxmin(), "id_immeuble"],209           "n_rcd_sous_1.20": int((d["rcd"] < 1.20).sum()), "n_rcd_sous_1.00": int((d["rcd"] < 1.0).sum()),210           "dette_rcd_sous_1.20": float(d.loc[d["rcd"] < 1.20, "dette"].sum()), "dette_rcd_sous_1.00": float(d.loc[d["rcd"] < 1.0, "dette"].sum()),211           "constante_pondere": float(d["service_dette"].sum() / d["dette"].sum()), "n_levier_negatif": int((d["levier"] == "négatif").sum()),212           "valeur_levier_negatif": float(d.loc[d["levier"] == "négatif", "valeur"].sum() / d["valeur"].sum())}213    for pb in chocs:214        tot[f"rcd_agrege_+{pb}"] = float(d["rne_echeance"].sum() / d[f"sd_+{pb}"].sum())215        tot[f"n_rcd_sous_1.20_+{pb}"] = int((d[f"rcd_+{pb}"] < 1.20).sum()); tot[f"n_rcd_sous_1.00_+{pb}"] = int((d[f"rcd_+{pb}"] < 1.0).sum())216        tot[f"injection_+{pb}"] = float(d[f"injection_+{pb}"].sum()); tot[f"n_injection_+{pb}"] = int((d[f"injection_+{pb}"] > 0).sum())217        tot[f"solde_echeance"] = float(d["solde_echeance"].sum())218    ech = d.groupby("echeance").agg(n=("id_immeuble", "count"), dette=("dette", "sum"), solde=("solde_echeance", "sum"), rne=("rne_echeance", "sum"))219    for pb in chocs:220        ech[f"sd_+{pb}"] = d.groupby("echeance")[f"sd_+{pb}"].sum(); ech[f"rcd_+{pb}"] = ech["rne"] / ech[f"sd_+{pb}"]221        ech[f"injection_+{pb}"] = d.groupby("echeance")[f"injection_+{pb}"].sum()222    ech["part_dette"] = ech["dette"] / tot["dette"]223    ech_rmr = d.pivot_table(index="echeance", columns="rmr", values="dette", aggfunc="sum").fillna(0.0)[RMR]224    par_rmr = d.groupby("rmr").agg(dette=("dette", "sum"), sd=("service_dette", "sum"), rpv=("rpv", "mean")).loc[RMR]225    par_rmr["rcd"] = pf.groupby("rmr")["rne_2026"].sum().loc[RMR] / par_rmr["sd"]; par_rmr["rpv_pondere"] = par_rmr["dette"] / pf.groupby("rmr")["valeur_2026"].sum().loc[RMR]226    return {"par_immeuble": d, "total": tot, "echeancier": ech, "echeancier_rmr": ech_rmr, "par_rmr": par_rmr, "amortissement": amort}227228# ----------------------------------------------------------------------------- 48.7 Monte-Carlo avec corrélation régionale229230def matrice_correlation(rmr_vec: np.ndarray, rho_global: float, rho_regional: float) -> np.ndarray:231    """Matrice de corrélation à deux facteurs (national, régional) : ρ_ij = ρ_reg si même RMR, ρ_glob sinon, 1 sur la diagonale."""232    same = np.equal.outer(rmr_vec, rmr_vec)233    S = np.where(same, rho_regional, rho_global).astype(float); np.fill_diagonal(S, 1.0)234    return S235236def simuler_portefeuille(pf: pd.DataFrame, dyn: dict, reg: dict, n: int = N_MC, graine: int = GRAINE, rho_taux: dict = RHO_TAUX, rho_rne: dict | None = None,237                         sigma_taux: float | None = None, regime: str = "declare") -> dict:238    """Simulation de la valeur de chaque immeuble : ln V_i = ln RNE_i + ε_i − ln R_i − η_i, avec (ε, η) gaussiens corrélés entre immeubles par une239    structure à deux facteurs (copule gaussienne, échantillonnée par décomposition de Cholesky). σ_ε,i = volatilité annualisée estimée du RNE de240    l'immeuble ; σ_η = écart-type résiduel de la régression du ln taux (même pour tous). regime ∈ {declare, independant, parfait}."""241    ids = pf["id_immeuble"].values; rmr_vec = pf["rmr"].values; N = len(pf)242    vol = dyn["par_immeuble"].set_index("id_immeuble").loc[ids, "volatilite"].values243    sig_eta = reg["sigma"] if sigma_taux is None else sigma_taux244    corr = dyn["correlations"]245    # estimation sur les séries : corrélation inter-RMR moyenne des variations trimestrielles ; la composante régionale additionnelle est246    # max(intra − inter, 0) — nulle dans ce jeu, où le seul facteur commun est le choc pandémique (voir §48.5)247    if rho_rne is None: rho_rne = {"global": max(corr["inter_rmr_moyenne"], 0.0), "regional": max(corr["intra_rmr_moyenne"], corr["inter_rmr_moyenne"], 0.0)}248    if regime == "independant": S_e = np.eye(N); S_n = np.eye(N)249    elif regime == "parfait": S_e = np.ones((N, N)); S_n = np.ones((N, N))250    else:251        S_e = matrice_correlation(rmr_vec, rho_rne["global"], rho_rne["regional"]); S_n = matrice_correlation(rmr_vec, rho_taux["global"], rho_taux["regional"])252    L_e = np.linalg.cholesky(S_e + 1e-10 * np.eye(N)); L_n = np.linalg.cholesky(S_n + 1e-10 * np.eye(N))253    rng = np.random.default_rng(graine)254    Z_e = rng.standard_normal((n, N)) @ L_e.T; Z_n = rng.standard_normal((n, N)) @ L_n.T255    eps = Z_e * vol[None, :]; eta = Z_n * sig_eta256    V0 = pf["valeur_2026"].values.astype(float); RNE0 = pf["rne_2026"].values.astype(float); R0 = pf["taux_global"].values.astype(float)257    V = (RNE0[None, :] * np.exp(eps)) / (R0[None, :] * np.exp(eta))258    Vp = V.sum(1); tot0 = V0.sum()259    q = lambda a, p: float(np.quantile(a, p))260    stats = {"V0": float(tot0), "moyenne": float(Vp.mean()), "mediane": q(Vp, 0.5), "ecart_type": float(Vp.std(ddof=1)), "cv": float(Vp.std(ddof=1) / Vp.mean()),261             "q05": q(Vp, 0.05), "q25": q(Vp, 0.25), "q75": q(Vp, 0.75), "q95": q(Vp, 0.95), "q01": q(Vp, 0.01), "min": float(Vp.min()), "max": float(Vp.max())}262    # VaR à 95 % (perte par rapport à la valeur centrale V0) : portefeuille vs somme des VaR individuelles263    var_p = tot0 - q(Vp, 0.05); var_i = V0 - np.quantile(V, 0.05, axis=0); somme_var_i = float(var_i.sum())264    es_p = tot0 - float(Vp[Vp <= q(Vp, 0.05)].mean())265    dette_tot = float(pf["dette"].sum())266    probs = {"rpv_sup_0.65": float((dette_tot / Vp > 0.65).mean()), "rpv_sup_0.70": float((dette_tot / Vp > 0.70).mean()), "rpv_sup_0.75": float((dette_tot / Vp > 0.75).mean()),267             "V_sous_130M": float((Vp < 130e6).mean()), "V_sous_120M": float((Vp < 120e6).mean()), "baisse_sup_10pct": float((Vp < 0.9 * tot0).mean()), "baisse_sup_20pct": float((Vp < 0.8 * tot0).mean())}268    # par RMR269    par_rmr = {}270    for r in RMR:271        m = rmr_vec == r; Vr = V[:, m].sum(1); v0 = V0[m].sum()272        par_rmr[r] = {"V0": float(v0), "mediane": q(Vr, 0.5), "q05": q(Vr, 0.05), "q95": q(Vr, 0.95), "cv": float(Vr.std(ddof=1) / Vr.mean()), "var95": float(v0 - q(Vr, 0.05)),273                      "somme_var_individuelles": float((V0[m] - np.quantile(V[:, m], 0.05, axis=0)).sum())}274    par_rmr = pd.DataFrame(par_rmr).T275    sigma_ln_i = np.sqrt(vol ** 2 + sig_eta ** 2)276    return {"n": n, "graine": graine, "regime": regime, "valeur_portefeuille": Vp, "valeurs": V, "stats": stats, "var95_portefeuille": var_p, "somme_var95_individuelles": somme_var_i,277            "ratio_diversification": var_p / somme_var_i, "es95_portefeuille": es_p, "var95_individuelles": var_i, "probabilites": probs, "par_rmr": par_rmr,278            "parametres": {"sigma_taux": sig_eta, "rho_taux": rho_taux, "rho_rne": rho_rne, "vol_rne_mediane": float(np.median(vol)), "vol_rne_min": float(vol.min()), "vol_rne_max": float(vol.max())},279            "sigma_ln_individuel": sigma_ln_i, "cv_individuel_median": float(np.median(V.std(axis=0, ddof=1) / V.mean(axis=0)))}280281def variance_normale_portefeuille(sigmas: np.ndarray, poids: np.ndarray, S: np.ndarray) -> dict:282    """Démonstration analytique : σ_P² = w'Σw ; comparaison avec (Σ w_i σ_i)² (corrélation parfaite) et Σ w_i² σ_i² (indépendance)."""283    Sig = np.outer(sigmas, sigmas) * S284    return {"sigma_portefeuille": float(np.sqrt(poids @ Sig @ poids)), "sigma_parfait": float((poids * sigmas).sum()), "sigma_independant": float(np.sqrt((poids ** 2 * sigmas ** 2).sum()))}285286# ----------------------------------------------------------------------------- 48.8 allocation et arbitrage287288def allocation(pf: pd.DataFrame, dyn: dict, mc: dict, det: dict) -> dict:289    t = pf[["id_immeuble", "rmr", "nb_logements", "age", "etat_1a5", "zone_inondable", "contamination_sol", "rne_2026", "taux_global", "valeur_2026", "dette", "echeance_dette"]].copy()290    cr = dyn["par_immeuble"].set_index("id_immeuble")291    t["croissance"] = cr.loc[t["id_immeuble"], "croissance_annuelle"].values; t["volatilite_rne"] = cr.loc[t["id_immeuble"], "volatilite"].values292    t["rendement_total"] = t["taux_global"] + t["croissance"]                       # R + g (Gordon, ch. 29)293    t["sigma_valeur"] = mc["sigma_ln_individuel"]294    t["ratio_rr"] = (t["rendement_total"] - OBLIG_10_ANS) / t["sigma_valeur"]          # excédent de rendement par unité de risque295    d = det["par_immeuble"].set_index("id_immeuble")296    t["rcd"] = d.loc[t["id_immeuble"], "rcd"].values; t["rcd_+200"] = d.loc[t["id_immeuble"], "rcd_+200"].values; t["rpv"] = t["dette"] / t["valeur_2026"]297    t["drapeaux"] = t["zone_inondable"] + t["contamination_sol"] + (t["rcd_+200"] < 1.0).astype(int) + (t["etat_1a5"] <= 2).astype(int)298    t["score"] = t["ratio_rr"].rank(pct=True) - 0.10 * t["drapeaux"]299    t = t.sort_values("score", ascending=False).reset_index(drop=True)300    vendre = t.tail(6).copy(); conserver = t.head(6).copy()301    # portefeuille après cession des 6 candidats (produit net = valeur − dette ; coûts de vente 2 %)302    reste = pf[~pf["id_immeuble"].isin(vendre["id_immeuble"])]303    apres = {"n": len(reste), "valeur": float(reste["valeur_2026"].sum()), "rne": float(reste["rne_2026"].sum()), "dette": float(reste["dette"].sum()),304             "taux_pondere": float(reste["rne_2026"].sum() / reste["valeur_2026"].sum()), "rpv": float(reste["dette"].sum() / reste["valeur_2026"].sum()),305             "produit_net_cession": float((vendre["valeur_2026"] * 0.98 - vendre["dette"]).sum()), "valeur_cedee": float(vendre["valeur_2026"].sum()),306             "part_inondable": float(reste.loc[reste["zone_inondable"] == 1, "valeur_2026"].sum() / reste["valeur_2026"].sum())}307    # frontière rendement-risque par RMR : μ_r = taux pondéré + croissance agrégée ; Σ_r = covariance des rendements simulés des agrégats régionaux308    g = cas4.sommaire(pf)["par_rmr"].loc[RMR]309    mu = (g["taux_pondere"] + dyn["par_rmr"]["croissance_agregee"]).values310    rmr_vec = pf["rmr"].values; V = mc["valeurs"]311    rend = np.column_stack([np.log(V[:, rmr_vec == r].sum(1) / pf.loc[rmr_vec == r, "valeur_2026"].sum()) for r in RMR])312    Sigma = np.cov(rend.T); sig = np.sqrt(np.diag(Sigma))313    w0 = (g["valeur"] / g["valeur"].sum()).values314    from scipy.optimize import minimize315    def frontiere(cibles):316        pts = []317        for m in cibles:318            cons = [{"type": "eq", "fun": lambda w: w.sum() - 1}, {"type": "eq", "fun": lambda w, m=m: w @ mu - m}]319            r = minimize(lambda w: w @ Sigma @ w, np.full(6, 1 / 6), bounds=[(0, 1)] * 6, constraints=cons, method="SLSQP", options={"ftol": 1e-12, "maxiter": 500})320            if r.success: pts.append({"mu": m, "sigma": float(np.sqrt(r.fun)), "poids": r.x})321        return pts322    cibles = np.linspace(mu.min() + 1e-6, mu.max() - 1e-6, 41)323    fr = frontiere(cibles)324    r_min = minimize(lambda w: w @ Sigma @ w, np.full(6, 1 / 6), bounds=[(0, 1)] * 6, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}], method="SLSQP")325    w_min = r_min.x326    # portefeuille de même rendement attendu que l'actuel, à risque minimal327    mu0 = float(w0 @ mu); sig0 = float(np.sqrt(w0 @ Sigma @ w0))328    r_same = minimize(lambda w: w @ Sigma @ w, w0, bounds=[(0, 1)] * 6, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}, {"type": "eq", "fun": lambda w: w @ mu - mu0}], method="SLSQP", options={"ftol": 1e-12})329    # portefeuille de même risque, rendement maximal330    r_maxmu = minimize(lambda w: -(w @ mu), w0, bounds=[(0, 1)] * 6, constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}, {"type": "ineq", "fun": lambda w: sig0 ** 2 - w @ Sigma @ w}], method="SLSQP", options={"ftol": 1e-12})331    front = {"mu": mu, "sigma": sig, "Sigma": Sigma, "corr": Sigma / np.outer(sig, sig), "w_actuel": w0, "mu_actuel": mu0, "sigma_actuel": sig0,332             "frontiere": fr, "w_min_var": w_min, "mu_min_var": float(w_min @ mu), "sigma_min_var": float(np.sqrt(w_min @ Sigma @ w_min)),333             "w_meme_mu": r_same.x, "sigma_meme_mu": float(np.sqrt(r_same.x @ Sigma @ r_same.x)), "w_meme_sigma": r_maxmu.x, "mu_meme_sigma": float(r_maxmu.x @ mu)}334    return {"table": t, "vendre": vendre, "conserver": conserver, "apres_cession": apres, "frontiere": front}335336# ----------------------------------------------------------------------------- 48.9 réconciliation337338def reconciliation(pf: pd.DataFrame, iaao: dict, mc: dict, det: dict, som: dict) -> dict:339    ind = float(pf["valeur_2026"].sum()); pred = iaao["somme_predite"]340    V = arrondir(ind)341    return {"somme_individuelle": ind, "somme_modele": pred, "ecart_modele_pct": pred / ind - 1, "valeur_retenue": V,342            "fourchette_mc": (mc["stats"]["q05"], mc["stats"]["q95"]), "fourchette_mc_pct": (mc["stats"]["q05"] / ind - 1, mc["stats"]["q95"] / ind - 1),343            "valeur_par_logement": V / som["total"]["logements"], "taux_implicite": som["total"]["rne"] / V, "rpv": som["total"]["dette"] / V,344            "rcd_agrege": det["total"]["rcd_agrege"], "par_rmr": som["par_rmr"]["valeur"], "mrb": V / float(pf["revenu_brut_potentiel"].sum()),345            "fonds_propres": V - som["total"]["dette"]}346347# ----------------------------------------------------------------------------- exercices348349def exercices(D: dict) -> dict:350    pf, reg, dyn, det, mc = D["portefeuille"], D["regression"], D["dynamique"], D["dette"], D["mc"]351    out = {}352    # ex1 : Sherbrooke — taux pondéré vs moyenne simple, valeur/logement, RPV353    s = pf[pf["rmr"] == "Sherbrooke"]354    out["ex1"] = {"n": len(s), "logements": int(s["nb_logements"].sum()), "valeur": float(s["valeur_2026"].sum()), "rne": float(s["rne_2026"].sum()), "dette": float(s["dette"].sum()),355                  "taux_pondere": float(s["rne_2026"].sum() / s["valeur_2026"].sum()), "taux_moyen_simple": float(s["taux_global"].mean()), "taux_median": float(s["taux_global"].median()),356                  "valeur_par_logement": float(s["valeur_2026"].sum() / s["nb_logements"].sum()), "rpv": float(s["dette"].sum() / s["valeur_2026"].sum()),357                  "valeur_au_taux_moyen": float(s["rne_2026"].sum() / s["taux_global"].mean()), "ecart_pct": float(s["rne_2026"].sum() / s["taux_global"].mean() / s["valeur_2026"].sum() - 1)}358    # ex2 : modèle de masse — 24 log., état 3, 45 ans, Trois-Rivières, inondable ; RNE 150 000359    rne = 150_000.0360    R_in = taux_predit(reg, "Trois-Rivières", 3, 24, 45, inond=1); R_hors = taux_predit(reg, "Trois-Rivières", 3, 24, 45, inond=0); R_mtl = taux_predit(reg, "Montréal", 3, 24, 45, inond=0)361    out["ex2"] = {"taux_inondable": R_in, "taux_hors_zone": R_hors, "taux_montreal": R_mtl, "valeur_inondable": rne / R_in, "valeur_hors_zone": rne / R_hors, "valeur_montreal": rne / R_mtl,362                  "ecart_inondation": rne / R_in / (rne / R_hors) - 1, "pb_inondation": (R_in - R_hors) * 1e4, "duan": reg["duan"], "sans_duan": float(np.exp(x_ligne("Trois-Rivières", 3, 24, 45, 1) @ reg["beta"]))}363    # ex3 : P45 — RCD 480 et 300 mois, renouvellement 2028 à +200 pb, capacité de refinancement364    p = pf.set_index("id_immeuble").loc["P45"]365    h480 = fin.Hypotheque(float(p["dette"]), float(p["taux_dette"]), 480); h300 = fin.Hypotheque(float(p["dette"]), float(p["taux_dette"]), 300)366    mois = (int(p["echeance_dette"]) - 2026) * 12; solde = h480.solde(mois); t2 = float(p["taux_dette"]) + 0.02367    h2 = fin.Hypotheque(solde, t2, 480 - mois); cap = min(fin.pret_max_par_rcd(float(p["rne_2026"]), RCD_MIN, t2, 480 - mois), 0.75 * float(p["valeur_2026"]))368    out["ex3"] = {"dette": float(p["dette"]), "taux": float(p["taux_dette"]), "rne": float(p["rne_2026"]), "sd_480": h480.service_dette_annuel, "rcd_480": float(p["rne_2026"]) / h480.service_dette_annuel,369                  "sd_300": h300.service_dette_annuel, "rcd_300": float(p["rne_2026"]) / h300.service_dette_annuel, "solde_2028": solde, "taux_renouvele": t2, "sd_renouvele": h2.service_dette_annuel,370                  "rcd_renouvele": float(p["rne_2026"]) / h2.service_dette_annuel, "capacite_rcd": fin.pret_max_par_rcd(float(p["rne_2026"]), RCD_MIN, t2, 480 - mois), "capacite_rpv": 0.75 * float(p["valeur_2026"]),371                  "capacite": cap, "injection": max(solde - cap, 0.0), "valeur_ch33": 6_716_534.0, "rpv_ch33": float(p["dette"]) / 6_716_534.0}372    # ex4 : ratios IAAO sur le sous-ensemble montréalais373    t = D["predites"]; m = t[t["rmr"] == "Montréal"]374    out["ex4"] = {**h.statistiques_ratio(m["valeur_predite"].values, m["valeur_2026"].values), "portefeuille": D["iaao"]["portefeuille"]}375    # ex6 : choc de taux généralisé — ρ_taux global 0,90 / régional 0,95376    mc5 = simuler_portefeuille(pf, dyn, reg, n=mc["n"], graine=GRAINE, rho_taux={"global": 0.90, "regional": 0.95})377    out["ex6"] = {"q05": mc5["stats"]["q05"], "q95": mc5["stats"]["q95"], "cv": mc5["stats"]["cv"], "var95": mc5["var95_portefeuille"], "ratio_div": mc5["ratio_diversification"],378                  "p_rpv_sup_0.75": mc5["probabilites"]["rpv_sup_0.75"], "base_var95": mc["var95_portefeuille"], "base_ratio_div": mc["ratio_diversification"], "base_cv": mc["stats"]["cv"]}379    # ex7 : cession des 3 immeubles en zone inondable380    inond = pf[pf["zone_inondable"] == 1]; reste = pf[pf["zone_inondable"] == 0]381    mc6 = simuler_portefeuille(reste.reset_index(drop=True), dyn, reg, n=mc["n"], graine=GRAINE)382    d6 = dette(reste)383    out["ex7"] = {"ids": inond["id_immeuble"].tolist(), "valeur_cedee": float(inond["valeur_2026"].sum()), "dette_remboursee": float(inond["dette"].sum()), "produit_net": float(inond["valeur_2026"].sum() * 0.98 - inond["dette"].sum()),384                  "rne_perdu": float(inond["rne_2026"].sum()), "taux_pondere_cede": float(inond["rne_2026"].sum() / inond["valeur_2026"].sum()),385                  "apres_valeur": float(reste["valeur_2026"].sum()), "apres_taux_pondere": float(reste["rne_2026"].sum() / reste["valeur_2026"].sum()), "apres_rpv": float(reste["dette"].sum() / reste["valeur_2026"].sum()),386                  "apres_rcd": d6["total"]["rcd_agrege"], "avant_rcd": det["total"]["rcd_agrege"], "apres_cv": mc6["stats"]["cv"], "avant_cv": mc["stats"]["cv"], "apres_var95": mc6["var95_portefeuille"], "avant_var95": mc["var95_portefeuille"],387                  "apres_ratio_div": mc6["ratio_diversification"], "avant_ratio_div": mc["ratio_diversification"]}388    # ex5 : croissance de P36 (72 log., Québec, inondable) vs agrégat de Québec ; choc pandémique389    cr = dyn["par_immeuble"].set_index("id_immeuble").loc["P36"]390    out["ex5"] = {"croissance_P36": float(cr["croissance_annuelle"]), "volatilite_P36": float(cr["volatilite"]), "croissance_quebec": float(dyn["par_rmr"].loc["Québec", "croissance_agregee"]),391                  "pandemie_quebec": dyn["pandemie"]["Québec"], "pandemie_portefeuille": dyn["pandemie"]["Portefeuille"], "rendement_total_P36": float(pf.set_index("id_immeuble").loc["P36", "taux_global"]) + float(cr["croissance_annuelle"])}392    return out393394# ----------------------------------------------------------------------------- dossier complet395396def dossier(n_mc: int = N_MC, graine: int = GRAINE) -> dict:397    d = cas4.charger(); pf, se = d["portefeuille"], d["series"]398    som = sommaire_etendu(pf)399    reg = regression_masse(pf); pred = valeurs_predites(pf, reg); iaao = ratios_iaao(pred)400    dyn = dynamique_rne(se, pf)401    ris = risques(pf, reg, pred)402    det = dette(pf, AMORT_BASE); det_alt = dette(pf, AMORT_ALT)403    det_g = dette(pf, AMORT_BASE, croissance=dyn["par_immeuble"].set_index("id_immeuble")["croissance_annuelle"])404    mc = simuler_portefeuille(pf, dyn, reg, n_mc, graine, regime="declare")405    mc_ind = simuler_portefeuille(pf, dyn, reg, n_mc, graine, regime="independant")406    mc_parf = simuler_portefeuille(pf, dyn, reg, n_mc, graine, regime="parfait")407    w = pf["valeur_2026"].values / pf["valeur_2026"].sum()408    S_e = matrice_correlation(pf["rmr"].values, mc["parametres"]["rho_rne"]["global"], mc["parametres"]["rho_rne"]["regional"])409    analytique = variance_normale_portefeuille(mc["sigma_ln_individuel"], w, 0.5 * (S_e + matrice_correlation(pf["rmr"].values, RHO_TAUX["global"], RHO_TAUX["regional"])))410    alloc = allocation(pf, dyn, mc, det)411    rec = reconciliation(pf, iaao, mc, det, som)412    D = {"portefeuille": pf, "series": se, "sommaire": som, "regression": reg, "predites": pred, "iaao": iaao, "dynamique": dyn, "risques": ris,413         "dette": det, "dette_alt": det_alt, "dette_croissance": det_g, "mc": mc, "mc_independant": mc_ind, "mc_parfait": mc_parf, "analytique": analytique,414         "allocation": alloc, "reconciliation": rec}415    D["exercices"] = exercices(D)416    return D417418def imprimer(D: dict) -> None:419    pd.set_option("display.width", 250); pd.set_option("display.max_columns", 40); pd.set_option("display.float_format", lambda v: f"{v:,.4f}")420    pf, som, reg = D["portefeuille"], D["sommaire"], D["regression"]421    print("=" * 110 + "\nCH. 48 — CAS 4 : PORTEFEUILLE INTERRÉGIONAL (DONNÉES SYNTHÉTIQUES, 48 immeubles fictifs) — date d'évaluation", DATE_EVAL)422    print("\n--- 48.2 Sommaire par RMR ---"); print(som["par_rmr"].to_string()); print({k: (round(v, 6) if isinstance(v, float) else v) for k, v in som["total"].items()})423    print("tailles :\n", som["tailles"].to_string()); print("âges :\n", som["ages"].to_string()); print("états :\n", som["etats"].to_string()); print("concentration", som["concentration"])424    print("\n--- 48.4 Modèle de masse ---"); print(reg["table"].to_string()); print("R²", round(reg["r2"], 4), "R² aj.", round(reg["r2_ajuste"], 4), "σ résiduel", round(reg["sigma"], 5), "Duan", round(reg["duan"], 5), "n", reg["n"], "k", reg["k"])425    t = D["predites"]; print(t[["id_immeuble", "rmr", "nb_logements", "age", "etat_1a5", "zone_inondable", "contamination_sol", "rne_2026", "taux_global", "taux_predit", "ecart_pb", "valeur_2026", "valeur_predite", "ratio", "dette", "rpv", "echeance_dette"]].to_string(index=False))426    ia = D["iaao"]; print("IAAO portefeuille", {k: round(v, 4) for k, v in ia["portefeuille"].items()})427    for r in RMR: print("  ", r, {k: (round(v, 4) if isinstance(v, float) else v) for k, v in ia[r].items() if k in ["n", "ratio_median", "COD", "PRD", "PRB"]})428    print("somme prédite", _d(ia["somme_predite"]), "vs individuelle", _d(ia["somme_individuelle"]), _p(ia["ecart_agrege_pct"])); print(ia["extremes"].to_string(index=False))429    print("\n--- 48.5 Dynamique des RNE ---"); dyn = D["dynamique"]; print(dyn["par_rmr"].to_string()); print({k: round(v, 5) for k, v in dyn["total"].items()})430    print("choc pandémique (écart à la tendance, 2020T2-2021T2)", {k: round(v, 4) for k, v in dyn["pandemie"].items()}); print("variation glissante 4 trim. min :", round(dyn["glissant4"].min(), 4), dyn["glissant4"].idxmin())431    print("corrélations", {k: round(v, 4) for k, v in dyn["correlations"].items()}); print(dyn["corr_rmr"].round(3).to_string())432    print("\n--- 48.6 Risques ---"); ris = D["risques"]; print(ris["inondables"][["id_immeuble", "rmr", "nb_logements", "taux_global", "taux_sans_risque", "pb", "valeur_2026", "valeur_sans_risque", "decote", "dette"]].to_string(index=False))433    print({k: (round(v, 5) if isinstance(v, float) else v) for k, v in ris.items() if not isinstance(v, pd.DataFrame)})434    print(ris["sols"][["id_immeuble", "rmr", "nb_logements", "taux_global", "taux_sans_risque", "pb", "valeur_2026", "valeur_sans_risque", "decote"]].to_string(index=False))435    for nom in ["dette", "dette_alt", "dette_croissance"]:436        det = D[nom]; print(f"\n--- Dette ({nom}, amortissement {det['amortissement']} mois) ---"); print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in det["total"].items()})437        print(det["echeancier"].to_string()); print(det["par_rmr"].to_string())438    det = D["dette"]; print(det["par_immeuble"][["id_immeuble", "rmr", "dette", "taux_dette", "echeance", "rpv", "service_dette", "constante", "rcd", "levier", "solde_echeance", "rcd_+0", "rcd_+100", "rcd_+200", "capacite_+200", "injection_+200"]].to_string(index=False))439    print(det["echeancier_rmr"].to_string())440    print("\n--- 48.7 Monte-Carlo ---")441    for nom in ["mc", "mc_independant", "mc_parfait"]:442        m = D[nom]; print(nom, m["regime"], {k: round(v, 1) for k, v in m["stats"].items()}); print("   VaR95 portefeuille", _d(m["var95_portefeuille"]), "somme VaR95 indiv.", _d(m["somme_var95_individuelles"]), "ratio", round(m["ratio_diversification"], 4), "ES95", _d(m["es95_portefeuille"]))443        print("   probabilités", {k: round(v, 4) for k, v in m["probabilites"].items()}); print("   paramètres", m["parametres"], "CV individuel médian", round(m["cv_individuel_median"], 4))444    print(D["mc"]["par_rmr"].to_string()); print("analytique (normale)", {k: round(v, 5) for k, v in D["analytique"].items()})445    print("\n--- 48.8 Allocation ---"); al = D["allocation"]446    print(al["table"][["id_immeuble", "rmr", "nb_logements", "etat_1a5", "taux_global", "croissance", "rendement_total", "sigma_valeur", "ratio_rr", "rcd", "rcd_+200", "rpv", "drapeaux", "score"]].to_string(index=False))447    print("vendre :", al["vendre"]["id_immeuble"].tolist(), "conserver :", al["conserver"]["id_immeuble"].tolist()); print("après cession", {k: round(v, 4) for k, v in al["apres_cession"].items()})448    fr = al["frontiere"]; print("μ par RMR", dict(zip(RMR, fr["mu"].round(5)))); print("σ par RMR", dict(zip(RMR, fr["sigma"].round(5)))); print("corr RMR\n", pd.DataFrame(fr["corr"], index=RMR, columns=RMR).round(3).to_string())449    print("actuel : w", fr["w_actuel"].round(3), "μ", round(fr["mu_actuel"], 5), "σ", round(fr["sigma_actuel"], 5)); print("min var : w", fr["w_min_var"].round(3), "μ", round(fr["mu_min_var"], 5), "σ", round(fr["sigma_min_var"], 5))450    print("même μ : w", fr["w_meme_mu"].round(3), "σ", round(fr["sigma_meme_mu"], 5)); print("même σ : w", fr["w_meme_sigma"].round(3), "μ", round(fr["mu_meme_sigma"], 5))451    print("\n--- 48.9 Réconciliation ---"); print({k: (round(v, 5) if isinstance(v, float) else v) for k, v in D["reconciliation"].items() if not isinstance(v, pd.Series)})452    print("\n--- Exercices ---")453    for k, v in D["exercices"].items(): print(k, {kk: (round(vv, 5) if isinstance(vv, float) else vv) for kk, vv in v.items()})454455if __name__ == "__main__":456    ap = argparse.ArgumentParser(); ap.add_argument("--n", type=int, default=N_MC); a = ap.parse_args()457    imprimer(dossier(a.n))458
Files · 739a7ae283cf11e4f280ccc5836e47f331cb784a
Menu

Theme