SPB Git
27.1 KB · 346 lines python
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
Files · 185d04a62ff66f3d004407340590d377c512efce
Menu

Theme