spb/traite-evaluation-qc
Public
1"""Chapitre 2 — Droits immobiliers et bases de valeur : identités (additivité, Gordon, annuité) et valeurs citées dans le texte,2les tableaux et les solutions (tolérances déclarées). Toutes les grandeurs du cas 3 proviennent de traite.cas3 (module partagé)."""3import sys, pathlib, math4import numpy as np, pytest5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))6import ch02_droits as c27from traite import cas3, finance as fin8910@pytest.fixture(scope="module")11def res():12 return c2.main(verbose=False)131415# ----------------------------------------------------------------------------- identités1617def test_additivite_droit_temporaire_et_reversion():18 """V_pleine = V_droit temporaire + V_réversion lorsque la réversion est la capitalisation du revenu suivant (éq. 2.6, Gordon)."""19 for n in (1, 5, 15, 40):20 u = c2.usufruit_nue_propriete(30000.0, 0.01, 0.06, n)21 assert u["somme"] == pytest.approx(u["V_pleine"], rel=1e-12)22 assert 0 < u["part_usufruit"] < 123 # croissance nulle : usufruit = annuité, réversion = V/(1+r)^n24 d = c2.valeur_droit_temporaire_et_reversion(30000.0, 15, 0.06, 500000.0)25 assert d["droit_temporaire"] == pytest.approx(30000 * fin.facteur_va_annuite(0.06, 15))26 assert d["total"] == pytest.approx(500000.0) # 30 000/0,06 = 500 000 : la somme redonne la perpétuité272829def test_formule_fermee_rotation():30 """Σ Δ[(1−ρ)(1+g)]^{t−1}/(1+r)^t = Δ/(r − g + ρ(1+g)) ; sans rotation = Δ/(r−g) ; cohérence somme finie / infinie."""31 D, r, g = 19697.0, 0.0725, 0.03532 for rho in (0.0, 0.05, 0.15, 0.5):33 brute = sum(D * ((1 - rho) * (1 + g)) ** (t - 1) / (1 + r) ** t for t in range(1, 5000))34 assert c2.va_ecart_rotation(D, r, g, rho) == pytest.approx(brute, rel=1e-9)35 assert c2.va_ecart_rotation(D, r, g, 0.0) == pytest.approx(D / (r - g))36 assert c2.va_ecart_rotation(D, r, g, 0.15, T=10) < c2.va_ecart_rotation(D, r, g, 0.15)37 with pytest.raises(ValueError):38 c2.va_ecart_rotation(D, 0.02, 0.035, 0.0)394041def test_valeur_bail_locataire_annuite():42 """éq. 2.4 : annuité ; croissante en n ; tend vers Δ/r."""43 v5 = c2.valeur_bail_locataire(3828.0, 5, 0.0725); v10 = c2.valeur_bail_locataire(3828.0, 10, 0.0725)44 assert v5 == pytest.approx(sum(3828 / 1.0725 ** t for t in range(1, 6)), rel=1e-12)45 assert v5 < v10 < 3828 / 0.072546 assert c2.valeur_bail_locataire(3828.0, 10000, 0.0725) == pytest.approx(3828 / 0.0725, rel=1e-6)474849# ----------------------------------------------------------------------------- valeurs citées (cas 3)5051def test_ecart_contractuel(res):52 r = res["revenus"]; e = res["ecarts"]53 assert r["rbp_contrat"] == pytest.approx(165540) and r["rbp_marche"] == pytest.approx(186036) and r["ecart_marche"] == pytest.approx(20496)54 assert res["ferme"]["ecart_net_annee1"] == pytest.approx(20496 * (1 - 0.039), abs=0.5)55 assert round(res["ferme"]["ecart_net_annee1"]) == 1969756 assert r["ecart_marche"] / r["rbp_marche"] == pytest.approx(0.110, abs=0.0005)57 l103 = e.loc[e["logement"] == 103].iloc[0]58 assert l103["ecart_mensuel"] == 319 and l103["ecart_annuel"] == 3828 and l103["anciennete"] == 1859 assert l103["ratio_contrat_marche"] == pytest.approx(0.695, abs=0.0005)60 assert e["ratio_contrat_marche"].mean() == pytest.approx(0.884, abs=0.0005)61 assert e["ratio_contrat_marche"].max() == pytest.approx(0.980, abs=0.0005)62 assert e["ecart_annuel"].sum() == 20496 and len(e) == 1263 # tableau 2.4, colonne écart annuel64 assert list(e["ecart_annuel"].astype(int)) == [1176, 2592, 3828, 264, 2412, 588, 2460, 2244, 336, 2640, 1152, 804]656667def test_decomposition_dcf(res):68 b = res["base"]69 # cohérence avec le module partagé (valeurs des ch. 29/30/47)70 t = cas3.tout()71 assert b["V_locateur"] == pytest.approx(t["dcf"]["valeur"], rel=1e-12)72 assert b["V_locateur"] == pytest.approx(2208566, abs=1)73 assert b["V_pleine"] == pytest.approx(2235712, abs=1)74 assert b["V_locataires"] == pytest.approx(27146, abs=1)75 assert b["V_locataires"] == pytest.approx(b["V_pleine"] - b["V_locateur"], rel=1e-12)76 assert b["part_locataires"] == pytest.approx(0.0121, abs=0.00005)77 assert b["rne1_contrat"] == pytest.approx(89956, abs=1) and b["rne1_marche"] == pytest.approx(108865, abs=1)78 assert b["rbe_contrat"] == pytest.approx(168444, abs=1) and b["rbe_marche"] == pytest.approx(188141, abs=1)79 assert b["va_flux_contrat"] == pytest.approx(662410, abs=1) and b["va_flux_marche"] == pytest.approx(699966, abs=1)80 assert b["va_vt_contrat"] == pytest.approx(1546156, abs=1) and b["va_vt_marche"] == pytest.approx(1535746, abs=1)81 pc, pm = b["proj_contrat"], b["proj_marche"]82 ecart_rne = (pm["rne"] - pc["rne"]).round(0).astype(int).tolist()83 assert ecart_rne[:6] == [18909, 13166, 8821, 3692, 1241, -394]84 assert ecart_rne[10] == -107585 ecart_rbp = (pm["rbp"] - pc["rbp"]).round(0).astype(int).tolist()86 assert ecart_rbp[:3] == [20496, 14545, 10048]87 assert abs(b["va_vt_marche"] / b["va_vt_contrat"] - 1) < 0.01 # valeurs terminales à moins de 1 %888990def test_capitalisation_naive_et_formules(res):91 c = res["cap"]; f = res["ferme"]92 assert c["Vcap_contrat"] == pytest.approx(2037503, abs=1) and c["Vcap_marche"] == pytest.approx(2465788, abs=1)93 assert c["ecart_capitalise"] == pytest.approx(428285, abs=1)94 assert c["ecart_capitalise"] / res["base"]["V_locataires"] > 1595 assert f["perpetuite_sans_rotation"] == pytest.approx(525244, abs=1)96 assert f["perpetuite_rotation"] == pytest.approx(102188, abs=1)97 assert 0.0375 + 0.15 * 1.035 == pytest.approx(0.19275)98 assert f["horizon10_rotation"] == pytest.approx(88094, abs=1)99100101def test_sensibilite(res):102 s = res["sensibilite"]103 assert len(s) == 15104 def v(rot, r): return float(s[(np.isclose(s["rotation"], rot)) & (np.isclose(s["taux"], r))]["V_locataires"].iloc[0])105 assert v(0.0, 0.0725) == pytest.approx(397186, abs=1)106 assert v(1 / 12, 0.0725) == pytest.approx(59858, abs=1)107 assert v(2 / 12, 0.0725) == pytest.approx(27146, abs=1)108 assert v(3 / 12, 0.0725) == pytest.approx(17360, abs=1)109 assert v(6 / 12, 0.0725) == pytest.approx(5195, abs=1)110 assert v(0.0, 0.0725) / v(6 / 12, 0.0725) == pytest.approx(76, abs=1)111 assert v(2 / 12, 0.0625) == pytest.approx(26673, abs=1) and v(2 / 12, 0.0825) == pytest.approx(27527, abs=1)112 part = s.set_index(["rotation", "taux"])["part"]113 assert part.max() == pytest.approx(0.1850, abs=0.0001) and part.min() == pytest.approx(0.0017, abs=0.0001)114 # monotonie : l'écart décroît avec la rotation à taux donné115 for r in c2.TAUX:116 col = s[np.isclose(s["taux"], r)].sort_values("rotation")["V_locataires"].tolist()117 assert all(a > b for a, b in zip(col, col[1:]))118 # V_locateur très sensible au taux (≈ 15 %), écart peu sensible119 assert float(s[(np.isclose(s["rotation"], 2 / 12)) & np.isclose(s["taux"], 0.0625)]["V_locateur"].iloc[0]) == pytest.approx(2396754, abs=1)120 assert float(s[(np.isclose(s["rotation"], 2 / 12)) & np.isclose(s["taux"], 0.0825)]["V_locateur"].iloc[0]) == pytest.approx(2037842, abs=1)121122123# ----------------------------------------------------------------------------- exercices et encadré124125class TestExercices:126 def test_ex3_bail_locataire(self, res):127 e = res["exercices"]["ex3"]128 assert e["ecart_annuel"] == 3828 and e["va_5ans"] == pytest.approx(15591, abs=1) and e["va_10ans"] == pytest.approx(26578, abs=1)129 assert e["va_perpetuite"] == pytest.approx(52800, abs=1)130 assert fin.facteur_va_annuite(0.0725, 5) == pytest.approx(4.0728, abs=0.0001) and fin.facteur_va_annuite(0.0725, 10) == pytest.approx(6.9431, abs=0.0001)131132 def test_ex4_usufruit(self, res):133 e = res["exercices"]["ex4"]134 assert e["V_pleine"] == pytest.approx(600000) and e["usufruit"] == pytest.approx(309341, abs=1) and e["nue_propriete"] == pytest.approx(290659, abs=1)135 assert e["V_n"] == pytest.approx(696581, abs=1) and e["part_usufruit"] == pytest.approx(0.516, abs=0.001)136 assert (1.01 / 1.06) ** 15 == pytest.approx(0.48443, abs=1e-5) and 1.06 ** -15 == pytest.approx(0.41727, abs=1e-5)137 v8 = c2.usufruit_nue_propriete(30000.0, 0.01, 0.06, 8)138 assert v8["usufruit"] == pytest.approx(192361, abs=1) and v8["nue_propriete"] == pytest.approx(407639, abs=1)139 assert v8["part_usufruit"] == pytest.approx(0.321, abs=0.001) and (1.01 / 1.06) ** 8 == pytest.approx(0.67940, abs=1e-5)140141 def test_ex5_date(self, res):142 e = res["exercices"]["ex5"]143 assert e["indice"] == pytest.approx(1.0985, abs=0.0001) and e["valeur_indexee"] == pytest.approx(514084, abs=1)144145 def test_ex6_formule_fermee(self):146 D, r, g = 19697.0, 0.0725, 0.035147 assert c2.va_ecart_rotation(D, r, g, 1 / 12) == pytest.approx(159168, abs=1)148 assert c2.va_ecart_rotation(D, r, g, 2 / 12) == pytest.approx(93795, abs=1)149 assert 0.0375 + (1 / 12) * 1.035 == pytest.approx(0.12375, abs=1e-5)150 assert 1 - (10 / 12) ** 2 == pytest.approx(0.306, abs=0.001)151152 def test_ex7_et_encadre(self, res):153 e = res["exercices"]["ex7"]154 assert e["V_sans_hyp"] == pytest.approx(1955638, abs=1) and e["incidence"] == pytest.approx(252928, abs=1)155 assert e["incidence"] / e["V_hyp"] == pytest.approx(0.1145, abs=0.0005)156 # ex. 2.7 : CAPEX 2027 de 85 000 $ actualisé un an157 assert 85000 / 1.0725 == pytest.approx(79254, abs=1)158 assert (2208566 + 85000 / 1.0725) == pytest.approx(2287820, abs=1)159 assert (85000 / 1.0725) / 2208566 == pytest.approx(0.036, abs=0.0005)160