#!/usr/bin/env python3 """Chapitre 9 — Évaluation foncière municipale et contestation (DONNÉES SYNTHÉTIQUES du cas 1, graine 20261031). Étude de ratios (proportion médiane, facteur comparatif, COD, PRD, PRB, écart type relatif de l'annexe III du Règlement sur le rôle), dossier de contestation du sujet du cas 1 (valeur au rôle fictive 468 000 $) et d'un acheteur récent (vente G0258), compte de taxes 2026 de Gatineau (règlement 994-2025), droits de mutation 2026, échéancier des recours (LFM art. 70, 81, 130, 138.3, 138.4, 138.5), sensibilité et exercices. Imprime tous les chiffres cités. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch09_ratios.py Bibliothèque : traite.cas1 (modèle hédonique partagé), traite.hedonique.statistiques_ratio, traite.comparables.""" from __future__ import annotations import sys, pathlib, datetime as dt import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from traite import cas1, hedonique as h, comparables as cp ROOT = pathlib.Path(__file__).resolve().parents[3] REF = pd.Timestamp("2024-07-01") # date de référence au marché du rôle fictif (LFM art. 46 al. 1) ROLE_FICTIF = (2026, 2027, 2028) # exercices couverts par un rôle dont la référence est le 1er juillet 2024 GRAINE = 20261031 # ----------------------------------------------------------------------------- paramètres fiscaux vérifiés (Gatineau 2026) # Règlement 994-2025 (adopté le 18 décembre 2025), par 1 000 $ d'évaluation ; voir data/raw/gatineau_r994_2025/README.md TAUX_GATINEAU_2026 = { "base": 5.224, # art. 1 (catégorie résiduelle) "non_residentiel": 29.372, # art. 2 "industriel": 26.120, # art. 3 "terrains_vagues": 20.896, # art. 4 "agricole": 5.090, # art. 5 "six_logements_plus": 6.964, # art. 8 "sto": 1.417, # art. 11 taxe spéciale transport en commun "dette": 0.830, # art. 12 taxe spéciale service de la dette "ex_hull": 0.036, # art. 15, 1° (secteur de l'ex-Ville de Hull) "credit_aqueduc": 0.386, "credit_egout": 0.523, # art. 13-14 (immeubles non raccordés) } COMPENSATION_MATIERES_RESIDUELLES = 178.83 # $ par logement, art. 16 TAUX_SCOLAIRE_2026_2027 = 0.07899 / 100 # $ par $ d'évaluation uniformisée ajustée (LIP art. 303.4 ; avis 2026) EXEMPTION_SCOLAIRE = 25_000 # LIP art. 303.1 : tranche exemptée FRAIS_REVISION_GATINEAU = [(500_000, 80.00), (2_000_000, 325.00), (5_000_000, 574.50), (float("inf"), 1_149.25)] # règl. 943-2023 FRAIS_REVISION_MONTREAL = [(500_000, 90.00), (2_000_000, 360.00), (5_000_000, 605.00), (float("inf"), 1_215.00)] FRAIS_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.2 # Droits sur les mutations immobilières 2026 (LDMI art. 2 ; barèmes municipaux) BAREME_MUTATION_QC = [(62_900, 0.005), (315_000, 0.01), (float("inf"), 0.015)] BAREME_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)] BAREME_MUTATION_GAT = [(62_900, 0.005), (315_000, 0.01), (500_000, 0.015), (float("inf"), 0.03)] # à compter du 25 février 2026 FACTEUR_GATINEAU_2026 = 1.02 # facteur comparatif publié par la Ville (page « Base d'imposition ») FACTEUR_MONTREAL_2026 = 1.00 # Seuils IAAO (Standard on Ratio Studies, 2013, table 1-3 et § 9.2.7) IAAO = {"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)} # Règlement sur le rôle (F-2.1, r. 13), art. 13 al. 2 et art. 14 al. 1 R13_PROPORTION = (0.95, 1.05) R13_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) # ----------------------------------------------------------------------------- outils def charger() -> dict: d = cas1.charger() d["modele"] = cas1.hedonique(d["ventes"]) return d def mois_depuis_ref(dates) -> np.ndarray: return ((pd.to_datetime(dates) - REF).dt.days / 30.4375).to_numpy() def ratios(v: pd.DataFrame, modele: dict) -> pd.DataFrame: """Ratios bruts (rôle / prix) et ratios après rajustement des prix aux conditions du marché de la date de référence (r. 10, art. 11-12) : P_ref = P / (1+g)^mois, g = croissance mensuelle du modèle hédonique partagé.""" g = modele["croissance_mensuelle"] out = v.copy() out["mois_ref"] = mois_depuis_ref(out["date_vente"]) out["prix_ref"] = out["prix_vente"] / (1.0 + g) ** out["mois_ref"] out["r_brut"] = out["valeur_role_2025"] / out["prix_vente"] out["r_aj"] = out["valeur_role_2025"] / out["prix_ref"] return out def epuration_r10(r: np.ndarray) -> dict: """Épuration du r. 10 (art. 15-16) : retrait des ratios hors [médiane − 2σ ; médiane + 2σ], σ sur n − 1.""" r = np.asarray(r, float); med = np.median(r); sd = r.std(ddof=1) lo, hi = med - 2 * sd, med + 2 * sd garde = (r >= lo) & (r <= hi) return {"mediane_initiale": med, "sigma": sd, "borne_basse": lo, "borne_haute": hi, "n": len(r), "n_retires": int((~garde).sum()), "garde": garde, "proportion_mediane": float(np.median(r[garde]))} def ecart_type_relatif(r: np.ndarray) -> float: """Annexe III du r. 13 : écart type des ratios (n − 1) divisé par le ratio médian.""" r = np.asarray(r, float) return float(r.std(ddof=1) / np.median(r)) def plafond_r13(indice_concentration: float) -> float: for borne, plafond in R13_ECART_TYPE_RELATIF: if indice_concentration < borne: return plafond return 0.14 def liste_de_base(N: int) -> int: """r. 10, art. 5 : nombre de ventes de la liste de base = max(30 ; N / (15,5 + 0,001 N)).""" return int(np.ceil(max(30.0, N / (15.5 + 0.001 * N)))) def prb_proxy_prix(valeurs, prix) -> dict: """PRB calculé avec le seul prix (log2 P) comme variable explicative — variante de contrôle (hors norme IAAO).""" V = np.asarray(valeurs, float); P = np.asarray(prix, float); r = V / P; med = np.median(r) x = np.log2(P); yv = (r - med) / med X = np.column_stack([np.ones_like(x), x]); b, *_ = np.linalg.lstsq(X, yv, rcond=None) res = yv - X @ b; se = np.sqrt((res @ res) / (len(x) - 2) / ((x - x.mean()) ** 2).sum()) return {"PRB": float(b[1]), "PRB_se": float(se)} def frais(valeur_role: float, bareme) -> float: for borne, montant in bareme: if valeur_role <= borne: return montant return bareme[-1][1] def droits_mutation(base: float, bareme=BAREME_MUTATION_QC) -> float: """Droit sur les mutations immobilières par tranches (LDMI art. 2) ; base = max(prix, contrepartie, rôle × facteur).""" total, bas = 0.0, 0.0 for haut, taux in bareme: if base > bas: total += (min(base, haut) - bas) * taux bas = haut return total def compte_taxes_gatineau(valeur_role: float, facteur: float, taux_voisinage: float, secteur_hull: bool = True, raccorde: bool = True, logements: int = 1) -> dict: """Compte de taxes d'une résidence (catégorie résiduelle, moins de six logements) selon le règlement 994-2025 et la LIP. Taxes ad valorem par 1 000 $ : taux de l'unité de voisinage (art. 7) + STO (art. 11) + dette (art. 12) [+ ex-Hull 0,036] ; crédits aqueduc/égout si non raccordé ; taxe scolaire sur (rôle × facteur − 25 000 $) ; compensation matières résiduelles.""" t = TAUX_GATINEAU_2026 taux_municipal = taux_voisinage + t["sto"] + t["dette"] + (t["ex_hull"] if secteur_hull else 0.0) if not raccorde: taux_municipal -= t["credit_aqueduc"] + t["credit_egout"] municipal = valeur_role * taux_municipal / 1000.0 uniformisee = valeur_role * facteur scolaire = max(uniformisee - EXEMPTION_SCOLAIRE, 0.0) * TAUX_SCOLAIRE_2026_2027 compensations = COMPENSATION_MATIERES_RESIDUELLES * logements return {"taux_municipal_pour_1000": taux_municipal, "municipal": municipal, "evaluation_uniformisee": uniformisee, "scolaire": scolaire, "compensations": compensations, "total": municipal + scolaire + compensations, "taux_marginal_pour_1000": taux_municipal + facteur * TAUX_SCOLAIRE_2026_2027 * 1000.0} def taux_voisinage_gatineau() -> dict: """Statistiques des 1 092 taux de l'annexe I du règlement 994-2025 (data/raw/gatineau_r994_2025).""" a = pd.read_csv(ROOT / "data/raw/gatineau_r994_2025/annexe1_taux_voisinage.csv") t = a["taux_2026_par_1000"].astype(float) return {"n": len(t), "min": float(t.min()), "max": float(t.max()), "mediane": float(t.median()), "moyenne": float(t.mean())} # ----------------------------------------------------------------------------- étude de ratios def etude(d: dict) -> dict: v = ratios(d["ventes"], d["modele"]) brut = h.statistiques_ratio(v["valeur_role_2025"], v["prix_vente"]) aj = h.statistiques_ratio(v["valeur_role_2025"], v["prix_ref"]) ep = epuration_r10(v["r_aj"].to_numpy()) k = v[ep["garde"]] PM = ep["proportion_mediane"]; F = 1.0 / PM apres = h.statistiques_ratio(k["valeur_role_2025"], k["prix_ref"]) secteurs = v.groupby("secteur")["r_aj"].agg(mediane="median", n="count").sort_values("n", ascending=False) types = v.groupby("type")["r_aj"].agg(mediane="median", n="count") bornes = [0, 350_000, 450_000, 550_000, 700_000, np.inf] v["tranche"] = pd.cut(v["prix_ref"], bornes, labels=["< 350 k$", "350-450 k$", "450-550 k$", "550-700 k$", "≥ 700 k$"]) tranches = v.groupby("tranche", observed=True)["r_aj"].agg(mediane="median", n="count") v["trimestre"] = v["date_vente"].dt.to_period("Q").astype(str) trimestres = v.groupby("trimestre").agg(r_brut=("r_brut", "median"), r_aj=("r_aj", "median"), n=("r_brut", "count")) annees = v.groupby(v["date_vente"].dt.year).agg(r_brut=("r_brut", "median"), r_aj=("r_aj", "median"), n=("r_brut", "count")) return {"v": v, "brut": brut, "aj": aj, "epuration": ep, "apres_epuration": apres, "PM": PM, "F": F, "ecart_type_relatif_brut": ecart_type_relatif(v["r_brut"]), "ecart_type_relatif_aj": ecart_type_relatif(v["r_aj"]), "ecart_type_relatif_epure": ecart_type_relatif(k["r_aj"]), "plafond_r13": plafond_r13(1.0), "secteurs": secteurs, "types": types, "tranches": tranches, "trimestres": trimestres, "annees": annees, "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"]), "croissance_mensuelle": d["modele"]["croissance_mensuelle"], "croissance_annuelle": (1 + d["modele"]["croissance_mensuelle"]) ** 12 - 1} # ----------------------------------------------------------------------------- dossier du sujet def comparables_reference(d: dict, k: int = 5, fenetre_mois: float = 12.0, rayon_km: float = 3.0) -> dict: """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'attributs que le chapitre 16 (ch16_selection), puis ajustements dérivés du modèle hédonique vers la date de référence.""" import ch16_selection as c16 v = c16.preparer(d["ventes"].copy(), d["sujet"]); s = d["sujet"] v["mois_ref"] = mois_depuis_ref(v["date_vente"]) # la distance d'attributs du chapitre 16 mesure l'ancienneté par rapport à la date d'évaluation ; ici, par rapport à la date de référence v["mois"] = v["mois_ref"].abs() suj = dict(s); suj["mois"] = 0.0; suj["dist_km"] = 0.0 v["distance"] = [cp.distance_attributs(suj, r, c16.ECHELLES, c16.POIDS) for r in v.to_dict("records")] f = v[(v["mois_ref"].abs() <= fenetre_mois) & (v["dist_km"] <= rayon_km) & (v["type"] == s["type"]) & v["superficie_habitable_m2"].between(0.75 * s["superficie_habitable_m2"], 1.25 * s["superficie_habitable_m2"])].sort_values("distance") top = f.head(k) adj = cas1.ajustements_hedoniques(d["modele"], top, s, REF.date().isoformat()) 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"}) adj = adj.merge(desc, on="id_vente") adj["ratio_comparable"] = adj["valeur_role_2025"] / adj["prix_ajuste"] return {"candidats": f, "comparables": top, "ajustements": adj, "indication_mediane": float(adj["prix_ajuste"].median()), "indication_moyenne": float(adj["prix_ajuste"].mean())} def dossier_sujet(d: dict, et: dict) -> dict: s = d["sujet"]; m = d["modele"] sd = cas1.sujet_df(s, REF.date().isoformat()) hed = float(cas1.predire(m, sd)[0]); ip = cas1.intervalle_prediction(m, sd).iloc[0] comps = comparables_reference(d) # réconciliation : moyenne simple des deux indications (même poids), arrondie au millier v_reelle = float(np.round((hed + comps["indication_mediane"]) / 2.0, -3)) role = float(s["valeur_role_2025"]) ratio_sujet = role / v_reelle v_inscrire = v_reelle / et["F"] # art. 147 : valeur réelle ÷ facteur du rôle tv = taux_voisinage_gatineau() taxes_role = compte_taxes_gatineau(role, et["F"], tv["mediane"]) taxes_inscrire = compte_taxes_gatineau(v_inscrire, et["F"], tv["mediane"]) return {"hedonique": hed, "ic_bas": float(ip["bas"]), "ic_haut": float(ip["haut"]), "comparables": comps, "valeur_reelle": v_reelle, "valeur_role": role, "ratio_sujet": ratio_sujet, "ecart_ratio_pct": (ratio_sujet / et["PM"] - 1) * 100, "valeur_a_inscrire": v_inscrire, "variation_si_conteste": v_inscrire - role, "taxes_role": taxes_role, "taxes_inscrire": taxes_inscrire, "variation_taxes_annuelle": taxes_inscrire["total"] - taxes_role["total"], "frais_revision": frais(role, FRAIS_REVISION_GATINEAU), "frais_taq": frais(role, FRAIS_TAQ_2026), "taux_voisinage": tv} def dossier_acheteur(d: dict, et: dict, id_vente: str = "G0258") -> dict: """Second dossier : l'acquéreur d'une vente du jeu (G0258, Hull) dont le prix, rajusté à la date de référence, est inférieur à la valeur inscrite.""" v = et["v"]; r = v[v["id_vente"] == id_vente].iloc[0] prix_ref = float(r["prix_ref"]); role = float(r["valeur_role_2025"]) v_inscrire = prix_ref / et["F"] tv = et.get("taux_voisinage") or taux_voisinage_gatineau() t_role = compte_taxes_gatineau(role, et["F"], tv["mediane"]); t_new = compte_taxes_gatineau(v_inscrire, et["F"], tv["mediane"]) gain_annuel = t_role["total"] - t_new["total"] fr = frais(role, FRAIS_REVISION_GATINEAU) gain_cycle = gain_annuel * len(ROLE_FICTIF) # écart de valeur minimal pour couvrir les frais de révision sur un cycle (taux marginal par 1 000 $) seuil_valeur = fr / (t_role["taux_marginal_pour_1000"] / 1000.0) / len(ROLE_FICTIF) return {"vente": r, "prix_vente": float(r["prix_vente"]), "date_vente": r["date_vente"], "mois_ref": float(r["mois_ref"]), "prix_ref": prix_ref, "valeur_role": role, "ratio_brut": float(r["r_brut"]), "ratio_aj": float(r["r_aj"]), "valeur_a_inscrire": v_inscrire, "reduction": role - v_inscrire, "taxes_role": t_role, "taxes_nouvelle": t_new, "gain_annuel": gain_annuel, "gain_cycle": gain_cycle, "frais_revision": fr, "frais_taq": frais(role, FRAIS_TAQ_2026), "gain_net_cycle": gain_cycle - fr, "seuil_valeur": seuil_valeur} def sensibilite_acheteur(d: dict, et: dict, da: dict) -> pd.DataFrame: """Gain net sur le cycle triennal selon la valeur réelle conclue (−10 % à +10 % autour du prix rajusté) et le taux de voisinage (minimum, médiane, maximum de l'annexe I).""" tv = taux_voisinage_gatineau(); rows = [] for ecart in np.arange(-0.10, 0.1001, 0.025): v_reelle = da["prix_ref"] * (1 + ecart); v_insc = v_reelle / et["F"] row = {"ecart_pct": ecart * 100, "valeur_reelle": v_reelle, "valeur_a_inscrire": v_insc} for nom, taux in [("min", tv["min"]), ("mediane", tv["mediane"]), ("max", tv["max"])]: g = (compte_taxes_gatineau(da["valeur_role"], et["F"], taux)["total"] - compte_taxes_gatineau(v_insc, et["F"], taux)["total"]) * len(ROLE_FICTIF) row[f"gain_net_{nom}"] = g - da["frais_revision"] rows.append(row) return pd.DataFrame(rows) def mutations_sujet(ds: dict, et: dict) -> dict: """Droits de mutation sur une vente hypothétique du sujet au prix de sa valeur réelle à la date de référence : base = max(prix ; rôle × facteur) (LDMI art. 1.1 et 2 al. 2).""" prix = ds["valeur_reelle"]; role = ds["valeur_role"] out = {} 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"])]: base = max(prix, role * F) out[nom] = {"base": base, "droit": droits_mutation(base, bareme), "droit_sur_prix": droits_mutation(prix, bareme)} # effet du facteur : un prix de 450 000 $ pour le sujet à Gatineau, facteur 1,02 réel vs fictif 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"]), "droit_F_reel": droits_mutation(max(450_000.0, role * FACTEUR_GATINEAU_2026), BAREME_MUTATION_GAT), "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)} return out def echeancier() -> list[dict]: """É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).""" def jalons(ref: dt.date, premier: int, depot: dt.date): return [ {"jalon": "Date de référence au marché (art. 46 al. 1)", "date": ref}, {"jalon": "Dépôt du rôle (art. 70-71 : 15 août – 15 sept., report possible au 1er nov.)", "date": depot}, {"jalon": "Entrée en vigueur (premier exercice)", "date": dt.date(premier, 1, 1)}, {"jalon": "Expédition de l'avis d'évaluation (art. 81 : avant le 1er mars)", "date": dt.date(premier, 3, 1)}, {"jalon": "Date limite de la demande de révision (art. 130 : avant le 1er mai)", "date": dt.date(premier, 4, 30)}, {"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)}, {"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)}, {"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)}, {"jalon": "Fin du cycle triennal (art. 14)", "date": dt.date(premier + 2, 12, 31)}, ] 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))} # ----------------------------------------------------------------------------- exercices def exercices(et: dict) -> dict: ex = {} # ex 1 : facteur comparatif et évaluation uniformisée 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} # ex 2 : compte de taxes Gatineau 2026 du sujet (rôle 468 000, voisinage médian, Hull, raccordé) et scénario non raccordé tv = taux_voisinage_gatineau() 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), "six_logements": None} # ex 3 : petite étude de ratios à la main (8 ventes) 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]) st = h.statistiques_ratio(V, P); ep = epuration_r10(V / P) ex["ex3"] = {"ratios": V / P, **st, "epuration": ep, "ecart_type_relatif": ecart_type_relatif(V / P)} # ex 4 : droits de mutation 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), "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)} # ex 5 : liste de base ex["ex5"] = {N: liste_de_base(N) for N in [1_000, 10_000, 103_355, 514_614]} # ex 6 : échéancier — calculé dans la solution à partir de echeancier() # ex 7 : seuil de rentabilité d'une contestation (voir dossier_acheteur) et régressivité : PRD d'un jeu construit régressif 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)) 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 valeur ex["ex7"] = h.statistiques_ratio(A, Pt) return ex # ----------------------------------------------------------------------------- impression def main(): pd.set_option("display.width", 200) d = charger(); et = etude(d) print("== Modèle hédonique partagé : croissance mensuelle %.4f %% (%.2f %% par an), σ = %.4f" % (et["croissance_mensuelle"] * 100, et["croissance_annuelle"] * 100, d["modele"]["sigma"])) 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)" % (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"])) 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)" % (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"])) 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"])) ep = et["epuration"] print("== Épuration r. 10 : médiane %.4f, σ %.4f, bornes [%.4f ; %.4f], %d ventes retirées sur %d ; proportion médiane %.4f ; facteur comparatif %.4f" % (ep["mediane_initiale"], ep["sigma"], ep["borne_basse"], ep["borne_haute"], ep["n_retires"], ep["n"], et["PM"], et["F"])) print("== Après épuration : COD %.2f ; PRD %.4f ; PRB %.4f" % (et["apres_epuration"]["COD"], et["apres_epuration"]["PRD"], et["apres_epuration"]["PRB"])) 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"])) print("== Par secteur (ratio rajusté) :\n", et["secteurs"].round(4).to_string()) print("== Par type :\n", et["types"].round(4).to_string()) print("== Par tranche de prix rajusté :\n", et["tranches"].round(4).to_string()) print("== Par année de vente (médianes) :\n", et["annees"].round(4).to_string()) 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))) 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"])) ds = dossier_sujet(d, et) print("== Sujet au 1er juillet 2024 : hédonique %.0f $ (IC 95 %% %.0f – %.0f) ; comparables (médiane %.0f, moyenne %.0f) ; valeur réelle retenue %.0f $" % (ds["hedonique"], ds["ic_bas"], ds["ic_haut"], ds["comparables"]["indication_mediane"], ds["comparables"]["indication_moyenne"], ds["valeur_reelle"])) a = ds["comparables"]["ajustements"].copy(); a["date_vente"] = a["date_vente"].dt.date 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"] with pd.option_context("display.float_format", lambda x: f"{x:,.2f}" if abs(x) < 10 else f"{x:,.0f}"): print(a[cols].to_string(index=False)) 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"])) 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" % (tr["taux_municipal_pour_1000"], tr["municipal"], tr["evaluation_uniformisee"], tr["scolaire"], tr["compensations"], tr["total"], tr["taux_marginal_pour_1000"])) 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"])) da = dossier_acheteur(d, et) 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"])) 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 $" % (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"])) print("== Sensibilité (gain net sur le cycle, $) :\n", sensibilite_acheteur(d, et, da).round(0).to_string(index=False)) mu = mutations_sujet(ds, et) for k_, x in mu.items(): print("== Mutation", k_, {a: round(b, 2) for a, b in x.items()}) for nom, lst in echeancier().items(): print("== Échéancier", nom); [print(" %s : %s" % (j["date"].isoformat(), j["jalon"])) for j in lst] ex = exercices(et) 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()] return {"d": d, "etude": et, "sujet": ds, "acheteur": da, "mutations": mu, "exercices": ex} if __name__ == "__main__": main()