SPB Git
12.2 KB · 191 lines python
1"""Tests du chapitre 9 — étude de ratios (COD, PRD, PRB, proportion médiane, facteur comparatif), dossiers de contestation,2compte de taxes de Gatineau 2026, droits de mutation, échéancier et exercices.3Tolérances : identités 1e-9 ; statistiques citées à 5e-4 (ratios) ou 0,05 (COD) ; montants cités au dollar près ou 0,5 $ ; valeurs arrondies au millier exactes."""4import sys, pathlib, datetime as dt5import numpy as np, pandas as pd, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch09_ratios as cas8from traite import hedonique as h91011@pytest.fixture(scope="module")12def data():13    d = cas.charger(); et = cas.etude(d)14    return {"d": d, "et": et, "ds": cas.dossier_sujet(d, et), "da": cas.dossier_acheteur(d, et), "ex": cas.exercices(et)}151617class TestIdentitesRatio:18    def test_ratios_uniformes(self):19        """Rôle exactement proportionnel au prix : COD = 0, PRD = 1, PRB = 0, médiane = proportion."""20        P = np.linspace(200_000, 900_000, 50); st = h.statistiques_ratio(0.93 * P, P)21        assert st["COD"] == pytest.approx(0.0, abs=1e-9) and st["PRD"] == pytest.approx(1.0, abs=1e-9)22        assert st["PRB"] == pytest.approx(0.0, abs=1e-9) and st["ratio_median"] == pytest.approx(0.93, abs=1e-12)2324    def test_definitions(self, data):25        """COD = 100 · moyenne|r − méd| / méd ; PRD = moyenne(r) / (ΣV/ΣP) ; facteur = 1 / proportion médiane."""26        v = data["et"]["v"]; r = (v["valeur_role_2025"] / v["prix_vente"]).to_numpy(); med = np.median(r)27        st = data["et"]["brut"]28        assert st["COD"] == pytest.approx(100 * np.mean(np.abs(r - med)) / med, rel=1e-12)29        assert st["PRD"] == pytest.approx(r.mean() / (v["valeur_role_2025"].sum() / v["prix_vente"].sum()), rel=1e-12)30        assert data["et"]["F"] * data["et"]["PM"] == pytest.approx(1.0, abs=1e-12)3132    def test_regressivite_prd(self):33        """Un rôle d'élasticité 0,9 par rapport à la valeur est régressif : PRD > 1,03 et PRB < 0."""34        rng = np.random.default_rng(1); V = np.exp(rng.normal(13, 0.4, 2000)); P = V * np.exp(rng.normal(0, 0.05, 2000)); A = 0.93 * V ** 0.9 * np.exp(0.1 * 13)35        st = h.statistiques_ratio(A, P)36        assert st["PRD"] > 1.01 and st["PRB"] < -0.053738    def test_biais_du_proxy(self):39        """Lorsque l'erreur du rôle est la seule source de dispersion des ratios, le proxy IAAO induit un PRB positif ; le proxy « prix seul » ne l'induit pas."""40        rng = np.random.default_rng(2); P = np.exp(rng.normal(13, 0.3, 5000)); A = 0.93 * P * np.exp(rng.normal(0, 0.07, 5000))41        st = h.statistiques_ratio(A, P); alt = cas.prb_proxy_prix(A, P)42        assert st["PRB"] > 0.01 and st["PRB"] > 4 * st["PRB_se"] and abs(alt["PRB"]) < 0.014344    def test_epuration(self):45        r = np.array([0.9, 0.92, 0.95, 0.88, 0.91, 1.6, 0.93, 0.5])46        ep = cas.epuration_r10(r)47        med = np.median(r); sd = r.std(ddof=1)48        assert ep["borne_basse"] == pytest.approx(med - 2 * sd) and ep["borne_haute"] == pytest.approx(med + 2 * sd)49        assert ep["n_retires"] == int(((r < med - 2 * sd) | (r > med + 2 * sd)).sum())5051    def test_liste_de_base(self):52        assert cas.liste_de_base(100) == 3053        assert cas.liste_de_base(103_355) == int(np.ceil(103_355 / (15.5 + 103.355)))54        assert cas.liste_de_base(103_355) == 870 and cas.liste_de_base(514_614) == 9715556    def test_plafond_r13(self):57        assert cas.plafond_r13(0.10) == 0.36 and cas.plafond_r13(0.30) == 0.31 and cas.plafond_r13(0.50) == 0.25 and cas.plafond_r13(0.70) == 0.19 and cas.plafond_r13(0.95) == 0.14585960class TestEtude:61    def test_valeurs_citees_brut(self, data):62        st = data["et"]["brut"]63        assert st["n"] == 60064        assert st["ratio_median"] == pytest.approx(0.9136, abs=5e-4) and st["COD"] == pytest.approx(6.90, abs=0.05)65        assert st["PRD"] == pytest.approx(1.0015, abs=5e-4) and st["PRB"] == pytest.approx(0.0327, abs=5e-4)6667    def test_valeurs_citees_rajuste(self, data):68        et = data["et"]; st = et["aj"]69        assert et["croissance_annuelle"] == pytest.approx(0.0446, abs=5e-4)70        assert st["ratio_median"] == pytest.approx(0.9245, abs=5e-4) and st["COD"] == pytest.approx(6.12, abs=0.05)71        assert st["PRD"] == pytest.approx(0.9988, abs=5e-4) and st["PRB"] == pytest.approx(0.0640, abs=5e-4)72        assert st["COD"] < et["brut"]["COD"]7374    def test_epuration_et_facteur(self, data):75        et = data["et"]; ep = et["epuration"]76        assert ep["n_retires"] == 29 and ep["borne_basse"] == pytest.approx(0.7830, abs=5e-4) and ep["borne_haute"] == pytest.approx(1.0661, abs=5e-4)77        assert et["PM"] == pytest.approx(0.9244, abs=5e-4) and et["F"] == pytest.approx(1.0818, abs=5e-4)78        assert et["apres_epuration"]["COD"] == pytest.approx(5.48, abs=0.05)79        assert et["ecart_type_relatif_epure"] == pytest.approx(0.0660, abs=5e-4) and et["ecart_type_relatif_epure"] < et["plafond_r13"]80        assert cas.R13_PROPORTION[0] > et["PM"]   # le rôle fictif est sous le plancher de 95 % du r. 138182    def test_sous_groupes(self, data):83        et = data["et"]84        assert et["secteurs"]["n"].sum() == 600 and et["secteurs"]["mediane"].between(0.915, 0.935).all()85        assert et["tranches"]["mediane"].between(0.915, 0.935).all()86        assert et["annees"].loc[2023, "r_brut"] == pytest.approx(0.9538, abs=5e-4) and et["annees"].loc[2026, "r_brut"] == pytest.approx(0.8692, abs=5e-4)8788    def test_taux_voisinage(self):89        tv = cas.taux_voisinage_gatineau()90        assert tv["n"] == 1092 and tv["min"] == pytest.approx(1.613) and tv["max"] == pytest.approx(7.544) and tv["mediane"] == pytest.approx(5.232, abs=5e-4)919293class TestTaxes:94    def test_compte_sujet(self, data):95        t = data["ds"]["taxes_role"]96        assert t["taux_municipal_pour_1000"] == pytest.approx(5.232 + 1.417 + 0.830 + 0.036, abs=1e-9)97        assert t["municipal"] == pytest.approx(468_000 * t["taux_municipal_pour_1000"] / 1000, rel=1e-12)98        assert t["evaluation_uniformisee"] == pytest.approx(468_000 * data["et"]["F"], rel=1e-12)99        assert t["scolaire"] == pytest.approx((t["evaluation_uniformisee"] - 25_000) * 0.0007899, rel=1e-12)100        assert t["total"] == pytest.approx(4076.01, abs=0.5)101        assert t["taux_marginal_pour_1000"] == pytest.approx(8.370, abs=5e-4)102103    def test_credits_non_raccorde(self):104        a = cas.compte_taxes_gatineau(468_000, 1.02, 5.232); b = cas.compte_taxes_gatineau(468_000, 1.02, 5.232, raccorde=False)105        assert a["municipal"] - b["municipal"] == pytest.approx(468_000 * (0.386 + 0.523) / 1000, rel=1e-12)106107    def test_droits_mutation(self):108        assert cas.droits_mutation(500_000, cas.BAREME_MUTATION_QC) == pytest.approx(0.005 * 62_900 + 0.01 * (315_000 - 62_900) + 0.015 * (500_000 - 315_000), rel=1e-12)109        assert cas.droits_mutation(575_000, cas.BAREME_MUTATION_QC) == pytest.approx(6735.50, abs=0.01)110        assert cas.droits_mutation(575_000, cas.BAREME_MUTATION_GAT) == pytest.approx(7860.50, abs=0.01)111        assert cas.droits_mutation(575_000, cas.BAREME_MUTATION_MTL) == pytest.approx(6849.00, abs=0.01)112        assert cas.droits_mutation(4_000, cas.BAREME_MUTATION_QC) == pytest.approx(20.0)113114    def test_frais(self):115        assert cas.frais(468_000, cas.FRAIS_REVISION_GATINEAU) == 80.0 and cas.frais(592_100, cas.FRAIS_REVISION_GATINEAU) == 325.0116        assert cas.frais(468_000, cas.FRAIS_TAQ_2026) == 93.20 and cas.frais(6_000_000, cas.FRAIS_TAQ_2026) == 1242.50117        for b_mun, b_taq in zip(cas.FRAIS_REVISION_GATINEAU, cas.FRAIS_TAQ_2026):118            assert b_mun[1] <= b_taq[1]   # LFM art. 263.2 al. 2 : plafond = tarif du TAQ119120121class TestDossiers:122    def test_sujet(self, data):123        ds = data["ds"]; et = data["et"]124        assert ds["hedonique"] == pytest.approx(580_375, abs=1) and ds["comparables"]["indication_mediane"] == pytest.approx(565_414, abs=1)125        assert ds["valeur_reelle"] == 573_000126        assert ds["ratio_sujet"] == pytest.approx(468_000 / 573_000, rel=1e-12) and ds["ratio_sujet"] == pytest.approx(0.8168, abs=5e-4)127        assert ds["valeur_a_inscrire"] * et["F"] == pytest.approx(ds["valeur_reelle"], rel=1e-12)128        assert ds["valeur_a_inscrire"] == pytest.approx(529_681, abs=1) and ds["variation_si_conteste"] > 0129        assert ds["variation_taxes_annuelle"] == pytest.approx(516.24, abs=0.5)130        assert len(ds["comparables"]["ajustements"]) == 5131        assert list(ds["comparables"]["ajustements"]["id_vente"]) == ["G0264", "G0334", "G0404", "G0388", "G0188"]132        a = ds["comparables"]["ajustements"]133        groupes = ["conditions_marche", "localisation", "superficie", "terrain", "age", "qualite", "garage", "sous_sol", "piscine", "type"]134        assert np.allclose(a["prix_vente"] + a[groupes].sum(axis=1), a["prix_ajuste"])135136    def test_acheteur(self, data):137        da = data["da"]; et = data["et"]138        assert da["vente"]["id_vente"] == "G0258" and da["date_vente"] == pd.Timestamp("2024-07-04")139        assert da["prix_vente"] == 509_500 and da["valeur_role"] == 592_100140        assert da["prix_ref"] == pytest.approx(509_317, abs=1) and da["ratio_aj"] == pytest.approx(1.1625, abs=5e-4)141        assert da["valeur_a_inscrire"] == pytest.approx(470_813, abs=1) and da["reduction"] == pytest.approx(121_287, abs=1)142        assert da["gain_annuel"] == pytest.approx(1015.11, abs=0.5) and da["gain_cycle"] == pytest.approx(3 * da["gain_annuel"], rel=1e-12)143        assert da["frais_revision"] == 325.0 and da["gain_net_cycle"] == pytest.approx(2720.34, abs=0.5)144        assert da["seuil_valeur"] == pytest.approx(12_944, abs=1)145        # gain annuel = réduction × taux marginal (identité)146        assert da["gain_annuel"] == pytest.approx(da["reduction"] * da["taxes_role"]["taux_marginal_pour_1000"] / 1000, rel=1e-9)147148    def test_sensibilite(self, data):149        s = cas.sensibilite_acheteur(data["d"], data["et"], data["da"])150        assert len(s) == 9 and (s["gain_net_min"] > 0).all()151        assert s["gain_net_mediane"].is_monotonic_decreasing152        ligne0 = s.iloc[(s["ecart_pct"].abs()).idxmin()]153        assert ligne0["gain_net_mediane"] == pytest.approx(data["da"]["gain_net_cycle"], abs=0.5)154155    def test_mutations(self, data):156        mu = cas.mutations_sujet(data["ds"], data["et"])157        assert mu["Gatineau 2026 (dès le 25 février)"]["droit"] == pytest.approx(7800.50, abs=0.01)158        ex = mu["exemple_facteur"]159        assert ex["base_F_reel"] == pytest.approx(468_000 * 1.02) and ex["droit_F_reel"] == pytest.approx(5270.90, abs=0.01) and ex["droit_prix"] == pytest.approx(4860.50, abs=0.01)160161    def test_echeancier(self):162        e = cas.echeancier()["fictif"]; dates = {j["jalon"][:20]: j["date"] for j in e}163        assert e[0]["date"] == dt.date(2024, 7, 1) and e[2]["date"] == dt.date(2026, 1, 1)164        assert e[4]["date"] == dt.date(2026, 4, 30) and e[5]["date"] == dt.date(2026, 9, 1) and e[7]["date"] == dt.date(2026, 10, 31)165        assert (e[6]["date"] - e[5]["date"]).days == 30 and (e[7]["date"] - e[6]["date"]).days == 30166        g = cas.echeancier()["gatineau_2027"]; assert g[0]["date"] == dt.date(2025, 7, 1) and g[4]["date"] == dt.date(2027, 4, 30)167168169class TestExercices:170    def test_ex1(self, data):171        ex = data["ex"]["ex1"]172        assert ex["F"] == pytest.approx(1.0870, abs=5e-5) and ex["uniformisee_468000"] == pytest.approx(508_695.65, abs=0.01) and ex["taxe_scolaire"] == pytest.approx(382.07, abs=0.01)173174    def test_ex2(self, data):175        ex = data["ex"]["ex2"]176        assert ex["raccorde"]["total"] == pytest.approx(468_000 * 7.515 / 1000 + (468_000 * 1.02 - 25_000) * 0.0007899 + 178.83, abs=0.01)177        assert ex["raccorde"]["total"] == pytest.approx(4053.17, abs=0.01) and ex["non_raccorde"]["total"] == pytest.approx(3627.76, abs=0.01)178179    def test_ex3(self, data):180        ex = data["ex"]["ex3"]181        assert ex["ratio_median"] == pytest.approx(0.9221, abs=5e-4) and ex["COD"] == pytest.approx(2.50, abs=0.05) and ex["PRD"] == pytest.approx(1.0031, abs=5e-4)182        assert ex["epuration"]["n_retires"] == 0 and ex["ecart_type_relatif"] == pytest.approx(0.0311, abs=5e-4)183184    def test_ex4(self, data):185        ex = data["ex"]["ex4"]186        assert ex["gat_base_role"] == pytest.approx(632_400) and ex["gat_droit_base_role"] == pytest.approx(9582.50, abs=0.01)187188    def test_ex7(self, data):189        ex = data["ex"]["ex7"]190        assert ex["PRD"] > 1.0 and ex["PRB"] < -0.05191
Files · main
Menu

Theme