"""Tests du chapitre 32 — Duplex à quintuplex : identités, cohérence avec traite.cas2 (partagé avec les ch. 26, 27, 46), valeurs citées dans le texte (tolérances déclarées), règles de financement vérifiées, 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 ch32_plex as C # noqa: E402 from traite import cas2, finance as fin # noqa: E402 @pytest.fixture(scope="module") def R(): return C.calculer() # ----------------------------------------------------------------------------- cohérence avec le module partagé def test_coherence_cas2_partage(R): r = cas2.tout(); b = R["base"] assert b["rbp_contrat"] == pytest.approx(41832.0) and b["rbp_marche"] == pytest.approx(49332.0) assert b["rne"] == pytest.approx(22033.9584, abs=0.01) and b["ratio_depenses"] == pytest.approx(0.457, abs=0.001) assert R["indicateurs"] == r["indicateurs"] assert R["indications"]["v_mrb_127"] == pytest.approx(r["valeur_mrb"], abs=0.01) assert R["indications"]["v_cap_127"] == pytest.approx(r["valeur_capitalisation"], abs=0.01) assert R["indications"]["v_log_127"] == pytest.approx(r["valeur_par_logement"], abs=0.01) def test_valeurs_citees_indicateurs(R): ind = R["indicateurs"] assert ind["n"] == 127 and ind["n_triplex"] == 67 assert ind["mrb_median_triplex"] == pytest.approx(19.82, abs=0.005) and ind["taux_median"] == pytest.approx(0.0317, abs=5e-5) assert ind["prix_par_logement_triplex"] == pytest.approx(281000, abs=1) assert R["indications"]["v_mrb_127"] == pytest.approx(829110.24, abs=1) assert R["indications"]["v_cap_127"] == pytest.approx(695077.55, abs=1) assert R["indications"]["v_log_127"] == pytest.approx(843000, abs=1) pt = R["par_taille"]; assert pt.loc[2, "n"] == 60 and pt.loc[3, "n"] == 67 assert pt.loc[3, "prix_median"] == pytest.approx(843000, abs=1) and pt.loc[2, "prix_median"] == pytest.approx(583500, abs=1) # ----------------------------------------------------------------------------- identités def test_identite_mrb_taux(R): """R = (1 − v)(1 − d)/MRB pour chaque vente du jeu (v = 2,4 %, d = ratio de la vente), aux arrondis du fichier près.""" g = R["decomposition"] lhs = g["taux_global"]; rhs = (1 - 0.024) * (1 - g["ratio_depenses"]) / g["mrb"] assert np.max(np.abs(lhs - rhs)) < 2e-4 def test_decomposition_rho(R): """R_c = ρ R_m lorsque les dépenses sont proportionnelles au revenu (jeu synthétique).""" g = R["decomposition"] assert np.max(np.abs(g["taux_contrat_reconstitue"] - g["taux_global"])) < 2e-4 assert g["taux_marche"].median() > g["taux_global"].median() assert R["regression"]["b_rho"] > 0 and 0.03 < R["regression"]["a"] < 0.04 def test_valeurs_citees_regression_et_indications(R): reg = R["regression"]; I = R["indications"] assert reg["a"] == pytest.approx(0.0345, abs=1e-4) and reg["b_rho"] == pytest.approx(0.0284, abs=1e-4) assert I["rho_sujet"] == pytest.approx(0.848, abs=5e-4) assert I["taux_ajuste"] == pytest.approx(0.0302, abs=1e-4) assert I["v_cap_ajuste"] == pytest.approx(729549, abs=1) assert I["v_log_comp"] == pytest.approx(918000, abs=1) and I["v_mrb_comp"] == pytest.approx(922396, abs=1) assert I["v_cap_comp"] == pytest.approx(741884, abs=1) assert I["valeur_ponderee"] == pytest.approx(872426, abs=1) and I["valeur_arrondie"] == 870000 assert I["valeur_ponderee"] == pytest.approx(sum(C.POIDS_INDICATIONS[k] * v for k, v in I["retenues"].items())) assert abs(sum(C.POIDS_INDICATIONS.values()) - 1) < 1e-12 assert I["v_cap_ratio_ventes"] == pytest.approx(863880, abs=1) and I["v_log_127_indexe"] == pytest.approx(900926, abs=1) def test_comparables_selection(R): comp5 = R["comparables5"] assert len(comp5) == 5 and set(comp5["id_vente"]) == {"Q0187", "Q0222", "Q0243", "Q0207", "Q0235"} assert (comp5["quartier"] == "Limoilou").all() and (comp5["nb_logements"] == 3).all() assert comp5["distance"].is_monotonic_increasing and len(R["candidats"]) == 28 assert comp5["prix_par_logement"].median() == pytest.approx(306000, abs=1) and comp5["mrb"].median() == pytest.approx(22.05, abs=0.005) def test_prime_occupant(R): p = R["prime"]; I = R["indications"] assert p["v_investisseur_contrat"] + p["v_rattrapage"] + p["prime"] == pytest.approx(I["valeur_ponderee"]) assert p["v_investisseur_contrat"] == pytest.approx(440679.17, abs=0.01) and p["prime_pct"] == pytest.approx(0.335, abs=0.001) # ----------------------------------------------------------------------------- financement (règles vérifiées, notes-C §4.3, §4.2) def test_mise_de_fonds_minimale(): assert C.mise_de_fonds_minimale_assuree(450_000) == pytest.approx(22_500) assert C.mise_de_fonds_minimale_assuree(843_000) == pytest.approx(59_300) assert C.mise_de_fonds_minimale_assuree(870_000) == pytest.approx(62_000) assert C.mise_de_fonds_minimale_assuree(1_500_000) == pytest.approx(125_000) assert np.isnan(C.mise_de_fonds_minimale_assuree(1_600_000)) def test_financement_occupant(R): f = R["financement"] assert f["pret"] == pytest.approx(808_000) and f["rpv"] == pytest.approx(0.9287, abs=1e-4) assert f["paiement_mensuel"] == pytest.approx(4302.97, abs=0.01) and f["service_dette"] == pytest.approx(51635.62, abs=0.01) # composition semestrielle : taux mensuel équivalent (1 + 0,0412/2)^(1/6) − 1 i_m = (1 + 0.0412 / 2) ** (1 / 6) - 1 assert f["paiement_mensuel"] == pytest.approx(808_000 * i_m / (1 - (1 + i_m) ** -300), abs=0.01) assert f["taux_admissible"] == pytest.approx(0.0612) and f["paiement_mensuel_tam"] == pytest.approx(5227.41, abs=0.01) h = fin.Hypotheque(f["pret"], f["taux"], f["amort_mois"]); assert h.solde(300) == pytest.approx(0.0, abs=1e-6) o = R["occupation"] assert o["cout_net_mensuel"] == pytest.approx(3532.11, abs=0.01) and o["cout_net_mensuel_hors_capital"] == pytest.approx(1813.08, abs=0.01) assert o["cout_net_annuel"] == pytest.approx(o["sortie_brute"] - o["loyers_percus_nets"]) def test_sensibilite(R): S = R["sensibilite"]; L = S["loyers"] assert L["v_cap_ajuste"].is_monotonic_increasing and L["v_mrb"].is_monotonic_increasing assert L.loc[L["rho"] == 1.0, "rne"].item() == pytest.approx(R["rne_marche"], abs=0.01) F = S["financement"] assert F["cout_net_mensuel"].iloc[4] > F["cout_net_mensuel"].iloc[0] # +100 pb coûte plus cher assert F["service_dette"].iloc[1] < F["service_dette"].iloc[0] # 30 ans < 25 ans T = S["taux"]; assert np.allclose(T["valeur"], R["base"]["rne"] / T["taux"]) # ----------------------------------------------------------------------------- exercices (solutions/ch32.tex) def test_exercices(): E = C.exercices() assert E["1"]["mrb"] == pytest.approx(18.75) and E["1"]["taux"] == pytest.approx(E["1"]["identite"]) assert E["1"]["rne"] == pytest.approx(18763.68) assert E["2"]["843000.0"]["mise"] == pytest.approx(59300) and E["2"]["450000.0"]["mise"] == pytest.approx(22500) e3 = E["3"]; assert e3["R_m"] - e3["R_c"] == pytest.approx(e3["terme"]) and e3["ecart_pb"] == pytest.approx(105.36, abs=0.01) assert E["4"]["2e"]["cout_net_mensuel"] > E["4"]["RC"]["cout_net_mensuel"] assert E["6"]["ponderations"]["comparaison seule (50/50/0)"] == pytest.approx(920197.8, abs=0.1) assert E["6"]["ponderations"]["revenu dominant (20/20/60)"] == pytest.approx(805809, abs=1) assert E["7"]["va_1an"] == pytest.approx(4335.85, abs=0.01) and E["7"]["va_10ans"] == pytest.approx(40791.47, abs=0.01) def test_figures_presentes(): for slug in ("mrb-taille-quartier", "taux-loyer-relatif", "prix-logement-quartier", "plex-apciq-rmr", "sensibilite-occupant"): assert (ROOT / f"assets/figures/ch32/ch32-fig-{slug}.pdf").exists(), slug