spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 9 — Évaluation foncière municipale et contestation (DONNÉES SYNTHÉTIQUES du cas 1, graine 20261031).34Étude de ratios (proportion médiane, facteur comparatif, COD, PRD, PRB, écart type relatif de l'annexe III du5Règlement sur le rôle), dossier de contestation du sujet du cas 1 (valeur au rôle fictive 468 000 $) et d'un acheteur6récent (vente G0258), compte de taxes 2026 de Gatineau (règlement 994-2025), droits de mutation 2026, échéancier des7recours (LFM art. 70, 81, 130, 138.3, 138.4, 138.5), sensibilité et exercices. Imprime tous les chiffres cités.89Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch09_ratios.py10Bibliothèque : traite.cas1 (modèle hédonique partagé), traite.hedonique.statistiques_ratio, traite.comparables."""11from __future__ import annotations12import sys, pathlib, datetime as dt13import numpy as np, pandas as pd14sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))15from traite import cas1, hedonique as h, comparables as cp1617ROOT = pathlib.Path(__file__).resolve().parents[3]18REF = pd.Timestamp("2024-07-01") # date de référence au marché du rôle fictif (LFM art. 46 al. 1)19ROLE_FICTIF = (2026, 2027, 2028) # exercices couverts par un rôle dont la référence est le 1er juillet 202420GRAINE = 202610312122# ----------------------------------------------------------------------------- paramètres fiscaux vérifiés (Gatineau 2026)23# Règlement 994-2025 (adopté le 18 décembre 2025), par 1 000 $ d'évaluation ; voir data/raw/gatineau_r994_2025/README.md24TAUX_GATINEAU_2026 = {25 "base": 5.224, # art. 1 (catégorie résiduelle)26 "non_residentiel": 29.372, # art. 227 "industriel": 26.120, # art. 328 "terrains_vagues": 20.896, # art. 429 "agricole": 5.090, # art. 530 "six_logements_plus": 6.964, # art. 831 "sto": 1.417, # art. 11 taxe spéciale transport en commun32 "dette": 0.830, # art. 12 taxe spéciale service de la dette33 "ex_hull": 0.036, # art. 15, 1° (secteur de l'ex-Ville de Hull)34 "credit_aqueduc": 0.386, "credit_egout": 0.523, # art. 13-14 (immeubles non raccordés)35}36COMPENSATION_MATIERES_RESIDUELLES = 178.83 # $ par logement, art. 1637TAUX_SCOLAIRE_2026_2027 = 0.07899 / 100 # $ par $ d'évaluation uniformisée ajustée (LIP art. 303.4 ; avis 2026)38EXEMPTION_SCOLAIRE = 25_000 # LIP art. 303.1 : tranche exemptée39FRAIS_REVISION_GATINEAU = [(500_000, 80.00), (2_000_000, 325.00), (5_000_000, 574.50), (float("inf"), 1_149.25)] # règl. 943-202340FRAIS_REVISION_MONTREAL = [(500_000, 90.00), (2_000_000, 360.00), (5_000_000, 605.00), (float("inf"), 1_215.00)]41FRAIS_TAQ_2026 = [(500_000, 93.20), (2_000_000, 372.60), (5_000_000, 621.10), (float("inf"), 1_242.50)] # Tarif J-3, r. 3.242# Droits sur les mutations immobilières 2026 (LDMI art. 2 ; barèmes municipaux)43BAREME_MUTATION_QC = [(62_900, 0.005), (315_000, 0.01), (float("inf"), 0.015)]44BAREME_MUTATION_MTL = [(62_900, 0.005), (315_000, 0.01), (552_300, 0.015), (1_104_700, 0.02), (2_136_500, 0.025), (3_113_000, 0.035), (float("inf"), 0.04)]45BAREME_MUTATION_GAT = [(62_900, 0.005), (315_000, 0.01), (500_000, 0.015), (float("inf"), 0.03)] # à compter du 25 février 202646FACTEUR_GATINEAU_2026 = 1.02 # facteur comparatif publié par la Ville (page « Base d'imposition »)47FACTEUR_MONTREAL_2026 = 1.0048# Seuils IAAO (Standard on Ratio Studies, 2013, table 1-3 et § 9.2.7)49IAAO = {"COD_unifamilial_heterogene": (5.0, 15.0), "COD_unifamilial_homogene": (5.0, 10.0), "PRD": (0.98, 1.03), "PRB": (-0.05, 0.05), "niveau": (0.90, 1.10)}50# Règlement sur le rôle (F-2.1, r. 13), art. 13 al. 2 et art. 14 al. 151R13_PROPORTION = (0.95, 1.05)52R13_ECART_TYPE_RELATIF = [(0.20, 0.36), (0.40, 0.31), (0.60, 0.25), (0.80, 0.19), (1.01, 0.14)] # (indice de concentration unifamiliale < borne, plafond)535455# ----------------------------------------------------------------------------- outils56def charger() -> dict:57 d = cas1.charger()58 d["modele"] = cas1.hedonique(d["ventes"])59 return d6061def mois_depuis_ref(dates) -> np.ndarray:62 return ((pd.to_datetime(dates) - REF).dt.days / 30.4375).to_numpy()6364def ratios(v: pd.DataFrame, modele: dict) -> pd.DataFrame:65 """Ratios bruts (rôle / prix) et ratios après rajustement des prix aux conditions du marché de la date de référence66 (r. 10, art. 11-12) : P_ref = P / (1+g)^mois, g = croissance mensuelle du modèle hédonique partagé."""67 g = modele["croissance_mensuelle"]68 out = v.copy()69 out["mois_ref"] = mois_depuis_ref(out["date_vente"])70 out["prix_ref"] = out["prix_vente"] / (1.0 + g) ** out["mois_ref"]71 out["r_brut"] = out["valeur_role_2025"] / out["prix_vente"]72 out["r_aj"] = out["valeur_role_2025"] / out["prix_ref"]73 return out7475def epuration_r10(r: np.ndarray) -> dict:76 """Épuration du r. 10 (art. 15-16) : retrait des ratios hors [médiane − 2σ ; médiane + 2σ], σ sur n − 1."""77 r = np.asarray(r, float); med = np.median(r); sd = r.std(ddof=1)78 lo, hi = med - 2 * sd, med + 2 * sd79 garde = (r >= lo) & (r <= hi)80 return {"mediane_initiale": med, "sigma": sd, "borne_basse": lo, "borne_haute": hi, "n": len(r), "n_retires": int((~garde).sum()), "garde": garde,81 "proportion_mediane": float(np.median(r[garde]))}8283def ecart_type_relatif(r: np.ndarray) -> float:84 """Annexe III du r. 13 : écart type des ratios (n − 1) divisé par le ratio médian."""85 r = np.asarray(r, float)86 return float(r.std(ddof=1) / np.median(r))8788def plafond_r13(indice_concentration: float) -> float:89 for borne, plafond in R13_ECART_TYPE_RELATIF:90 if indice_concentration < borne:91 return plafond92 return 0.149394def liste_de_base(N: int) -> int:95 """r. 10, art. 5 : nombre de ventes de la liste de base = max(30 ; N / (15,5 + 0,001 N))."""96 return int(np.ceil(max(30.0, N / (15.5 + 0.001 * N))))9798def prb_proxy_prix(valeurs, prix) -> dict:99 """PRB calculé avec le seul prix (log2 P) comme variable explicative — variante de contrôle (hors norme IAAO)."""100 V = np.asarray(valeurs, float); P = np.asarray(prix, float); r = V / P; med = np.median(r)101 x = np.log2(P); yv = (r - med) / med102 X = np.column_stack([np.ones_like(x), x]); b, *_ = np.linalg.lstsq(X, yv, rcond=None)103 res = yv - X @ b; se = np.sqrt((res @ res) / (len(x) - 2) / ((x - x.mean()) ** 2).sum())104 return {"PRB": float(b[1]), "PRB_se": float(se)}105106def frais(valeur_role: float, bareme) -> float:107 for borne, montant in bareme:108 if valeur_role <= borne:109 return montant110 return bareme[-1][1]111112def droits_mutation(base: float, bareme=BAREME_MUTATION_QC) -> float:113 """Droit sur les mutations immobilières par tranches (LDMI art. 2) ; base = max(prix, contrepartie, rôle × facteur)."""114 total, bas = 0.0, 0.0115 for haut, taux in bareme:116 if base > bas:117 total += (min(base, haut) - bas) * taux118 bas = haut119 return total120121def compte_taxes_gatineau(valeur_role: float, facteur: float, taux_voisinage: float, secteur_hull: bool = True, raccorde: bool = True, logements: int = 1) -> dict:122 """Compte de taxes d'une résidence (catégorie résiduelle, moins de six logements) selon le règlement 994-2025 et la LIP.123 Taxes ad valorem par 1 000 $ : taux de l'unité de voisinage (art. 7) + STO (art. 11) + dette (art. 12) [+ ex-Hull 0,036] ;124 crédits aqueduc/égout si non raccordé ; taxe scolaire sur (rôle × facteur − 25 000 $) ; compensation matières résiduelles."""125 t = TAUX_GATINEAU_2026126 taux_municipal = taux_voisinage + t["sto"] + t["dette"] + (t["ex_hull"] if secteur_hull else 0.0)127 if not raccorde:128 taux_municipal -= t["credit_aqueduc"] + t["credit_egout"]129 municipal = valeur_role * taux_municipal / 1000.0130 uniformisee = valeur_role * facteur131 scolaire = max(uniformisee - EXEMPTION_SCOLAIRE, 0.0) * TAUX_SCOLAIRE_2026_2027132 compensations = COMPENSATION_MATIERES_RESIDUELLES * logements133 return {"taux_municipal_pour_1000": taux_municipal, "municipal": municipal, "evaluation_uniformisee": uniformisee, "scolaire": scolaire,134 "compensations": compensations, "total": municipal + scolaire + compensations,135 "taux_marginal_pour_1000": taux_municipal + facteur * TAUX_SCOLAIRE_2026_2027 * 1000.0}136137def taux_voisinage_gatineau() -> dict:138 """Statistiques des 1 092 taux de l'annexe I du règlement 994-2025 (data/raw/gatineau_r994_2025)."""139 a = pd.read_csv(ROOT / "data/raw/gatineau_r994_2025/annexe1_taux_voisinage.csv")140 t = a["taux_2026_par_1000"].astype(float)141 return {"n": len(t), "min": float(t.min()), "max": float(t.max()), "mediane": float(t.median()), "moyenne": float(t.mean())}142143144# ----------------------------------------------------------------------------- étude de ratios145def etude(d: dict) -> dict:146 v = ratios(d["ventes"], d["modele"])147 brut = h.statistiques_ratio(v["valeur_role_2025"], v["prix_vente"])148 aj = h.statistiques_ratio(v["valeur_role_2025"], v["prix_ref"])149 ep = epuration_r10(v["r_aj"].to_numpy())150 k = v[ep["garde"]]151 PM = ep["proportion_mediane"]; F = 1.0 / PM152 apres = h.statistiques_ratio(k["valeur_role_2025"], k["prix_ref"])153 secteurs = v.groupby("secteur")["r_aj"].agg(mediane="median", n="count").sort_values("n", ascending=False)154 types = v.groupby("type")["r_aj"].agg(mediane="median", n="count")155 bornes = [0, 350_000, 450_000, 550_000, 700_000, np.inf]156 v["tranche"] = pd.cut(v["prix_ref"], bornes, labels=["< 350 k$", "350-450 k$", "450-550 k$", "550-700 k$", "≥ 700 k$"])157 tranches = v.groupby("tranche", observed=True)["r_aj"].agg(mediane="median", n="count")158 v["trimestre"] = v["date_vente"].dt.to_period("Q").astype(str)159 trimestres = v.groupby("trimestre").agg(r_brut=("r_brut", "median"), r_aj=("r_aj", "median"), n=("r_brut", "count"))160 annees = v.groupby(v["date_vente"].dt.year).agg(r_brut=("r_brut", "median"), r_aj=("r_aj", "median"), n=("r_brut", "count"))161 return {"v": v, "brut": brut, "aj": aj, "epuration": ep, "apres_epuration": apres, "PM": PM, "F": F,162 "ecart_type_relatif_brut": ecart_type_relatif(v["r_brut"]), "ecart_type_relatif_aj": ecart_type_relatif(v["r_aj"]),163 "ecart_type_relatif_epure": ecart_type_relatif(k["r_aj"]), "plafond_r13": plafond_r13(1.0),164 "secteurs": secteurs, "types": types, "tranches": tranches, "trimestres": trimestres, "annees": annees,165 "prb_prix_aj": prb_proxy_prix(v["valeur_role_2025"], v["prix_ref"]), "prb_prix_brut": prb_proxy_prix(v["valeur_role_2025"], v["prix_vente"]),166 "croissance_mensuelle": d["modele"]["croissance_mensuelle"], "croissance_annuelle": (1 + d["modele"]["croissance_mensuelle"]) ** 12 - 1}167168169# ----------------------------------------------------------------------------- dossier du sujet170def comparables_reference(d: dict, k: int = 5, fenetre_mois: float = 12.0, rayon_km: float = 3.0) -> dict:171 """Comparables du sujet autour de la date de référence (± 12 mois, art. 46 al. 5), mêmes filtres et même distance d'attributs172 que le chapitre 16 (ch16_selection), puis ajustements dérivés du modèle hédonique vers la date de référence."""173 import ch16_selection as c16174 v = c16.preparer(d["ventes"].copy(), d["sujet"]); s = d["sujet"]175 v["mois_ref"] = mois_depuis_ref(v["date_vente"])176 # la distance d'attributs du chapitre 16 mesure l'ancienneté par rapport à la date d'évaluation ; ici, par rapport à la date de référence177 v["mois"] = v["mois_ref"].abs()178 suj = dict(s); suj["mois"] = 0.0; suj["dist_km"] = 0.0179 v["distance"] = [cp.distance_attributs(suj, r, c16.ECHELLES, c16.POIDS) for r in v.to_dict("records")]180 f = v[(v["mois_ref"].abs() <= fenetre_mois) & (v["dist_km"] <= rayon_km) & (v["type"] == s["type"])181 & v["superficie_habitable_m2"].between(0.75 * s["superficie_habitable_m2"], 1.25 * s["superficie_habitable_m2"])].sort_values("distance")182 top = f.head(k)183 adj = cas1.ajustements_hedoniques(d["modele"], top, s, REF.date().isoformat())184 desc = top[["id_vente", "date_vente", "superficie_habitable_m2", "superficie_terrain_m2", "age", "qualite_1a5", "garage_places", "sous_sol_fini", "dist_km", "valeur_role_2025"]].rename(columns={"age": "age_bat"})185 adj = adj.merge(desc, on="id_vente")186 adj["ratio_comparable"] = adj["valeur_role_2025"] / adj["prix_ajuste"]187 return {"candidats": f, "comparables": top, "ajustements": adj, "indication_mediane": float(adj["prix_ajuste"].median()),188 "indication_moyenne": float(adj["prix_ajuste"].mean())}189190def dossier_sujet(d: dict, et: dict) -> dict:191 s = d["sujet"]; m = d["modele"]192 sd = cas1.sujet_df(s, REF.date().isoformat())193 hed = float(cas1.predire(m, sd)[0]); ip = cas1.intervalle_prediction(m, sd).iloc[0]194 comps = comparables_reference(d)195 # réconciliation : moyenne simple des deux indications (même poids), arrondie au millier196 v_reelle = float(np.round((hed + comps["indication_mediane"]) / 2.0, -3))197 role = float(s["valeur_role_2025"])198 ratio_sujet = role / v_reelle199 v_inscrire = v_reelle / et["F"] # art. 147 : valeur réelle ÷ facteur du rôle200 tv = taux_voisinage_gatineau()201 taxes_role = compte_taxes_gatineau(role, et["F"], tv["mediane"])202 taxes_inscrire = compte_taxes_gatineau(v_inscrire, et["F"], tv["mediane"])203 return {"hedonique": hed, "ic_bas": float(ip["bas"]), "ic_haut": float(ip["haut"]), "comparables": comps, "valeur_reelle": v_reelle,204 "valeur_role": role, "ratio_sujet": ratio_sujet, "ecart_ratio_pct": (ratio_sujet / et["PM"] - 1) * 100,205 "valeur_a_inscrire": v_inscrire, "variation_si_conteste": v_inscrire - role, "taxes_role": taxes_role, "taxes_inscrire": taxes_inscrire,206 "variation_taxes_annuelle": taxes_inscrire["total"] - taxes_role["total"], "frais_revision": frais(role, FRAIS_REVISION_GATINEAU),207 "frais_taq": frais(role, FRAIS_TAQ_2026), "taux_voisinage": tv}208209def dossier_acheteur(d: dict, et: dict, id_vente: str = "G0258") -> dict:210 """Second dossier : l'acquéreur d'une vente du jeu (G0258, Hull) dont le prix, rajusté à la date de référence,211 est inférieur à la valeur inscrite."""212 v = et["v"]; r = v[v["id_vente"] == id_vente].iloc[0]213 prix_ref = float(r["prix_ref"]); role = float(r["valeur_role_2025"])214 v_inscrire = prix_ref / et["F"]215 tv = et.get("taux_voisinage") or taux_voisinage_gatineau()216 t_role = compte_taxes_gatineau(role, et["F"], tv["mediane"]); t_new = compte_taxes_gatineau(v_inscrire, et["F"], tv["mediane"])217 gain_annuel = t_role["total"] - t_new["total"]218 fr = frais(role, FRAIS_REVISION_GATINEAU)219 gain_cycle = gain_annuel * len(ROLE_FICTIF)220 # écart de valeur minimal pour couvrir les frais de révision sur un cycle (taux marginal par 1 000 $)221 seuil_valeur = fr / (t_role["taux_marginal_pour_1000"] / 1000.0) / len(ROLE_FICTIF)222 return {"vente": r, "prix_vente": float(r["prix_vente"]), "date_vente": r["date_vente"], "mois_ref": float(r["mois_ref"]), "prix_ref": prix_ref,223 "valeur_role": role, "ratio_brut": float(r["r_brut"]), "ratio_aj": float(r["r_aj"]), "valeur_a_inscrire": v_inscrire, "reduction": role - v_inscrire,224 "taxes_role": t_role, "taxes_nouvelle": t_new, "gain_annuel": gain_annuel, "gain_cycle": gain_cycle, "frais_revision": fr,225 "frais_taq": frais(role, FRAIS_TAQ_2026), "gain_net_cycle": gain_cycle - fr, "seuil_valeur": seuil_valeur}226227def sensibilite_acheteur(d: dict, et: dict, da: dict) -> pd.DataFrame:228 """Gain net sur le cycle triennal selon la valeur réelle conclue (−10 % à +10 % autour du prix rajusté) et le taux de voisinage229 (minimum, médiane, maximum de l'annexe I)."""230 tv = taux_voisinage_gatineau(); rows = []231 for ecart in np.arange(-0.10, 0.1001, 0.025):232 v_reelle = da["prix_ref"] * (1 + ecart); v_insc = v_reelle / et["F"]233 row = {"ecart_pct": ecart * 100, "valeur_reelle": v_reelle, "valeur_a_inscrire": v_insc}234 for nom, taux in [("min", tv["min"]), ("mediane", tv["mediane"]), ("max", tv["max"])]:235 g = (compte_taxes_gatineau(da["valeur_role"], et["F"], taux)["total"] - compte_taxes_gatineau(v_insc, et["F"], taux)["total"]) * len(ROLE_FICTIF)236 row[f"gain_net_{nom}"] = g - da["frais_revision"]237 rows.append(row)238 return pd.DataFrame(rows)239240def mutations_sujet(ds: dict, et: dict) -> dict:241 """Droits de mutation sur une vente hypothétique du sujet au prix de sa valeur réelle à la date de référence :242 base = max(prix ; rôle × facteur) (LDMI art. 1.1 et 2 al. 2)."""243 prix = ds["valeur_reelle"]; role = ds["valeur_role"]244 out = {}245 for nom, bareme, F in [("Québec (barème général)", BAREME_MUTATION_QC, et["F"]), ("Montréal 2026", BAREME_MUTATION_MTL, FACTEUR_MONTREAL_2026), ("Gatineau 2026 (dès le 25 février)", BAREME_MUTATION_GAT, et["F"])]:246 base = max(prix, role * F)247 out[nom] = {"base": base, "droit": droits_mutation(base, bareme), "droit_sur_prix": droits_mutation(prix, bareme)}248 # effet du facteur : un prix de 450 000 $ pour le sujet à Gatineau, facteur 1,02 réel vs fictif249 out["exemple_facteur"] = {"prix": 450_000.0, "base_F_reel": max(450_000.0, role * FACTEUR_GATINEAU_2026), "base_F_fictif": max(450_000.0, role * et["F"]),250 "droit_F_reel": droits_mutation(max(450_000.0, role * FACTEUR_GATINEAU_2026), BAREME_MUTATION_GAT),251 "droit_F_fictif": droits_mutation(max(450_000.0, role * et["F"]), BAREME_MUTATION_GAT), "droit_prix": droits_mutation(450_000.0, BAREME_MUTATION_GAT)}252 return out253254def echeancier() -> list[dict]:255 """Échéancier légal du rôle fictif 2026-2028 (référence 2024-07-01) et du rôle réel 2027-2029 de Gatineau (référence 2025-07-01)."""256 def jalons(ref: dt.date, premier: int, depot: dt.date):257 return [258 {"jalon": "Date de référence au marché (art. 46 al. 1)", "date": ref},259 {"jalon": "Dépôt du rôle (art. 70-71 : 15 août – 15 sept., report possible au 1er nov.)", "date": depot},260 {"jalon": "Entrée en vigueur (premier exercice)", "date": dt.date(premier, 1, 1)},261 {"jalon": "Expédition de l'avis d'évaluation (art. 81 : avant le 1er mars)", "date": dt.date(premier, 3, 1)},262 {"jalon": "Date limite de la demande de révision (art. 130 : avant le 1er mai)", "date": dt.date(premier, 4, 30)},263 {"jalon": "Réponse écrite de l'évaluateur (art. 138.3 : au plus tard le 1er sept., report possible au 1er nov. ou au 1er avril suivant)", "date": dt.date(premier, 9, 1)},264 {"jalon": "Fin du délai d'entente (art. 138.4 : 30e jour après la réponse du 1er sept.)", "date": dt.date(premier, 10, 1)},265 {"jalon": "Date limite du recours au TAQ (art. 138.5 : avant le 31e jour suivant l'expiration du délai d'entente)", "date": dt.date(premier, 10, 31)},266 {"jalon": "Fin du cycle triennal (art. 14)", "date": dt.date(premier + 2, 12, 31)},267 ]268 return {"fictif": jalons(REF.date(), ROLE_FICTIF[0], dt.date(2025, 9, 15)), "gatineau_2027": jalons(dt.date(2025, 7, 1), 2027, dt.date(2026, 10, 27))}269270271# ----------------------------------------------------------------------------- exercices272def exercices(et: dict) -> dict:273 ex = {}274 # ex 1 : facteur comparatif et évaluation uniformisée275 pm = 0.92; F = 1 / pm; ex["ex1"] = {"PM": pm, "F": F, "uniformisee_468000": 468_000 * F, "taxe_scolaire": max(468_000 * F - 25_000, 0) * TAUX_SCOLAIRE_2026_2027}276 # ex 2 : compte de taxes Gatineau 2026 du sujet (rôle 468 000, voisinage médian, Hull, raccordé) et scénario non raccordé277 tv = taux_voisinage_gatineau()278 ex["ex2"] = {"raccorde": compte_taxes_gatineau(468_000, 1.02, tv["mediane"]), "non_raccorde": compte_taxes_gatineau(468_000, 1.02, tv["mediane"], raccorde=False),279 "six_logements": None}280 # ex 3 : petite étude de ratios à la main (8 ventes)281 V = np.array([412_000, 455_000, 398_000, 520_000, 610_000, 470_000, 388_000, 545_000.0]); P = np.array([450_000, 480_000, 445_000, 560_000, 690_000, 500_000, 400_000, 600_000.0])282 st = h.statistiques_ratio(V, P); ep = epuration_r10(V / P)283 ex["ex3"] = {"ratios": V / P, **st, "epuration": ep, "ecart_type_relatif": ecart_type_relatif(V / P)}284 # ex 4 : droits de mutation285 ex["ex4"] = {"qc_575000": droits_mutation(575_000, BAREME_MUTATION_QC), "gat_575000": droits_mutation(575_000, BAREME_MUTATION_GAT), "mtl_575000": droits_mutation(575_000, BAREME_MUTATION_MTL),286 "gat_base_role": max(575_000, 620_000 * 1.02), "gat_droit_base_role": droits_mutation(max(575_000, 620_000 * 1.02), BAREME_MUTATION_GAT)}287 # ex 5 : liste de base288 ex["ex5"] = {N: liste_de_base(N) for N in [1_000, 10_000, 103_355, 514_614]}289 # ex 6 : échéancier — calculé dans la solution à partir de echeancier()290 # ex 7 : seuil de rentabilité d'une contestation (voir dossier_acheteur) et régressivité : PRD d'un jeu construit régressif291 rng = np.random.default_rng(GRAINE); Vt = np.exp(rng.normal(13.0, 0.35, 400)); Pt = Vt * np.exp(rng.normal(0, 0.08, 400))292 A = Vt ** 0.9 * np.exp(0.1 * 13.0) * 0.93 * np.exp(rng.normal(0, 0.06, 400)) # rôle régressif : élasticité 0,9 par rapport à la valeur293 ex["ex7"] = h.statistiques_ratio(A, Pt)294 return ex295296297# ----------------------------------------------------------------------------- impression298def main():299 pd.set_option("display.width", 200)300 d = charger(); et = etude(d)301 print("== Modèle hédonique partagé : croissance mensuelle %.4f %% (%.2f %% par an), σ = %.4f" % (et["croissance_mensuelle"] * 100, et["croissance_annuelle"] * 100, d["modele"]["sigma"]))302 print("== Ratios bruts (rôle / prix de vente), n = %d : médiane %.4f ; moyenne %.4f ; pondéré %.4f ; COD %.2f ; PRD %.4f ; PRB %.4f (e.-t. %.4f)" %303 (et["brut"]["n"], et["brut"]["ratio_median"], et["brut"]["ratio_moyen"], et["brut"]["ratio_pondere"], et["brut"]["COD"], et["brut"]["PRD"], et["brut"]["PRB"], et["brut"]["PRB_se"]))304 print("== Ratios rajustés au 1er juillet 2024 : médiane %.4f ; moyenne %.4f ; pondéré %.4f ; COD %.2f ; PRD %.4f ; PRB %.4f (e.-t. %.4f)" %305 (et["aj"]["ratio_median"], et["aj"]["ratio_moyen"], et["aj"]["ratio_pondere"], et["aj"]["COD"], et["aj"]["PRD"], et["aj"]["PRB"], et["aj"]["PRB_se"]))306 print("== PRB de contrôle (log2 du seul prix) : brut %.4f (e.-t. %.4f) ; rajusté %.4f (e.-t. %.4f)" % (et["prb_prix_brut"]["PRB"], et["prb_prix_brut"]["PRB_se"], et["prb_prix_aj"]["PRB"], et["prb_prix_aj"]["PRB_se"]))307 ep = et["epuration"]308 print("== Épuration r. 10 : médiane %.4f, σ %.4f, bornes [%.4f ; %.4f], %d ventes retirées sur %d ; proportion médiane %.4f ; facteur comparatif %.4f" %309 (ep["mediane_initiale"], ep["sigma"], ep["borne_basse"], ep["borne_haute"], ep["n_retires"], ep["n"], et["PM"], et["F"]))310 print("== Après épuration : COD %.2f ; PRD %.4f ; PRB %.4f" % (et["apres_epuration"]["COD"], et["apres_epuration"]["PRD"], et["apres_epuration"]["PRB"]))311 print("== Écart type relatif (annexe III) : brut %.4f ; rajusté %.4f ; épuré %.4f ; plafond r. 13 (100 %% unifamilial) %.2f" % (et["ecart_type_relatif_brut"], et["ecart_type_relatif_aj"], et["ecart_type_relatif_epure"], et["plafond_r13"]))312 print("== Par secteur (ratio rajusté) :\n", et["secteurs"].round(4).to_string())313 print("== Par type :\n", et["types"].round(4).to_string())314 print("== Par tranche de prix rajusté :\n", et["tranches"].round(4).to_string())315 print("== Par année de vente (médianes) :\n", et["annees"].round(4).to_string())316 print("== Liste de base r. 10 : Gatineau (103 355 unités) → %d ventes ; Montréal (514 614) → %d ; 10 000 → %d ; 1 000 → %d" % (liste_de_base(103_355), liste_de_base(514_614), liste_de_base(10_000), liste_de_base(1_000)))317 tv = taux_voisinage_gatineau(); print("== Taux de voisinage Gatineau 2026 (annexe I, n = %d) : min %.3f, médiane %.3f, moyenne %.3f, max %.3f $/1 000 $" % (tv["n"], tv["min"], tv["mediane"], tv["moyenne"], tv["max"]))318 ds = dossier_sujet(d, et)319 print("== Sujet au 1er juillet 2024 : hédonique %.0f $ (IC 95 %% %.0f – %.0f) ; comparables (médiane %.0f, moyenne %.0f) ; valeur réelle retenue %.0f $" %320 (ds["hedonique"], ds["ic_bas"], ds["ic_haut"], ds["comparables"]["indication_mediane"], ds["comparables"]["indication_moyenne"], ds["valeur_reelle"]))321 a = ds["comparables"]["ajustements"].copy(); a["date_vente"] = a["date_vente"].dt.date322 cols = ["id_vente", "date_vente", "superficie_habitable_m2", "superficie_terrain_m2", "age_bat", "qualite_1a5", "garage_places", "sous_sol_fini", "dist_km", "prix_vente", "conditions_marche", "superficie", "terrain", "age", "qualite", "garage", "sous_sol", "piscine", "prix_ajuste", "ajust_brut_pct", "ajust_net_pct"]323 with pd.option_context("display.float_format", lambda x: f"{x:,.2f}" if abs(x) < 10 else f"{x:,.0f}"):324 print(a[cols].to_string(index=False))325 print("== Ratio du sujet %.4f (écart à la proportion médiane %.1f %%) ; valeur à inscrire (art. 147) %.0f $ ; variation si contestation %.0f $" % (ds["ratio_sujet"], ds["ecart_ratio_pct"], ds["valeur_a_inscrire"], ds["variation_si_conteste"]))326 tr = ds["taxes_role"]; print("== Compte de taxes 2026 du sujet (rôle 468 000 $, voisinage médian, Hull) : taux municipal %.3f /1 000 ; municipal %.2f ; uniformisée %.0f ; scolaire %.2f ; compensations %.2f ; total %.2f ; taux marginal %.3f /1 000" %327 (tr["taux_municipal_pour_1000"], tr["municipal"], tr["evaluation_uniformisee"], tr["scolaire"], tr["compensations"], tr["total"], tr["taux_marginal_pour_1000"]))328 print("== Si la valeur à inscrire était retenue : total %.2f ; variation annuelle %.2f $ ; frais de révision %.2f $ ; frais TAQ %.2f $" % (ds["taxes_inscrire"]["total"], ds["variation_taxes_annuelle"], ds["frais_revision"], ds["frais_taq"]))329 da = dossier_acheteur(d, et)330 print("== Dossier acheteur %s (%s, %.1f mois après la référence) : prix %.0f ; prix rajusté %.0f ; rôle %.0f ; ratio brut %.4f, rajusté %.4f" % (da["vente"]["id_vente"], da["date_vente"].date(), da["mois_ref"], da["prix_vente"], da["prix_ref"], da["valeur_role"], da["ratio_brut"], da["ratio_aj"]))331 print(" valeur à inscrire %.0f (réduction %.0f) ; taxes rôle %.2f → %.2f ; gain annuel %.2f ; gain sur 3 ans %.2f ; frais %.2f ; gain net %.2f ; seuil de valeur %.0f $" %332 (da["valeur_a_inscrire"], da["reduction"], da["taxes_role"]["total"], da["taxes_nouvelle"]["total"], da["gain_annuel"], da["gain_cycle"], da["frais_revision"], da["gain_net_cycle"], da["seuil_valeur"]))333 print("== Sensibilité (gain net sur le cycle, $) :\n", sensibilite_acheteur(d, et, da).round(0).to_string(index=False))334 mu = mutations_sujet(ds, et)335 for k_, x in mu.items():336 print("== Mutation", k_, {a: round(b, 2) for a, b in x.items()})337 for nom, lst in echeancier().items():338 print("== Échéancier", nom); [print(" %s : %s" % (j["date"].isoformat(), j["jalon"])) for j in lst]339 ex = exercices(et)340 print("== Exercices :"); [print(" ", k_, (v_ if not isinstance(v_, dict) else {a: (round(b, 4) if isinstance(b, (int, float, np.floating)) else "…") for a, b in v_.items()})) for k_, v_ in ex.items()]341 return {"d": d, "etude": et, "sujet": ds, "acheteur": da, "mutations": mu, "exercices": ex}342343344if __name__ == "__main__":345 main()346