SPB Git

Repository Public Research preview

calculs_s09.py 59.9 KB 697 lines python

1#!/usr/bin/env python32"""IMM1033 — Séance 9 (refonte 2026-11-08) : moteur de calcul unique de la séance « Concepts de dépréciation ».34Tout chiffre du deck et du chapitre 9 des notes vient d'ici. Le script :5  1. lit les DONNÉES RÉELLES exportées d'UQO Éval (M2U64, vraiprix.db : transactions du Registre foncier ⋈ rôle MAMH ;6     immoka.db : fiche Centris/RE/MAX du sujet ; Cas 1 du module Quantity Survey : lignes RSMeans 2026 T4 localisées Gatineau) ;7  2. construit le coût de remplacement neuf du sujet (99, rue de Magog, Gatineau) avec la chaîne du cours (séances 7–8, TP2) ;8  3. applique les quatre méthodes (âge-vie, âge-vie modifiée, ventilation, extraction du marché) et les réconcilie ;9  4. calcule les illustrations (modèles linéaire / exponentiel / non linéaire, curabilité, profil empirique de 655 ventes) ;10  5. écrit : RESULTATS_S09.md, resultats_s09.json, les macros LaTeX (parties/s09_valeurs.tex), les tableaux générés11     (parties/s09_tables.tex) et les fichiers de données pgfplots (sources/data/s09_*.dat).1213Conventions : arrondi au dollar (ROUND_HALF_UP) à chaque ligne ; montants « 314~600 » dans les macros ; pourcentages à 0,1.14Toute hypothèse pédagogique est étiquetée HYP dans le dictionnaire HYPOTHESES (reprise telle quelle dans le deck).15Exécuter : python3 calculs_s09.py16"""17from __future__ import annotations18import csv, json, math, os, statistics as st19from decimal import Decimal, ROUND_HALF_UP20import numpy as np2122HERE = os.path.dirname(os.path.abspath(__file__))23DATA = os.path.join(HERE, "data")24SRC = os.path.normpath(os.path.join(HERE, "..", "..", "..", "IMM1033-20_Methodes_du_cout", "02_Slides", "sources"))25PARTIES = os.path.join(SRC, "parties")26PGFDATA = os.path.join(SRC, "data")27os.makedirs(PGFDATA, exist_ok=True)2829# ----------------------------------------------------------------- utilitaires30def r0(x): return int(Decimal(str(x)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))31def r1(x): return float(Decimal(str(x)).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP))32def r2(x): return float(Decimal(str(x)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))33def arrondi(x, pas=5000): return int(pas * Decimal(str(x / pas)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))34def f(x):  # 314600 -> « 314~600 » (LaTeX) ; négatifs avec $-$35    s = f"{abs(int(round(x))):,}".replace(",", "~")36    return ("$-$" if x < 0 else "") + s37def fm(x):  # Markdown : espace fine38    s = f"{abs(int(round(x))):,}".replace(",", " ")39    return ("−" if x < 0 else "") + s40def pct(x, d=1):  # 0.2345 -> « 23,5 »41    return (f"{x*100:.{d}f}").replace(".", ",")42def dec(x, d=2): return (f"{x:.{d}f}").replace(".", ",")4344# =================================================================== 1. DONNÉES RÉELLES45# ---- Sujet : 99, rue de Magog, Gatineau (secteur Gatineau, La Sablonnière)46SUJET = {47    "adresse": "99, rue de Magog, Gatineau (secteur Gatineau, quartier La Sablonnière)",48    "id_provinc": "81017753988876200000000", "matricule_role": "rôle 2024-2026 de Gatineau (conditions du marché au 1er juillet 2022)",49    "annee": 1985, "aire_m2": 96.2, "aire_pi2": 1035, "lot_m2": 545.9, "lot_pi2": 5873, "front_m": 16.17,50    "vt_role": 180400, "vb_role": 198500, "vr_role": 378900,51    "vente_date": "2026-01-30", "vente_prix": 447000,                      # Registre foncier (transactions UQO Éval)52    "remise_en_vente_date": "2026-08-17", "prix_demande": 455000,         # RE/MAX Québec, fiche Centris 10348445 (Immo-Ka)53    "fiche": "5 chambres (2 au RDC, 3 au sous-sol), 2 salles de bains, sous-sol totalement aménagé (6 pi et plus), brique et vinyle, "54             "plinthes électriques + thermopompe murale, fenêtres à manivelle et porte-fenêtre, allée asphaltée (4 places), "55             "fondation béton coulé, terrain clôturé, sans garage",56    "description_courtier": "Bien entretenue au fil des années, elle pourra bénéficier d'un léger rajeunissement",57    "uqoeval_est": 441100, "uqoeval_cout": 497400, "uqoeval_role_indexe": 452200,   # estimations UQO Éval (immoka.db uqo_eval / units.est_2026)58    "date_evaluation": "12 novembre 2026",59}60AGE_CHRONO = 2026 - SUJET["annee"]  # 41 ans6162# ---- Cas 1 UQO Éval (module Quantity Survey) : bungalow 3 chambres, Gatineau, RSMeans 2026 T4 localisé (C57), révision figée63CAS1 = json.load(open(os.path.join(DATA, "cas1_totaux.json"), encoding="utf-8"))64LIGNES = list(csv.DictReader(open(os.path.join(DATA, "cas1_lignes_rsmeans.csv"), encoding="utf-8")))65for l in LIGNES:66    for k in ("quantity", "unitDirect", "material", "labour", "equipment", "direct", "located"):67        l[k] = float(l[k])68CAS1_NUS = CAS1["totals"]["located"]          # 171 612,74 $69CAS1_DC = CAS1["totals"]["total"]             # 222 761,57 $ (directs du cours + contingences)70CAS1_EMPREINTE = CAS1["footprint_m2"]         # 117,5 m²7172# ---- 655 ventes réelles de plain-pieds détachés (Gatineau, nov. 2025 → août 2026), Registre foncier ⋈ rôle 2024-202673VENTES = []74for r in csv.DictReader(open(os.path.join(DATA, "ventes_gatineau_plainpied.csv"), encoding="utf-8")):75    try:76        v = dict(date=r["date"], prix=float(r["amount"]), rue=r["street"], annee=int(float(r["annee_construction"])),77                 aire=float(r["aire_etages_m2"]), lot=float(r["superficie_terrain_m2"]), vt=float(r["valeur_terrain"]),78                 vb=float(r["valeur_batiment"]), vr=float(r["valeur_role"]), lat=float(r["lat"]), lng=float(r["lng"]), uv=r["unite_voisinage"])79    except (ValueError, KeyError):80        continue81    v["age"] = 2026 - v["annee"]82    VENTES.append(v)83N_VENTES = len(VENTES)84RATIO_PRIX_ROLE = st.median(v["prix"] / v["vr"] for v in VENTES)        # ≈ 1,11 : indexation rôle (juill. 2022) → marché 20268586# ---- Terrains vacants (Registre foncier ⋈ rôle « espace de terrain non aménagé », Gatineau, 2025-2026)87TERRAINS = []88la0, lo0 = 45.4951, -75.590589for r in csv.DictReader(open(os.path.join(DATA, "terrains_gatineau.csv"), encoding="utf-8")):90    try:91        a = float(r["amount"]); s = float(r["superficie_terrain_m2"] or 0); la = float(r["lat"]); lo = float(r["lng"]); vt = float(r["valeur_terrain"] or 0)92    except ValueError:93        continue94    if s <= 0 or vt <= 0:95        continue96    d = math.hypot((la - la0) * 111320, (lo - lo0) * 111320 * math.cos(math.radians(la0)))97    if 250 <= s <= 2500 and a / s < 1200 and a / vt < 2.5 and "Logement" not in r["cubf_libelle"]:   # exclut les maisons neuves bâties après le rôle98        TERRAINS.append(dict(dist=d, prix=a, lot=s, pm2=a / s, ratio=a / vt, date=r["date"], rue=r["street"], vt=vt))99TERRAINS.sort(key=lambda t: t["dist"])100TERRAINS_5KM = [t for t in TERRAINS if t["dist"] <= 5000]101RATIO_TERRAIN = st.median(t["ratio"] for t in TERRAINS_5KM)   # ≈ 1,22 : ratio prix vendu / valeur au rôle du terrain (convention TP1)102PM2_TERRAIN = st.median(t["pm2"] for t in TERRAINS_5KM)103104# =================================================================== 2. HYPOTHÈSES PÉDAGOGIQUES (étiquetées HYP dans le deck)105HYP = {106    "chaine": "conventions du cours (TP2, séance 8) : CG 8 %, FG 7 %, profit EG 8 % en cascade ; contingences 5 % des nus ; "107              "permis Gatineau max(4,70 $/1 000 $ ; 335 $/logement) ; GCR 2 656,68 $ (unifamiliale visée) ; intérêts intercalaires "108              "(D+C) × 6,75 % × 6/12 × ½ ; honoraires 4 % des directs du cours ; assurance 0,8 % de D+C ; juridique 4 000 $ ; profit entrepreneurial 11 %",109    "duree_travaux_mois": 6, "taux_pret": 6.75, "juridique": 4000,110    "terrain": "valeur du terrain = valeur au rôle × ratio médian prix/rôle des terrains vacants vendus à ≤ 5 km (2025-2026), arrondie au 1 000 $",111    "ajouts_sujet": {  # ordres de grandeur 2026 (prix installés, niveau « directs nus ») — HYP112        "Thermopompe murale 12 000 BTU installée": 4500,113        "Allée asphaltée 4 places (65 m² × 60 $/m²)": 3900,114        "Terrasse de bois 20 m² (450 $/m²)": 9000,115        "Clôture 45 m (85 $/m)": 3825,116        "Remise 8 m²": 3500,117    },118    "inspection": "constats d'inspection HYPOTHÉTIQUES, cohérents avec la fiche et les photos (août 2026) : toiture 2013, fenêtres et portes 2009, "119                  "chauffe-eau 2021, thermopompe 2020, plinthes électriques 2005, cuisine et salle de bain du RDC d'origine (1985), "120                  "sous-sol aménagé en 2006 (salle de bain comprise), finitions du RDC 2015, terrasse 2018, clôture et remise 2012",121    "entretien_differe": {"Remplacement du revêtement de vinyle décoloré et fendu (faces latérales et arrière)": 9800,122                          "Injection d'une fissure de fondation (mur arrière)": 1800,123                          "Resurfaçage de l'allée asphaltée fissurée": 4000,124                          "Peinture des soffites, bordures et portes extérieures": 2500},125    "cuisine_cout_correction": 28000, "garage_perte_valeur": 15000, "garage_cout_ajout": 45000,126    "vrc_cout_ajout": 4200,127    "AE_LT": 30, "VE": 75, "AE_global": 30,128    "ve_note": "vie économique totale retenue : 75 ans — borne haute de la fourchette résidentielle (bois, 50–75 ans), justifiée par "129               "l'extraction : le marché de Gatineau déprécie lentement le bâti ancien bien situé",130    "externe": "aucune nuisance documentée (rue résidentielle, pas d'artère, ni ligne à haute tension, ni zone inondable) ; "131               "test par paires dans l'unité de voisinage : aucun écart systématique → D_externe = 0 $",132}133134# =================================================================== 3. COÛT DE REMPLACEMENT NEUF DU SUJET135K = SUJET["aire_m2"] / CAS1_EMPREINTE  # 0,8187 : mise à l'échelle des quantités du Cas 1 (même programme, surface moindre)136137def lignes_par(mots):138    return [l for l in LIGNES if any(m.lower() in l["description"].lower() for m in mots)]139140def somme(ls): return sum(l["located"] for l in ls)141142# Composantes du Cas 1 (nus localisés), mises à l'échelle K ; classement CT / LT selon AoRE (short-lived vs long-lived)143COMP_CAS1 = {144    "Fondations, structure et enveloppe (LT)": {"wbs": ["03", "05", "06", "31", "33"], "mots": ["Isolation", "Membrane", "Gypse", "Escalier", "brick", "Perron"]},145    "Toiture (bardeaux, soffites, gouttières)": {"mots": ["Couverture", "Soffites", "Gouttières"]},146    "Fenêtres et portes extérieures": {"mots": ["Fenêtres —", "Fenêtres de sous-sol", "Portes extérieures", "Portes-patio"]},147    "Revêtement de vinyle": {"mots": ["Revêtement vinyl"]},148    "Installations électriques (LT)": {"wbs": ["26"]},149    "Plomberie brute et drainage (LT)": {"mots": ["Plomberie brute", "Drainage"]},150    "Chauffe-eau": {"mots": ["Chauffe-eau"]},151    "Appareils de la salle de bain (RDC)": {"mots": ["Salles de bain complètes"]},152    "Plinthes électriques": {"mots": ["Plinthes électriques"]},153    "Échangeur d'air (VRC)": {"mots": ["Échangeur"]},154    "Cuisine (armoires, comptoirs, évier)": {"wbs": ["11"]},155    "Finitions intérieures du RDC": {"mots": ["Peinture", "Céramique", "Plancher hardwood", "Plancher vinyl", "Plinthes —", "Tablettes", "Portes intérieures", "Portes de garde-robe"]},156}157# Résolution sans double affectation : on affecte chaque ligne à la première composante qui la réclame (ordre ci-dessus),158# les composantes « wbs » attrapant ce qui reste de leur division.159affect = {}160for nom, spec in COMP_CAS1.items():161    for l in LIGNES:162        if id(l) in affect: continue163        if any(m.lower() in l["description"].lower() for m in spec.get("mots", [])):164            affect[id(l)] = nom165for nom, spec in COMP_CAS1.items():166    for l in LIGNES:167        if id(l) in affect: continue168        if l["wbs"] in spec.get("wbs", []):169            affect[id(l)] = nom170assert len(affect) == len(LIGNES), [l["description"] for l in LIGNES if id(l) not in affect]171NUS_CAS1 = {nom: sum(l["located"] for l in LIGNES if affect[id(l)] == nom) for nom in COMP_CAS1}172assert abs(sum(NUS_CAS1.values()) - CAS1_NUS) < 1173174def prix(mot):  # prix unitaire nu RSMeans (located/quantity) d'une ligne du Cas 1175    l = lignes_par([mot])[0]; return l["located"] / l["quantity"]176177# Aménagement du sous-sol du sujet (86 m² utiles, 3 chambres + salle de bain) — quantités HYP × prix unitaires RSMeans du Cas 1178SS = {179    "Cloisons 2×4 (40 m lin.)": 40 * 3.2808 * prix("Cloisons ossature"),180    "Gypse murs (282 m²)": 282 * 10.764 * prix("Gypse ½"),181    "Gypse plafond (86 m²)": 86 * 10.764 * prix("Gypse ⅝"),182    "Peinture (368 m²)": 368 * 10.764 * prix("Peinture 2 couches"),183    "Plancher flottant (86 m²)": 86 * 10.764 * prix("Plancher vinyl_plank"),184    "Plinthes (120 m)": 120 * 3.2808 * prix("Plinthes —"),185    "Portes intérieures (5)": 5 * prix("Portes intérieures"),186    "Portes de garde-robe (3)": 3 * prix("Portes de garde-robe"),187    "Luminaires (10)": 10 * (prix("luminaire plafonnier") + prix("sortie d'éclairage boîte + câble NM — RSMeans 26059010610")),188    "Prises et interrupteurs": 12 * prix("prises doubles") + 6 * prix("interrupteurs"),189    "Plinthes électriques (6)": 6 * prix("Plinthes électriques"),190    "Salle de bain complète (appareils + robinetterie)": somme(lignes_par(["Salles de bain complètes"])),191    "Plomberie brute (3 appareils)": 3 * prix("Plomberie brute"),192    "Céramique salle de bain (8 m²)": 8 * 10.764 * prix("Céramique"),193}194SS_LT = SS["Cloisons 2×4 (40 m lin.)"] + SS["Gypse murs (282 m²)"] + SS["Gypse plafond (86 m²)"] + SS["Plomberie brute (3 appareils)"] + SS["Prises et interrupteurs"]195SS_FINITIONS = SS["Peinture (368 m²)"] + SS["Plancher flottant (86 m²)"] + SS["Plinthes (120 m)"] + SS["Portes intérieures (5)"] + SS["Portes de garde-robe (3)"] + SS["Luminaires (10)"]196SS_SDB = SS["Salle de bain complète (appareils + robinetterie)"] + SS["Céramique salle de bain (8 m²)"]197SS_PLINTHES = SS["Plinthes électriques (6)"]198SS_TOTAL = sum(SS.values())199200# Composantes du SUJET au niveau « directs nus » : Cas 1 × K + aménagement du sous-sol + ajouts201NUS_SUJET = {nom: v * K for nom, v in NUS_CAS1.items()}202NUS_SUJET["Fondations, structure et enveloppe (LT)"] += SS_LT203NUS_SUJET["Finitions intérieures du sous-sol"] = SS_FINITIONS204NUS_SUJET["Appareils de la salle de bain (sous-sol)"] = SS_SDB205NUS_SUJET["Plinthes électriques"] += SS_PLINTHES206NUS_SUJET["Thermopompe murale"] = HYP["ajouts_sujet"]["Thermopompe murale 12 000 BTU installée"]207NUS_SUJET["Aménagements extérieurs (allée, terrasse, clôture, remise)"] = sum(v for k, v in HYP["ajouts_sujet"].items() if "Thermopompe" not in k)208NUS_TOTAL = sum(NUS_SUJET.values())209210# Chaîne du cours (séance 8 / TP2)211def chaine(nus, logements=1, T=HYP["duree_travaux_mois"], i=HYP["taux_pret"], juridique=HYP["juridique"], gcr=True):212    cg = r0(nus * 0.08); a1 = nus + cg213    fg = r0(a1 * 0.07); a2 = a1 + fg214    peg = r0(a2 * 0.08); dcours = a2 + peg215    cont = r0(nus * 0.05); dc = dcours + cont216    permis = max(r0(dc / 1000 * 4.70), 335 * logements)217    gcr_m = r0(2656.68 * logements) if gcr else 0218    interets = r0(dc * i / 100 * T / 12 * 0.5)219    honoraires = r0(dcours * 0.04)220    assurance = r0(dc * 0.008)221    indirects = permis + gcr_m + interets + honoraires + assurance + juridique222    assiette = dc + indirects223    profit = r0(assiette * 0.11)224    total = assiette + profit225    return dict(nus=r0(nus), cg=cg, fg=fg, peg=peg, directs_cours=r0(dcours), contingences=cont, dc=r0(dc), permis=permis, gcr=gcr_m,226                interets=interets, honoraires=honoraires, assurance=assurance, juridique=juridique, indirects=indirects,227                assiette=r0(assiette), profit=profit, total=total, multiplicateur=total / nus)228229CH = chaine(NUS_TOTAL)230CN = CH["total"]                       # coût de remplacement neuf complet du sujet231M = CH["multiplicateur"]               # tout composant « nu » → sa part du coût neuf complet (indirects et profit au prorata : convention du cours)232CN_M2 = CN / SUJET["aire_m2"]; CN_PI2 = CN / SUJET["aire_pi2"]233CH_CAS1 = chaine(CAS1_NUS)             # Cas 1 complet (contrôle : D+C doit redonner 222 762 $)234assert abs(CH_CAS1["dc"] - CAS1_DC) < 3, (CH_CAS1["dc"], CAS1_DC)235236# =================================================================== 4. TERRAIN237VT = round(SUJET["vt_role"] * RATIO_TERRAIN / 1000) * 1000       # 220 000 $238VT_AFFECT = SUJET["vente_prix"] * st.median(v["vt"] * RATIO_PRIX_ROLE / v["prix"] for v in VENTES)  # contrôle par affectation239VT_PM2 = SUJET["lot_m2"] * PM2_TERRAIN                            # contrôle $/m² (lots plus grands → biais)240241# =================================================================== 5. VENTILATION (méthode 3)242# 5a. Physique curable = entretien différé (coût de remise en état)243CURABLE = {k: v for k, v in HYP["entretien_differe"].items()}244D_CURABLE = sum(CURABLE.values())245# composantes retirées de la liste CT parce que traitées en curable : revêtement de vinyle, allée (dans « aménagements extérieurs » on retire l'allée)246NUS_SUJET["Aménagements extérieurs (allée, terrasse, clôture, remise)"] -= HYP["ajouts_sujet"]["Allée asphaltée 4 places (65 m² × 60 $/m²)"]247NUS_SUJET["Aménagements extérieurs (terrasse, clôture, remise)"] = NUS_SUJET.pop("Aménagements extérieurs (allée, terrasse, clôture, remise)")248NUS_SUJET["Allée asphaltée"] = HYP["ajouts_sujet"]["Allée asphaltée 4 places (65 m² × 60 $/m²)"]249250# 5b. Composantes à courte durée de vie : (coût neuf complet, âge effectif, vie utile)251CT_SPEC = {  # nom : (âge, vie, note)252    "Toiture (bardeaux, soffites, gouttières)": (13, 25, "refaite en 2013 ; bardeaux laminés 25 ans"),253    "Fenêtres et portes extérieures": (17, 30, "PVC à manivelle, 2009"),254    "Chauffe-eau": (5, 12, "2021"),255    "Plinthes électriques": (21, 25, "2005"),256    "Échangeur d'air (VRC)": (None, 20, "absent du sujet → traité en fonctionnelle (ajout)"),257    "Thermopompe murale": (6, 15, "2020"),258    "Cuisine (armoires, comptoirs, évier)": (41, 25, "d'origine 1985 → plafonnée à 100 %"),259    "Appareils de la salle de bain (RDC)": (41, 25, "d'origine 1985 → 100 %"),260    "Appareils de la salle de bain (sous-sol)": (20, 25, "2006"),261    "Finitions intérieures du RDC": (11, 20, "planchers flottants, céramique, peinture 2015"),262    "Finitions intérieures du sous-sol": (20, 25, "2006"),263    "Aménagements extérieurs (terrasse, clôture, remise)": (12, 20, "terrasse 2018, clôture et remise 2012 (âge moyen pondéré ≈ 12 ans)"),264}265CURABLE_COMPS = ["Revêtement de vinyle", "Allée asphaltée"]   # mesurées au coût de remise en état (entretien différé) → exclues du CT266LT_COMPS = ["Fondations, structure et enveloppe (LT)", "Installations électriques (LT)", "Plomberie brute et drainage (LT)"]267268VENT = []  # lignes du tableau de ventilation269CN_COMP = {nom: v * M for nom, v in NUS_SUJET.items()}270assert abs(sum(CN_COMP.values()) - CN) < 2271D_CT = 0; CN_CT = 0; VRC_CN = 0272for nom, (age, vie, note) in CT_SPEC.items():273    cn_c = CN_COMP[nom]274    if age is None:  # VRC absent275        VRC_CN = cn_c276        VENT.append(dict(comp=nom, cn=r0(cn_c), age="—", vie=vie, ratio=None, d=0, vr=r0(cn_c), note=note, type="fonct."))277        continue278    ratio = min(age / vie, 1.0)279    d = r0(cn_c * ratio); CN_CT += cn_c; D_CT += d280    VENT.append(dict(comp=nom, cn=r0(cn_c), age=age, vie=vie, ratio=ratio, d=d, vr=r0(cn_c) - d, note=note, type="CT"))281CN_CURABLE_COMPS = sum(CN_COMP[n] for n in CURABLE_COMPS)282# 5c. Longue durée de vie : base résiduelle = CN − curable (coût de remise en état) − CN des composantes CT − CN des composantes traitées en curable283BASE_LT = CN - D_CURABLE - CN_CT - CN_CURABLE_COMPS - VRC_CN284CN_LT = sum(CN_COMP[n] for n in LT_COMPS)285RATIO_LT = HYP["AE_LT"] / HYP["VE"]286D_LT = r0(BASE_LT * RATIO_LT)287# 5d. Fonctionnelle288CUISINE_CN = CN_COMP["Cuisine (armoires, comptoirs, évier)"]289D_F_CUISINE = r0(HYP["cuisine_cout_correction"] - CUISINE_CN)          # substitution : excédent du coût de correction sur le coût à neuf (item déjà déprécié à 100 %)290D_F_VRC = r0(HYP["vrc_cout_ajout"] - VRC_CN)                          # ajout : excédent du coût d'ajout dans l'existant sur le coût « à neuf »291D_F_GARAGE = HYP["garage_perte_valeur"]                                # incurable : perte de valeur (paires) < coût d'ajout → incurable, mesurée par la perte292D_FONCT_CUR = D_F_CUISINE + D_F_VRC293D_FONCT_INC = D_F_GARAGE294D_EXT = 0295D_VENT = D_CURABLE + D_CT + D_LT + D_FONCT_CUR + D_FONCT_INC + D_EXT296V_VENT = VT + CN - D_VENT297298# =================================================================== 6. ÂGE EFFECTIF DÉFENDABLE (pondération par le coût neuf)299# âge effectif « pondéré » = Σ (âge_i × CN_i) / Σ CN_i sur toutes les composantes (LT à l'âge chronologique si d'origine)300AGES = {}301for nom in NUS_SUJET:302    if nom in CT_SPEC and CT_SPEC[nom][0] is not None: AGES[nom] = CT_SPEC[nom][0]303    elif nom in CT_SPEC: AGES[nom] = AGE_CHRONO304    elif nom in CURABLE_COMPS: AGES[nom] = AGE_CHRONO305    else: AGES[nom] = HYP["AE_LT"]   # LT : âge effectif jugé (structure bien entretenue)306AE_POND = sum(AGES[n] * CN_COMP[n] for n in NUS_SUJET) / CN307AE = HYP["AE_global"]308309# =================================================================== 7. MÉTHODES 1 ET 2310VE = HYP["VE"]311D_AV = r0(CN * AE / VE); V_AV = VT + CN - D_AV312D_AVM = D_CURABLE + r0((CN - D_CURABLE) * AE / VE); V_AVM = VT + CN - D_AVM313D_AV_CHRONO = r0(CN * AGE_CHRONO / VE)          # si l'on utilisait (à tort) l'âge chronologique314D_AV_VE60 = r0(CN * AE / 60)                    # sensibilité : vie économique de manuel 60 ans315316# =================================================================== 8. EXTRACTION DU MARCHÉ (méthode 4) — 7 ventes réelles à ≤ 400 m317COMPS_EXTRACTION = ["115, rue du Saguenay", "64, rue de la Yamaska", "25, rue de la Châteauguay", "37, rue de la Châteauguay",318                    "117, rue Sainte-Rose", "75, rue des Sables", "74, rue de la Châteauguay"]319def dist(v): return math.hypot((v["lat"] - la0) * 111320, (v["lng"] - lo0) * 111320 * math.cos(math.radians(la0)))320EXTR = []321for rue in COMPS_EXTRACTION:322    v = next(x for x in VENTES if x["rue"].startswith(rue))323    terrain = round(v["vt"] * RATIO_TERRAIN / 1000) * 1000324    cn = r0(v["aire"] * CN_M2)                                        # HYP : même devis que le sujet (coût neuf au m² hors sol identique)325    vb = v["prix"] - terrain326    d = cn - vb; ratio = d / cn; taux = ratio / v["age"]327    EXTR.append(dict(rue=v["rue"], date=v["date"], prix=r0(v["prix"]), annee=v["annee"], age=v["age"], aire=v["aire"], terrain=terrain, cn=cn,328                     vb=r0(vb), d=r0(d), ratio=ratio, taux=taux, dist=r0(dist(v))))329EXTR_RATIOS = sorted(e["ratio"] for e in EXTR); EXTR_TAUX = sorted(e["taux"] for e in EXTR)330TAUX_MEDIAN = st.median(EXTR_TAUX); TAUX_MOYEN = st.mean(EXTR_TAUX)331RATIO_MEDIAN = st.median(EXTR_RATIOS)332# écart interquartile des taux ; sous-ensemble « sans extrêmes » (on retire min et max)333TAUX_TRIM = st.mean(EXTR_TAUX[1:-1])334D_EXTR = r0(CN * TAUX_MEDIAN * AGE_CHRONO); V_EXTR = VT + CN - D_EXTR335VE_IMPLICITE = 1 / TAUX_MEDIAN   # vie économique implicite du marché (âge-vie linéaire)336337# =================================================================== 9. RÉCONCILIATION338METHODES = {"Âge-vie": (D_AV, V_AV), "Âge-vie modifiée": (D_AVM, V_AVM), "Ventilation": (D_VENT, V_VENT), "Extraction du marché": (D_EXTR, V_EXTR)}339POIDS = {"Âge-vie": 10, "Âge-vie modifiée": 15, "Ventilation": 40, "Extraction du marché": 35}340D_POND = sum(METHODES[k][0] * POIDS[k] / 100 for k in METHODES)341D_MECA = r0(D_POND); V_MECA = arrondi(VT + CN - D_MECA)                 # moyenne pondérée « mécanique » : montrée pour être critiquée342D_IMPLICITE_VENTE = CN - (SUJET["vente_prix"] - VT)                      # dépréciation que le marché a « payée » le 30 janvier 2026343D_FINAL = D_EXTR                                                         # réconciliation raisonnée : l'extraction, corroborée par la vente du sujet344V_FINAL = arrondi(VT + CN - D_FINAL)345ECART_VENTE = V_FINAL / SUJET["vente_prix"] - 1346ECART_DEMANDE = V_FINAL / SUJET["prix_demande"] - 1347ECART_MECA = V_MECA / SUJET["vente_prix"] - 1348ECART_VENT_MARCHE = D_VENT - D_IMPLICITE_VENTE                           # ce que la ventilation « de tables » déduit de plus que le marché349350# Sensibilités351SENS_TERRAIN = [(t, r0(CN - (SUJET["vente_prix"] - t))) for t in (180400, 200000, 212000, VT, 240000, 260000)]352SENS_VE = [(ve, r0(CN * AE / ve), VT + CN - r0(CN * AE / ve)) for ve in (50, 60, 75, 100)]353SENS_AE = [(ae, r0(CN * ae / VE), VT + CN - r0(CN * ae / VE)) for ae in (20, 25, 30, 35, 41)]354355# =================================================================== 10. PROFIL EMPIRIQUE — 655 ventes (approche « prix par millésime », Hulten–Wykoff)356for v in VENTES:357    v["terrain_i"] = v["vt"] * RATIO_TERRAIN358    v["vb_impl"] = v["prix"] - v["terrain_i"]359    v["vb_m2"] = v["vb_impl"] / v["aire"]360BINS = [(0, 15), (15, 25), (25, 35), (35, 45), (45, 55), (55, 65), (65, 75), (75, 110)]361PROFIL = []362ref = None363for a, b in BINS:364    s = [v for v in VENTES if a <= v["age"] < b]365    vals = sorted(v["vb_m2"] for v in s)366    med = st.median(vals); q1 = vals[len(vals) // 4]; q3 = vals[3 * len(vals) // 4]367    if ref is None: ref = med368    PROFIL.append(dict(a=a, b=b, n=len(s), med=med, q1=q1, q3=q3, age_med=st.median(v["age"] for v in s), d_rel=1 - med / ref,369                       part_terrain=st.median(v["terrain_i"] / v["prix"] for v in s), prix_med=st.median(v["prix"] for v in s), aire_med=st.median(v["aire"] for v in s)))370POS = [v for v in VENTES if v["vb_m2"] > 300]371X = np.array([v["age"] for v in POS], float); Y = np.log(np.array([v["vb_m2"] for v in POS]))372A = np.vstack([np.ones_like(X), X]).T373beta, res, *_ = np.linalg.lstsq(A, Y, rcond=None)374resid = Y - A @ beta; s2 = resid @ resid / (len(Y) - 2); cov = s2 * np.linalg.inv(A.T @ A); se = np.sqrt(np.diag(cov))375LAMBDA_EMP = -beta[1]; LAMBDA_SE = se[1]; VBM2_NEUF_EMP = math.exp(beta[0])376R2_EXP = 1 - (resid @ resid) / ((Y - Y.mean()) @ (Y - Y.mean()))377# comparaison : le CN du sujet par m² face à la valeur implicite d'un bâtiment quasi neuf378CN_VS_NEUF = CN_M2 / PROFIL[0]["med"]379380# Régression hédonique illustrative (log-prix) : âge, ln aire, ln lot, latitude, longitude — données réelles, coefficients avec écarts-types381Xh = np.column_stack([np.ones(N_VENTES), [v["age"] for v in VENTES], np.log([v["aire"] for v in VENTES]), np.log([v["lot"] for v in VENTES]),382                      [v["lat"] - la0 for v in VENTES], [v["lng"] - lo0 for v in VENTES]])383Yh = np.log([v["prix"] for v in VENTES])384bh, *_ = np.linalg.lstsq(Xh, Yh, rcond=None)385rh = Yh - Xh @ bh; s2h = rh @ rh / (N_VENTES - Xh.shape[1]); seh = np.sqrt(np.diag(s2h * np.linalg.inv(Xh.T @ Xh)))386R2_HED = 1 - (rh @ rh) / ((Yh - Yh.mean()) @ (Yh - Yh.mean()))387HED = dict(noms=["constante", "âge (années)", "ln aire hors sol", "ln superficie du terrain", "latitude (degrés, écart au sujet)", "longitude (degrés, écart au sujet)"],388           beta=bh.tolist(), se=seh.tolist(), r2=R2_HED, n=N_VENTES, sigma=math.sqrt(s2h))389390# =================================================================== 11. MODÈLES MATHÉMATIQUES (bloc 6) — paramètres391# linéaire : V(t) = CN (1 − δ t), δ = 1/VE ; exponentiel : V(t) = CN e^{−λ t}, λ = λ empirique ; non linéaire : V(t) = CN e^{−λ' t^γ}, γ = 1,5, λ' calé pour V(41) égal à l'exponentiel392DELTA = 1 / VE393LAMBDA = LAMBDA_EMP394GAMMA = 1.5395LAMBDA_NL = LAMBDA * AGE_CHRONO / AGE_CHRONO ** GAMMA  # e^{-λ' t^γ} = e^{-λ t} en t = 41396def V_lin(t): return CN * max(0.0, 1 - DELTA * t)397def V_exp(t): return CN * math.exp(-LAMBDA * t)398def V_nl(t): return CN * math.exp(-LAMBDA_NL * t ** GAMMA)399def dV_lin(t): return -CN * DELTA400def dV_exp(t): return -LAMBDA * CN * math.exp(-LAMBDA * t)401def dV_nl(t): return -LAMBDA_NL * GAMMA * t ** (GAMMA - 1) * CN * math.exp(-LAMBDA_NL * t ** GAMMA)402MODELES = {t: dict(lin=V_lin(t), exp=V_exp(t), nl=V_nl(t), dlin=dV_lin(t), dexp=dV_exp(t), dnl=dV_nl(t)) for t in (0, 10, 20, 30, 41, 50, 60, 75)}403DEMI_VIE_EXP = math.log(2) / LAMBDA404405# =================================================================== 12. CURABILITÉ (bloc 3) — courbes coût / bénéfice d'une rénovation de cuisine406# ΔV(C) = A (1 − e^{−C/B}) : gain de valeur concave dans le budget ; GN(C) = ΔV(C) − C ; optimum : ΔV'(C) = 1 → C* = B ln(A/B)407A_CUR, B_CUR = 48000, 25000408C_OPT = B_CUR * math.log(A_CUR / B_CUR); DV_OPT = A_CUR * (1 - math.exp(-C_OPT / B_CUR)); GN_OPT = DV_OPT - C_OPT409# seuil : ΔV(C) = C (gain net nul) → résolution numérique410def gn(c): return A_CUR * (1 - math.exp(-c / B_CUR)) - c411lo, hi = C_OPT, 120000412for _ in range(80):413    mid = (lo + hi) / 2414    if gn(mid) > 0: lo = mid415    else: hi = mid416C_SEUIL = (lo + hi) / 2417RENOS = [  # comparaison de rénovations (HYP) : coût, gain de valeur418    ("Peinture intérieure complète", 6000, 10000), ("Rénovation de la cuisine", 28000, r0(A_CUR * (1 - math.exp(-28000 / B_CUR)))), ("Rénovation de la salle de bain du RDC", 16000, 19000),419    ("Ajout d'un garage attaché", 45000, 15000), ("Rehaussement du sous-sol (plafond 7 pi 6)", 60000, 12000), ("Thermopompe centrale", 9000, 8000),420]421RENOS = [dict(nom=n, c=c, dv=dv, gn=dv - c, ratio=dv / c, curable=dv >= c) for n, c, dv in RENOS]422# sensibilité : si le gain de valeur de la cuisine varie de ±20 %423DV_CUISINE = r0(A_CUR * (1 - math.exp(-28000 / B_CUR)))424SENS_CUISINE = [(k, r0(DV_CUISINE * k) - 28000) for k in (0.7, 0.8, 0.9, 1.0, 1.1, 1.2)]425426# =================================================================== 13. EXEMPLES DIDACTIQUES (blocs 1, 2, 4) — chiffres ronds, HYP427EX = {}428EX["bloc1"] = dict(cn=1200000, vb=780000); EX["bloc1"]["d"] = EX["bloc1"]["cn"] - EX["bloc1"]["vb"]; EX["bloc1"]["ratio"] = EX["bloc1"]["d"] / EX["bloc1"]["cn"]429# deux bâtiments du même âge, états différents (bloc 1) : CN 400 000, 30 ans ; A bien entretenu AE 18, B négligé AE 42, VE 60430EX["jumeaux"] = dict(cn=400000, age=30, ve=60, aeA=18, aeB=42)431EX["jumeaux"]["dA"] = r0(400000 * 18 / 60); EX["jumeaux"]["dB"] = r0(400000 * 42 / 60)432# rénovation majeure (bloc 1) : V(t) exponentiel λ = 1,5 %, rénovation à t = 25 qui ramène l'âge effectif à 12 ans433EX["reno"] = dict(lam=0.015, t_reno=25, ae_apres=12)434# bloc 4 : cas numériques pas à pas435EX["b4_1"] = dict(annee=1996, eval=2026, ve=50, vp=65, ae=22)436EX["b4_1"].update(chrono=30, ver=50 - 22, ratio=22 / 50)437EX["b4_2"] = dict(cn=420000, chrono=24, ae=26, ve=55, terrain=150000)438EX["b4_2"]["d"] = r0(420000 * 26 / 55); EX["b4_2"]["vb"] = 420000 - EX["b4_2"]["d"]; EX["b4_2"]["v"] = 150000 + EX["b4_2"]["vb"]439EX["b4_3"] = dict(cur=32000); EX["b4_3"]["d"] = 32000 + r0((420000 - 32000) * 26 / 55); EX["b4_3"]["v"] = 150000 + 420000 - EX["b4_3"]["d"]440# bloc 2 : externe — effet de la distance à une autoroute (HYP illustrative) : perte(%) = 10 % × e^{−d/150 m}441EX["ext_dist"] = [(d, 10 * math.exp(-d / 150)) for d in (0, 50, 100, 150, 200, 300, 400, 600)]442# bloc 2 : allocation terrain/bâtiment de l'externe443EX["alloc"] = dict(d_tot=50000, ratio_bat=0.70); EX["alloc"]["bat"] = r0(50000 * 0.70); EX["alloc"]["ter"] = 50000 - EX["alloc"]["bat"]444# bloc 5 : extraction didactique à 4 ventes (chiffres ronds, conservée de l'ancienne version, vérifiée)445EX["extr4"] = [(650000, 200000, 600000, 20), (580000, 180000, 550000, 22), (720000, 230000, 630000, 18), (610000, 190000, 570000, 21)]446EX["extr4"] = [dict(prix=p, terrain=t, cn=c, age=a, vb=p - t, d=c - (p - t), ratio=(c - (p - t)) / c, taux=(c - (p - t)) / c / a) for p, t, c, a in EX["extr4"]]447EX["extr4_moy_ratio"] = st.mean(e["ratio"] for e in EX["extr4"]); EX["extr4_moy_taux"] = st.mean(e["taux"] for e in EX["extr4"])448449# =================================================================== 14. SORTIES450R = dict(sujet=SUJET, age_chrono=AGE_CHRONO, hyp=HYP, K=K, nus_cas1=NUS_CAS1, nus_sujet=NUS_SUJET, ss=SS, ss_total=SS_TOTAL, chaine=CH, chaine_cas1=CH_CAS1,451         CN=CN, M=M, CN_m2=CN_M2, CN_pi2=CN_PI2, terrain=dict(VT=VT, ratio=RATIO_TERRAIN, pm2=PM2_TERRAIN, n_terrains_5km=len(TERRAINS_5KM), affectation=VT_AFFECT, par_m2=VT_PM2,452         terrains_5km=TERRAINS_5KM), ventilation=dict(lignes=VENT, curable=CURABLE, D_curable=D_CURABLE, D_CT=D_CT, CN_CT=CN_CT, base_LT=BASE_LT, CN_LT=CN_LT, D_LT=D_LT,453         cuisine_cn=CUISINE_CN, D_f_cuisine=D_F_CUISINE, vrc_cn=VRC_CN, D_f_vrc=D_F_VRC, D_f_garage=D_F_GARAGE, D_fonct_cur=D_FONCT_CUR, D_fonct_inc=D_FONCT_INC, D_ext=D_EXT,454         D_total=D_VENT, V=V_VENT), AE_pondere=AE_POND, AE=AE, VE=VE, age_vie=dict(D=D_AV, V=V_AV, D_chrono=D_AV_CHRONO, D_ve60=D_AV_VE60), age_vie_mod=dict(D=D_AVM, V=V_AVM),455         extraction=dict(ventes=EXTR, taux_median=TAUX_MEDIAN, taux_moyen=TAUX_MOYEN, taux_trim=TAUX_TRIM, ratio_median=RATIO_MEDIAN, D=D_EXTR, V=V_EXTR, VE_implicite=VE_IMPLICITE),456         reconciliation=dict(methodes=METHODES, poids=POIDS, D_pond=D_POND, D_meca=D_MECA, V_meca=V_MECA, ecart_meca=ECART_MECA, D_final=D_FINAL, V_final=V_FINAL, ecart_vente=ECART_VENTE, ecart_demande=ECART_DEMANDE, D_implicite_vente=D_IMPLICITE_VENTE,457         sens_terrain=SENS_TERRAIN, sens_VE=SENS_VE, sens_AE=SENS_AE), profil=PROFIL, exp_fit=dict(lam=LAMBDA_EMP, se=LAMBDA_SE, vbm2_neuf=VBM2_NEUF_EMP, r2=R2_EXP, n=len(POS)),458         ratio_prix_role=RATIO_PRIX_ROLE, n_ventes=N_VENTES, hedonique=HED, modeles=dict(delta=DELTA, lam=LAMBDA, gamma=GAMMA, lam_nl=LAMBDA_NL, table=MODELES, demi_vie=DEMI_VIE_EXP),459         curabilite=dict(A=A_CUR, B=B_CUR, C_opt=C_OPT, dV_opt=DV_OPT, GN_opt=GN_OPT, C_seuil=C_SEUIL, renos=RENOS, sens_cuisine=SENS_CUISINE), exemples=EX, CN_vs_neuf=CN_VS_NEUF)460461json.dump(R, open(os.path.join(HERE, "resultats_s09.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1, default=lambda o: float(o) if isinstance(o, (np.floating,)) else str(o))462463# ---- 14a. Données pgfplots464with open(os.path.join(PGFDATA, "s09_ventes_vbm2.dat"), "w") as fh:465    fh.write("age vbm2\n")466    for v in VENTES:467        if v["vb_m2"] > 0: fh.write(f"{v['age']} {v['vb_m2']:.0f}\n")468with open(os.path.join(PGFDATA, "s09_profil_bins.dat"), "w") as fh:469    fh.write("age med q1 q3 n drel\n")470    for p in PROFIL: fh.write(f"{p['age_med']:.0f} {p['med']:.0f} {p['q1']:.0f} {p['q3']:.0f} {p['n']} {p['d_rel']*100:.1f}\n")471with open(os.path.join(PGFDATA, "s09_extraction.dat"), "w") as fh:472    fh.write("age ratio\n")473    for e in EXTR: fh.write(f"{e['age']} {e['ratio']*100:.1f}\n")474with open(os.path.join(PGFDATA, "s09_ventilation.dat"), "w") as fh:475    fh.write("comp d\n")476    for i, l in enumerate(VENT):477        if l["type"] == "CT": fh.write(f"{i} {l['d']}\n")478479# ---- 14b. Macros LaTeX480mac = []481def M_(name, val): mac.append(f"\\newcommand{{\\{name}}}{{{val}}}")482M_("vAgeChrono", AGE_CHRONO); M_("vAE", AE); M_("vVE", VE); M_("vAELT", HYP["AE_LT"]); M_("vAEpond", dec(AE_POND, 1))483M_("vAireMdeux", dec(SUJET["aire_m2"], 1)); M_("vAirePi", f(SUJET["aire_pi2"])); M_("vLotMdeux", dec(SUJET["lot_m2"], 1)); M_("vLotPi", f(SUJET["lot_pi2"]))484M_("vVTrole", f(SUJET["vt_role"])); M_("vVBrole", f(SUJET["vb_role"])); M_("vVRrole", f(SUJET["vr_role"]))485M_("vVente", f(SUJET["vente_prix"])); M_("vDemande", f(SUJET["prix_demande"]))486M_("vUqoEst", f(SUJET["uqoeval_est"])); M_("vUqoCout", f(SUJET["uqoeval_cout"])); M_("vUqoRole", f(SUJET["uqoeval_role_indexe"]))487M_("vK", dec(K, 4)); M_("vCasNus", f(CAS1_NUS)); M_("vCasDC", f(CAS1_DC)); M_("vCasEmpreinte", dec(CAS1_EMPREINTE, 1))488M_("vSStotal", f(SS_TOTAL)); M_("vSSLT", f(SS_LT)); M_("vSSfin", f(SS_FINITIONS)); M_("vSSsdb", f(SS_SDB)); M_("vSSplinthes", f(SS_PLINTHES))489M_("vAjouts", f(sum(HYP["ajouts_sujet"].values())))490for k, v in CH.items():491    if k != "multiplicateur": M_("vCh" + k.capitalize().replace("_", ""), f(v))492M_("vM", dec(M, 3)); M_("vCN", f(CN)); M_("vCNmdeux", f(CN_M2)); M_("vCNpi", f(CN_PI2))493M_("vCNvsNeuf", pct(CN_VS_NEUF - 1, 1))494M_("vVT", f(VT)); M_("vRatioTerrain", dec(RATIO_TERRAIN, 2)); M_("vPMdeuxTerrain", f(PM2_TERRAIN)); M_("vNterrains", len(TERRAINS_5KM))495M_("vVTaffect", f(VT_AFFECT)); M_("vVTpm", f(VT_PM2)); M_("vRatioPrixRole", dec(RATIO_PRIX_ROLE, 2)); M_("vNventes", N_VENTES)496M_("vDcur", f(D_CURABLE)); M_("vDCT", f(D_CT)); M_("vCNCT", f(CN_CT)); M_("vBaseLT", f(BASE_LT)); M_("vDLT", f(D_LT)); M_("vRatioLT", pct(RATIO_LT, 1))497M_("vCNcurComps", f(CN_CURABLE_COMPS)); M_("vCuisineCN", f(CUISINE_CN)); M_("vDfCuisine", f(D_F_CUISINE)); M_("vVrcCN", f(VRC_CN)); M_("vDfVrc", f(D_F_VRC))498M_("vDfGarage", f(D_F_GARAGE)); M_("vDfCur", f(D_FONCT_CUR)); M_("vDfInc", f(D_FONCT_INC)); M_("vDvent", f(D_VENT)); M_("vDventPct", pct(D_VENT / CN, 1)); M_("vVvent", f(V_VENT))499M_("vDav", f(D_AV)); M_("vDavPct", pct(D_AV / CN, 1)); M_("vVav", f(V_AV)); M_("vDavChrono", f(D_AV_CHRONO)); M_("vDavChronoPct", pct(D_AV_CHRONO / CN, 1)); M_("vDavVEsoixante", f(D_AV_VE60))500M_("vDavm", f(D_AVM)); M_("vDavmPct", pct(D_AVM / CN, 1)); M_("vVavm", f(V_AVM)); M_("vDavmSolde", f(D_AVM - D_CURABLE)); M_("vCNmoinsCur", f(CN - D_CURABLE))501M_("vTauxMedian", pct(TAUX_MEDIAN, 2)); M_("vTauxMoyen", pct(TAUX_MOYEN, 2)); M_("vTauxTrim", pct(TAUX_TRIM, 2)); M_("vRatioMedianExtr", pct(RATIO_MEDIAN, 1))502M_("vDextr", f(D_EXTR)); M_("vDextrPct", pct(D_EXTR / CN, 1)); M_("vVextr", f(V_EXTR)); M_("vVEimpl", f(VE_IMPLICITE))503M_("vRatioMin", pct(EXTR_RATIOS[0], 1)); M_("vRatioMax", pct(EXTR_RATIOS[-1], 1)); M_("vTauxMin", pct(EXTR_TAUX[0], 2)); M_("vTauxMax", pct(EXTR_TAUX[-1], 2))504M_("vDmeca", f(D_MECA)); M_("vDmecaPct", pct(D_MECA / CN, 1)); M_("vVmeca", f(V_MECA)); M_("vEcartMeca", (("$+$" if ECART_MECA >= 0 else "$-$") + pct(abs(ECART_MECA), 1))); M_("vEcartVentMarche", f(ECART_VENT_MARCHE)); M_("vDVcuisine", f(DV_CUISINE));505M_("vDfinal", f(D_FINAL)); M_("vDfinalPct", pct(D_FINAL / CN, 1)); M_("vVfinal", f(V_FINAL)); M_("vVfinalBrut", f(VT + CN - D_FINAL))506M_("vEcartVente", (("$+$" if ECART_VENTE >= 0 else "$-$") + pct(abs(ECART_VENTE), 1))); M_("vEcartDemande", (("$+$" if ECART_DEMANDE >= 0 else "$-$") + pct(abs(ECART_DEMANDE), 1)))507M_("vDimplVente", f(D_IMPLICITE_VENTE)); M_("vDimplVentePct", pct(D_IMPLICITE_VENTE / CN, 1)); M_("vVBimplVente", f(SUJET["vente_prix"] - VT))508M_("vLambdaEmp", pct(LAMBDA_EMP, 2)); M_("vLambdaSE", pct(LAMBDA_SE, 2)); M_("vRdeuxExp", dec(R2_EXP, 2)); M_("vVBneufEmp", f(VBM2_NEUF_EMP)); M_("vNpos", len(POS))509M_("vDemiVie", f(DEMI_VIE_EXP)); M_("vDelta", pct(DELTA, 2)); M_("vGamma", dec(GAMMA, 1)); M_("vLambdaNL", f"{LAMBDA_NL:.5f}".replace(".", ","))510M_("vLambdaEmpDec", f"{LAMBDA_EMP:.4f}".replace(".", ",")); M_("vDeltaDec", f"{DELTA:.4f}".replace(".", ","))511M_("vProfilRefMed", f(PROFIL[0]["med"])); M_("vProfilVieuxMed", f(PROFIL[-2]["med"])); M_("vProfilVieuxDrel", pct(PROFIL[-2]["d_rel"], 1)); M_("vProfilVieuxAge", f(PROFIL[-2]["age_med"]))512M_("vProfilQuaranteDrel", pct(PROFIL[3]["d_rel"], 1)); M_("vProfilQuaranteAge", f(PROFIL[3]["age_med"]))513M_("vHedBetaAge", f"{HED['beta'][1]*100:.2f}".replace(".", ",")); M_("vHedSEAge", f"{HED['se'][1]*100:.2f}".replace(".", ",")); M_("vHedRdeux", dec(HED["r2"], 2))514M_("vHedBetaAire", dec(HED["beta"][2], 2)); M_("vHedSEAire", dec(HED["se"][2], 2)); M_("vHedBetaLot", dec(HED["beta"][3], 2)); M_("vHedSELot", dec(HED["se"][3], 2)); M_("vHedSigma", dec(HED["sigma"], 2))515M_("vCopt", f(C_OPT)); M_("vDVopt", f(DV_OPT)); M_("vGNopt", f(GN_OPT)); M_("vCseuil", f(C_SEUIL)); M_("vAcur", f(A_CUR)); M_("vBcur", f(B_CUR))516for t, m in MODELES.items():517    for key in ("lin", "exp", "nl"):518        M_(f"vMod{key.capitalize()}{['zero','dix','vingt','trente','quaranteun','cinquante','soixante','septantecinq'][list(MODELES).index(t)]}", f(m[key]))519M_("vdVlin", f(-dV_lin(0))); M_("vdVexpZero", f(-dV_exp(0))); M_("vdVexpQ", f(-dV_exp(41))); M_("vdVnlDix", f(-dV_nl(10))); M_("vdVnlQ", f(-dV_nl(41)))520M_("vExJumA", f(EX["jumeaux"]["dA"])); M_("vExJumB", f(EX["jumeaux"]["dB"]))521M_("vExtrQuatreRatio", pct(EX["extr4_moy_ratio"], 1)); M_("vExtrQuatreTaux", pct(EX["extr4_moy_taux"], 2))522M_("vDateEval", SUJET["date_evaluation"])523# valeurs numériques brutes (point décimal) pour les expressions pgfplots524M_("nCN", CN); M_("nLambda", f"{LAMBDA:.5f}"); M_("nDelta", f"{DELTA:.6f}"); M_("nLambdaNL", f"{LAMBDA_NL:.6f}"); M_("nGamma", GAMMA); M_("nVE", VE); M_("nAE", AE)525M_("nVT", VT); M_("nAcur", A_CUR); M_("nBcur", B_CUR); M_("nCopt", round(C_OPT)); M_("nCseuil", round(C_SEUIL)); M_("nDVopt", round(DV_OPT))526M_("nVBneufEmp", round(VBM2_NEUF_EMP)); M_("nLambdaEmp", f"{LAMBDA_EMP:.5f}"); M_("nTauxMedian", f"{TAUX_MEDIAN:.5f}"); M_("nAgeChrono", AGE_CHRONO)527M_("nDLT", D_LT); M_("nDfCur", D_FONCT_CUR + D_FONCT_INC)528VD = {l["comp"]: l["d"] for l in VENT}529for key, nom in (("nDToit", "Toiture (bardeaux, soffites, gouttières)"), ("nDFen", "Fenêtres et portes extérieures"), ("nDChauffe", "Chauffe-eau"), ("nDPlinthes", "Plinthes électriques"),530                 ("nDThermo", "Thermopompe murale"), ("nDCuisine", "Cuisine (armoires, comptoirs, évier)"), ("nDSdbRDC", "Appareils de la salle de bain (RDC)"),531                 ("nDSdbSS", "Appareils de la salle de bain (sous-sol)"), ("nDFinRDC", "Finitions intérieures du RDC"), ("nDFinSS", "Finitions intérieures du sous-sol"),532                 ("nDAmen", "Aménagements extérieurs (terrasse, clôture, remise)")):533    M_(key, VD[nom])534M_("nDextr", D_EXTR); M_("nDav", D_AV); M_("nDavm", D_AVM); M_("nDvent", D_VENT); M_("nDimpl", D_IMPLICITE_VENTE); M_("nDcur", D_CURABLE)535open(os.path.join(PARTIES, "s09_valeurs.tex"), "w", encoding="utf-8").write("% Généré par UQO_COURS_UPGRADE/IMM1033/seance09/calculs_s09.py — NE PAS ÉDITER À LA MAIN\n" + "\n".join(mac) + "\n")536537# ---- 14c. Tableaux LaTeX générés538T = []539T.append("% Généré par calculs_s09.py — NE PAS ÉDITER À LA MAIN")540# chaîne du coût neuf du sujet541T.append(r"\newcommand{\tabChaineSujet}{\begin{tabular}{@{}l l r r@{}}\toprule \textbf{Ligne} & \textbf{Base (convention du cours)} & \textbf{Montant} & \textbf{Cumul} \\ \midrule")542cum = CH["nus"]; rows = [("Directs nus (Cas 1 $\\times$ " + dec(K, 3) + " + sous-sol + ajouts)", "quantités $\\times$ prix nus RSMeans 2026 T4", CH["nus"], cum)]543for lab, base, key in (("Conditions générales", "8\\,\\% des nus", "cg"), ("Frais généraux", "7\\,\\% du cumul", "fg"), ("Profit de l'entrepreneur général", "8\\,\\% du cumul", "peg")):544    cum += CH[key]; rows.append((lab, base, CH[key], cum))545rows.append(("\\textbf{Directs au sens du cours}", "", None, CH["directs_cours"]))546cum = CH["directs_cours"] + CH["contingences"]; rows.append(("Contingences", "5\\,\\% des nus", CH["contingences"], cum))547rows.append(("\\textbf{Directs + contingences (D+C)}", "", None, CH["dc"]))548for lab, base, key in (("Permis (Gatineau)", "max(4,70~\\$/1~000~\\$ ; 335~\\$)", "permis"), ("GCR (unifamiliale visée)", "2~656,68~\\$/unité", "gcr"), ("Intérêts intercalaires", "D+C $\\times$ 6,75\\,\\% $\\times$ 6/12 $\\times$ ½", "interets"),549                       ("Honoraires professionnels", "4\\,\\% des directs du cours", "honoraires"), ("Assurance chantier", "0,8\\,\\% de D+C", "assurance"), ("Frais juridiques", "forfait", "juridique")):550    cum += CH[key]; rows.append((lab, base, CH[key], cum))551rows.append(("\\textbf{Indirects}", "", CH["indirects"], None))552rows.append(("Profit entrepreneurial", "11\\,\\% de (D+C + indirects)", CH["profit"], CH["total"]))553rows.append(("\\textbf{Coût de remplacement neuf complet}", "", None, CH["total"]))554for lab, base, m_, c_ in rows:555    T.append(f"{lab} & {base} & {f(m_) if m_ is not None else ''} & {f(c_) if c_ is not None else ''} \\\\")556T.append(r"\bottomrule\end{tabular}}")557# composantes (coût neuf complet)558T.append(r"\newcommand{\tabComposantesCN}{\begin{tabular}{@{}l r r r@{}}\toprule \textbf{Composante} & \textbf{Nus (\$)} & \textbf{Coût neuf complet (\$)} & \textbf{Part} \\ \midrule")559ordre = ["Fondations, structure et enveloppe (LT)", "Installations électriques (LT)", "Plomberie brute et drainage (LT)", "Toiture (bardeaux, soffites, gouttières)", "Fenêtres et portes extérieures",560         "Revêtement de vinyle", "Chauffe-eau", "Plinthes électriques", "Échangeur d'air (VRC)", "Thermopompe murale", "Cuisine (armoires, comptoirs, évier)", "Appareils de la salle de bain (RDC)",561         "Appareils de la salle de bain (sous-sol)", "Finitions intérieures du RDC", "Finitions intérieures du sous-sol", "Allée asphaltée", "Aménagements extérieurs (terrasse, clôture, remise)"]562assert set(ordre) == set(NUS_SUJET), set(NUS_SUJET) ^ set(ordre)563for nom in ordre:564    T.append(f"{nom} & {f(NUS_SUJET[nom])} & {f(CN_COMP[nom])} & {pct(CN_COMP[nom]/CN,1)}\\,\\% \\\\")565T.append(f"\\midrule \\textbf{{Total}} & \\textbf{{{f(NUS_TOTAL)}}} & \\textbf{{{f(CN)}}} & 100\\,\\% \\\\ \\bottomrule\\end{{tabular}}}}")566# ventilation CT (deux moitiés)567def tab_vent(nom_macro, lignes):568    T.append(f"\\newcommand{{\\{nom_macro}}}{{\\begin{{tabular}}{{@{{}}l r c c r r r@{{}}}}\\toprule \\textbf{{Composante}} & \\textbf{{Coût neuf}} & \\textbf{{Âge eff.}} & \\textbf{{Vie}} & \\textbf{{Ratio}} & \\textbf{{Dépréciation}} & \\textbf{{Valeur résiduelle}} \\\\ \\midrule")569    for l in lignes:570        ratio = "—" if l["ratio"] is None else (pct(l["ratio"], 0) + "\\,\\%")571        T.append(f"{l['comp']} & {f(l['cn'])} & {l['age']} & {l['vie']} & {ratio} & {f(l['d'])} & {f(l['vr'])} \\\\")572    T.append(r"\bottomrule\end{tabular}}")573CTL = [l for l in VENT if l["type"] == "CT"]574tab_vent("tabVentCTa", CTL[:6]); tab_vent("tabVentCTb", CTL[6:])575# synthèse de la ventilation576T.append(r"\newcommand{\tabVentSynthese}{\begin{tabular}{@{}p{2.2cm} p{1.9cm} r r@{}}\toprule \textbf{Étage} & \textbf{Mesure} & \textbf{\$} & \textbf{\% $C_N$} \\ \midrule")577for lab, mes, val in (("1. Physique curable", "coût de remise en état", D_CURABLE), ("2. Physique CT", "coût $\\times$ âge/vie", D_CT),578                      ("3. Physique LT", f"base {f(BASE_LT)} $\\times$ {HYP['AE_LT']}/{VE}", D_LT),579                      ("4. Fonct. curable", "excédent de coût", D_FONCT_CUR), ("5. Fonct. incurable", "perte (paires)", D_FONCT_INC),580                      ("6. Externe", "paires : nulle", D_EXT)):581    T.append(f"{lab} & {mes} & {f(val)} & {pct(val/CN,1)} \\\\")582T.append(f"\\midrule \\textbf{{Total}} & & \\textbf{{{f(D_VENT)}}} & \\textbf{{{pct(D_VENT/CN,1)}}} \\\\ \\bottomrule\\end{{tabular}}}}")583# extraction : ventes réelles584T.append(r"\newcommand{\tabExtraction}{\begin{tabular}{@{}l c r r r r r r@{}}\toprule \textbf{Vente (réelle)} & \textbf{Date} & \textbf{Prix} & \textbf{Terrain} & \textbf{Bâtiment} & \textbf{Coût neuf} & \textbf{$D/C_N$} & \textbf{Taux/an} \\ \midrule")585for e in EXTR:586    T.append(f"{e['rue']} ({e['annee']}) & {e['date'][:7]} & {f(e['prix'])} & {f(e['terrain'])} & {f(e['vb'])} & {f(e['cn'])} & {pct(e['ratio'],1)}\\,\\% & {pct(e['taux'],2)}\\,\\% \\\\")587T.append(f"\\midrule \\textbf{{Médiane}} & & & & & & \\textbf{{{pct(RATIO_MEDIAN,1)}\\,\\%}} & \\textbf{{{pct(TAUX_MEDIAN,2)}\\,\\%}} \\\\ \\bottomrule\\end{{tabular}}}}")588# extraction : données brutes des ventes (aire, âge, distance)589T.append(r"\newcommand{\tabExtractionDonnees}{\begin{tabular}{@{}l c c r r r r@{}}\toprule \textbf{Vente} & \textbf{Année} & \textbf{Âge} & \textbf{Aire (m\textsuperscript{2})} & \textbf{Terrain au rôle} & \textbf{Prix} & \textbf{Distance} \\ \midrule")590for e in EXTR:591    v = next(x for x in VENTES if x["rue"] == e["rue"])592    T.append(f"{e['rue']} & {e['annee']} & {e['age']} & {dec(e['aire'],1)} & {f(v['vt'])} & {f(e['prix'])} & {e['dist']}~m \\\\")593T.append(r"\bottomrule\end{tabular}}")594# réconciliation595T.append(r"\newcommand{\tabReconciliation}{\begin{tabular}{@{}l r r r r@{}}\toprule \textbf{Méthode} & \textbf{Dépréciation} & \textbf{\% du coût neuf} & \textbf{Valeur indiquée} & \textbf{Poids} \\ \midrule")596for k, (d, v) in METHODES.items():597    T.append(f"{k} & {f(d)} & {pct(d/CN,1)}\\,\\% & {f(v)} & {POIDS[k]}\\,\\% \\\\")598T.append(f"\\midrule Moyenne pondérée (mécanique) & {f(D_MECA)} & {pct(D_MECA/CN,1)}\\,\\% & {f(VT + CN - D_MECA)} $\\rightarrow$ {f(V_MECA)} & 100\\,\\% \\\\")599T.append(f"Dépréciation implicite de la vente du sujet & {f(D_IMPLICITE_VENTE)} & {pct(D_IMPLICITE_VENTE/CN,1)}\\,\\% & {f(SUJET['vente_prix'])} (prix payé) & --- \\\\")600T.append(f"\\textbf{{Retenue : extraction, corroborée par la vente}} & \\textbf{{{f(D_FINAL)}}} & \\textbf{{{pct(D_FINAL/CN,1)}\\,\\%}} & {f(VT + CN - D_FINAL)} $\\rightarrow$ \\textbf{{{f(V_FINAL)}}} & --- \\\\ \\bottomrule\\end{{tabular}}}}")601# sensibilité terrain602T.append(r"\newcommand{\tabSensTerrain}{\begin{tabular}{@{}l r r r@{}}\toprule \textbf{Terrain} & \textbf{Bât. implicite} & \textbf{$D$ implicite} & \textbf{\% $C_N$} \\ \midrule")603for t, d in SENS_TERRAIN:604    lab = {180400: "180~400 (rôle 2022)", 200000: "200~000 (rôle $\\times$ 1,11)", 212000: "212~000 (affectation)", VT: f"{f(VT)} (retenu)", 240000: "240~000", 260000: "260~000"}[t]605    T.append(f"{lab} & {f(SUJET['vente_prix']-t)} & {f(d)} & {pct(d/CN,1)} \\\\")606T.append(r"\bottomrule\end{tabular}}")607# sensibilités VE et AE608T.append(r"\newcommand{\tabSensVE}{\begin{tabular}{@{}c r r r@{}}\toprule \textbf{$VE$} & \textbf{$AE/VE$} & \textbf{$D$} & \textbf{Valeur} \\ \midrule")609for ve, d, v in SENS_VE: T.append(f"{ve} ans & {pct(AE/ve,1)}\\,\\% & {f(d)} & {f(v)} \\\\")610T.append(r"\bottomrule\end{tabular}}")611T.append(r"\newcommand{\tabSensAE}{\begin{tabular}{@{}c r r r@{}}\toprule \textbf{$AE$} & \textbf{$AE/VE$} & \textbf{$D$} & \textbf{Valeur} \\ \midrule")612for ae, d, v in SENS_AE: T.append(f"{ae} ans & {pct(ae/VE,1)}\\,\\% & {f(d)} & {f(v)} \\\\")613T.append(r"\bottomrule\end{tabular}}")614# profil empirique615T.append(r"\newcommand{\tabProfil}{\begin{tabular}{@{}c r r r r r r@{}}\toprule \textbf{Âge} & \textbf{n} & \textbf{Bât./m\textsuperscript{2} (méd.)} & \textbf{Q1--Q3} & \textbf{Part terrain} & \textbf{Prix méd.} & \textbf{Dépréciation relative} \\ \midrule")616for p in PROFIL:617    T.append(f"{p['a']}--{p['b']-1} ans & {p['n']} & {f(p['med'])} & {f(p['q1'])}--{f(p['q3'])} & {pct(p['part_terrain'],0)}\\,\\% & {f(p['prix_med'])} & {pct(p['d_rel'],1)}\\,\\% \\\\")618T.append(r"\bottomrule\end{tabular}}")619# hédonique (coefficient, écart-type, t, lecture)620T.append(r"\newcommand{\tabHedonique}{\begin{tabular}{@{}l r r r l@{}}\toprule \textbf{Variable} & \textbf{Coefficient} & \textbf{Écart-type} & \textbf{$t$} & \textbf{Lecture} \\ \midrule")621LECT = ["niveau de référence", f"{HED['beta'][1]*100:.2f} \\% du prix par année d'âge".replace(".", ","), f"élasticité {HED['beta'][2]:.2f} : +10 \\% d'aire $\\approx$ +{HED['beta'][2]*10:.1f} \\% de prix".replace(".", ","),622        f"élasticité {HED['beta'][3]:.2f} : effet faible du terrain".replace(".", ","), "axe nord--sud : non significatif" if abs(HED['beta'][4]/HED['se'][4]) < 2 else "axe nord--sud", "axe ouest--est : l'ouest (Aylmer) plus cher"]623for nom, b, se_, lect in zip(HED["noms"], HED["beta"], HED["se"], LECT):624    T.append(f"{nom} & {dec(b,4)} & {dec(se_,4)} & {dec(b/se_,1)} & {lect} \\\\")625T.append(f"\\midrule \\multicolumn{{5}}{{@{{}}l}}{{$n = {N_VENTES}$ ; $R^2 = {dec(HED['r2'],2)}$ ; écart-type des résidus {dec(HED['sigma'],2)} (log-prix) ; MCO, écarts-types classiques}} \\\\ \\bottomrule\\end{{tabular}}}}")626# modèles627T.append(r"\newcommand{\tabModeles}{\begin{tabular}{@{}c r r r@{}}\toprule \textbf{$t$ (ans)} & \textbf{Linéaire} & \textbf{Exponentiel} & \textbf{Non linéaire} \\ \midrule")628for t, m in MODELES.items(): T.append(f"{t} & {f(m['lin'])} & {f(m['exp'])} & {f(m['nl'])} \\\\")629T.append(r"\bottomrule\end{tabular}}")630# curabilité : rénovations631T.append(r"\newcommand{\tabRenos}{\begin{tabular}{@{}l r r r r c@{}}\toprule \textbf{Intervention (hypothèses)} & \textbf{Coût $C$} & \textbf{Gain $\Delta V$} & \textbf{Gain net} & \textbf{$\Delta V/C$} & \textbf{Verdict} \\ \midrule")632for r_ in RENOS:633    verdict = "\\textcolor{uqoVert}{\\textbf{curable}}" if r_["curable"] else "\\textcolor{uqoRouge}{\\textbf{incurable}}"634    T.append(f"{r_['nom']} & {f(r_['c'])} & {f(r_['dv'])} & {f(r_['gn'])} & {dec(r_['ratio'],2)} & {verdict} \\\\")635T.append(r"\bottomrule\end{tabular}}")636# entretien différé637T.append(r"\newcommand{\tabCurable}{\begin{tabular}{@{}l r@{}}\toprule \textbf{Constat (hypothèses d'inspection)} & \textbf{Coût de remise en état} \\ \midrule")638for k, v in CURABLE.items(): T.append(f"{k} & {f(v)} \\\\")639T.append(f"\\midrule \\textbf{{Dépréciation physique curable}} & \\textbf{{{f(D_CURABLE)}}} \\\\ \\bottomrule\\end{{tabular}}}}")640# sous-sol641T.append(r"\newcommand{\tabSousSol}{\begin{tabular}{@{}l r@{}}\toprule \textbf{Poste (quantités hypothétiques $\times$ prix nus RSMeans du Cas 1)} & \textbf{Montant} \\ \midrule")642for k, v in SS.items(): T.append(f"{k} & {f(v)} \\\\")643T.append(f"\\midrule \\textbf{{Aménagement du sous-sol (nus)}} & \\textbf{{{f(SS_TOTAL)}}} \\\\ \\bottomrule\\end{{tabular}}}}")644# exemple didactique extraction 4 ventes645T.append(r"\newcommand{\tabExtrQuatre}{\begin{tabular}{@{}l r r r c r r@{}}\toprule & \textbf{Prix} & \textbf{Terrain} & \textbf{Coût neuf} & \textbf{Âge} & \textbf{$D/C_N$} & \textbf{Taux/an} \\ \midrule")646for i, e in enumerate(EX["extr4"], 1):647    T.append(f"Vente {i} & {f(e['prix'])} & {f(e['terrain'])} & {f(e['cn'])} & {e['age']} & {pct(e['ratio'],1)}\\,\\% & {pct(e['taux'],2)}\\,\\% \\\\")648T.append(f"\\midrule \\textbf{{Moyenne}} & & & & & \\textbf{{{pct(EX['extr4_moy_ratio'],1)}\\,\\%}} & \\textbf{{{pct(EX['extr4_moy_taux'],2)}\\,\\%}} \\\\ \\bottomrule\\end{{tabular}}}}")649# terrains vacants retenus650T.append(r"\newcommand{\tabTerrains}{\begin{tabular}{@{}l c r r r r@{}}\toprule \textbf{Terrain vendu} & \textbf{Date} & \textbf{Prix} & \textbf{Superficie} & \textbf{Rôle (terrain)} & \textbf{Prix/rôle} \\ \midrule")651for t in TERRAINS_5KM:652    T.append(f"{t['rue']} & {t['date'][:7]} & {f(t['prix'])} & {dec(t['lot'],0)}~m\\textsuperscript{{2}} & {f(t['vt'])} & {dec(t['ratio'],2)} \\\\")653T.append(f"\\midrule \\textbf{{Médiane ({len(TERRAINS_5KM)} ventes à $\\le$ 5~km)}} & & & & & \\textbf{{{dec(RATIO_TERRAIN,2)}}} \\\\ \\bottomrule\\end{{tabular}}}}")654open(os.path.join(PARTIES, "s09_tables.tex"), "w", encoding="utf-8").write("\n".join(T) + "\n")655656# ---- 14d. Rapport Markdown657L = ["# IMM1033 — Séance 9 : résultats des calculs (calculs_s09.py, 2026-11-08)", "",658     f"Sujet réel : {SUJET['adresse']} — plain-pied détaché {SUJET['annee']} ({AGE_CHRONO} ans), {SUJET['aire_m2']} m² hors sol ({SUJET['aire_pi2']} pi²), terrain {SUJET['lot_m2']} m² ; "659     f"rôle 2024-2026 : terrain {fm(SUJET['vt_role'])} $, bâtiment {fm(SUJET['vb_role'])} $, total {fm(SUJET['vr_role'])} $ ; vendu {fm(SUJET['vente_prix'])} $ le {SUJET['vente_date']} (Registre foncier) ; "660     f"remis en vente le {SUJET['remise_en_vente_date']} à {fm(SUJET['prix_demande'])} $ (Centris 10348445). UQO Éval : estimation {fm(SUJET['uqoeval_est'])} $, coût {fm(SUJET['uqoeval_cout'])} $, rôle indexé {fm(SUJET['uqoeval_role_indexe'])} $.", "",661     "## Coût de remplacement neuf", f"Cas 1 UQO Éval (RSMeans 2026 T4, Gatineau C57) : nus {fm(CAS1_NUS)} $, D+C {fm(CAS1_DC)} $ pour {CAS1_EMPREINTE} m² ; facteur d'échelle K = {K:.4f}.",662     f"Aménagement du sous-sol (nus) : {fm(SS_TOTAL)} $ (LT {fm(SS_LT)}, finitions {fm(SS_FINITIONS)}, salle de bain {fm(SS_SDB)}, plinthes {fm(SS_PLINTHES)}). Ajouts : {fm(sum(HYP['ajouts_sujet'].values()))} $.", "",663     "| Ligne | Montant |", "|---|---:|"]664for k, v in CH.items():665    L.append(f"| {k} | {fm(v) if k != 'multiplicateur' else round(v, 4)} |")666L += ["", f"**Coût neuf complet : {fm(CN)} $** = {fm(CN_M2)} $/m² = {fm(CN_PI2)} $/pi² ; multiplicateur nus → complet {M:.4f} ; vs bâtiment implicite quasi neuf (profil) : {pct(CN_VS_NEUF-1)} %.", "",667      "## Terrain", f"Ratio médian prix/rôle des {len(TERRAINS_5KM)} terrains vacants à ≤ 5 km (2025-2026) : {RATIO_TERRAIN:.3f} → terrain {fm(VT)} $ ; contrôles : affectation {fm(VT_AFFECT)} $, $/m² médian {fm(PM2_TERRAIN)} → {fm(VT_PM2)} $.", ""]668for t in TERRAINS_5KM: L.append(f"- {t['rue']} ({t['date']}) : {fm(t['prix'])} $, {t['lot']:.0f} m², rôle {fm(t['vt'])} $, ratio {t['ratio']:.2f}, {t['dist']:.0f} m")669L += ["", "## Composantes (nus → coût neuf complet)", "| Composante | Nus | CN | Part |", "|---|---:|---:|---:|"]670for nom in ordre: L.append(f"| {nom} | {fm(NUS_SUJET[nom])} | {fm(CN_COMP[nom])} | {pct(CN_COMP[nom]/CN)} % |")671L += ["", "## Ventilation", f"Curable {fm(D_CURABLE)} ; CT {fm(D_CT)} (CN des CT {fm(CN_CT)}) ; base LT {fm(BASE_LT)} × {HYP['AE_LT']}/{VE} = {fm(D_LT)} ; fonctionnelle curable {fm(D_FONCT_CUR)} (cuisine {fm(D_F_CUISINE)} = 28 000 − {fm(CUISINE_CN)} ; VRC {fm(D_F_VRC)} = 4 200 − {fm(VRC_CN)}) ; "672      f"fonctionnelle incurable {fm(D_FONCT_INC)} ; externe {D_EXT} → **total {fm(D_VENT)} $ ({pct(D_VENT/CN)} %)**, valeur {fm(V_VENT)} $.", "", "| Composante | CN | Âge | Vie | Ratio | D | VR |", "|---|---:|:-:|:-:|---:|---:|---:|"]673for l in VENT: L.append(f"| {l['comp']} | {fm(l['cn'])} | {l['age']} | {l['vie']} | {'' if l['ratio'] is None else pct(l['ratio'],0)+' %'} | {fm(l['d'])} | {fm(l['vr'])} |")674L += ["", f"Âge effectif pondéré par le coût neuf (toutes composantes) : {AE_POND:.1f} ans → âge effectif retenu {AE} ans ; VE {VE} ans.", "",675      "## Méthodes", f"- Âge-vie : {fm(D_AV)} $ ({pct(D_AV/CN)} %) → {fm(V_AV)} $ ; avec l'âge chronologique : {fm(D_AV_CHRONO)} $ ; avec VE = 60 : {fm(D_AV_VE60)} $",676      f"- Âge-vie modifiée : {fm(D_AVM)} $ ({pct(D_AVM/CN)} %) → {fm(V_AVM)} $", f"- Ventilation : {fm(D_VENT)} $ ({pct(D_VENT/CN)} %) → {fm(V_VENT)} $",677      f"- Extraction (7 ventes réelles ≤ 400 m) : taux médian {pct(TAUX_MEDIAN,2)} %/an (moyenne {pct(TAUX_MOYEN,2)}, tronquée {pct(TAUX_TRIM,2)} ; ratios {pct(EXTR_RATIOS[0])}–{pct(EXTR_RATIOS[-1])} %) → D = {fm(D_EXTR)} $ ({pct(D_EXTR/CN)} %) → {fm(V_EXTR)} $ ; VE implicite {VE_IMPLICITE:.0f} ans", ""]678for e in EXTR: L.append(f"  - {e['rue']} ({e['annee']}, {e['age']} ans, {e['aire']} m², {e['dist']} m) : prix {fm(e['prix'])}, terrain {fm(e['terrain'])}, bât. {fm(e['vb'])}, CN {fm(e['cn'])}, D {fm(e['d'])} = {pct(e['ratio'])} %, {pct(e['taux'],2)} %/an")679L += ["", f"## Réconciliation : moyenne mécanique (poids {POIDS}) D = {fm(D_MECA)} $ ({pct(D_MECA/CN)} %) → {fm(V_MECA)} $ ({pct(ECART_MECA,1)} % vs vente) — rejetée ; retenue = extraction {fm(D_FINAL)} $ ({pct(D_FINAL/CN)} %) → valeur {fm(VT + CN - D_FINAL)} → **{fm(V_FINAL)} $** ; vente réelle {fm(SUJET['vente_prix'])} ({pct(ECART_VENTE,1)} %), prix demandé {fm(SUJET['prix_demande'])} ({pct(ECART_DEMANDE,1)} %) ; "680      f"dépréciation implicite de la vente : {fm(D_IMPLICITE_VENTE)} $ ({pct(D_IMPLICITE_VENTE/CN)} %).", "", "Sensibilité au terrain : " + " ; ".join(f"{fm(t)} → D {fm(d)} ({pct(d/CN)} %)" for t, d in SENS_TERRAIN),681      "Sensibilité VE : " + " ; ".join(f"{ve} ans → {fm(d)} ({fm(v)})" for ve, d, v in SENS_VE), "Sensibilité AE : " + " ; ".join(f"{ae} ans → {fm(d)} ({fm(v)})" for ae, d, v in SENS_AE), "",682      f"## Profil empirique ({N_VENTES} ventes réelles, ratio prix/rôle médian {RATIO_PRIX_ROLE:.3f}, terrain indexé × {RATIO_TERRAIN:.2f})", "| Âge | n | Bât./m² méd. | Q1 | Q3 | part terrain | prix méd. | d relative |", "|---|---:|---:|---:|---:|---:|---:|---:|"]683for p in PROFIL: L.append(f"| {p['a']}–{p['b']-1} | {p['n']} | {fm(p['med'])} | {fm(p['q1'])} | {fm(p['q3'])} | {pct(p['part_terrain'],0)} % | {fm(p['prix_med'])} | {pct(p['d_rel'])} % |")684L += ["", f"Ajustement exponentiel ln(bât./m²) = a − λ·âge : λ = {pct(LAMBDA_EMP,2)} %/an (é.-t. {pct(LAMBDA_SE,2)}), exp(a) = {fm(VBM2_NEUF_EMP)} $/m², R² = {R2_EXP:.2f}, n = {len(POS)} ; demi-vie {DEMI_VIE_EXP:.0f} ans.",685      f"Régression hédonique ln(prix) : " + " ; ".join(f"{n} {b:.4f} ({s:.4f})" for n, b, s in zip(HED['noms'], HED['beta'], HED['se'])) + f" ; R² {HED['r2']:.2f} ; σ {HED['sigma']:.2f}.", "",686      f"## Modèles : δ = {DELTA:.4f} (1/{VE}), λ = {LAMBDA:.4f}, γ = {GAMMA}, λ' = {LAMBDA_NL:.5f}", "| t | linéaire | exponentiel | non linéaire | dV/dt lin | exp | nl |", "|---|---:|---:|---:|---:|---:|---:|"]687for t, m in MODELES.items(): L.append(f"| {t} | {fm(m['lin'])} | {fm(m['exp'])} | {fm(m['nl'])} | {fm(m['dlin'])} | {fm(m['dexp'])} | {fm(m['dnl'])} |")688L += ["", f"## Curabilité : ΔV(C) = {A_CUR} (1 − e^(−C/{B_CUR})) ; C* = {fm(C_OPT)} $, ΔV* = {fm(DV_OPT)} $, GN* = {fm(GN_OPT)} $ ; seuil ΔV = C à C = {fm(C_SEUIL)} $.",689      "| Intervention | C | ΔV | GN | ΔV/C | verdict |", "|---|---:|---:|---:|---:|---|"]690for r_ in RENOS: L.append(f"| {r_['nom']} | {fm(r_['c'])} | {fm(r_['dv'])} | {fm(r_['gn'])} | {r_['ratio']:.2f} | {'curable' if r_['curable'] else 'incurable'} |")691L += ["", "## Exemples didactiques", json.dumps(EX, ensure_ascii=False, default=str)[:3000]]692open(os.path.join(HERE, "RESULTATS_S09.md"), "w", encoding="utf-8").write("\n".join(L) + "\n")693print("\n".join(L[:60]))694print("...")695print(f"CN {fm(CN)} | VT {fm(VT)} | D : AV {fm(D_AV)} ({pct(D_AV/CN)} %) AVM {fm(D_AVM)} VENT {fm(D_VENT)} ({pct(D_VENT/CN)} %) EXTR {fm(D_EXTR)} ({pct(D_EXTR/CN)} %) | final {fm(D_FINAL)} → {fm(V_FINAL)} vs vente {fm(SUJET['vente_prix'])} ({pct(ECART_VENTE)} %) | D implicite {fm(D_IMPLICITE_VENTE)} ({pct(D_IMPLICITE_VENTE/CN)} %)")696print(f"macros : {len(mac)} ; tableaux : {sum(1 for t in T if t.startswith(chr(92)+'newcommand'))}")697
Files main
Menu

Theme