spb/traite-evaluation-qc
Public
1"""Tests du chapitre 48 (cas 4, portefeuille interrégional — données synthétiques) : identités du sommaire, cohérence du modèle de masse2avec traite.cas4.effets_regionaux et avec le ch. 33 (P45), identités financières (RCD, soldes), propriétés de la simulation (reproductibilité,3diversification : VaR portefeuille ≤ somme des VaR, cas parfaitement corrélé ≈ additif, cas indépendant très inférieur), frontière, valeurs citées.4Tolérances déclarées : montants ±1 $ (valeurs citées au dollar), taux ±1e-6, Monte-Carlo ±1 $ (même graine)."""5import sys, pathlib6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8from traite import cas4, finance as fin9import ch48_portefeuille as ch1011@pytest.fixture(scope="module")12def D():13 return ch.dossier(n_mc=10_000, graine=ch.GRAINE)1415def test_sommaire_identites(D):16 s = D["sommaire"]; g = s["par_rmr"]; t = s["total"]; pf = D["portefeuille"]17 assert t["n"] == 48 and t["logements"] == 888 and g["n"].sum() == 48 and g["logements"].sum() == 88818 assert t["valeur"] == pytest.approx(143_363_000) and t["rne"] == pytest.approx(7_447_030) and t["dette"] == pytest.approx(85_766_000)19 assert t["taux_pondere"] == pytest.approx(t["rne"] / t["valeur"]) and abs(t["taux_pondere"] - 0.0519) < 5e-420 assert t["rpv"] == pytest.approx(0.5982, abs=5e-4) and t["part_zone_inondable"] == pytest.approx(0.0666, abs=5e-4)21 assert (g["valeur"] / g["logements"]).loc["Montréal"] == pytest.approx(206_237.34, abs=0.01)22 assert g["part_valeur"].sum() == pytest.approx(1.0) and g["valeur"].sum() == pytest.approx(t["valeur"])23 # taux pondéré ≠ moyenne simple des taux (Sherbrooke, exercice 1)24 e1 = D["exercices"]["ex1"]; assert e1["taux_pondere"] != pytest.approx(e1["taux_moyen_simple"], abs=1e-4)25 assert t["valeur_par_logement"] == pytest.approx(161_444.82, abs=0.01)2627def test_modele_de_masse_coherent_avec_cas4_et_ch33(D):28 reg = D["regression"]; eff = cas4.effets_regionaux(D["portefeuille"])29 assert np.allclose(reg["beta"], eff["coef"].values)30 assert reg["table"].loc["rmr_Gatineau", "effet_pct"] == pytest.approx(0.1962, abs=5e-4) # +19,6 % (ch. 33)31 assert reg["table"].loc["ln_logements", "coef"] == pytest.approx(-0.0315, abs=5e-4) # ln n −0,0315 (ch. 33)32 assert reg["duan"] == pytest.approx(1.0011, abs=5e-5) # facteur de Duan (ch. 33)33 # P45 : taux de régression 5,461 % → 6 716 534 $ (ch. 33)34 R = ch.taux_predit(reg, "Gatineau", 2, 36, 72)35 assert R == pytest.approx(0.05461, abs=5e-6) and 366_800 / R == pytest.approx(6_716_534, abs=1)36 t = D["predites"]; assert t.set_index("id_immeuble").loc["P45", "valeur_predite"] == pytest.approx(6_716_534, abs=1)37 # prédiction = exp(Xβ) × Duan pour tous les immeubles ; somme prédite vs individuelle38 assert np.allclose(t["valeur_predite"], t["rne_2026"] / (np.exp(reg["X"] @ reg["beta"]) * reg["duan"]))39 assert D["iaao"]["ecart_agrege_pct"] == pytest.approx(-0.0055, abs=5e-4)40 ia = D["iaao"]["portefeuille"]; assert ia["n"] == 48 and 0.99 < ia["ratio_median"] < 1.01 and ia["COD"] < 5 and 0.98 < ia["PRD"] < 1.03 and abs(ia["PRB"]) < 0.054142def test_dynamique_rne(D):43 dyn = D["dynamique"]; pf = D["portefeuille"]44 assert len(dyn["par_immeuble"]) == 48 and dyn["total"]["rne_2026T2_annualise"] == pytest.approx(pf["rne_2026"].sum(), abs=10)45 assert 0.025 < dyn["total"]["croissance_agregee"] < 0.03546 assert dyn["par_rmr"].loc["Gatineau", "croissance_agregee"] > dyn["par_rmr"].loc["Saguenay", "croissance_agregee"]47 assert -0.05 < dyn["pandemie"]["Portefeuille"] < 0 # choc négatif48 c = dyn["correlations"]; assert 0.1 < c["inter_rmr_moyenne"] < 0.25 and abs(c["inter_hors_pandemie"]) < 0.05 # corrélation due au choc commun4950def test_dette_identites(D):51 det = D["dette"]; d = det["par_immeuble"]; pf = D["portefeuille"].set_index("id_immeuble")52 r = d.set_index("id_immeuble").loc["P45"]; hyp = fin.Hypotheque(5_416_000, 0.0517, 480)53 assert r["service_dette"] == pytest.approx(hyp.service_dette_annuel) and r["rcd"] == pytest.approx(366_800 / hyp.service_dette_annuel)54 assert r["rcd"] == pytest.approx(1.15, abs=5e-3) # ch. 33 : RCD 1,15 sur 40 ans55 assert D["dette_alt"]["par_immeuble"].set_index("id_immeuble").loc["P45", "rcd"] == pytest.approx(0.95, abs=5e-3) # ch. 33 : 0,95 sur 25 ans56 assert r["solde_echeance"] == pytest.approx(hyp.solde(24)) and det["total"]["rcd_agrege"] == pytest.approx(pf["rne_2026"].sum() / d["service_dette"].sum())57 assert det["echeancier"]["dette"].sum() == pytest.approx(85_766_000) and det["echeancier_rmr"].values.sum() == pytest.approx(85_766_000)58 # RCD décroît avec la hausse de taux ; capacité RCD = RNE / (1,20 f')59 assert (d["rcd_+0"] >= d["rcd_+100"]).all() and (d["rcd_+100"] >= d["rcd_+200"]).all()60 assert det["total"]["rcd_agrege_+200"] < det["total"]["rcd_agrege_+100"] < det["total"]["rcd_agrege_+0"]61 assert (d["injection_+200"] >= d["injection_+0"]).all()62 e3 = D["exercices"]["ex3"]; assert e3["injection"] == pytest.approx(max(e3["solde_2028"] - e3["capacite"], 0))6364def test_monte_carlo_reproductible_et_diversification(D):65 mc = D["mc"]; m2 = ch.simuler_portefeuille(D["portefeuille"], D["dynamique"], D["regression"], 10_000, ch.GRAINE)66 assert mc["stats"]["mediane"] == pytest.approx(m2["stats"]["mediane"], abs=1) and mc["var95_portefeuille"] == pytest.approx(m2["var95_portefeuille"], abs=1)67 assert mc["valeurs"].shape == (10_000, 48) and np.allclose(mc["valeurs"].sum(1), mc["valeur_portefeuille"])68 # espérance ≈ V0 à l'ordre des termes de convexité ; médiane proche de V069 assert abs(mc["stats"]["mediane"] / mc["stats"]["V0"] - 1) < 0.0170 # diversification : VaR portefeuille ≤ somme des VaR ; parfait ≈ additif ; indépendant ≪ déclaré71 assert mc["var95_portefeuille"] <= mc["somme_var95_individuelles"]72 assert D["mc_parfait"]["ratio_diversification"] == pytest.approx(1.0, abs=0.01)73 assert D["mc_independant"]["ratio_diversification"] < mc["ratio_diversification"] < 1.074 assert D["mc_independant"]["stats"]["ecart_type"] < mc["stats"]["ecart_type"] < D["mc_parfait"]["stats"]["ecart_type"]75 # matrice de corrélation à deux facteurs : symétrique, diagonale 1, définie positive76 S = ch.matrice_correlation(D["portefeuille"]["rmr"].values, 0.5, 0.7)77 assert np.allclose(S, S.T) and np.allclose(np.diag(S), 1) and np.linalg.eigvalsh(S).min() > 078 # version analytique normale : indépendant < portefeuille < parfait79 a = D["analytique"]; assert a["sigma_independant"] < a["sigma_portefeuille"] < a["sigma_parfait"]8081def test_frontiere_et_allocation(D):82 fr = D["allocation"]["frontiere"]83 assert fr["w_actuel"].sum() == pytest.approx(1.0) and fr["w_min_var"].sum() == pytest.approx(1.0, abs=1e-6)84 assert fr["sigma_min_var"] <= fr["sigma_actuel"] + 1e-9 and fr["sigma_meme_mu"] <= fr["sigma_actuel"] + 1e-9 and fr["mu_meme_sigma"] >= fr["mu_actuel"] - 1e-985 assert fr["mu_actuel"] == pytest.approx(float(fr["w_actuel"] @ fr["mu"]))86 al = D["allocation"]; assert len(al["vendre"]) == 6 and len(al["conserver"]) == 6 and not set(al["vendre"]["id_immeuble"]) & set(al["conserver"]["id_immeuble"])87 ap = al["apres_cession"]; assert ap["n"] == 42 and ap["valeur"] == pytest.approx(143_363_000 - ap["valeur_cedee"])8889def test_risques_et_reconciliation(D):90 ris = D["risques"]; rec = D["reconciliation"]91 assert ris["part_inondable"] == pytest.approx(0.0666, abs=5e-4) and len(ris["inondables"]) == 3 and len(ris["sols"]) == 292 assert ris["effet_inondable_pct_taux"] == pytest.approx(np.exp(D["regression"]["beta"][3]) - 1)93 assert ris["decote_inondable_agregee"] == pytest.approx(-ris["effet_inondable_pct_taux"] / (1 + ris["effet_inondable_pct_taux"]), abs=1e-9) # V/V' − 1 = 1/(1+e) − 194 assert rec["valeur_retenue"] == 143_400_000 and rec["rpv"] == pytest.approx(85_766_000 / 143_400_000)95 assert rec["fourchette_mc"][0] < rec["somme_individuelle"] < rec["fourchette_mc"][1]96 e6 = D["exercices"]["ex7"]; assert e6["apres_valeur"] == pytest.approx(143_363_000 - 9_546_000) and e6["ids"] == ["P36", "P41", "P43"]97