spb/traite-evaluation-qc
Public
1"""Tests du chapitre 7 — servitude par ventes appariées, quotes-parts et charges de copropriété, fonds de prévoyance, exercices.2Tolérances : 1 $ sur les montants cités arrondis au dollar ; 1e-9 sur les identités comptables ; 0,0001 sur les proportions."""3import sys, pathlib, math4import numpy as np, pytest5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))6import ch07_servitudes_copro as cas7from traite import comparables as cp8from traite import finance as fin91011@pytest.fixture(scope="module")12def res():13 return cas.main()141516class TestServitude:17 def test_paires_citees(self, res):18 p = res["paires"]19 assert p["n"] == 1220 assert p["moyenne"] == pytest.approx(-20333, abs=1)21 assert p["mediane"] == pytest.approx(-19000, abs=1)22 assert p["ecart_type"] == pytest.approx(10026, abs=1)23 assert p["se"] == pytest.approx(2894, abs=1)24 assert p["t"] == pytest.approx(-7.03, abs=0.01)25 assert p["ic95"][0] == pytest.approx(-26703, abs=1) and p["ic95"][1] == pytest.approx(-13963, abs=1)26 assert p["effet_relatif_moyen"] == pytest.approx(-0.0451, abs=0.0001)27 assert p["effet_relatif_ratio"] == pytest.approx(-0.0446, abs=0.0001)28 assert p["effet_semilog"] == pytest.approx(-0.0454, abs=0.0001)29 assert p["sans"].mean() == pytest.approx(455792, abs=1)30 assert all(a < s for a, s in zip(p["avec"], p["sans"]))3132 def test_identites_paires(self, res):33 p = res["paires"]34 # la moyenne des différences est la différence des moyennes ; l'écart-type est celui de l'échantillon (n−1)35 assert p["moyenne"] == pytest.approx(p["avec"].mean() - p["sans"].mean(), abs=1e-9)36 assert p["ecart_type"] == pytest.approx(float(np.std(p["diffs"], ddof=1)), abs=1e-9)37 assert p["se"] == pytest.approx(p["ecart_type"] / math.sqrt(12), abs=1e-9)38 # cohérence avec traite.comparables39 va = cp.ventes_appariees(list(p["avec"]), list(p["sans"]))40 assert va["moyenne"] == pytest.approx(p["moyenne"]) and va["mediane"] == pytest.approx(p["mediane"])4142 def test_modele_bande(self, res):43 b = res["bande"]44 assert b["superficie_bande_m2"] == 120 and b["valeur_bande"] == 21600 and b["effet"] == 1080045 assert b["part_terrain"] == pytest.approx(120 / 700)464748class TestCopropriete:49 def test_quotes_parts(self, res):50 qp = res["qp"]51 assert list(qp["q_dixmilliemes"]) == [935, 1352, 974, 1408, 1196, 1644, 1186, 1305]52 assert int(qp["q_dixmilliemes"].sum()) == 10000 and qp["q"].sum() == pytest.approx(1.0, abs=1e-12)53 assert qp["superficies"].sum() == 71054 assert qp["valeur_relative"].sum() == pytest.approx(716.91, abs=0.01)55 # méthode déclarée : F6 = 110 × 1,04 × 1,0356 assert qp["valeur_relative"][5] == pytest.approx(110 * 1.04 * 1.03)5758 def test_repartition(self, res):59 rep = res["rep"]; qp = res["qp"]60 assert rep["courantes"] == 40000 and rep["auto_assurance"] == 500061 assert rep["general"] == pytest.approx(40000 + res["A_star"] + 5000)62 assert rep["par_fraction_general"].sum() == pytest.approx(rep["general"], abs=1e-6)63 assert rep["par_fraction_restreint"].sum() == pytest.approx(3200) and int(rep["n_stationnements"]) == 464 assert rep["par_fraction_total"].sum() == pytest.approx(rep["total"], abs=1e-6)65 assert rep["total"] == pytest.approx(70091, abs=1)66 # chiffres cités : F4 et F667 assert rep["par_fraction_total"][3] == pytest.approx(10218, abs=1) and rep["par_fraction_total"][5] == pytest.approx(11797, abs=1)68 assert rep["par_fraction_total"][0] == pytest.approx(6254, abs=1)69 assert rep["par_fraction_restreint"][3] == 800 and rep["par_fraction_restreint"][0] == 070 assert rep["par_fraction_fonds"][3] == pytest.approx(qp["q"][3] * res["A_star"])7172 def test_auto_assurance(self):73 assert cas.contribution_auto_assurance(10000, 0) == 500074 assert cas.contribution_auto_assurance(10000, 7000) == 300075 assert cas.contribution_auto_assurance(10000, 12000) == 0767778class TestFondsPrevoyance:79 def test_depenses(self, res):80 D = res["tr_star"]["D"]81 assert sum(c for _, c, a in cas.COMPOSANTES for _ in a) == 47900082 assert D.sum() == pytest.approx(651959, abs=1)83 assert D[14] == pytest.approx(140000 * 1.025 ** 14, abs=1e-6) and D[14] == pytest.approx(197816, abs=1)84 assert D[7] == pytest.approx(89151, abs=1)8586 def test_cotisation_seuil(self, res):87 A = res["A_star"]88 assert A == pytest.approx(21891, abs=1)89 tr = res["tr_star"]90 assert tr["min"] == pytest.approx(0.0, abs=0.01) and tr["annee_min"] == 1491 assert tr["F"][-1] == pytest.approx(239241, abs=1)92 assert tr["A"][-1] == pytest.approx(A * 1.025 ** 24, abs=1e-6)93 # identité de récurrence F_t = F_{t-1}(1+r) + A_t − D_t94 for t in range(1, cas.HORIZON + 1):95 assert tr["F"][t] == pytest.approx(tr["F"][t - 1] * 1.02 + tr["A"][t] - tr["D"][t], abs=1e-6)96 assert res["A_floor"] == pytest.approx(23228, abs=1)9798 def test_scenarios(self, res):99 act, regle = res["tr_act"], res["tr_regle"]100 assert res["regle_0_5"] == 21000101 assert act["premiere_annee_negative"] == 13 and act["min"] == pytest.approx(-233300, abs=1) and act["annee_min"] == 22102 assert act["F"][-1] == pytest.approx(-182783, abs=1)103 assert regle["premiere_annee_negative"] == 14 and regle["min"] == pytest.approx(-16660, abs=1)104105 def test_toiture_sff(self, res):106 fc = res["toiture"]107 assert fc["cout_indexe"] == pytest.approx(89151, abs=1)108 assert fc["sff"] == pytest.approx(fin.facteur_fonds_amortissement(0.02, 7)) and fc["sff"] == pytest.approx(0.13451, abs=1e-5)109 assert fc["versement"] == pytest.approx(11992, abs=1)110 # le versement accumule exactement le coût indexé en 7 ans à 2 %111 acc = sum(fc["versement"] * 1.02 ** (7 - k) for k in range(1, 8))112 assert acc == pytest.approx(fc["cout_indexe"], abs=1e-6)113114 def test_sensibilite(self, res):115 s = res["sens"]116 g = s["grille"]117 assert g[0.02][2] == pytest.approx(res["A_star"], abs=1)118 assert g[0.01][0] == pytest.approx(21683, abs=1) and g[0.01][4] == pytest.approx(24442, abs=1)119 assert g[0.03][0] == pytest.approx(19342, abs=1) and g[0.03][4] == pytest.approx(22099, abs=1)120 # monotonie : A* croît avec π et décroît avec r121 for r in s["rendements"]:122 assert all(np.diff(g[r]) > 0)123 for j in range(len(s["inflations"])):124 assert g[0.01][j] > g[0.02][j] > g[0.03][j]125126 def test_incidence_valeur(self, res):127 inc = res["incidence"]; qp = res["qp"]128 assert inc["solde_requis"] == pytest.approx(235908, abs=1) and inc["deficit"] == pytest.approx(150908, abs=1)129 assert inc["ajustement_par_fraction"][3] == pytest.approx(-21248, abs=1)130 assert inc["ajustement_par_fraction"][5] == pytest.approx(-24809, abs=1)131 assert inc["ajustement_par_fraction"].sum() == pytest.approx(-inc["deficit"], abs=1e-6)132 assert cas.trajectoire(cas.COTISATION_ACTUELLE, inc["solde_requis"])["min"] == pytest.approx(0.0, abs=0.01)133134135class TestExercices:136 def test_ex1_tarifs(self):137 # 1 fiche + 6 actes + 1 plan à 1 $, état certifié 16 $ + 16 $ pour 1 réquisition, copie certifiée 42 $138 assert 8 * 1 + (16 + 16) + 42 == 82139140 def test_ex2_quotes_parts(self):141 s = np.array([65.0, 80.0, 95.0, 120.0]); q = s / s.sum()142 assert q.sum() == pytest.approx(1) and q[3] == pytest.approx(1 / 3)143 charges = q * 54000144 assert charges[0] == pytest.approx(9750) and charges[3] == pytest.approx(18000)145 # variante avec coefficient d'étage 1,05 pour la fraction de 120 m²146 v = s * np.array([1, 1, 1, 1.05]); q2 = v / v.sum()147 assert q2[3] == pytest.approx(126 / 366, abs=1e-9) and (q2 * 54000)[3] == pytest.approx(18590, abs=1)148149 def test_ex3_paires(self):150 sans = np.array([398000, 412500, 430000, 445000, 462000, 489500]); avec = np.array([384000, 395500, 416000, 423000, 448000, 466500])151 va = cp.ventes_appariees(list(avec), list(sans))152 assert va["moyenne"] == pytest.approx(-17333, abs=1) and va["mediane"] == pytest.approx(-15500, abs=1)153 assert va["ecart_type"] == pytest.approx(4179, abs=1)154 se = va["ecart_type"] / math.sqrt(6); t = 2.571155 assert se == pytest.approx(1706, abs=1)156 assert va["moyenne"] - t * se == pytest.approx(-21720, abs=2) and va["moyenne"] + t * se == pytest.approx(-12947, abs=2)157 assert va["moyenne"] / sans.mean() == pytest.approx(-0.0394, abs=0.0001)158159 def test_ex4_bande(self):160 effet = 4 * 35 * 165 * 0.6161 assert effet == pytest.approx(13860)162 assert effet / 20000 == pytest.approx(0.693, abs=0.001)163164 def test_ex5_toiture(self):165 cT = 75000 * 1.025 ** 7; sff = fin.facteur_fonds_amortissement(0.02, 7)166 assert cT == pytest.approx(89151, abs=1) and cT * sff == pytest.approx(11992, abs=1)167 # sans indexation, versement 10 088 $ ; déficit à l'an 7168 a0 = 75000 * sff169 assert a0 == pytest.approx(10088, abs=1)170 assert cT - 75000 == pytest.approx(14151, abs=1)171 # variante r = 3 %172 assert cT * fin.facteur_fonds_amortissement(0.03, 7) == pytest.approx(11635, abs=1)173174 def test_ex6_script(self):175 # l'exercice demande : A* avec r = 3 % et π = 3 % ; A* avec plancher 25 000 $ ; solde initial requis176 assert cas.cotisation_seuil(inflation=0.03, rendement=0.03) == pytest.approx(21409, abs=1)177 assert cas.cotisation_seuil(plancher=25000) == pytest.approx(23228, abs=1)178 assert cas.cotisation_seuil(solde0=150000) == pytest.approx(17304, abs=1)179 assert cas.cotisation_seuil(indexer=False) == pytest.approx(25625, abs=1)180181 def test_ex7_indivision(self):182 V = 1050000.0183 part = V / 3184 assert part == pytest.approx(350000)185 assert part * (1 - 0.08) == pytest.approx(322000) and part * (1 - 0.15) == pytest.approx(297500)186 assert 350000 - 322000 == 28000 and 350000 - 297500 == 52500187