spb/traite-evaluation-qc
Public
1"""Tests du chapitre 46 — Cas 2 : triplex à Québec (dossier complet, données SYNTHÉTIQUES).2Identités (RNE, grille d'ajustements, hypothèque), cohérence avec traite.cas2 et ch32_plex (mêmes comparables, mêmes indications3de référence), valeurs citées dans le texte (tolérances : montants ±1 $ sauf mention, taux ±1e-4), exercices, présence des figures."""4import pathlib, sys5import numpy as np, pytest67ROOT = pathlib.Path(__file__).resolve().parents[3]8sys.path.insert(0, str(ROOT / "analysis/python/cases"))9import ch46_dossier as ch # noqa: E40210import ch32_plex as C # noqa: E40211from traite import cas2, finance as fin # noqa: E402121314@pytest.fixture(scope="module")15def D():16 return ch.dossier()171819# ----------------------------------------------------------------------------- cohérence avec le module partagé et le ch. 3220def test_coherence_cas2_et_ch32(D):21 r = cas2.tout(); b = D["base"]22 assert b["rbp_contrat"] == pytest.approx(41832.0) and b["rbp_marche"] == pytest.approx(49332.0) and b["rbe"] == pytest.approx(40577.04)23 assert b["rne_declare"] == pytest.approx(r["base"]["rne"], abs=1e-6) # RNE « déclaré » = RNE du ch. 32 / traite.cas224 assert b["rne_declare"] == pytest.approx(22033.9584, abs=0.01)25 assert list(D["comparables5"]["id_vente"]) == ["Q0187", "Q0222", "Q0243", "Q0207", "Q0235"] # mêmes cinq comparables que le ch. 3226 assert D["revenu"]["v_mrb_comp"] == pytest.approx(922395.6, abs=0.01)27 assert D["revenu"]["taux_ajuste"] == pytest.approx(0.0302, abs=1e-4)28 assert D["revenu"]["v_cap_ajuste_rne_declare"] == pytest.approx(729549.19, abs=0.01) # indication du ch. 32 retrouvée29 assert D["ch32"]["valeur_arrondie"] == 870000303132# ----------------------------------------------------------------------------- 46.5 taxes 2026 et RNE normalisé33def test_taxes_2026_et_rne(D):34 b = D["base"]; t = b["taxes"]35 assert t["fonciere"] == pytest.approx(495000 * 0.007464) and t["tarifs"] == pytest.approx(3 * (386 + 195))36 assert t["municipale"] == pytest.approx(5437.68, abs=0.01) and t["scolaire"] == pytest.approx((495000 - 25000) * 0.0007899, abs=0.01)37 assert b["ecart_taxes"] == pytest.approx(6370 - 5437.68 - 371.253, abs=0.01)38 assert b["rne"] == pytest.approx(b["rne_declare"] + b["ecart_taxes"], abs=1e-6)39 assert b["rne"] == pytest.approx(22595.03, abs=0.01)40 assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"])41 det = b["detail"]; assert det.loc[det["type"] == "imputée", "normalise"].iloc[0] == pytest.approx(0.04 * b["rbe"])42 assert b["ratio_depenses"] == pytest.approx(0.443, abs=0.001)43 assert b["rne_marche"] == pytest.approx(29579.03, abs=0.01)444546# ----------------------------------------------------------------------------- 46.6 régression auxiliaire et grille47def test_regression_et_grille(D):48 reg = D["regression"]; g = D["grille"]; t = g["tableau"]49 assert reg["n"] == 127 and 0.70 < reg["r2"] < 0.7550 assert reg["taux_annuel_marche"] == pytest.approx(np.exp(-12 * reg["beta"]["mois"]) - 1)51 assert abs(reg["t"]["loyer_relatif"]) < ch.T_MIN and (t["loyer_relatif"] == 0).all() # coefficient non significatif → pas d'ajustement52 assert (t["stationnement"] == 0).all() # tous les comparables ont une case, comme le sujet53 for _, r in t.iterrows(): # identité : prix ajusté = prix + Σ ajustements ; brut = Σ|A|/P54 aj = [r[k] for k in ("temps", "superficie", "age", "etat", "stationnement", "loyer_marche", "loyer_relatif")]55 assert r["prix_ajuste"] == pytest.approx(r["prix_logement"] + sum(aj))56 assert r["brut_pct"] == pytest.approx(sum(abs(a) for a in aj) / r["prix_logement"])57 assert sum(g["poids"]) == pytest.approx(1.0)58 assert g["valeur"] == pytest.approx(3 * sum(w * p for w, p in zip(g["poids"], t["prix_ajuste"])))59 assert g["valeur"] == pytest.approx(1020366.74, abs=0.01) and g["valeur_par_logement"] == pytest.approx(340122.25, abs=0.01)60 q = t[t["id_vente"] == "Q0243"].iloc[0]61 assert q["prix_ajuste"] == pytest.approx(383849.14, abs=0.01) and q["brut_pct"] == pytest.approx(0.126, abs=0.001)626364# ----------------------------------------------------------------------------- 46.7 revenu65def test_revenu(D):66 rev = D["revenu"]; b = D["base"]67 assert rev["v_cap_ajuste"] == pytest.approx(b["rne"] / rev["taux_ajuste"]) and rev["v_cap_ajuste"] == pytest.approx(748126.24, abs=0.01)68 assert rev["v_cap_median"] == pytest.approx(b["rne"] / 0.0317) and rev["v_cap_comp"] == pytest.approx(b["rne"] / 0.0297)69 assert rev["taux_implicite_comparaison"] == pytest.approx(b["rne"] / D["grille"]["valeur"])70 assert rev["mrb_comp_ajuste_rho"] == pytest.approx(24.38, abs=0.01)717273# ----------------------------------------------------------------------------- 46.8 coût (contrôle)74def test_cout(D):75 co = D["cout"]76 assert co["cu_taille"] == pytest.approx(2409 * (262 / 165) ** -0.10) and co["cu"] == pytest.approx(co["cu_taille"] * 118.5 / 116.5)77 assert co["cout_neuf"] == pytest.approx(co["cu"] * 262) and co["age_effectif"] == pytest.approx(0.5 * 78 + 0.5 * 0.5 * 90)78 assert co["valeur"] == pytest.approx(co["terrain"] + co["cout_neuf"] * (1 - co["age_effectif"] / 90))79 assert co["valeur"] == pytest.approx(473111.44, abs=0.01)80 assert D["depreciation_extraite"]["depreciation"] < 0 # le marché implique une dépréciation négative : hypothèses de coût rejetées818283# ----------------------------------------------------------------------------- 46.10 réconciliation84def test_reconciliation(D):85 rec = D["reconciliation"]; ret = rec["retenues"]86 assert sum(rec["poids"].values()) == pytest.approx(1.0)87 assert rec["valeur_ponderee"] == pytest.approx(0.40 * ret["par_logement_ajuste"] + 0.35 * ret["mrb"] + 0.25 * ret["capitalisation"])88 assert rec["valeur_ponderee"] == pytest.approx(918016.72, abs=0.01) and rec["valeur_finale"] == 92000089 assert rec["fourchette_conclue"] == (870000.0, 970000.0)90 assert rec["taux_implicite"] == pytest.approx(D["base"]["rne"] / 920000) and rec["mrb_implicite"] == pytest.approx(920000 / 41832)91 assert rec["ratio_role"] == pytest.approx(920000 / 495000)92 assert rec["ecart_ch32"] == pytest.approx(50000)939495# ----------------------------------------------------------------------------- 46.9 financement96def test_financement(D):97 f = D["financement"]; h = fin.Hypotheque(f["pret"], 0.0412, 300)98 assert f["mise_de_fonds"] == pytest.approx(0.05 * 500000 + 0.10 * (920000 - 500000)) and f["mise_de_fonds"] == pytest.approx(C.mise_de_fonds_minimale_assuree(920000))99 assert f["pret"] == pytest.approx(920000 - f["mise_de_fonds"])100 assert h.taux_periodique == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1) and f["paiement_mensuel"] == pytest.approx(h.paiement)101 assert h.solde(300) == pytest.approx(0.0, abs=1e-6)102 assert f["interets_an1"] + f["capital_an1"] == pytest.approx(f["service_dette"], abs=1e-6)103 assert f["taux_admissible"] == pytest.approx(0.0612) and f["paiement_mensuel_tam"] == pytest.approx(fin.Hypotheque(f["pret"], 0.0612, 300).paiement)104 o = f["occupation"]105 assert o["cout_net_annuel"] == pytest.approx(f["service_dette"] + o["depenses_hors_gestion"] + o["cout_opportunite"] - o["loyers_percus_nets"])106 assert o["loyers_percus_nets"] == pytest.approx((1188 + 1251) * 12 * 0.97)107 assert o["cout_net_mensuel_hors_capital"] == pytest.approx(o["cout_net_mensuel"] - f["capital_rembourse_5ans"] / 60)108 m = f["mutation"]; assert m["base"] == 920000109 assert m["droits"] == pytest.approx(0.005 * 62900 + 0.01 * (315000 - 62900) + 0.015 * 185000 + 0.025 * 250000 + 0.03 * (920000 - 750000))110 assert 0.5 < f["part_paiement_couverte_brute"] < 0.6 and f["ratio_couverture_investisseur"] < 0.5111112113def test_reprise(D):114 r = D["reprise"]115 assert r["delta1"] == pytest.approx((1430 - 1047) * 12 * 0.97)116 assert r["va_rotation"] == pytest.approx(r["delta1"] / (0.06 - r["g_ecart"] + 0.13 * (1 + r["g_ecart"])))117 assert r["va_10ans_sans_rotation"] == pytest.approx(0.97 * 40791.4685, abs=0.5) # ex. 32.7 du ch. 32 (sans la provision d'inoccupation)118 assert r["valeur_faculte_reprise"] == pytest.approx(r["va_10ans_sans_rotation"] - r["va_rotation"])119120121def test_exercices_et_figures(D):122 E = ch.exercices(D)123 assert E["1"]["v_cap_2026"] - E["1"]["v_cap_declare"] == pytest.approx(E["1"]["ecart"] / D["revenu"]["taux_ajuste"])124 assert E["5"]["cout_net_2e"] > E["5"]["cout_net_rc"]125 assert min(E["6"].values()) < 920000 < max(E["6"].values())126 for slug in ("loyers", "cascade", "mrb", "occupation", "fourchettes"):127 assert (ROOT / f"assets/figures/ch46/ch46-fig-{slug}.pdf").exists()128