SPB Git
18.3 KB · 248 lines python
1"""Tests du chapitre 5 — modèle monocentrique simulé, gradient prix/distance du cas 1, décomposition du cas 4,2rente résiduelle, capitalisation fiscale et exercices.3Tolérances : identités à 1e-9 ; valeurs citées dans le texte à l'arrondi affiché (0,5 $ sur les montants au dollar,40,005 point sur les pourcentages à deux décimales, 0,0005 sur les coefficients en log à trois décimales)."""5import sys, pathlib, math6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch05_rente as c59from traite import cas1, cas410from traite import hedonique as h1112P = c5.PARAMS131415@pytest.fixture(scope="module")16def data():17    d1 = cas1.charger(); v = d1["ventes"]; m = cas1.hedonique(v)18    d4 = cas4.charger()19    return {"v": v, "sujet": d1["sujet"], "modele": m, "tranches": c5.tranches_distance(v), "gradients": c5.gradients_cas1(v, m),20            "pf": d4["portefeuille"], "dec": c5.decomposition_rmr(d4["portefeuille"])}212223class TestMonocentrique:24    def test_gradient_et_frontiere(self):25        g = c5.gradient_exponentiel(P)26        assert g == pytest.approx(0.029167, abs=5e-6) and c5.demi_distance(g) == pytest.approx(23.8, abs=0.05)27        dbar = c5.frontiere_urbaine(P)28        assert dbar == pytest.approx(32.29, abs=0.005)29        # identité : r(d_bar) = ra30        assert c5.rente_fonciere(dbar, P) == pytest.approx(P["ra"], rel=1e-9)31        assert c5.prix_logement(dbar, P) == pytest.approx(85.97, abs=0.005)3233    def test_condition_de_muth_numerique(self):34        # p'(d) q(d) = -t, vérifié par différences finies35        for d in (0.0, 5.0, 12.0, 25.0):36            eps = 1e-537            dp = (c5.prix_logement(d + eps, P) - c5.prix_logement(d - eps, P)) / (2 * eps)38            assert dp * c5.consommation_logement(d, P) == pytest.approx(-P["t"], rel=1e-6)3940    def test_valeurs_tableau(self):41        tab = c5.gradient_exact_vs_exponentiel(P).set_index("d_km")42        assert tab.loc[10.0, "p_exact"] == pytest.approx(191.61, abs=0.005) and tab.loc[10.0, "p_exponentiel"] == pytest.approx(194.22, abs=0.005)43        assert tab.loc[10.0, "ecart_pct"] == pytest.approx(0.014, abs=5e-4)44        assert tab.loc[25.0, "p_exact"] == pytest.approx(114.18, abs=0.005) and tab.loc[25.0, "p_exponentiel"] == pytest.approx(125.40, abs=0.005)45        assert tab.loc[25.0, "ecart_pct"] == pytest.approx(0.098, abs=5e-4)46        assert tab.loc[0.0, "q_m2"] == pytest.approx(92.3, abs=0.05) and tab.loc[10.0, "q_m2"] == pytest.approx(114.3, abs=0.05)47        assert c5.consommation_logement(c5.frontiere_urbaine(P), P) == pytest.approx(200.3, abs=0.05)48        assert tab.loc[10.0, "rente_fonciere"] == pytest.approx(28.92, abs=0.005) and tab.loc[10.0, "densite_relative"] == pytest.approx(0.396, abs=5e-4)49        assert c5.valeur_terrain(P["r0"], P["i"]) == 1600 and c5.valeur_terrain(P["ra"], P["i"]) == 4050        assert c5.elasticite_rente_prix(P["alpha"]) == pytest.approx(3.333, abs=5e-4)5152    def test_identites_production(self):53        d = np.array([3.0, 8.0, 20.0])54        ratio_p = c5.prix_logement(d, P) / P["p0"]55        assert np.allclose(c5.rente_fonciere(d, P) / P["r0"], ratio_p ** (1 / P["alpha"]))56        # densité = (p/p0)^(1/alpha) * y/(y - t d)57        assert np.allclose(c5.densite(d, P), ratio_p ** (1 / P["alpha"]) * P["y"] / (P["y"] - P["t"] * d))58        # l'approximation exponentielle surestime le prix pour d > 059        assert np.all(c5.prix_exponentiel(d, P) > c5.prix_logement(d, P))6061    def test_alonso(self):62        (R1, t1), (R2, t2), (R3, t3) = c5.ALONSO.values()63        assert c5.frontiere_usages(R1, t1, R2, t2) == pytest.approx(9.47, abs=0.005)64        assert c5.frontiere_usages(R2, t2, R3, t3) == pytest.approx(14.29, abs=0.005)65        assert (R3 - P["ra"]) / t3 == pytest.approx(32.22, abs=0.005)66        d = np.array([5.0, 12.0, 20.0, 35.0]); env, courbes = c5.enveloppe_alonso(d)67        assert np.all(env >= np.array([max(c[i] for c in courbes.values()) for i in range(len(d))])) and env[-1] == P["ra"]686970class TestCas1:71    def test_tranches(self, data):72        tr = data["tranches"]73        assert int(tr["n"].sum()) == 600 and list(tr["n"]) == [116, 67, 37, 97, 100, 64, 30, 89]74        assert tr.loc["0-2", "prix_m2_median"] == pytest.approx(3600, abs=0.5) and tr.loc["20 et plus", "prix_m2_median"] == pytest.approx(2684, abs=0.5)75        assert tr.loc["10-12", "prix_m2_median"] == pytest.approx(3714, abs=0.5) and tr.loc["10-12", "prix_median"] == 54800076        assert tr.loc["20 et plus", "terrain_median"] == 1184 and tr.loc["0-2", "terrain_median"] == pytest.approx(535.5, abs=0.01)77        assert tr.loc["10-12", "secteur_dominant"] == "Aylmer" and tr.loc["20 et plus", "secteur_dominant"] == "Masson-Angers"7879    def test_gradients(self, data):80        g = data["gradients"]81        assert g["brut_prix_m2"]["gamma"] == pytest.approx(0.01020, abs=5e-6) and g["brut_prix_m2"]["se"] == pytest.approx(0.00089, abs=5e-6)82        assert g["brut_prix_m2"]["r2"] == pytest.approx(0.185, abs=5e-4) and c5.demi_distance(g["brut_prix_m2"]["gamma"]) == pytest.approx(68, abs=0.5)83        assert g["controle_lineaire"]["gamma"] == pytest.approx(0.01083, abs=5e-6) and g["controle_lineaire"]["se"] == pytest.approx(0.00067, abs=5e-6)84        assert g["controle_lineaire"]["r2"] == pytest.approx(0.759, abs=5e-4)85        u = g["controle_urbain_16km"]86        assert u["n"] == 511 and u["gamma"] == pytest.approx(-0.00266, abs=5e-6) and u["se"] == pytest.approx(0.00103, abs=5e-6) and u["r2"] == pytest.approx(0.771, abs=5e-4)87        q = g["controle_quadratique"]88        assert q["b1"] == pytest.approx(0.00900, abs=5e-6) and q["se1"] == pytest.approx(0.00169, abs=5e-6)89        assert q["b2_100"] == pytest.approx(-0.06966, abs=5e-5) and q["se2"] == pytest.approx(0.0056, abs=5e-5) and q["r2"] == pytest.approx(0.808, abs=5e-4)90        sommet = -q["b1"] / (2 * q["b2_100"] / 100)91        assert sommet == pytest.approx(6.5, abs=0.05)92        assert q["gamma_10"] == pytest.approx(0.00493, abs=5e-6)93        pente20 = -(q["b1"] + 2 * q["b2_100"] * 20 / 100)94        assert pente20 == pytest.approx(0.0189, abs=5e-5)9596    def test_modele_reference(self, data):97        r = data["gradients"]["reference"]98        assert r["gamma_au_dela_3km"] == pytest.approx(0.00357, abs=5e-6) and r["se"] == pytest.approx(0.00265, abs=5e-6) and r["t"] == pytest.approx(-1.35, abs=0.005)99        assert r["r2"] == pytest.approx(0.853, abs=5e-4)100        pct = r["secteurs_pct"]101        assert pct["Gatineau"] == pytest.approx(-0.022, abs=5e-4) and pct["Aylmer"] == pytest.approx(0.089, abs=5e-4)102        assert pct["Buckingham"] == pytest.approx(-0.221, abs=5e-4) and pct["Masson-Angers"] == pytest.approx(-0.231, abs=5e-4)103        gs = data["gradients"]["secteurs_medianes"]104        assert gs.loc["Hull", "dist"] == pytest.approx(1.6, abs=0.05) and gs.loc["Buckingham", "dist"] == pytest.approx(29.2, abs=0.05)105        p_hg = (math.log(gs.loc["Gatineau", "prix"]) - math.log(gs.loc["Hull", "prix"])) / (gs.loc["Gatineau", "dist"] - gs.loc["Hull", "dist"])106        p_hb = (math.log(gs.loc["Buckingham", "prix"]) - math.log(gs.loc["Hull", "prix"])) / (gs.loc["Buckingham", "dist"] - gs.loc["Hull", "dist"])107        assert p_hg == pytest.approx(-0.00345, abs=5e-6) and p_hb == pytest.approx(-0.00973, abs=5e-6)108        # effet distance nul pour le sujet (2,1 km < 3 km)109        assert max(data["sujet"]["dist_centre_km"] - 3, 0) == 0110        g = data["gradients"]["controle_lineaire"]["gamma"]111        assert 551029 * g == pytest.approx(5969, abs=0.5) and 551029 * (1 - math.exp(-5 * g)) == pytest.approx(29050, abs=0.5)112113114class TestCas4:115    def test_decomposition(self, data):116        dec = data["dec"]117        attendu = {"Montréal": (206237, 9270, 0.0449), "Gatineau": (184173, 10011, 0.0544), "Sherbrooke": (137364, 7884, 0.0574),118                   "Québec": (125455, 7237, 0.0577), "Trois-Rivières": (116806, 7345, 0.0629), "Saguenay": (101969, 6520, 0.0639)}119        for k, (vl, rl, tx) in attendu.items():120            assert dec.loc[k, "valeur_par_logement"] == pytest.approx(vl, abs=0.5) and dec.loc[k, "rne_par_logement"] == pytest.approx(rl, abs=0.5)121            assert dec.loc[k, "taux_pondere"] == pytest.approx(tx, abs=5e-5)122        # identité : écart total = contribution RNE + contribution taux123        assert np.allclose(dec["ecart_ln_V_n"], dec["contrib_rne"] + dec["contrib_taux"], atol=1e-12)124        assert dec.loc["Québec", "contrib_rne"] == pytest.approx(-0.248, abs=5e-4) and dec.loc["Québec", "contrib_taux"] == pytest.approx(-0.249, abs=5e-4)125        assert dec.loc["Gatineau", "contrib_rne"] == pytest.approx(0.077, abs=5e-4) and dec.loc["Gatineau", "contrib_taux"] == pytest.approx(-0.190, abs=5e-4)126        assert dec.loc["Saguenay", "ecart_ln_V_n"] == pytest.approx(-0.704, abs=5e-4) and dec.loc["Saguenay", "ratio_valeur"] == pytest.approx(0.494, abs=5e-4)127        for k in ("Sherbrooke", "Trois-Rivières"):128            part_taux = dec.loc[k, "contrib_taux"] / dec.loc[k, "ecart_ln_V_n"]129            assert 0.55 < part_taux < 0.65130        assert list(dec.index) == ["Montréal", "Gatineau", "Sherbrooke", "Québec", "Trois-Rivières", "Saguenay"]131        assert int(dec["logements"].sum()) == 888 and int(dec["n"].sum()) == 48132133134class TestResidu:135    def test_valeur_residuelle(self):136        CN = 397567.33; dep = 0.30 * CN; V = 551029.0137        assert dep == pytest.approx(119270.20, abs=0.005) and CN - dep == pytest.approx(278297.13, abs=0.005)138        VL = c5.valeur_residuelle(V, CN, dep)139        assert VL == pytest.approx(272732, abs=0.5) and VL / V == pytest.approx(0.495, abs=5e-4) and c5.levier_residuel(V, VL) == pytest.approx(2.02, abs=0.005)140        assert c5.valeur_residuelle(0.95 * V, CN, dep) == pytest.approx(245180, abs=0.5)141        assert c5.valeur_residuelle(0.95 * V, CN, dep) / VL - 1 == pytest.approx(-0.101, abs=5e-4)142        assert c5.valeur_residuelle(0.90 * V, CN, dep) / VL - 1 == pytest.approx(-0.202, abs=5e-4)143        # identité du levier : dVL/VL = (V/VL) dV/V144        assert (c5.valeur_residuelle(0.95 * V, CN, dep) - VL) / VL == pytest.approx(c5.levier_residuel(V, VL) * (-0.05), rel=1e-12)145        assert 1.1 ** c5.elasticite_rente_prix(0.3) - 1 == pytest.approx(0.374, abs=5e-4)146147    def test_capozza(self):148        cz = c5.decomposition_capozza(P["ra"], P["i"], 25.0, c5.rente_fonciere(10.0, P), 60.0)149        assert cz["valeur_agricole"] == 40 and cz["accessibilite"] == pytest.approx(538.4, abs=0.05) and cz["total"] == pytest.approx(663.4, abs=0.05)150        assert cz["accessibilite"] / cz["total"] == pytest.approx(0.81, abs=5e-3)151152153class TestTaxe:154    def test_formule_exacte_et_approchee(self):155        c = c5.capitalisation_taxe(500000, 0.0070, 0.0080, 0.05)156        assert c["dT"] == 500 and c["dV_approx"] == -10000 and c["dV_exact"] == pytest.approx(-8621, abs=0.5) and c["dV_pct_exact"] == pytest.approx(-0.0172, abs=5e-5)157        # identité : dV_exact = -dT/(i + tau1) et V1 = R/(i+tau1)158        assert c["dV_exact"] == pytest.approx(-500 / 0.058, rel=1e-12) and c["V1_exact"] == pytest.approx(c["R"] / 0.058, rel=1e-12)159        assert c["dV_approx"] / c["dV_exact"] == pytest.approx(0.058 / 0.05, rel=1e-12)160        assert 0.6 * c["dV_approx"] == -6000161        c2 = c5.capitalisation_taxe(500000, 0.0070, 0.0070 + 22.4 / 1e4, 0.05)162        assert c2["dT"] == pytest.approx(1120, abs=1e-6) and c2["dV_approx"] == pytest.approx(-22400, abs=1e-6)163        assert c2["dV_exact"] == pytest.approx(-18906, abs=0.5) and c2["dV_pct_exact"] == pytest.approx(-0.0378, abs=5e-5)164        assert c5.valeur_avec_taxe(28500, 0.05, 0.0075) / c5.valeur_avec_taxe(28500, 0.05, 0.0) - 1 == pytest.approx(-0.13, abs=5e-3)165166    def test_taux_2026(self):167        T = c5.TAUX_2026168        ecart = (T["Québec (1 à 5 logements)"] - T["Gatineau (taux de base)"]) / 100169        assert ecart * 1e4 == pytest.approx(22.4, abs=0.005) and ecart * 500000 == pytest.approx(1120, abs=0.005)170        assert c5.taux_effectif(T["Gatineau (taux de base)"], 1.02) == pytest.approx(0.5122, abs=5e-5)171        assert c5.taux_effectif(T["Québec (1 à 5 logements)"], 1.10) == pytest.approx(0.6785, abs=5e-5)172        assert 468000 * T["Gatineau (taux de base)"] / 100 == pytest.approx(2445, abs=0.5) and 468000 * T["Taxe scolaire 2026-2027 (toute la province)"] / 100 == pytest.approx(370, abs=0.5)173        d6 = (T["Québec (6 logements et plus)"] - T["Québec (1 à 5 logements)"]) / 100174        assert d6 * 1e4 == pytest.approx(2.33, abs=0.005) and d6 * 2e6 == pytest.approx(466, abs=0.5) and d6 * 2e6 / 0.055 == pytest.approx(8473, abs=0.5)175        tau = c5.taux_effectif(T["Gatineau (taux de base)"], 1.02) / 100 + T["Taxe scolaire 2026-2027 (toute la province)"] / 100176        assert tau / (0.0412 + tau + 0.015 - 0.02) == pytest.approx(0.14, abs=5e-3)177178    def test_grille(self):179        G = c5.grille_capitalisation(500000, [5, 10, 25, 50, 100], [0.04, 0.05, 0.06, 0.07])180        assert G.loc[10, "r=5%"] == -10000 and G.loc[50, "r=4%"] == -62500 and G.loc[100, "r=7%"] == pytest.approx(-71429, abs=0.5)181        assert G.loc[25, "r=6%"] == pytest.approx(-20833, abs=0.5) and G.loc[5, "r=7%"] == pytest.approx(-3571, abs=0.5)182        assert G.loc[10, "r=4%"] / G.loc[10, "r=7%"] - 1 == pytest.approx(0.75, rel=1e-9)183184185class TestExercices:186    def test_ex1(self):187        a, b = c5.THUNEN["Maraîchage"], c5.THUNEN["Grandes cultures"]188        assert a["Y"] * (a["p"] - a["c"]) == 1000 and a["Y"] * a["f"] == 60 and b["Y"] * (b["p"] - b["c"]) == 600 and b["Y"] * b["f"] == 20189        assert c5.distance_limite_thunen(**a) == pytest.approx(16.67, abs=0.005) and c5.distance_limite_thunen(**b) == 30190        assert c5.frontiere_usages(1000, 60, 600, 20) == 10 and c5.rente_thunen(10, **a) == 400 == c5.rente_thunen(10, **b)191        assert c5.valeur_terrain(1000, 0.04) == 25000192193    def test_ex2(self):194        c = c5.capitalisation_taxe(500000, 0.0070, 0.0080, 0.05)195        assert c["dT"] == 500 and c["dV_approx"] == -10000 and c["dV_exact"] == pytest.approx(-8621, abs=0.5) and c["V1_exact"] == pytest.approx(491379, abs=0.5)196        net = c5.capitalisation_taxe(500000, 0.0070, 0.0070 + 200 / 500000, 0.05)197        assert net["dV_approx"] == pytest.approx(-4000, abs=1e-6) and net["dV_exact"] == pytest.approx(-3484, abs=0.5)198199    def test_ex3(self):200        assert c5.prix_logement(5.0, P) == pytest.approx(223.98, abs=0.005) and c5.prix_logement(10.0, P) == pytest.approx(191.61, abs=0.005)201        assert c5.prix_exponentiel(5.0, P) == pytest.approx(224.72, abs=0.005) and c5.prix_exponentiel(10.0, P) == pytest.approx(194.22, abs=0.005)202        assert c5.consommation_logement(0.0, P) == pytest.approx(92.3, abs=0.05) and c5.consommation_logement(10.0, P) == pytest.approx(114.3, abs=0.05)203        P2 = dict(P, t=525.0)204        assert c5.gradient_exponentiel(P2) == pytest.approx(0.021875, abs=1e-9) and c5.demi_distance(c5.gradient_exponentiel(P2)) == pytest.approx(31.7, abs=0.05)205206    def test_ex4(self):207        assert 1.1 ** (1 / 0.3) - 1 == pytest.approx(0.374, abs=5e-4) and 0.9 ** (1 / 0.3) - 1 == pytest.approx(-0.296, abs=5e-4)208        CN = 397567.33; dep = 0.3 * CN209        VL = c5.valeur_residuelle(560000, CN, dep); VL2 = c5.valeur_residuelle(532000, CN, dep)210        assert VL == pytest.approx(281703, abs=0.5) and c5.levier_residuel(560000, VL) == pytest.approx(1.99, abs=0.005)211        assert VL2 == pytest.approx(253703, abs=0.5) and VL2 / VL - 1 == pytest.approx(-0.099, abs=5e-4)212213    def test_ex5(self):214        tq, tg, tm = c5.taux_effectif(0.7464, 1.10), c5.taux_effectif(0.5224, 1.02), 0.4631215        assert tq == pytest.approx(0.6785, abs=5e-5) and tg == pytest.approx(0.5122, abs=5e-5)216        assert 500000 * tq / 100 == pytest.approx(3393, abs=0.5) and 500000 * tg / 100 == pytest.approx(2561, abs=0.5) and 500000 * tm / 100 == pytest.approx(2316, abs=0.5)217        ecart = (tq - tg) / 100 * 500000218        assert ecart == pytest.approx(832, abs=0.5) and ecart / 0.05 == pytest.approx(16639, abs=0.5) and 0.6 * ecart / 0.05 == pytest.approx(9983, abs=0.5)219220    def test_ex6(self, data):221        v = data["v"]; y = np.log(v["prix_vente"].values); d = v["dist_centre_km"].values222        X = cas1.matrice(v); Xa = X.copy(); Xa["dist_lin"] = d223        r = h.mco(Xa.values, y); cols = list(Xa.columns)224        assert r["beta"][cols.index("dist_centre")] == pytest.approx(0.0029, abs=5e-5) and r["se_hc1"][cols.index("dist_centre")] == pytest.approx(0.0081, abs=5e-5)225        assert r["beta"][cols.index("dist_lin")] == pytest.approx(-0.0064, abs=5e-5) and r["se_hc1"][cols.index("dist_lin")] == pytest.approx(0.0076, abs=1e-4)226        assert r["beta"][cols.index("sect_Aylmer")] == pytest.approx(0.093, abs=5e-4) and r["r2"] == pytest.approx(0.853, abs=5e-4)227        Xc = X.drop(columns=["dist_centre"] + [f"sect_{s}" for s in cas1.SECTEURS[1:]]); Xc["dist"] = d228        m = (d <= 10) & v["secteur"].isin(["Hull", "Gatineau"]).values229        rr = h.mco(Xc.values[m], y[m])230        assert int(m.sum()) == 380 and rr["beta"][-1] == pytest.approx(-0.00589, abs=5e-6) and rr["se_hc1"][-1] == pytest.approx(0.00144, abs=5e-6) and rr["r2"] == pytest.approx(0.80, abs=5e-3)231        m2 = (v["secteur"] == "Aylmer").values; ra = h.mco(Xc.values[m2], y[m2])232        assert int(m2.sum()) == 125 and ra["beta"][-1] == pytest.approx(-0.00675, abs=5e-6) and ra["se_hc1"][-1] == pytest.approx(0.00458, abs=5e-6)233        m3 = (v["secteur"] == "Hull").values; rh = h.mco(Xc.values[m3], y[m3])234        assert int(m3.sum()) == 186 and rh["beta"][-1] == pytest.approx(-0.00746, abs=5e-6) and rh["se_hc1"][-1] == pytest.approx(0.00646, abs=5e-6)235236    def test_ex7(self):237        E = [(0, 0, 100000), (12, 4, 30000)]; pts = {"A": (3, 0), "B": (9, 3), "C": (12, -6)}238        dist = {k: [math.hypot(x - cx, y - cy) for cx, cy, _ in E] for k, (x, y) in pts.items()}239        assert dist["A"] == pytest.approx([3.00, 9.85], abs=0.005) and dist["B"] == pytest.approx([9.49, 3.16], abs=0.005) and dist["C"] == pytest.approx([13.42, 10.00], abs=0.005)240        for lam, att, ab, bc in [(0.10, (85286, 60592, 37178), 0.089, 0.130), (0.20, (59066, 30935, 10894), 0.175, 0.298)]:241            A = {k: sum(e * math.exp(-lam * dd) for (_, _, e), dd in zip(E, dist[k])) for k in pts}242            assert (A["A"], A["B"], A["C"]) == pytest.approx(att, abs=0.5)243            assert (A["A"] / A["B"]) ** 0.25 - 1 == pytest.approx(ab, abs=5e-4) and (A["B"] / A["C"]) ** 0.25 - 1 == pytest.approx(bc, abs=5e-4)244        # un seul centre : theta ln A = theta ln E - theta lambda d245        lam, theta = 0.1, 0.25246        for dd in (2.0, 7.0):247            assert theta * math.log(100000 * math.exp(-lam * dd)) == pytest.approx(theta * math.log(100000) - theta * lam * dd, rel=1e-12)248
Files · 52713f1726a682cc02b48561b6b82a2be2ec84ba
Menu

Theme