"""Tests du chapitre 46 — Cas 2 : triplex à Québec (dossier complet, données SYNTHÉTIQUES). Identités (RNE, grille d'ajustements, hypothèque), cohérence avec traite.cas2 et ch32_plex (mêmes comparables, mêmes indications de référence), valeurs citées dans le texte (tolérances : montants ±1 $ sauf mention, taux ±1e-4), exercices, présence des figures.""" import pathlib, sys import numpy as np, pytest ROOT = pathlib.Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT / "analysis/python/cases")) import ch46_dossier as ch # noqa: E402 import ch32_plex as C # noqa: E402 from traite import cas2, finance as fin # noqa: E402 @pytest.fixture(scope="module") def D(): return ch.dossier() # ----------------------------------------------------------------------------- cohérence avec le module partagé et le ch. 32 def test_coherence_cas2_et_ch32(D): r = cas2.tout(); b = D["base"] assert b["rbp_contrat"] == pytest.approx(41832.0) and b["rbp_marche"] == pytest.approx(49332.0) and b["rbe"] == pytest.approx(40577.04) assert b["rne_declare"] == pytest.approx(r["base"]["rne"], abs=1e-6) # RNE « déclaré » = RNE du ch. 32 / traite.cas2 assert b["rne_declare"] == pytest.approx(22033.9584, abs=0.01) assert list(D["comparables5"]["id_vente"]) == ["Q0187", "Q0222", "Q0243", "Q0207", "Q0235"] # mêmes cinq comparables que le ch. 32 assert D["revenu"]["v_mrb_comp"] == pytest.approx(922395.6, abs=0.01) assert D["revenu"]["taux_ajuste"] == pytest.approx(0.0302, abs=1e-4) assert D["revenu"]["v_cap_ajuste_rne_declare"] == pytest.approx(729549.19, abs=0.01) # indication du ch. 32 retrouvée assert D["ch32"]["valeur_arrondie"] == 870000 # ----------------------------------------------------------------------------- 46.5 taxes 2026 et RNE normalisé def test_taxes_2026_et_rne(D): b = D["base"]; t = b["taxes"] assert t["fonciere"] == pytest.approx(495000 * 0.007464) and t["tarifs"] == pytest.approx(3 * (386 + 195)) assert t["municipale"] == pytest.approx(5437.68, abs=0.01) and t["scolaire"] == pytest.approx((495000 - 25000) * 0.0007899, abs=0.01) assert b["ecart_taxes"] == pytest.approx(6370 - 5437.68 - 371.253, abs=0.01) assert b["rne"] == pytest.approx(b["rne_declare"] + b["ecart_taxes"], abs=1e-6) assert b["rne"] == pytest.approx(22595.03, abs=0.01) assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"]) det = b["detail"]; assert det.loc[det["type"] == "imputée", "normalise"].iloc[0] == pytest.approx(0.04 * b["rbe"]) assert b["ratio_depenses"] == pytest.approx(0.443, abs=0.001) assert b["rne_marche"] == pytest.approx(29579.03, abs=0.01) # ----------------------------------------------------------------------------- 46.6 régression auxiliaire et grille def test_regression_et_grille(D): reg = D["regression"]; g = D["grille"]; t = g["tableau"] assert reg["n"] == 127 and 0.70 < reg["r2"] < 0.75 assert reg["taux_annuel_marche"] == pytest.approx(np.exp(-12 * reg["beta"]["mois"]) - 1) assert abs(reg["t"]["loyer_relatif"]) < ch.T_MIN and (t["loyer_relatif"] == 0).all() # coefficient non significatif → pas d'ajustement assert (t["stationnement"] == 0).all() # tous les comparables ont une case, comme le sujet for _, r in t.iterrows(): # identité : prix ajusté = prix + Σ ajustements ; brut = Σ|A|/P aj = [r[k] for k in ("temps", "superficie", "age", "etat", "stationnement", "loyer_marche", "loyer_relatif")] assert r["prix_ajuste"] == pytest.approx(r["prix_logement"] + sum(aj)) assert r["brut_pct"] == pytest.approx(sum(abs(a) for a in aj) / r["prix_logement"]) assert sum(g["poids"]) == pytest.approx(1.0) assert g["valeur"] == pytest.approx(3 * sum(w * p for w, p in zip(g["poids"], t["prix_ajuste"]))) assert g["valeur"] == pytest.approx(1020366.74, abs=0.01) and g["valeur_par_logement"] == pytest.approx(340122.25, abs=0.01) q = t[t["id_vente"] == "Q0243"].iloc[0] assert q["prix_ajuste"] == pytest.approx(383849.14, abs=0.01) and q["brut_pct"] == pytest.approx(0.126, abs=0.001) # ----------------------------------------------------------------------------- 46.7 revenu def test_revenu(D): rev = D["revenu"]; b = D["base"] assert rev["v_cap_ajuste"] == pytest.approx(b["rne"] / rev["taux_ajuste"]) and rev["v_cap_ajuste"] == pytest.approx(748126.24, abs=0.01) assert rev["v_cap_median"] == pytest.approx(b["rne"] / 0.0317) and rev["v_cap_comp"] == pytest.approx(b["rne"] / 0.0297) assert rev["taux_implicite_comparaison"] == pytest.approx(b["rne"] / D["grille"]["valeur"]) assert rev["mrb_comp_ajuste_rho"] == pytest.approx(24.38, abs=0.01) # ----------------------------------------------------------------------------- 46.8 coût (contrôle) def test_cout(D): co = D["cout"] assert co["cu_taille"] == pytest.approx(2409 * (262 / 165) ** -0.10) and co["cu"] == pytest.approx(co["cu_taille"] * 118.5 / 116.5) assert co["cout_neuf"] == pytest.approx(co["cu"] * 262) and co["age_effectif"] == pytest.approx(0.5 * 78 + 0.5 * 0.5 * 90) assert co["valeur"] == pytest.approx(co["terrain"] + co["cout_neuf"] * (1 - co["age_effectif"] / 90)) assert co["valeur"] == pytest.approx(473111.44, abs=0.01) assert D["depreciation_extraite"]["depreciation"] < 0 # le marché implique une dépréciation négative : hypothèses de coût rejetées # ----------------------------------------------------------------------------- 46.10 réconciliation def test_reconciliation(D): rec = D["reconciliation"]; ret = rec["retenues"] assert sum(rec["poids"].values()) == pytest.approx(1.0) assert rec["valeur_ponderee"] == pytest.approx(0.40 * ret["par_logement_ajuste"] + 0.35 * ret["mrb"] + 0.25 * ret["capitalisation"]) assert rec["valeur_ponderee"] == pytest.approx(918016.72, abs=0.01) and rec["valeur_finale"] == 920000 assert rec["fourchette_conclue"] == (870000.0, 970000.0) assert rec["taux_implicite"] == pytest.approx(D["base"]["rne"] / 920000) and rec["mrb_implicite"] == pytest.approx(920000 / 41832) assert rec["ratio_role"] == pytest.approx(920000 / 495000) assert rec["ecart_ch32"] == pytest.approx(50000) # ----------------------------------------------------------------------------- 46.9 financement def test_financement(D): f = D["financement"]; h = fin.Hypotheque(f["pret"], 0.0412, 300) 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)) assert f["pret"] == pytest.approx(920000 - f["mise_de_fonds"]) assert h.taux_periodique == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1) and f["paiement_mensuel"] == pytest.approx(h.paiement) assert h.solde(300) == pytest.approx(0.0, abs=1e-6) assert f["interets_an1"] + f["capital_an1"] == pytest.approx(f["service_dette"], abs=1e-6) assert f["taux_admissible"] == pytest.approx(0.0612) and f["paiement_mensuel_tam"] == pytest.approx(fin.Hypotheque(f["pret"], 0.0612, 300).paiement) o = f["occupation"] assert o["cout_net_annuel"] == pytest.approx(f["service_dette"] + o["depenses_hors_gestion"] + o["cout_opportunite"] - o["loyers_percus_nets"]) assert o["loyers_percus_nets"] == pytest.approx((1188 + 1251) * 12 * 0.97) assert o["cout_net_mensuel_hors_capital"] == pytest.approx(o["cout_net_mensuel"] - f["capital_rembourse_5ans"] / 60) m = f["mutation"]; assert m["base"] == 920000 assert m["droits"] == pytest.approx(0.005 * 62900 + 0.01 * (315000 - 62900) + 0.015 * 185000 + 0.025 * 250000 + 0.03 * (920000 - 750000)) assert 0.5 < f["part_paiement_couverte_brute"] < 0.6 and f["ratio_couverture_investisseur"] < 0.5 def test_reprise(D): r = D["reprise"] assert r["delta1"] == pytest.approx((1430 - 1047) * 12 * 0.97) assert r["va_rotation"] == pytest.approx(r["delta1"] / (0.06 - r["g_ecart"] + 0.13 * (1 + r["g_ecart"]))) 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) assert r["valeur_faculte_reprise"] == pytest.approx(r["va_10ans_sans_rotation"] - r["va_rotation"]) def test_exercices_et_figures(D): E = ch.exercices(D) assert E["1"]["v_cap_2026"] - E["1"]["v_cap_declare"] == pytest.approx(E["1"]["ecart"] / D["revenu"]["taux_ajuste"]) assert E["5"]["cout_net_2e"] > E["5"]["cout_net_rc"] alt = [E["6"][k] for k in ("retenue 40/35/25", "45/30/25", "égale", "comparaison seule 60/40/0", "revenu dominant 20/20/60")] assert min(alt) < 920000 < max(alt) and E["6"]["valeur_inverse_variance"] == pytest.approx(968084.32, abs=0.01) assert E["5"]["promesse"]["paiement"] == pytest.approx(fin.Hypotheque(830500, 0.0412, 300).paiement) and E["5"]["promesse"]["droits_mutation"] == pytest.approx(16210.5) assert E["2"]["va_rotation_9_4"] == pytest.approx(39668.94, abs=0.01) and E["2"]["va_rotation_20"] == pytest.approx(19968.98, abs=0.01) assert E["7"]["terrain_m2_pour_egaler"] == pytest.approx((920000 - D["cout"]["contribution_batiment"]) / 310) for slug in ("loyers", "cascade", "mrb", "occupation", "fourchettes"): assert (ROOT / f"assets/figures/ch46/ch46-fig-{slug}.pdf").exists()