SPB Git
30.3 KB · 334 lines python
1"""Génération des quatre dossiers de cas SYNTHÉTIQUES du traité.23Tout est déterministe (graine 20261031). Les paramètres de calibration proviennent de data/synthetic/calibration.json4(valeurs publiques sourcées, voir sources/registry.csv) ; en l'absence d'une valeur, un défaut PROVISOIRE est utilisé et5consigné comme NON_VERIFIE dans data/synthetic/README.md. Les relations (élasticités, primes, lois de tirage) sont des6HYPOTHÈSES PÉDAGOGIQUES déclarées dans le README — elles ne décrivent aucun bien ni marché réel."""7from __future__ import annotations8import json, math, pathlib, datetime9import numpy as np10import pandas as pd1112ROOT = pathlib.Path(__file__).resolve().parents[3]13OUT = ROOT / "data/synthetic"14SEED = 2026103115DATE_EVAL = datetime.date(2026, 9, 1)  # date d'évaluation commune des quatre cas1617class Calibration:18    """Accès aux paramètres avec traçabilité (valeur, source, provisoire ou non)."""19    def __init__(self, path=OUT / "calibration.json"):20        self.raw = json.loads(path.read_text()) if path.exists() else {}21        self.journal: list[dict] = []22    def get(self, key: str, defaut, unite: str = "", note: str = ""):23        item = self.raw.get(key)24        if isinstance(item, dict) and item.get("valeur") is not None:25            self.journal.append({"parametre": key, "valeur": item["valeur"], "unite": item.get("unite", unite),26                                 "source_id": item.get("source_id", "?"), "periode": item.get("periode", ""), "statut": "calibre"})27            return item["valeur"]28        self.journal.append({"parametre": key, "valeur": defaut, "unite": unite, "source_id": "NON_VERIFIE", "periode": "", "statut": "provisoire", "note": note})29        return defaut3031def _dates(rng, n, debut=datetime.date(2023, 1, 1), fin=datetime.date(2026, 8, 31)):32    span = (fin - debut).days33    d = np.sort(rng.integers(0, span + 1, n))34    return [debut + datetime.timedelta(days=int(x)) for x in d]3536def _mois_depuis(dates, ref=datetime.date(2023, 1, 1)):37    return np.array([(d.year - ref.year) * 12 + (d.month - ref.month) + (d.day - 1) / 30.0 for d in dates])3839# ============================================================================ CAS 140def cas1_gatineau(cal: Calibration, rng: np.random.Generator) -> dict:41    n = 60042    med = cal.get("prix_median_unifamiliale_rmr_gatineau", 520_000, "$", "prix médian unifamiliale RMR Gatineau (APCIQ)")43    g = cal.get("croissance_annuelle_prix_gatineau", 0.07, "proportion", "variation annuelle du prix médian")44    secteurs = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]45    prime_secteur = {"Hull": 0.00, "Gatineau": -0.05, "Aylmer": 0.08, "Buckingham": -0.15, "Masson-Angers": -0.18}  # hypothèse pédagogique46    p_sect = [0.30, 0.32, 0.20, 0.10, 0.08]47    sect = rng.choice(secteurs, n, p=p_sect)48    dates = _dates(rng, n)49    mois = _mois_depuis(dates)50    S = np.exp(rng.normal(np.log(140), 0.25, n)).round(0)                   # superficie habitable m²51    T = np.where(np.isin(sect, ["Buckingham", "Masson-Angers"]), np.exp(rng.normal(np.log(1200), 0.45, n)), np.exp(rng.normal(np.log(560), 0.30, n))).round(0)52    annee = np.clip(rng.normal(1985, 20, n).round(0), 1920, 2025).astype(int)53    age = 2026 - annee54    qualite = np.clip(rng.normal(3, 0.8, n).round(0), 1, 5).astype(int)55    garage = rng.choice([0, 1, 2], n, p=[0.45, 0.45, 0.10])56    sous_sol_fini = rng.binomial(1, 0.6, n)57    piscine = rng.binomial(1, 0.15, n)58    chambres = np.clip((S / 45 + rng.normal(0, 0.6, n)).round(0), 1, 6).astype(int)59    sdb = np.clip((S / 80 + rng.normal(0, 0.5, n)).round(0), 1, 4).astype(int)60    type_bat = np.where(rng.random(n) < 0.45, "plain-pied", "à étages")61    # centre de chaque secteur (lat, lon approximatifs du centre urbain, jitter ± ~2-4 km : coordonnées synthétiques)62    centres = {"Hull": (45.428, -75.714), "Gatineau": (45.477, -75.650), "Aylmer": (45.395, -75.843), "Buckingham": (45.588, -75.418), "Masson-Angers": (45.545, -75.420)}63    lat = np.array([centres[s][0] for s in sect]) + rng.normal(0, 0.012, n)64    lon = np.array([centres[s][1] for s in sect]) + rng.normal(0, 0.020, n)65    dist_centre_km = np.sqrt(((lat - 45.428) * 111) ** 2 + ((lon + 75.714) * 78) ** 2)66    # indice de marché : croissance g/an composée, référence = 1er juillet 2024 (date de référence du rôle fictif)67    ref_mois = _mois_depuis([datetime.date(2024, 7, 1)])[0]68    ln_index = np.log1p(g) * (mois - ref_mois) / 1269    lnP = (np.log(med) + 0.55 * np.log(S / 140) + 0.10 * np.log(T / 560) - 0.0055 * age + 0.000030 * age ** 270           + 0.06 * (qualite - 3) + 0.035 * garage + 0.03 * sous_sol_fini + 0.02 * piscine71           + np.array([prime_secteur[s] for s in sect]) - 0.006 * np.clip(dist_centre_km - 3, 0, None)72           + 0.02 * (type_bat == "à étages") + ln_index + rng.normal(0, 0.085, n))73    lnP += np.log(med) - np.median(lnP - ln_index)   # normalisation : médiane à la date de référence = prix médian calibré74    prix = (np.exp(lnP) / 500).round(0) * 50075    # valeur au rôle 2025-2027 fictif : valeur à la date de référence (2024-07-01) × ratio ~ N(0.93, 0.07)76    lnP_ref = lnP - ln_index77    role = (np.exp(lnP_ref) * np.clip(rng.normal(0.93, 0.07, n), 0.7, 1.2) / 100).round(0) * 10078    ventes = pd.DataFrame({79        "id_vente": [f"G{idx+1:04d}" for idx in range(n)], "date_vente": [d.isoformat() for d in dates], "secteur": sect,80        "type": type_bat, "superficie_habitable_m2": S, "superficie_terrain_m2": T, "annee_construction": annee, "age": age,81        "qualite_1a5": qualite, "garage_places": garage, "sous_sol_fini": sous_sol_fini, "piscine": piscine,82        "chambres": chambres, "salles_de_bain": sdb, "lat": lat.round(5), "lon": lon.round(5), "dist_centre_km": dist_centre_km.round(2),83        "valeur_role_2025": role, "prix_vente": prix})84    sujet = {"adresse_fictive": "123, rue des Érables (fictive), secteur Hull, Gatineau", "secteur": "Hull", "type": "à étages",85             "superficie_habitable_m2": 165, "superficie_terrain_m2": 620, "annee_construction": 1998, "age": 28, "qualite_1a5": 3,86             "garage_places": 1, "sous_sol_fini": 1, "piscine": 0, "chambres": 4, "salles_de_bain": 2, "lat": 45.4215, "lon": -75.7395,87             "dist_centre_km": 2.1, "valeur_role_2025": 468_000, "date_evaluation": DATE_EVAL.isoformat(),88             "note": "DONNÉES SYNTHÉTIQUES — propriété fictive construite pour le traité"}89    # devis par composantes (38 postes) : quantités cohérentes avec 165 m² habitables sur 2 étages, empreinte ~ 95 m²90    postes = [91        ("Excavation et remblai", "m³", 420, 28), ("Fondations béton (semelles, murs)", "m³", 38, 520), ("Dalle de sous-sol", "m²", 95, 95),92        ("Drain français et imperméabilisation", "m lin.", 42, 120), ("Charpente murs et planchers", "m²", 330, 165), ("Fermes de toit", "m²", 110, 95),93        ("Couverture bardeaux d'asphalte", "m²", 125, 85), ("Revêtement extérieur (brique 40 % / fibrociment 60 %)", "m²", 240, 165),94        ("Isolation et pare-vapeur", "m²", 420, 32), ("Fenêtres (PVC double vitrage)", "unité", 16, 1450), ("Portes extérieures", "unité", 3, 1900),95        ("Porte de garage", "unité", 1, 2600), ("Gypse et tirage de joints", "m²", 620, 38), ("Peinture intérieure", "m²", 620, 14),96        ("Planchers bois franc", "m²", 95, 120), ("Planchers céramique", "m²", 35, 135), ("Planchers flottants (sous-sol)", "m²", 70, 55),97        ("Escaliers", "unité", 2, 4800), ("Portes intérieures et moulures", "unité", 14, 650), ("Armoires de cuisine et comptoirs", "forfait", 1, 24000),98        ("Vanités et accessoires (2 sdb)", "forfait", 1, 7500), ("Plomberie brute et finition", "appareil", 14, 1650), ("Chauffe-eau", "unité", 1, 1600),99        ("Électricité (entrée 200 A, filage, appareils)", "forfait", 1, 19500), ("Ventilation (VRC) et conduits", "forfait", 1, 7800),100        ("Thermopompe centrale et plinthes", "forfait", 1, 14500), ("Éclairage et finition électrique", "forfait", 1, 4200),101        ("Gouttières et solins", "m lin.", 48, 45), ("Balcon et perron", "m²", 14, 650), ("Entrée asphaltée", "m²", 55, 65),102        ("Aménagement paysager de base", "forfait", 1, 6500), ("Raccordements aqueduc/égout", "forfait", 1, 9500), ("Nettoyage et conteneurs", "forfait", 1, 3800),103        ("Permis et études (arpentage, sol)", "forfait", 1, 6200), ("Supervision de chantier (salaires)", "semaine", 26, 1900),104        ("Assurances chantier et cautionnement", "forfait", 1, 5200), ("Plans et honoraires professionnels", "forfait", 1, 14000), ("Financement intérimaire", "forfait", 1, 11800),105    ]106    cible = cal.get("cout_construction_unifamiliale_par_m2_quebec", 2_900, "$/m² habitable", "coût neuf typique unifamiliale (tout compris)")107    couts = pd.DataFrame(postes, columns=["poste", "unite", "quantite", "prix_unitaire"])108    couts["indirect"] = couts["poste"].isin(["Permis et études (arpentage, sol)", "Supervision de chantier (salaires)", "Assurances chantier et cautionnement", "Plans et honoraires professionnels", "Financement intérimaire"])109    brut = (couts["quantite"] * couts["prix_unitaire"]).sum()110    echelle = cible * 165 / brut111    couts["prix_unitaire"] = (couts["prix_unitaire"] * echelle).round(2)112    couts["montant"] = (couts["quantite"] * couts["prix_unitaire"]).round(2)113    return {"ventes": ventes, "sujet": sujet, "couts": couts}114115# ============================================================================ CAS 2116def cas2_quebec(cal: Calibration, rng: np.random.Generator) -> dict:117    n = 250118    med_plex = cal.get("prix_median_plex_rmr_quebec", 560_000, "$", "prix médian plex 2-5 log. RMR Québec (APCIQ)")119    loyer_2ch = cal.get("loyer_moyen_2ch_rmr_quebec", 1_200, "$/mois", "loyer moyen 2 ch. RMR Québec (SCHL)")120    inocc = cal.get("taux_inoccupation_rmr_quebec", 0.01, "proportion", "taux d'inoccupation RMR Québec (SCHL)")121    g = cal.get("croissance_annuelle_prix_quebec", 0.10, "proportion", "variation annuelle du prix médian plex Québec")122    quartiers = ["Limoilou", "Saint-Sauveur", "Saint-Roch", "Montcalm", "Maizerets"]123    prime = {"Limoilou": 0.0, "Saint-Sauveur": -0.06, "Saint-Roch": 0.02, "Montcalm": 0.15, "Maizerets": -0.09}124    q = rng.choice(quartiers, n, p=[0.35, 0.2, 0.15, 0.1, 0.2])125    nb_log = rng.choice([2, 3], n, p=[0.45, 0.55])126    dates = _dates(rng, n); mois = _mois_depuis(dates)127    annee = np.clip(rng.normal(1945, 18, n).round(0), 1900, 2015).astype(int); age = 2026 - annee128    etat = np.clip(rng.normal(3, 0.8, n).round(0), 1, 5).astype(int)129    sup_hab = (nb_log * np.exp(rng.normal(np.log(85), 0.15, n))).round(0)130    stationnement = rng.binomial(1, 0.5, n)131    # loyers : par logement ~ loyer_2ch × facteur taille × écart contractuel (sous le marché selon l'ancienneté des baux)132    loyer_marche_unit = loyer_2ch * np.exp(rng.normal(0, 0.12, n)) * (1 + 0.05 * (etat - 3)) * np.exp(np.array([prime[x] for x in q]) * 0.5)133    anciennete_moy = np.clip(rng.exponential(5, n), 0, 25)134    ecart = -0.02 * anciennete_moy                           # ~2 % par année d'ancienneté sous le marché (hypothèse)135    loyer_contrat_unit = loyer_marche_unit * (1 + ecart)136    rbp = loyer_contrat_unit * nb_log * 12137    ratio_dep = np.clip(rng.normal(0.36, 0.05, n), 0.25, 0.5)138    rne = rbp * (1 - inocc) * (1 - ratio_dep)139    ref_mois = _mois_depuis([datetime.date(2024, 7, 1)])[0]140    ln_index = np.log1p(g) * (mois - ref_mois) / 12141    lnP = (np.log(med_plex) + 0.65 * np.log(sup_hab / (2.55 * 85)) + 0.12 * (nb_log - 2.55) - 0.004 * (age - 80) + 0.05 * (etat - 3)142           + np.array([prime[x] for x in q]) + 0.03 * stationnement + 0.25 * np.log(np.clip(loyer_marche_unit / loyer_2ch, 0.5, 2))143           + ln_index + rng.normal(0, 0.09, n))144    lnP += np.log(med_plex) - np.median(lnP - ln_index)   # normalisation à la médiane calibrée (date de référence)145    prix = (np.exp(lnP) / 1000).round(0) * 1000146    ventes = pd.DataFrame({"id_vente": [f"Q{i+1:04d}" for i in range(n)], "date_vente": [d.isoformat() for d in dates], "quartier": q,147        "nb_logements": nb_log, "superficie_habitable_m2": sup_hab, "annee_construction": annee, "age": age, "etat_1a5": etat,148        "stationnement": stationnement, "loyer_mensuel_moyen_contrat": loyer_contrat_unit.round(0), "loyer_mensuel_moyen_marche": loyer_marche_unit.round(0),149        "revenu_brut_potentiel": rbp.round(0), "ratio_depenses": ratio_dep.round(3), "rne": rne.round(0), "prix_vente": prix})150    ventes["mrb"] = (ventes["prix_vente"] / ventes["revenu_brut_potentiel"]).round(2)151    ventes["taux_global"] = (ventes["rne"] / ventes["prix_vente"]).round(4)152    ventes["prix_par_logement"] = (ventes["prix_vente"] / ventes["nb_logements"]).round(0)153    loyers = pd.DataFrame([154        {"logement": "RC", "pieces": "5½", "chambres": 2, "superficie_m2": 92, "loyer_mensuel": round(loyer_2ch * 0.82), "debut_bail": "2019-07-01", "chauffage_inclus": 0, "locataire_depuis": 2019},155        {"logement": "2e", "pieces": "5½", "chambres": 2, "superficie_m2": 92, "loyer_mensuel": round(loyer_2ch * 0.93), "debut_bail": "2022-07-01", "chauffage_inclus": 0, "locataire_depuis": 2022},156        {"logement": "3e", "pieces": "4½", "chambres": 1, "superficie_m2": 78, "loyer_mensuel": round(loyer_2ch * 0.98), "debut_bail": "2025-07-01", "chauffage_inclus": 0, "locataire_depuis": 2025},157    ])158    loyers["loyer_marche_estime"] = [round(loyer_2ch * 1.12), round(loyer_2ch * 1.12), round(loyer_2ch * 0.98)]159    sujet = {"adresse_fictive": "456, 3e Avenue (fictive), Limoilou, Québec", "quartier": "Limoilou", "nb_logements": 3, "annee_construction": 1948, "age": 78,160             "etat_1a5": 3, "superficie_habitable_m2": 262, "superficie_terrain_m2": 310, "stationnement": 1, "valeur_role_2025": 495_000,161             "date_evaluation": DATE_EVAL.isoformat(), "note": "DONNÉES SYNTHÉTIQUES — immeuble fictif"}162    depenses = pd.DataFrame([163        {"poste": "Taxes municipales", "montant_annuel": 5_900, "type": "fixe"}, {"poste": "Taxe scolaire", "montant_annuel": 470, "type": "fixe"},164        {"poste": "Assurances", "montant_annuel": 3_400, "type": "fixe"}, {"poste": "Électricité (aires communes)", "montant_annuel": 650, "type": "variable"},165        {"poste": "Déneigement et entretien extérieur", "montant_annuel": 1_500, "type": "variable"}, {"poste": "Entretien et réparations", "montant_annuel": 3_200, "type": "variable"},166        {"poste": "Gestion (propriétaire-occupant : imputée 4 %)", "montant_annuel": 0, "type": "imputée"}, {"poste": "Réserve de remplacement", "montant_annuel": 1_800, "type": "réserve"}])167    return {"ventes": ventes, "sujet": sujet, "loyers": loyers, "depenses": depenses}168169# ============================================================================ CAS 3170def cas3_montreal(cal: Calibration, rng: np.random.Generator) -> dict:171    loyer_2ch = cal.get("loyer_moyen_2ch_rmr_montreal", 1_250, "$/mois", "loyer moyen 2 ch. RMR Montréal (SCHL)")172    loyer_1ch = cal.get("loyer_moyen_1ch_rmr_montreal", 1_050, "$/mois", "loyer moyen 1 ch. RMR Montréal (SCHL)")173    loyer_3ch = cal.get("loyer_moyen_3ch_rmr_montreal", 1_550, "$/mois", "loyer moyen 3 ch.+ RMR Montréal (SCHL)")174    inocc = cal.get("taux_inoccupation_rmr_montreal", 0.02, "proportion", "taux d'inoccupation RMR Montréal (SCHL)")175    cap = cal.get("taux_capitalisation_multires_montreal", 0.0475, "proportion", "taux de capitalisation multirésidentiel Montréal (enquête publique)")176    taux_hyp = cal.get("taux_hypothecaire_multires_assure", 0.042, "proportion", "taux assuré SCHL 5 ans (indicatif)")177    taux_taxe = cal.get("taux_taxe_fonciere_montreal_residentiel_2026", 0.0065, "$/$ d'évaluation", "taux global résidentiel Montréal")178    taux_scolaire = cal.get("taux_taxe_scolaire_2025_2026", 0.0009152, "$/$ d'évaluation", "taux unique de la taxe scolaire 2025-2026")179    pct_tal = cal.get("tal_pourcentage_base_2026", 0.039, "proportion", "pourcentage de base TAL 2026 (logements non chauffés)")180    # état locatif : 12 logements, bâtiment 1962, 3 étages, Rosemont181    mix = [("3½", 1, loyer_1ch)] * 4 + [("4½", 2, loyer_2ch)] * 6 + [("5½", 3, loyer_3ch)] * 2182    rows = []183    for i, (p, ch, lm) in enumerate(mix, 1):184        anc = int(rng.choice([0, 1, 2, 3, 5, 8, 12, 18], p=[0.1, 0.15, 0.15, 0.15, 0.15, 0.12, 0.1, 0.08]))185        marche = lm * float(np.exp(rng.normal(0, 0.05)))186        contrat = marche * (1 - 0.025) ** anc * (1 if anc else 1.0)187        rows.append({"logement": f"{100 + i}" if i <= 4 else (f"{200 + i - 4}" if i <= 8 else f"{300 + i - 8}"), "pieces": p, "chambres": ch,188                     "superficie_m2": {1: 52, 2: 72, 3: 95}[ch] + int(rng.integers(-4, 5)), "loyer_mensuel_contrat": round(contrat), "loyer_mensuel_marche": round(marche),189                     "locataire_depuis": 2026 - anc, "echeance_bail": "2027-06-30", "chauffage_inclus": 0, "stationnement": int(rng.random() < 0.5)})190    etat_locatif = pd.DataFrame(rows)191    rbp = etat_locatif["loyer_mensuel_contrat"].sum() * 12192    autres = 12 * (6 * 60 + 12 * 35)  # stationnement (6 places × 60 $), buanderie (35 $/log.)193    valeur_role = 2_650_000194    dep = {195        "Taxes municipales": valeur_role * taux_taxe, "Taxe scolaire": valeur_role * taux_scolaire, "Assurances": 14_800,196        "Électricité (aires communes)": 3_900, "Chauffage (aires communes, gaz)": 2_600, "Eau et égouts (taxe)": 0,197        "Entretien et réparations": 0.065 * rbp, "Conciergerie": 9_600, "Déneigement et paysagement": 6_200, "Gestion (4 % du RBE)": None,198        "Frais juridiques et comptables": 2_400, "Publicité et location": 1_200, "Réserve de remplacement (par logement)": 12 * 650}199    hist = []200    for an, infl in [(2023, 0.0), (2024, 0.045), (2025, 0.03)]:201        for poste, m in dep.items():202            if m is None: continue203            base = m * (1 + infl) ** (an - 2023) * float(np.exp(rng.normal(0, 0.04))) if m else 0204            hist.append({"annee": an, "poste": poste, "montant": round(base)})205    depenses = pd.DataFrame(hist)206    # comparables : 40 ventes 6-30 logements, 2024-2026, Rosemont/Villeray/Plateau/Hochelaga/Ahuntsic207    n = 40208    quart = rng.choice(["Rosemont", "Villeray", "Plateau", "Hochelaga", "Ahuntsic"], n, p=[0.3, 0.25, 0.15, 0.15, 0.15])209    prime = {"Rosemont": 0.0, "Villeray": -0.01, "Plateau": 0.06, "Hochelaga": -0.07, "Ahuntsic": -0.03}210    nlog = rng.integers(6, 31, n)211    dates = _dates(rng, n, datetime.date(2024, 9, 1), datetime.date(2026, 8, 31))212    annee = np.clip(rng.normal(1960, 15, n).round(0), 1900, 2010).astype(int)213    etat = np.clip(rng.normal(3, 0.7, n).round(0), 1, 5).astype(int)214    loyer_moy = loyer_2ch * np.exp(rng.normal(-0.05, 0.1, n)) * (1 + 0.04 * (etat - 3))215    rbp_c = loyer_moy * nlog * 12216    ratio = np.clip(rng.normal(0.40, 0.04, n), 0.3, 0.52)217    rne_c = rbp_c * (1 - inocc) * (1 - ratio)218    taux_c = cap * np.exp(rng.normal(0, 0.07, n)) * (1 - 0.03 * (etat - 3)) * (1 - 0.004 * (nlog - 12)) * np.exp(-np.array([prime[x] for x in quart]) * 0.5)219    prix_c = (rne_c / taux_c / 1000).round(0) * 1000220    comps = pd.DataFrame({"id_vente": [f"M{i+1:03d}" for i in range(n)], "date_vente": [d.isoformat() for d in dates], "quartier": quart, "nb_logements": nlog,221        "annee_construction": annee, "etat_1a5": etat, "loyer_mensuel_moyen": loyer_moy.round(0), "revenu_brut_potentiel": rbp_c.round(0),222        "ratio_depenses": ratio.round(3), "rne": rne_c.round(0), "prix_vente": prix_c})223    comps["taux_global"] = (comps["rne"] / comps["prix_vente"]).round(4)224    comps["prix_par_logement"] = (comps["prix_vente"] / comps["nb_logements"]).round(0)225    comps["mrb"] = (comps["prix_vente"] / comps["revenu_brut_potentiel"]).round(2)226    hypotheses = {"date_evaluation": DATE_EVAL.isoformat(), "horizon_ans": 10, "taux_inoccupation_pertes": inocc + 0.01,227                  "croissance_loyers_contrat": pct_tal, "croissance_loyers_marche": 0.035, "rotation_annuelle": 0.15,228                  "croissance_depenses": 0.03, "taux_actualisation": 0.0725, "taux_terminal": cap + 0.005, "couts_vente": 0.025,229                  "capex": {"2027": 85_000, "2029": 40_000, "2032": 120_000}, "financement": {"rpv": 0.75, "taux": taux_hyp, "amortissement_mois": 480, "terme_ans": 5, "rcd_min": 1.20},230                  "autres_revenus_annuels": autres, "valeur_role_2026": valeur_role, "taux_taxe_fonciere": taux_taxe, "taux_taxe_scolaire": taux_scolaire,231                  "note": "HYPOTHÈSES SYNTHÉTIQUES — voir data/synthetic/README.md pour la provenance de chaque paramètre"}232    sujet = {"adresse_fictive": "789, rue Fictive, Rosemont–La Petite-Patrie, Montréal", "nb_logements": 12, "annee_construction": 1962, "etages": 3,233             "superficie_terrain_m2": 740, "superficie_locative_m2": int(etat_locatif["superficie_m2"].sum()), "stationnements": 6, "etat_1a5": 3,234             "valeur_role_2026": valeur_role, "date_evaluation": DATE_EVAL.isoformat(), "note": "DONNÉES SYNTHÉTIQUES — immeuble fictif"}235    return {"etat_locatif": etat_locatif, "depenses": depenses, "comparables": comps, "hypotheses": hypotheses, "sujet": sujet}236237# ============================================================================ CAS 4238def cas4_portefeuille(cal: Calibration, rng: np.random.Generator) -> dict:239    rmr = {"Montréal": (45.52, -73.60, cal.get("loyer_moyen_2ch_rmr_montreal", 1_250), cal.get("taux_inoccupation_rmr_montreal", 0.02), cal.get("taux_capitalisation_multires_montreal", 0.0475)),240           "Québec": (46.81, -71.22, cal.get("loyer_moyen_2ch_rmr_quebec", 1_200), cal.get("taux_inoccupation_rmr_quebec", 0.01), cal.get("taux_capitalisation_multires_quebec", 0.0525, "proportion", "taux de cap. multirés. Québec")),241           "Gatineau": (45.47, -75.70, cal.get("loyer_moyen_2ch_rmr_gatineau", 1_450, "$/mois", "loyer moyen 2 ch. RMR Gatineau (SCHL)"), cal.get("taux_inoccupation_rmr_gatineau", 0.015, "proportion", "inocc. Gatineau"), 0.05),242           "Sherbrooke": (45.40, -71.89, cal.get("loyer_moyen_2ch_rmr_sherbrooke", 1_100, "$/mois", "loyer moyen 2 ch. Sherbrooke"), cal.get("taux_inoccupation_rmr_sherbrooke", 0.01, "proportion", "inocc. Sherbrooke"), 0.055),243           "Trois-Rivières": (46.35, -72.55, cal.get("loyer_moyen_2ch_rmr_trois_rivieres", 1_000, "$/mois", "loyer moyen 2 ch. Trois-Rivières"), cal.get("taux_inoccupation_rmr_trois_rivieres", 0.01, "proportion", "inocc. Trois-Rivières"), 0.06),244           "Saguenay": (48.43, -71.07, cal.get("loyer_moyen_2ch_rmr_saguenay", 950, "$/mois", "loyer moyen 2 ch. Saguenay"), cal.get("taux_inoccupation_rmr_saguenay", 0.01, "proportion", "inocc. Saguenay"), 0.0625)}245    n = 48246    villes = rng.choice(list(rmr), n, p=[0.35, 0.2, 0.15, 0.1, 0.1, 0.1])247    rows = []248    for i, v in enumerate(villes, 1):249        lat0, lon0, l2, inocc, capv = rmr[v]250        nlog = int(rng.choice([6, 8, 12, 16, 24, 36, 48, 72], p=[0.2, 0.2, 0.2, 0.15, 0.1, 0.08, 0.05, 0.02]))251        annee = int(np.clip(rng.normal(1972, 18), 1920, 2018)); etat = int(np.clip(round(rng.normal(3, 0.8)), 1, 5))252        loyer = l2 * float(np.exp(rng.normal(-0.03, 0.1))) * (1 + 0.04 * (etat - 3))253        rbp = loyer * nlog * 12; ratio = float(np.clip(rng.normal(0.40, 0.04), 0.3, 0.52)); rne = rbp * (1 - inocc - 0.005) * (1 - ratio)254        zone_inond = int(rng.random() < 0.08); sol = int(rng.random() < 0.05)255        taux = capv * float(np.exp(rng.normal(0, 0.06))) * (1 - 0.03 * (etat - 3)) * (1 + 0.05 * zone_inond) * (1 + 0.03 * sol)256        valeur = rne / taux257        dette = valeur * float(np.clip(rng.normal(0.6, 0.12), 0.2, 0.8))258        rows.append({"id_immeuble": f"P{i:02d}", "rmr": v, "lat": round(lat0 + rng.normal(0, 0.04), 5), "lon": round(lon0 + rng.normal(0, 0.05), 5), "nb_logements": nlog,259                     "annee_construction": annee, "age": 2026 - annee, "etat_1a5": etat, "loyer_mensuel_moyen": round(loyer), "revenu_brut_potentiel": round(rbp),260                     "taux_inoccupation_marche": inocc, "ratio_depenses": round(ratio, 3), "rne_2026": round(rne), "zone_inondable": zone_inond, "contamination_sol": sol,261                     "taux_global": round(taux, 4), "valeur_2026": round(valeur / 1000) * 1000, "dette": round(dette / 1000) * 1000,262                     "taux_dette": round(float(rng.normal(0.045, 0.006)), 4), "echeance_dette": int(rng.integers(2027, 2032))})263    pf = pd.DataFrame(rows)264    # séries trimestrielles de RNE 2016T1-2026T2 : tendance régionale commune + choc idiosyncrasique265    trims = pd.period_range("2016Q1", "2026Q2", freq="Q")266    g_reg = {"Montréal": 0.035, "Québec": 0.03, "Gatineau": 0.04, "Sherbrooke": 0.03, "Trois-Rivières": 0.025, "Saguenay": 0.02}267    ser = []268    for _, r in pf.iterrows():269        t = np.arange(len(trims)); g = g_reg[r["rmr"]]270        base = r["rne_2026"] / (1 + g) ** ((len(trims) - 1) / 4)271        chocs = np.cumsum(rng.normal(0, 0.015, len(trims)))272        pandemie = np.where((trims >= "2020Q2") & (trims <= "2021Q2"), -0.03, 0)273        x = base * (1 + g) ** (t / 4) * np.exp(chocs - chocs[-1] + pandemie) / 4274        for tt, val in zip(trims, x):275            ser.append({"id_immeuble": r["id_immeuble"], "trimestre": str(tt), "rne_trimestriel": round(val)})276    series = pd.DataFrame(ser)277    return {"portefeuille": pf, "series": series}278279# ============================================================================ écriture280def ecrire_tout() -> dict:281    OUT.mkdir(parents=True, exist_ok=True)282    cal = Calibration()283    rng = np.random.default_rng(SEED)284    c1 = cas1_gatineau(cal, rng); c2 = cas2_quebec(cal, rng); c3 = cas3_montreal(cal, rng); c4 = cas4_portefeuille(cal, rng)285    c1["ventes"].to_csv(OUT / "cas1_gatineau_ventes.csv", index=False); (OUT / "cas1_gatineau_sujet.json").write_text(json.dumps(c1["sujet"], ensure_ascii=False, indent=1))286    c1["couts"].to_csv(OUT / "cas1_gatineau_couts.csv", index=False)287    c2["ventes"].to_csv(OUT / "cas2_quebec_ventes_plex.csv", index=False); (OUT / "cas2_quebec_sujet.json").write_text(json.dumps(c2["sujet"], ensure_ascii=False, indent=1))288    c2["loyers"].to_csv(OUT / "cas2_quebec_loyers.csv", index=False); c2["depenses"].to_csv(OUT / "cas2_quebec_depenses.csv", index=False)289    c3["etat_locatif"].to_csv(OUT / "cas3_montreal_etat_locatif.csv", index=False); c3["depenses"].to_csv(OUT / "cas3_montreal_depenses.csv", index=False)290    c3["comparables"].to_csv(OUT / "cas3_montreal_comparables.csv", index=False)291    (OUT / "cas3_montreal_hypotheses_dcf.json").write_text(json.dumps(c3["hypotheses"], ensure_ascii=False, indent=1)); (OUT / "cas3_montreal_sujet.json").write_text(json.dumps(c3["sujet"], ensure_ascii=False, indent=1))292    c4["portefeuille"].to_csv(OUT / "cas4_portefeuille.csv", index=False); c4["series"].to_csv(OUT / "cas4_portefeuille_series.csv", index=False)293    # journal de calibration dédoublonné294    seen = {}; 295    for j in cal.journal: seen.setdefault(j["parametre"], j)296    jour = pd.DataFrame(list(seen.values()))297    jour.to_csv(OUT / "calibration_journal.csv", index=False)298    n_cal = int((jour["statut"] == "calibre").sum()); n_prov = int((jour["statut"] == "provisoire").sum())299    readme = f"""# Données SYNTHÉTIQUES du traité — générées le {datetime.date.today().isoformat()} (graine {SEED})300301**Aucun fichier de ce dossier ne décrit une propriété, une transaction, un locataire ou un immeuble réel.** Les jeux sont produits par302`analysis/python/traite/synth.py` (`python3 -m analysis.python.cases.generate_all`). Les ordres de grandeur (prix médians, loyers, taux303d'inoccupation, taux de capitalisation, taux de taxe, coûts unitaires) sont calibrés sur des publications publiques consignées dans304`calibration.json` et `calibration_journal.csv` ({n_cal} paramètres calibrés avec source, {n_prov} paramètres PROVISOIRES marqués NON_VERIFIE).305Les relations entre variables (élasticités, primes de secteur, décote d'ancienneté des baux, lois de tirage) sont des hypothèses pédagogiques306énoncées ci-dessous ; elles sont vraisemblables mais ne résultent pas d'une estimation sur données réelles.307308## Dossiers309| Fichier | Lignes | Contenu |310|---|---|---|311| cas1_gatineau_ventes.csv | {len(c1['ventes'])} | ventes unifamiliales 2023-01 → 2026-08, 5 secteurs, attributs, coordonnées synthétiques, valeur au rôle fictive |312| cas1_gatineau_sujet.json | 1 | sujet : cottage 1998, 165 m², Hull |313| cas1_gatineau_couts.csv | {len(c1['couts'])} | devis par composantes (quantités × prix unitaires), postes directs/indirects |314| cas2_quebec_ventes_plex.csv | {len(c2['ventes'])} | ventes de 2-3 logements, 5 quartiers, loyers, RNE, MRB, taux |315| cas2_quebec_sujet.json / _loyers.csv / _depenses.csv | 1 / 3 / 8 | triplex 1948 Limoilou : 3 baux, dépenses |316| cas3_montreal_etat_locatif.csv | 12 | état locatif (contrat vs marché, ancienneté) |317| cas3_montreal_depenses.csv | {len(c3['depenses'])} | historique 2023-2025 par poste |318| cas3_montreal_comparables.csv | {len(c3['comparables'])} | ventes 6-30 logements 2024-09 → 2026-08, RNE, taux |319| cas3_montreal_hypotheses_dcf.json | 1 | hypothèses DCF, financement, CAPEX |320| cas4_portefeuille.csv | {len(c4['portefeuille'])} | 48 immeubles, 6 RMR, risques, dette |321| cas4_portefeuille_series.csv | {len(c4['series'])} | RNE trimestriel 2016T1 → 2026T2 |322323## Lois de génération (hypothèses pédagogiques)324- Cas 1 : ln P = ln(médiane) + 0,55 ln(S/140) + 0,10 ln(T/560) − 0,0055·âge + 0,00003·âge² + 0,06·(qualité−3) + 0,035·garage + 0,03·sous-sol + 0,02·piscine + prime de secteur (Hull 0, Gatineau −5 %, Aylmer +8 %, Buckingham −15 %, Masson-Angers −18 %) − 0,006·max(dist−3, 0) + 0,02·étages + indice temporel (croissance annuelle composée, référence 2024-07-01) + ε, σ = 0,085 ; prix arrondis à 500 $ ; valeur au rôle = valeur à la date de référence × N(0,93 ; 0,07).325- Cas 2 : loyer de marché par logement = loyer 2 ch. SCHL × N(0 ; 0,12) × (1 + 0,05(état−3)) × ½ prime de quartier ; loyer contractuel = marché × (1 − 0,02 × ancienneté moyenne) ; ratio de dépenses N(0,36 ; 0,05) ; ln P fonction de la superficie, du nombre de logements, de l'âge, de l'état, du quartier, du stationnement, du niveau de loyer et du temps, σ = 0,09.326- Cas 3 : 12 baux (4 × 3½, 6 × 4½, 2 × 5½), loyers contractuels = marché × 0,975^ancienneté ; dépenses par poste avec inflation 2024 +4,5 %, 2025 +3 % ; comparables : taux global = taux de référence × exp(N(0 ; 0,07)) × (1 − 0,03(état−3)) × (1 − 0,004(n−12)) × exp(−½ prime).327- Cas 4 : 48 immeubles ; taux global régional × état × (1 + 5 % si zone inondable) × (1 + 3 % si contamination) ; séries de RNE : croissance régionale + marche aléatoire (σ 1,5 %/trim.) + choc pandémique −3 % (2020T2-2021T2).328329## Paramètres et provenance330{jour.to_markdown(index=False)}331"""332    (OUT / "README.md").write_text(readme)333    return {"cas1": c1, "cas2": c2, "cas3": c3, "cas4": c4, "journal": jour}334
Files · 111d8441deb095c7e00ec3b957dd296ef7f724fa
Menu

Theme