"""Tests du chapitre 24 — dépréciation et désuétudes du sujet du cas 1, extraction de marché, profils et exercices. Tolérances : 0,01 $ sur les montants recalculés ; 0,5 $ sur les montants arrondis cités ; 5e-4 sur les taux cités en % à deux décimales ; 1e-9 sur les identités.""" import sys, pathlib import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch24_depreciation as cas from traite import cout as ct @pytest.fixture(scope="module") def data(): d = cas.charger() var = cas.variantes_physique(d["cn"], d["fiche"], d["travaux"]) se = cas.carence_salle_eau(d["devis"], d["cn"]) w, vl = cas.extraction(d["ventes"], d["modele"], d["cn"]["total"]) aj = cas.ajuster_profil(w); vlr = cas.valeur_terrain_retenue(vl) pr = cas.profils(d["modele"], d["sujet"], vlr["retenue"], aj["quadratique"]) vs = cas.valeur_sujet(d["cn"], vlr["retenue"], aj["quadratique"], se["desuetude"], var) return {**d, "var": var, "se": se, "w": w, "vl": vl, "aj": aj, "vlr": vlr, "pr": pr, "vs": vs, "ages": cas.ages_sujet(d["fiche"])} class TestAges: def test_ages_cites(self, data): a = data["ages"] assert a["chronologique"] == 28 and a["effectif_structure"] == pytest.approx(29.0, abs=1e-9) assert a["vie_restante"] == pytest.approx(31.0, abs=1e-9) and a["vie_economique"] == 60 assert a["indice_etat"] == pytest.approx(2.809, abs=5e-4) and a["age_effectif_pondere"] == pytest.approx(32.8, abs=0.05) def test_convention_age_effectif(self): # a_eff = (1 − λ) a + λ φ(c) L : structure 28 ans, état 3 (φ = 0,5), L = 60 → 0,5·28 + 0,5·30 = 29 import ch11_inspection as c11 assert c11.age_effectif(28, 60, 3, 0.5) == 29.0 and c11.age_effectif(28, 60, 1, 0.5) == 15.5 and c11.age_effectif(28, 60, 5, 0.5) == 44.0 assert ct.vie_restante(60, 29) == 31.0 and ct.vie_restante(60, 75) == 0.0 class TestPhysique: def test_quatre_lectures(self, data): v = data["var"]; cn = data["cn"]["total"] assert cn == pytest.approx(397567.33, abs=0.01) assert v["age_vie_chrono"] == pytest.approx(185531.42, abs=0.01) and v["age_vie_chrono"] / cn == pytest.approx(0.4667, abs=5e-4) assert v["age_vie_effectif"] == pytest.approx(192157.55, abs=0.01) assert v["modifiee"]["curable"] == pytest.approx(10479.98, abs=0.01) and v["modifiee"]["incurable"] == pytest.approx(187092.22, abs=0.01) assert v["modifiee"]["total"] == pytest.approx(197572.20, abs=0.01) and v["modifiee"]["total"] / cn == pytest.approx(0.4970, abs=5e-4) ve = v["ventilee"] assert sum(ve["courte_duree"].values()) == pytest.approx(130584.55, abs=0.01) and ve["longue_duree"] == pytest.approx(80025.35, abs=0.01) assert ve["base_longue"] == pytest.approx(165569.69, abs=0.01) and ve["total"] == pytest.approx(221089.88, abs=0.01) assert ve["taux"] == pytest.approx(0.5561, abs=5e-4) and len(ve["courte_duree"]) == 18 def test_identites_ventilee(self, data): v = data["var"]; ve = v["ventilee"]; f = data["fiche"]; cn = data["cn"]["total"] courtes = f[(~f["longue_duree"]) & (f["composante"] != "Entrée asphaltée")] assert courtes["cout_neuf"].sum() == pytest.approx(221517.66, abs=0.01) # base longue = C_N − curable − Σ coût neuf courte durée assert ve["base_longue"] == pytest.approx(cn - ve["curable"] - courtes["cout_neuf"].sum(), abs=1e-6) assert ve["total"] == pytest.approx(ve["curable"] + sum(ve["courte_duree"].values()) + ve["longue_duree"], abs=1e-9) # proposition : D_vent − D_simple = Cur (1 − r_S) + Σ C_k (r_k − r_S) rS = 29.0 / 60.0 ecart = ve["curable"] * (1 - rS) + sum(ve["courte_duree"].values()) - courtes["cout_neuf"].sum() * rS assert ve["total"] - v["age_vie_effectif"] == pytest.approx(ecart, abs=1e-6) and ecart == pytest.approx(28932.33, abs=0.01) # chaque composante : dépréciation = coût neuf × min(a_eff / L, 1) for r in courtes.itertuples(): assert ve["courte_duree"][r.composante] == pytest.approx(r.cout_neuf * min(r.age_effectif / r.vie_utile, 1.0), abs=1e-6) assert ve["courte_duree"]["Ventilateur récupérateur de chaleur (VRC)"] == pytest.approx(7501.27, abs=0.01) assert ve["courte_duree"]["Revêtement extérieur (brique et fibrociment)"] == pytest.approx(18089.57, abs=0.01) class TestDesuetudes: def test_salle_eau(self, data): se = data["se"] assert se["cout_si_neuf_direct"] == pytest.approx(6309.76, abs=0.01) and se["cout_si_neuf"] == pytest.approx(8394.69, abs=0.01) assert se["desuetude"] == pytest.approx(7605.31, abs=0.01) and se["desuetude"] == pytest.approx(16000 - se["cout_si_neuf"], abs=1e-9) def test_exemples(self): ex = cas.exemples_incurables() assert ex["mrb_annuel"] == pytest.approx(19.82, abs=1e-9) and ex["perte_loyer_capitalisee"] == pytest.approx(17838.00, abs=0.01) assert ex["perte_rne_capitalisee_totale"] == pytest.approx(133333.33, abs=0.01) and ex["perte_rne_batiment"] == pytest.approx(93333.33, abs=0.01) assert ex["paires"] == pytest.approx(18000.0, abs=1e-9) class TestExtraction: def test_valeur_terrain(self, data): vl, vlr = data["vl"], data["vlr"] assert int(vl.loc["Hull", "n"]) == 13 and vl.loc["Hull", "vl_mediane"] == pytest.approx(276953.99, abs=0.01) assert vl.loc["Hull", "part_mediane"] == pytest.approx(0.505, abs=5e-4) and vlr["retenue"] == 277000.0 assert vlr["retenue"] / 620 == pytest.approx(447, abs=0.5) assert vl.loc["Aylmer", "vl_mediane"] == pytest.approx(321579, abs=1) and vl.loc["Gatineau", "vl_mediane"] == pytest.approx(270089, abs=1) assert vl.loc["Buckingham", "vl_mediane"] == pytest.approx(182678, abs=1) and vl.loc["Masson-Angers", "vl_mediane"] == pytest.approx(175657, abs=1) assert int(vl["n"].sum()) == 33 def test_identite_extraction(self, data): w = data["w"] assert len(w) == 600 assert np.allclose(w["D"], w["cn"] - (w["p_adj"] - w["vl"]), atol=1e-6) and np.allclose(w["taux"], w["D"] / w["cn"], atol=1e-12) assert data["modele"]["croissance_mensuelle"] == pytest.approx(0.00364, abs=5e-6) def test_tranches_et_ajustements(self, data): tr = cas.tranches(data["w"]) assert list(tr["size"]) == [33, 47, 76, 115, 124, 101, 104] assert tr.loc["0-10", "median"] == pytest.approx(0.038, abs=5e-4) and tr.loc["21-30", "median"] == pytest.approx(0.192, abs=5e-4) assert tr.loc["61-100", "median"] == pytest.approx(0.403, abs=5e-4) aj = data["aj"]; q = aj["quadratique"]["beta"]; l = aj["lineaire"]["beta"] assert l[0] == pytest.approx(0.0878, abs=5e-5) and l[1] == pytest.approx(0.00447, abs=5e-6) and aj["lineaire"]["r2"] == pytest.approx(0.228, abs=5e-4) assert q[0] == pytest.approx(0.0107, abs=5e-5) and q[1] == pytest.approx(0.00891, abs=5e-6) and q[2] == pytest.approx(-0.0000514, abs=5e-8) assert aj["quadratique"]["r2"] == pytest.approx(0.250, abs=5e-4) assert aj["taux_annuel_origine"] == pytest.approx(0.00619, abs=5e-6) and aj["vie_implicite"] == pytest.approx(162, abs=0.5) assert float(cas.taux_quadratique(aj["quadratique"], 28)) == pytest.approx(0.2200, abs=5e-5) assert float(cas.taux_quadratique(aj["quadratique"], 60)) == pytest.approx(0.3605, abs=5e-5) class TestProfils: def test_profils_cites(self, data): pr = data["pr"] assert pr["prix_neuf"] == pytest.approx(720727, abs=1) and pr["alpha"] == pytest.approx(0.384, abs=5e-4) assert pr["hedonique_total"][28] == pytest.approx(0.1148, abs=5e-5) and pr["generateur_total"][28] == pytest.approx(0.1223, abs=5e-5) assert pr["hedonique_bat"][28] == pytest.approx(0.1865, abs=5e-4) and pr["generateur_bat"][28] == pytest.approx(0.1986, abs=5e-4) assert pr["age_vie"][28] == pytest.approx(28 / 60, abs=1e-12) and pr["age_vie"][60] == 1.0 # conversion : d_B (1 − α) = d_total assert np.allclose(pr["hedonique_bat"] * (1 - pr["alpha"]), pr["hedonique_total"]) # profils croissants sur 0-80 ans assert np.all(np.diff(pr["extraction"]) > 0) and np.all(np.diff(pr["hedonique_bat"]) > 0) def test_generateur(self): assert cas.profil_total_generateur(28) == pytest.approx(1 - np.exp(-0.0055 * 28 + 0.00003 * 784), abs=1e-12) class TestValeur: def test_deux_lectures(self, data): vs = data["vs"]; A, B = vs["A_ventilee"], vs["B_extraction"] assert vs["d_extraite"] == pytest.approx(87461.05, abs=0.01) assert A["valeur"] == pytest.approx(445872.14, abs=0.01) and A["taux_depreciation"] == pytest.approx(0.5752, abs=5e-4) assert B["valeur"] == pytest.approx(579500.97, abs=0.01) and B["depreciation_totale"] == pytest.approx(95066.36, abs=0.01) assert B["taux_depreciation"] == pytest.approx(0.2391, abs=5e-4) and B["contribution_batiment"] == pytest.approx(302500.97, abs=0.01) assert vs["ecart"] == pytest.approx(133628.83, abs=0.01) and vs["valeur_arrondie"] == 580000.0 # identité V = V_L + C_N − D_phys − D_fonc − D_ext assert B["valeur"] == pytest.approx(277000 + 397567.33 - 87461.05 - 7605.31, abs=0.02) def test_ordre_externe(self, data): cn = data["cn"]["total"]; vs = data["vs"] # une désuétude externe de 10 % appliquée sur C_N plutôt que sur la base restante surestime la déduction avant = 0.10 * cn; apres = 0.10 * vs["B_extraction"]["contribution_batiment"] assert avant == pytest.approx(39756.73, abs=0.01) and apres == pytest.approx(30250.10, abs=0.01) and avant - apres == pytest.approx(9506.64, abs=0.01) class TestSensibilite: def test_grille(self, data): g = cas.grille_age_vie(data["cn"], data["var"]["curable"]) assert g.loc[60, 29] == pytest.approx(0.4970, abs=5e-4) and g.loc[50, 37] == pytest.approx(0.747, abs=5e-4) and g.loc[100, 20] == pytest.approx(0.221, abs=5e-4) assert g.loc[80, 29] == pytest.approx(0.379, abs=5e-4) def test_extraction_variantes(self, data): sx = cas.sensibilite_extraction(data["ventes"], data["modele"], data["cn"]["total"]).set_index("variante") assert sx.loc["cas de base", "taux_28"] == pytest.approx(0.2200, abs=5e-5) assert sx.loc["ventes récentes ≤ 5 ans", "taux_28"] == pytest.approx(0.1968, abs=5e-5) and sx.loc["ventes récentes ≤ 15 ans", "taux_28"] == pytest.approx(0.2720, abs=5e-5) assert sx.loc["ventes récentes ≤ 15 ans", "vl_hull"] == pytest.approx(303775, abs=1) assert sx["taux_28"].min() == pytest.approx(0.1968, abs=5e-5) and sx["taux_28"].max() == pytest.approx(0.2720, abs=5e-5) def test_tornade(self, data): sx = cas.sensibilite_extraction(data["ventes"], data["modele"], data["cn"]["total"]) t = {o["param"]: o for o in cas.tornade(data["cn"], data["vlr"]["retenue"], data["vs"]["d_extraite"], data["se"]["desuetude"], data["var"], sx)} assert t["Valeur du terrain ±15 %"]["bas"] == pytest.approx(537951, abs=1) and t["Valeur du terrain ±15 %"]["haut"] == pytest.approx(621051, abs=1) assert t["Choix d'extraction (taux à 28 ans)"]["bas"] == pytest.approx(558807, abs=1) and t["Choix d'extraction (taux à 28 ans)"]["haut"] == pytest.approx(588716, abs=1) assert t["Coût neuf ±8,6 % (date des prix)"]["bas"] == pytest.approx(554944, abs=1) and t["Coût neuf ±8,6 % (date des prix)"]["haut"] == pytest.approx(606170, abs=1) assert t["Lecture ventilée (ch. 11) au lieu de l'extraction"]["bas"] == pytest.approx(445872, abs=1) class TestExercices: def test_ex1(self): assert ct.depreciation_age_vie(320000, 15, 60) == 80000 and ct.vie_restante(60, 15) == 45 m = ct.depreciation_age_vie_modifiee(320000, 9000, 15, 60) assert m["incurable"] == 77750 and m["total"] == 86750 and m["total"] / 320000 == pytest.approx(0.2711, abs=5e-5) assert ct.depreciation_age_vie_modifiee(320000, 9000, 20, 60)["total"] == pytest.approx(112666.67, abs=0.01) def test_ex2(self): comps = [ct.Composante("toiture", 9000, 12, 20), ct.Composante("fenêtres", 18000, 10, 30), ct.Composante("chauffe-eau", 1500, 8, 12)] v = ct.depreciation_ventilee(320000, 9000, comps, 15, 60) assert v["courte_duree"] == {"toiture": 5400, "fenêtres": 6000, "chauffe-eau": 1000} and v["base_longue"] == 282500 assert v["longue_duree"] == 70625 and v["total"] == 92025 and v["total"] / 320000 == pytest.approx(0.2876, abs=5e-5) assert v["total"] - 86750 == 5275 def test_ex3(self): assert ct.desuetude_fonctionnelle_curable(16000, 8394.69, 0) == pytest.approx(7605.31, abs=1e-9) assert 22000 * 0.6 + 3000 == 16200 # base de reproduction : solde non déprécié + coût d'enlèvement def test_ex4(self): assert ct.desuetude_incurable_par_loyer(75, 19.82 * 12) == pytest.approx(17838, abs=1e-9) assert ct.desuetude_incurable_par_rne(6000, 0.045, 0.7) == pytest.approx(93333.33, abs=0.01) assert ct.desuetude_incurable_par_rne(6000, 0.055, 0.7) == pytest.approx(76363.64, abs=0.01) def test_ex5(self): ventes = [(610000, 260000, 420000, 12), (545000, 250000, 390000, 25), (480000, 240000, 380000, 41), (520000, 255000, 410000, 33)] e = [ct.extraction_depreciation_marche(p, l, c, a) for p, l, c, a in ventes] assert [x["depreciation"] for x in e] == [70000, 95000, 140000, 145000] ta = [x["taux_annuel"] for x in e] assert ta[0] == pytest.approx(0.01389, abs=5e-6) and ta[2] == pytest.approx(0.00899, abs=5e-6) m = float(np.mean(ta)) assert m == pytest.approx(0.01083, abs=5e-6) and 1 / m == pytest.approx(92.3, abs=0.05) and 30 * m * 400000 == pytest.approx(130006, abs=1) m2 = float(np.mean([ct.extraction_depreciation_marche(p, l + 20000, c, a)["taux_annuel"] for p, l, c, a in ventes])) assert m2 == pytest.approx(0.01303, abs=5e-6) and 30 * m2 * 400000 == pytest.approx(156350, abs=1) def test_ex6(self): b1, b2, al = -0.005097, 0.00002642, 0.384 t28 = 1 - np.exp(b1 * 28 + b2 * 784) assert t28 == pytest.approx(0.1149, abs=5e-5) and t28 / (1 - al) == pytest.approx(0.1865, abs=5e-5) assert (1 - np.exp(b1 * 50 + b2 * 2500)) / (1 - al) == pytest.approx(0.2793, abs=5e-5) assert -(b1 + 2 * b2 * 28) == pytest.approx(0.003617, abs=5e-7) and -b1 / (2 * b2) == pytest.approx(96.5, abs=0.05) assert 1 - t28 / (28 / 60) == pytest.approx(0.754, abs=5e-4) def test_ex7(self): assert 6130.62 + 15989.85 == pytest.approx(22120.47, abs=0.01) assert 0.10 * 397567.33 - 0.10 * 302500.97 == pytest.approx(9506.64, abs=0.01)