spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 5 — Économie urbaine et rente foncière : calculs de l'exemple intégral et des encadrés.34Imprime tous les chiffres cités dans le chapitre 5 :5 * modèle monocentrique (Alonso–Muth–Mills) simulé avec des PARAMÈTRES PÉDAGOGIQUES DÉCLARÉS (section PARAMS) :6 gradient du prix du logement p(d) = p0 (1 - t d / y)^(1/beta), gradient exponentiel approché, rente foncière7 r(d) = r0 (p(d)/p0)^(1/alpha), densité, frontière urbaine, courbes d'enchère de von Thünen et d'Alonso ;8 * gradient prix/distance observé dans le dossier du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES, graine 20261031) :9 tranches de distance, régressions avec et sans indicatrices de secteur (traite.hedonique.mco), coefficient10 `dist_centre` du modèle hédonique de référence (traite.cas1.hedonique) ;11 * décomposition interrégionale de la valeur par logement du cas 4 (traite.cas4.sommaire) ;12 * valeur résiduelle du terrain et effet de levier ;13 * capitalisation de la taxe foncière : formule exacte et approchée, taux effectif, taux 2026 de Québec,14 Gatineau et Montréal (sources : notes-B/E du dépôt), grille de sensibilité.1516Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch05_rente.py17Les régressions utilisent traite.hedonique.mco ; les données des cas passent par traite.cas1 et traite.cas4."""18from __future__ import annotations19import sys, pathlib, math20import numpy as np, pandas as pd21sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))22from traite import cas1, cas423from traite import hedonique as h2425ROOT = pathlib.Path(__file__).resolve().parents[3]2627# ----------------------------------------------------------------------------- paramètres pédagogiques (déclarés)28PARAMS = {29 "y": 80000.0, # revenu annuel du ménage ($/an)30 "t": 700.0, # coût annuel de navettage par kilomètre de distance au centre ($/km/an), temps compris31 "beta": 0.30, # part du revenu net de transport consacrée au logement32 "alpha": 0.30, # part du terrain dans le coût de production du logement (Cobb-Douglas)33 "p0": 260.0, # prix annuel du service de logement au centre ($/m² de plancher/an)34 "r0": 80.0, # rente foncière annuelle au centre ($/m² de terrain/an)35 "ra": 2.0, # rente alternative (agricole, conversion comprise) à la frontière ($/m²/an)36 "i": 0.05, # taux de capitalisation des rentes foncières (annuel)37}38# courbes d'enchère linéaires de trois usages (illustration d'Alonso) : (rente au centre $/m²/an, pente $/m²/an/km)39ALONSO = {"Commerces et bureaux": (300.0, 25.0), "Résidentiel dense": (120.0, 6.0), "Résidentiel pavillonnaire": (60.0, 1.8)}40# von Thünen (exercice 5.1) : rendement Y (t/ha), prix p ($/t), coût c ($/t), fret f ($/t/km)41THUNEN = {"Maraîchage": dict(Y=20.0, p=300.0, c=250.0, f=3.0), "Grandes cultures": dict(Y=4.0, p=500.0, c=350.0, f=5.0)}4243# taux de taxe foncière 2026 (par 100 $ d'évaluation) — sources : vqtaux2026 ; gatineaureglement994 ; ledevoirmtl2026 (secondaire) ; taxescolaire202644TAUX_2026 = {45 "Québec (1 à 5 logements)": 0.7464, "Québec (6 logements et plus)": 0.7697,46 "Gatineau (taux de base)": 0.5224, "Montréal (taxe foncière générale, hors arrondissement)": 0.4631,47 "Taxe scolaire 2026-2027 (toute la province)": 0.07899,48}49FACTEUR_2026 = {"Gatineau": 1.02, "Montréal": 1.00} # facteurs comparatifs 2026 publiés (gatmutations ; mtlmutations2026)505152# ----------------------------------------------------------------------------- von Thünen et Alonso53def rente_thunen(d, Y: float, p: float, c: float, f: float) -> np.ndarray:54 """R(d) = Y (p - c) - Y f d (rente par hectare et par an)."""55 return Y * (p - c) - Y * f * np.asarray(d, float)5657def distance_limite_thunen(Y: float, p: float, c: float, f: float) -> float:58 """Distance où la rente s'annule : d* = (p - c) / f."""59 return (p - c) / f6061def frontiere_usages(R1: float, t1: float, R2: float, t2: float) -> float:62 """Distance où deux courbes d'enchère linéaires R1 - t1 d et R2 - t2 d se croisent."""63 return (R1 - R2) / (t1 - t2)6465def enveloppe_alonso(d, usages: dict = ALONSO, ra: float = PARAMS["ra"]) -> tuple[np.ndarray, list]:66 d = np.asarray(d, float)67 courbes = {k: R0 - t * d for k, (R0, t) in usages.items()}68 env = np.maximum.reduce([*courbes.values(), np.full_like(d, ra)])69 return env, courbes707172# ----------------------------------------------------------------------------- modèle monocentrique73def prix_logement(d, P: dict = PARAMS) -> np.ndarray:74 """p(d) = p0 (1 - t d / y)^(1/beta) : solution exacte de la condition de Muth avec part budgétaire constante."""75 d = np.asarray(d, float)76 base = np.clip(1.0 - P["t"] * d / P["y"], 1e-12, None)77 return P["p0"] * base ** (1.0 / P["beta"])7879def gradient_exponentiel(P: dict = PARAMS) -> float:80 """Pente logarithmique du prix au centre : gamma = t / (beta y) (approximation exponentielle)."""81 return P["t"] / (P["beta"] * P["y"])8283def prix_exponentiel(d, P: dict = PARAMS) -> np.ndarray:84 return P["p0"] * np.exp(-gradient_exponentiel(P) * np.asarray(d, float))8586def consommation_logement(d, P: dict = PARAMS) -> np.ndarray:87 """q(d) = beta (y - t d) / p(d) : surface de plancher consommée par ménage (m²)."""88 d = np.asarray(d, float)89 return P["beta"] * (P["y"] - P["t"] * d) / prix_logement(d, P)9091def rente_fonciere(d, P: dict = PARAMS) -> np.ndarray:92 """r(d) = r0 (p(d)/p0)^(1/alpha) : profit nul des producteurs de logement (Cobb-Douglas)."""93 return P["r0"] * (prix_logement(d, P) / P["p0"]) ** (1.0 / P["alpha"])9495def intensite_capital(d, P: dict = PARAMS) -> np.ndarray:96 """Plancher par m² de terrain, normalisé à 1 au centre : h(d)/h(0) = (p(d)/p0)^((1-alpha)/alpha)."""97 return (prix_logement(d, P) / P["p0"]) ** ((1.0 - P["alpha"]) / P["alpha"])9899def densite(d, P: dict = PARAMS) -> np.ndarray:100 """Ménages par m² de terrain, normalisée à 1 au centre : D(d) = h(d)/q(d)."""101 d = np.asarray(d, float)102 D = intensite_capital(d, P) / consommation_logement(d, P)103 return D / (intensite_capital(0.0, P) / consommation_logement(0.0, P))104105def frontiere_urbaine(P: dict = PARAMS) -> float:106 """Distance d_bar où r(d_bar) = ra (solution analytique)."""107 ratio_p = (P["ra"] / P["r0"]) ** P["alpha"] # p(d_bar)/p0108 return (1.0 - ratio_p ** P["beta"]) * P["y"] / P["t"]109110def demi_distance(gamma: float) -> float:111 """Distance qui divise le prix par deux sous un gradient exponentiel : ln 2 / gamma."""112 return math.log(2.0) / gamma113114def valeur_terrain(rente: float, i: float = PARAMS["i"], g: float = 0.0) -> float:115 """Valeur du terrain = rente / (i - g) (rente perpétuelle, croissance g)."""116 return rente / (i - g)117118def decomposition_capozza(ra: float, i: float, cout_conversion: float, rente_d: float, prime_croissance: float) -> dict:119 """Capozza–Helsley (1989) : P_L = ra/i + C + (r(d) - ra)/i + prime de croissance."""120 comp = {"valeur_agricole": ra / i, "cout_conversion": cout_conversion, "accessibilite": (rente_d - ra) / i, "prime_croissance": prime_croissance}121 comp["total"] = sum(comp.values()); return comp122123124# ----------------------------------------------------------------------------- rente résiduelle et levier125def valeur_residuelle(V: float, CN: float, depreciation: float) -> float:126 """V_L = V - (C_N - D) : valeur du terrain par résidu."""127 return V - (CN - depreciation)128129def levier_residuel(V: float, VL: float) -> float:130 """Élasticité de V_L par rapport à V : V / V_L."""131 return V / VL132133def elasticite_rente_prix(alpha: float) -> float:134 return 1.0 / alpha135136137# ----------------------------------------------------------------------------- capitalisation fiscale138def valeur_avec_taxe(R: float, i: float, tau: float) -> float:139 """V = R / (i + tau) : rente nette de taxe capitalisée (taxe proportionnelle à la valeur)."""140 return R / (i + tau)141142def capitalisation_taxe(V0: float, tau0: float, tau1: float, i: float, kappa: float = 1.0) -> dict:143 """Effet d'un changement de taux de taxe tau0 -> tau1 sur une valeur V0 établie sous tau0.144 exact : V1 = R/(i+tau1) avec R = V0 (i+tau0) ; approché : dV = -kappa * dT / i avec dT = (tau1-tau0) V0."""145 R = V0 * (i + tau0)146 V1 = R / (i + tau1)147 dT = (tau1 - tau0) * V0148 return {"R": R, "V1_exact": V1, "dV_exact": V1 - V0, "dT": dT, "dV_approx": -kappa * dT / i,149 "dV_exact_kappa": kappa * (V1 - V0), "dV_pct_exact": V1 / V0 - 1}150151def taux_effectif(tau_role: float, facteur: float) -> float:152 """Taux sur la valeur marchande : tau_eff = tau_role / F, où F est le facteur comparatif (V_marché = F × V_rôle)."""153 return tau_role / facteur154155def grille_capitalisation(V0: float, d_taux_pb: list, taux_cap: list, kappa: float = 1.0) -> pd.DataFrame:156 """Perte de valeur (en $) pour un écart de taux de taxe (points de base de la valeur) selon le taux de capitalisation."""157 rows = []158 for pb in d_taux_pb:159 row = {"ecart_pb": pb}160 for r in taux_cap:161 row[f"r={r:.0%}"] = -kappa * (pb / 10000.0) * V0 / r162 rows.append(row)163 return pd.DataFrame(rows).set_index("ecart_pb")164165166# ----------------------------------------------------------------------------- cas 1 : gradient observé167TRANCHES = [0, 2, 4, 6, 8, 10, 12, 20, 35]168ETIQUETTES = ["0-2", "2-4", "4-6", "6-8", "8-10", "10-12", "12-20", "20 et plus"]169170def tranches_distance(v: pd.DataFrame) -> pd.DataFrame:171 v = v.copy(); v["prix_m2"] = v["prix_vente"] / v["superficie_habitable_m2"]172 v["tranche"] = pd.cut(v["dist_centre_km"], TRANCHES, labels=ETIQUETTES, right=True)173 g = v.groupby("tranche", observed=True).agg(n=("prix_vente", "size"), dist_mediane=("dist_centre_km", "median"),174 prix_median=("prix_vente", "median"), prix_m2_median=("prix_m2", "median"),175 superficie_mediane=("superficie_habitable_m2", "median"),176 terrain_median=("superficie_terrain_m2", "median"))177 g["secteur_dominant"] = v.groupby("tranche", observed=True)["secteur"].agg(lambda s: s.value_counts().index[0])178 return g179180def gradients_cas1(v: pd.DataFrame, modele: dict | None = None) -> dict:181 """Quatre spécifications du gradient prix/distance (coefficients en log par km)."""182 y = np.log(v["prix_vente"].astype(float).values)183 d = v["dist_centre_km"].astype(float).values184 out = {}185 # (a) brut : ln(P/S) = a - gamma d186 r = h.mco(np.column_stack([np.ones_like(d), d]), np.log(v["prix_vente"] / v["superficie_habitable_m2"]).values)187 out["brut_prix_m2"] = {"gamma": -r["beta"][1], "se": r["se_hc1"][1], "r2": r["r2"], "n": r["n"]}188 # (b) contrôlé : caractéristiques du modèle de référence sans secteurs, distance linéaire189 X = cas1.matrice(v).drop(columns=["dist_centre"] + [f"sect_{s}" for s in cas1.SECTEURS[1:]])190 X["dist"] = d191 r = h.mco(X.values, y)192 out["controle_lineaire"] = {"gamma": -r["beta"][-1], "se": r["se_hc1"][-1], "r2": r["r2"], "n": r["n"]}193 # (c) contrôlé, quadratique194 Xq = X.copy(); Xq["dist2"] = d ** 2 / 100.0195 r = h.mco(Xq.values, y)196 out["controle_quadratique"] = {"b1": r["beta"][-2], "b2_100": r["beta"][-1], "se1": r["se_hc1"][-2], "se2": r["se_hc1"][-1], "r2": r["r2"],197 "gamma_0": -r["beta"][-2], "gamma_10": -(r["beta"][-2] + 2 * r["beta"][-1] * 10 / 100.0)}198 # (d) contrôlé, aire urbaine contiguë (d <= 16 km : Hull, Gatineau, Aylmer)199 m = d <= 16.0200 r = h.mco(X.values[m], y[m])201 out["controle_urbain_16km"] = {"gamma": -r["beta"][-1], "se": r["se_hc1"][-1], "r2": r["r2"], "n": int(m.sum())}202 # (e) modèle de référence (secteurs + max(d-3, 0))203 modele = modele or cas1.hedonique(v)204 c = modele["coef"]205 out["reference"] = {"gamma_au_dela_3km": -float(c.loc["dist_centre", "coef"]), "se": float(c.loc["dist_centre", "se_hc1"]),206 "t": float(c.loc["dist_centre", "t"]), "r2": modele["r2"],207 "secteurs": {s: float(c.loc[f"sect_{s}", "coef"]) for s in cas1.SECTEURS[1:]},208 "secteurs_pct": {s: float(c.loc[f"sect_{s}", "effet_pct"]) for s in cas1.SECTEURS[1:]}}209 # (f) gradient implicite entre secteurs : médianes par secteur vs distance médiane210 gs = v.groupby("secteur").agg(dist=("dist_centre_km", "median"), prix=("prix_vente", "median"), n=("prix_vente", "size"))211 out["secteurs_medianes"] = gs212 return out213214def gradient_exact_vs_exponentiel(P: dict = PARAMS) -> pd.DataFrame:215 d = np.array([0.0, 5.0, 10.0, 15.0, 20.0, 25.0])216 pe, pa = prix_logement(d, P), prix_exponentiel(d, P)217 return pd.DataFrame({"d_km": d, "p_exact": pe, "p_exponentiel": pa, "ecart_pct": pa / pe - 1, "q_m2": consommation_logement(d, P),218 "rente_fonciere": rente_fonciere(d, P), "densite_relative": densite(d, P)})219220221# ----------------------------------------------------------------------------- cas 4 : décomposition interrégionale222def decomposition_rmr(pf: pd.DataFrame, reference: str = "Montréal") -> pd.DataFrame:223 """ln(V/n) = ln(RNE/n) - ln(R) par RMR, et écarts à la RMR de référence."""224 s = cas4.sommaire(pf)["par_rmr"].copy()225 s["rne_par_logement"] = s["rne"] / s["logements"]226 s["ln_V_n"] = np.log(s["valeur_par_logement"]); s["ln_RNE_n"] = np.log(s["rne_par_logement"]); s["ln_R"] = np.log(s["taux_pondere"])227 ref = s.loc[reference]228 s["ecart_ln_V_n"] = s["ln_V_n"] - ref["ln_V_n"]229 s["contrib_rne"] = s["ln_RNE_n"] - ref["ln_RNE_n"]230 s["contrib_taux"] = -(s["ln_R"] - ref["ln_R"])231 s["ratio_valeur"] = s["valeur_par_logement"] / ref["valeur_par_logement"]232 return s.sort_values("valeur_par_logement", ascending=False)233234235# ----------------------------------------------------------------------------- exemple intégral (impression)236def fmt(x, dec=0):237 s = f"{x:,.{dec}f}".replace(",", " ").replace(".", ",")238 return s239240def main():241 P = PARAMS242 print("=== 5.A Modèle monocentrique simulé (paramètres pédagogiques déclarés) ===")243 print({k: v for k, v in P.items()})244 g = gradient_exponentiel(P); dbar = frontiere_urbaine(P)245 print(f"gradient exponentiel au centre gamma = t/(beta y) = {g:.5f} par km ({g*100:.2f} %/km) ; demi-distance ln2/gamma = {demi_distance(g):.1f} km")246 print(f"frontière urbaine d_bar (r = ra) = {dbar:.2f} km ; p(d_bar) = {prix_logement(dbar, P):.2f} $/m²/an ; élasticité rente/prix 1/alpha = {elasticite_rente_prix(P['alpha']):.3f}")247 print(f"valeur du terrain au centre r0/i = {fmt(valeur_terrain(P['r0'], P['i']))} $/m² ; à la frontière ra/i = {valeur_terrain(P['ra'], P['i']):.0f} $/m²")248 tab = gradient_exact_vs_exponentiel(P)249 print(tab.round(3).to_string(index=False))250 print(f"q(0) = {consommation_logement(0.0, P):.1f} m² ; q(10) = {consommation_logement(10.0, P):.1f} m² ; q(d_bar) = {consommation_logement(dbar, P):.1f} m²")251 print("\n--- Alonso : frontières entre usages (km) ---")252 (R1, t1), (R2, t2), (R3, t3) = ALONSO.values()253 f12, f23 = frontiere_usages(R1, t1, R2, t2), frontiere_usages(R2, t2, R3, t3)254 f3a = (R3 - P["ra"]) / t3255 print(f"commerces/dense : {f12:.2f} km ; dense/pavillonnaire : {f23:.2f} km ; pavillonnaire/agricole : {f3a:.2f} km")256 print("\n--- von Thünen (exercice 5.1) ---")257 for k, q in THUNEN.items():258 print(f"{k}: R0 = {q['Y']*(q['p']-q['c']):.0f} $/ha/an ; pente = {q['Y']*q['f']:.0f} $/ha/an/km ; distance limite = {distance_limite_thunen(**q):.2f} km")259 a, b = THUNEN.values()260 print(f"frontière maraîchage/grandes cultures : {frontiere_usages(a['Y']*(a['p']-a['c']), a['Y']*a['f'], b['Y']*(b['p']-b['c']), b['Y']*b['f']):.2f} km")261262 print("\n=== 5.B Cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES) : gradient prix/distance ===")263 d1 = cas1.charger(); v = d1["ventes"]; m = cas1.hedonique(v)264 tr = tranches_distance(v)265 print(tr.round(2).to_string())266 gr = gradients_cas1(v, m)267 for k in ["brut_prix_m2", "controle_lineaire", "controle_urbain_16km"]:268 r = gr[k]; print(f"{k}: gamma = {r['gamma']*100:.3f} %/km (e.-t. {r['se']*100:.3f}) ; R² = {r['r2']:.3f} ; n = {r['n']} ; demi-distance = {demi_distance(r['gamma']):.1f} km")269 q = gr["controle_quadratique"]270 print(f"controle_quadratique: b1 = {q['b1']*100:.3f} %/km (e.-t. {q['se1']*100:.3f}), b2/100 = {q['b2_100']*100:.4f} (e.-t. {q['se2']*100:.4f}) ; gamma(0) = {q['gamma_0']*100:.3f}, gamma(10 km) = {q['gamma_10']*100:.3f} %/km ; R² = {q['r2']:.3f}")271 rf = gr["reference"]272 print(f"reference (traite.cas1.hedonique) : dist_centre = -{rf['gamma_au_dela_3km']*100:.3f} %/km au-delà de 3 km (e.-t. {rf['se']*100:.3f}, t = {rf['t']:.2f}) ; R² = {rf['r2']:.3f}")273 print(" secteurs (réf. Hull), coef. et effet % :", {k: (round(v_, 4), round(rf['secteurs_pct'][k] * 100, 1)) for k, v_ in rf["secteurs"].items()})274 print(gr["secteurs_medianes"].round(2).to_string())275 gs = gr["secteurs_medianes"]276 pente = (math.log(gs.loc["Gatineau", "prix"]) - math.log(gs.loc["Hull", "prix"])) / (gs.loc["Gatineau", "dist"] - gs.loc["Hull", "dist"])277 print(f"gradient implicite Hull -> Gatineau (médianes) : {pente*100:.3f} %/km ; Hull -> Buckingham : {(math.log(gs.loc['Buckingham','prix'])-math.log(gs.loc['Hull','prix']))/(gs.loc['Buckingham','dist']-gs.loc['Hull','dist'])*100:.3f} %/km")278 print(f"sujet du cas 1 : dist_centre = {d1['sujet']['dist_centre_km']} km ; effet distance du modèle de référence = {max(d1['sujet']['dist_centre_km']-3,0)*rf['gamma_au_dela_3km']*100:.2f} %")279 # ce que vaut 1 km de plus pour le sujet selon la spécification contrôlée280 V_ind = 551029.0 # indication brute du ch. 16281 print(f"à partir de l'indication brute du ch. 16 ({fmt(V_ind)} $), 1 km de plus vaut {fmt(-V_ind*gr['controle_lineaire']['gamma'])} $ (spécification contrôlée) ; 5 km : {fmt(V_ind*(math.exp(-5*gr['controle_lineaire']['gamma'])-1))} $")282283 print("\n=== 5.C Cas 4 (portefeuille, DONNÉES SYNTHÉTIQUES) : valeur par logement par RMR ===")284 d4 = cas4.charger(); dec = decomposition_rmr(d4["portefeuille"])285 cols = ["n", "logements", "valeur_par_logement", "rne_par_logement", "taux_pondere", "ratio_valeur", "ecart_ln_V_n", "contrib_rne", "contrib_taux"]286 print(dec[cols].round(4).to_string())287 for rmr in dec.index:288 if rmr != "Montréal":289 e = dec.loc[rmr]; part = e["contrib_rne"] / e["ecart_ln_V_n"] if e["ecart_ln_V_n"] != 0 else float("nan")290 print(f" {rmr}: écart ln = {e['ecart_ln_V_n']:.3f} dont RNE/log. {e['contrib_rne']:.3f} ({part*100:.0f} %) et taux {e['contrib_taux']:.3f} ({(1-part)*100:.0f} %)")291292 print("\n=== 5.D Rente résiduelle et levier ===")293 CN = 397567.33; dep = 0.30 * CN; V = 551029.0294 VL = valeur_residuelle(V, CN, dep)295 print(f"C_N (ch. 21) = {fmt(CN,2)} $ ; dépréciation hypothétique 30 % = {fmt(dep,2)} $ ; bâtiment déprécié = {fmt(CN-dep,2)} $")296 print(f"V (indication brute ch. 16) = {fmt(V)} $ -> V_L = {fmt(VL)} $ ({VL/V*100:.1f} % de V) ; levier V/V_L = {levier_residuel(V, VL):.2f}")297 for dv in (-0.05, 0.05, -0.10):298 VL2 = valeur_residuelle(V * (1 + dv), CN, dep)299 print(f" V {dv:+.0%} -> V_L = {fmt(VL2)} $ ({VL2/VL-1:+.1%})")300 print(f"élasticité de la rente foncière au prix du logement (alpha = {P['alpha']}) : {elasticite_rente_prix(P['alpha']):.2f} ; +10 % de prix -> rente x {1.1**elasticite_rente_prix(P['alpha']):.3f}")301 cz = decomposition_capozza(P["ra"], P["i"], 25.0, rente_fonciere(10.0, P), 60.0)302 print("Capozza-Helsley à 10 km ($/m²) :", {k: round(v_, 1) for k, v_ in cz.items()})303304 print("\n=== 5.E Capitalisation de la taxe foncière ===")305 for k, t_ in TAUX_2026.items():306 print(f" {k}: {t_:.5f} $/100 $ = {t_/100*10000:.1f} pb de la valeur")307 V0 = 500000.0; i = 0.05308 for pb in (10, 22.4, 50):309 c = capitalisation_taxe(V0, 0.0070, 0.0070 + pb / 10000, i)310 print(f" écart de {pb} pb sur {fmt(V0)} $ : dT = {fmt(c['dT'])} $/an ; dV approché = {fmt(c['dV_approx'])} $ ; dV exact = {fmt(c['dV_exact'])} $ ({c['dV_pct_exact']*100:.2f} %) ; à 60 % : {fmt(0.6*c['dV_approx'])} $")311 ecart_qc_gat = (TAUX_2026["Québec (1 à 5 logements)"] - TAUX_2026["Gatineau (taux de base)"]) / 100312 print(f"écart nominal Québec (1-5 log.) - Gatineau (base) = {ecart_qc_gat*10000:.1f} pb ; sur {fmt(V0)} $ : {fmt(ecart_qc_gat*V0)} $/an ; capitalisé à 5 % : {fmt(ecart_qc_gat*V0/i)} $ ; à 60 % : {fmt(0.6*ecart_qc_gat*V0/i)} $")313 for ville, F in FACTEUR_2026.items():314 key = "Gatineau (taux de base)" if ville == "Gatineau" else "Montréal (taxe foncière générale, hors arrondissement)"315 print(f" taux effectif {ville} : {TAUX_2026[key]:.4f}/{F} = {taux_effectif(TAUX_2026[key], F):.4f} $/100 $ de valeur marchande")316 print(f" Québec avec un facteur hypothétique de 1,10 : {taux_effectif(TAUX_2026['Québec (1 à 5 logements)'], 1.10):.4f} $/100 $")317 sujet_role = d1["sujet"]["valeur_role_2025"]318 T_gat = sujet_role * TAUX_2026["Gatineau (taux de base)"] / 100; T_sco = sujet_role * TAUX_2026["Taxe scolaire 2026-2027 (toute la province)"] / 100319 print(f"sujet du cas 1 (rôle fictif {fmt(sujet_role)} $) : taxe générale au taux de base Gatineau = {fmt(T_gat)} $ ; scolaire (sans l'exemption de 25 000 $) = {fmt(T_sco)} $")320 print(f" écart 6+ vs 1-5 logements à Québec : {(TAUX_2026['Québec (6 logements et plus)']-TAUX_2026['Québec (1 à 5 logements)'])*100:.1f} pb ; sur un immeuble de 2 000 000 $ : {fmt((TAUX_2026['Québec (6 logements et plus)']-TAUX_2026['Québec (1 à 5 logements)'])/100*2e6)} $/an ; capitalisé à 5,5 % : {fmt((TAUX_2026['Québec (6 logements et plus)']-TAUX_2026['Québec (1 à 5 logements)'])/100*2e6/0.055)} $")321 G = grille_capitalisation(V0, [5, 10, 25, 50, 100], [0.04, 0.05, 0.06, 0.07])322 print("grille (kappa = 1, V0 = 500 000 $) :\n", G.round(0).to_string())323 # coût d'usage : part de la taxe324 tau_eff = taux_effectif(TAUX_2026["Gatineau (taux de base)"], 1.02) / 100 + TAUX_2026["Taxe scolaire 2026-2027 (toute la province)"] / 100325 print(f"taux effectif total Gatineau (base + scolaire) ≈ {tau_eff*100:.4f} $/100 $ ; part de la taxe dans un coût d'usage de (i=4,12 %, tau, delta=1,5 %, g=2 %) : {tau_eff/(0.0412+tau_eff+0.015-0.02)*100:.1f} %")326327if __name__ == "__main__":328 main()329