spb/traite-evaluation-qc
Public
1#!/usr/bin/env python32"""Chapitre 11 — inspection synthétique du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES) : fiche d'état par3composante, âge effectif (convention déclarée), liste des travaux, lien avec la dépréciation ventilée (traite.cout).45Imprime tous les chiffres cités dans le chapitre 11 :6 * fiche d'état : 19 composantes de courte/moyenne durée + bloc « structure et ouvrages de longue durée », chacune7 avec son coût neuf (postes du devis cas1_gatineau_couts.csv, majorés des indirects et de l'incitatif du ch. 21),8 son année d'installation, son âge chronologique, sa vie utile (hypothèse pédagogique), sa note d'état (1-5) et9 son âge effectif selon la convention a_eff = (1 − λ)·a + λ·φ(c)·L, bornée à [0, L] ;10 * indice d'état pondéré par les coûts ; âge effectif de la structure ; vie économique restante ;11 * travaux immédiats (dépréciation physique curable) ;12 * dépréciation ventilée (traite.cout.depreciation_ventilee) et comparaison avec l'âge-vie simple ;13 * sensibilité à λ et à la note d'état de la structure ;14 * valeur de l'information d'un test (décision de recommander un test de radon ou de pyrite).1516Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch11_inspection.py17Toutes les formules de dépréciation proviennent de analysis/python/traite/cout.py (aucune réécriture)."""18from __future__ import annotations19import sys, json, pathlib20from dataclasses import dataclass21import numpy as np, pandas as pd22sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))23sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))24from traite import cout as ct25import ch21_cout_neuf as c212627ROOT = pathlib.Path(__file__).resolve().parents[3]28D = ROOT / "data/synthetic"2930ANNEE_INSPECTION = 2026 # date d'évaluation du cas 1 : 2026-09-0131VIE_ECONOMIQUE = 60.0 # vie économique totale du bâtiment (hypothèse pédagogique, voir texte)32LAMBDA = 0.5 # poids de l'observation dans l'âge effectif (convention de l'ouvrage)33PHI = {1: 0.05, 2: 0.25, 3: 0.50, 4: 0.75, 5: 1.00} # fraction de vie utile consommée associée à chaque note d'état3435ETATS = {1: "Excellent — neuf ou remis à neuf, aucune usure visible",36 2: "Bon — usure légère, entretien à jour, aucune réparation requise",37 3: "Moyen — usure normale pour l'âge, entretien différé mineur",38 4: "Passable — usure avancée, réparations requises à court terme",39 5: "Mauvais — fin de vie, remplacement ou réfection nécessaire"}404142@dataclass43class Observation:44 composante: str45 postes: tuple[str, ...] # postes du devis (cas1_gatineau_couts.csv) qui forment la composante46 annee: int # année d'installation (origine 1998 ou remplacement)47 vie_utile: float # vie utile typique (hypothèse pédagogique)48 etat: int # note d'état 1-5 constatée à l'inspection (synthétique)49 constat: str # constat d'inspection (fictif)50 longue_duree: bool = False # True : fait partie du bloc structure (âge-vie sur la base restante)515253# Fiche d'état synthétique du sujet (cottage 1998, Hull) — constats FICTIFS construits pour le traité54OBSERVATIONS: list[Observation] = [55 Observation("Couverture (bardeaux d'asphalte)", ("Couverture bardeaux d'asphalte",), 2012, 20, 3,56 "Refaite en 2012 ; perte de granules localisée au versant sud, aucun relèvement de bardeaux"),57 Observation("Gouttières et solins", ("Gouttières et solins",), 1998, 25, 4,58 "D'origine ; solin de cheminée oxydé, gouttière arrière désolidarisée de la bordure"),59 Observation("Fenêtres (PVC, double vitrage)", ("Fenêtres (PVC double vitrage)",), 1998, 30, 3,60 "D'origine ; deux thermos embués (chambre nord-est, salle à manger), calfeutrage sec"),61 Observation("Portes extérieures et porte de garage", ("Portes extérieures", "Porte de garage"), 1998, 30, 3,62 "D'origine ; coupe-froid usés, porte de garage fonctionnelle"),63 Observation("Revêtement extérieur (brique et fibrociment)", ("Revêtement extérieur (brique 40 % / fibrociment 60 %)",), 1998, 40, 2,64 "Brique saine, joints intacts ; fibrociment repeint en 2018, aucune fissure"),65 Observation("Peinture intérieure", ("Peinture intérieure",), 2021, 10, 2, "Refaite en 2021, bon état général"),66 Observation("Planchers de bois franc", ("Planchers bois franc",), 1998, 40, 3, "D'origine ; vernis usé dans les aires de passage"),67 Observation("Planchers de céramique", ("Planchers céramique",), 1998, 35, 2, "D'origine ; joints sains"),68 Observation("Planchers flottants (sous-sol)", ("Planchers flottants (sous-sol)",), 2009, 20, 4,69 "Posés en 2009 ; gondolement au pied du mur nord-est (voir fondations)"),70 Observation("Armoires de cuisine et comptoirs", ("Armoires de cuisine et comptoirs",), 1998, 30, 3,71 "D'origine ; mélamine, quelques portes désajustées, comptoir stratifié usé"),72 Observation("Vanités et accessoires (2 salles de bain)", ("Vanités et accessoires (2 sdb)",), 2015, 25, 2, "Salles de bain rénovées en 2015"),73 Observation("Plomberie (cuivre et PEX)", ("Plomberie brute et finition",), 1998, 50, 2,74 "Cuivre d'origine, modifications en PEX (2015) ; aucune fuite, pression adéquate"),75 Observation("Chauffe-eau électrique", ("Chauffe-eau",), 2019, 12, 2, "Remplacé en 2019 (plaque signalétique)"),76 Observation("Électricité (entrée 200 A, filage cuivre, éclairage)", ("Électricité (entrée 200 A, filage, appareils)", "Éclairage et finition électrique"), 1998, 50, 2,77 "Panneau à disjoncteurs 200 A, filage de cuivre ; aucune anomalie visible"),78 Observation("Ventilateur récupérateur de chaleur (VRC)", ("Ventilation (VRC) et conduits",), 1998, 20, 4,79 "D'origine ; fonctionnel mais bruyant, noyau et filtres encrassés"),80 Observation("Thermopompe centrale et plinthes", ("Thermopompe centrale et plinthes",), 2016, 15, 3,81 "Thermopompe de 2016 ; plinthes d'origine fonctionnelles"),82 Observation("Balcon et perron", ("Balcon et perron",), 1998, 25, 4,83 "Bois traité d'origine ; garde-corps lâche, trois planches pourries"),84 Observation("Entrée asphaltée", ("Entrée asphaltée",), 1998, 20, 5, "D'origine ; fissuration généralisée et affaissement"),85 Observation("Aménagement paysager", ("Aménagement paysager de base",), 1998, 20, 3, "Entretenu ; pente négative côté nord-est"),86 # Bloc de longue durée (structure et ouvrages de base)87 Observation("Structure, fondations, dalle, drain, isolation, cloisons", (88 "Excavation et remblai", "Fondations béton (semelles, murs)", "Dalle de sous-sol", "Drain français et imperméabilisation",89 "Charpente murs et planchers", "Fermes de toit", "Escaliers", "Isolation et pare-vapeur", "Gypse et tirage de joints",90 "Portes intérieures et moulures", "Raccordements aqueduc/égout", "Nettoyage et conteneurs"), 1998, VIE_ECONOMIQUE, 3,91 "Fissure de retrait verticale au mur nord-est (0,5 mm) avec efflorescence ; charpente droite ; drain non vérifié", True),92]9394# Travaux immédiats (dépréciation physique curable) : coût = fraction déclarée du poste de référence, majorée comme le coût neuf95TRAVAUX = [96 ("Réfection de l'entrée asphaltée", "Entrée asphaltée", 1.00, "fin de vie (état 5) : remplacement complet"),97 ("Colmatage de la fissure de fondation (injection) et correction de la pente", "Drain français et imperméabilisation", 0.25, "fissure active avec efflorescence"),98 ("Remplacement de deux thermos embués et calfeutrage", "Fenêtres (PVC double vitrage)", 0.125, "2 unités sur 16"),99 ("Réparation du balcon (garde-corps, planches)", "Balcon et perron", 0.30, "sécurité des personnes"),100 ("Réfection du solin de cheminée et refixation de la gouttière", "Gouttières et solins", 0.20, "infiltration potentielle"),101]102103104def charger():105 devis = pd.read_csv(D / "cas1_gatineau_couts.csv")106 sujet = json.loads((D / "cas1_gatineau_sujet.json").read_text())107 cn = c21.cout_neuf_sujet(devis) # coût neuf du ch. 21 (397 567,33 $)108 return devis, sujet, cn109110111def facteur_majoration(cn: dict) -> float:112 """Coût neuf d'une composante = coût direct du devis × (1 + τ_I)(1 + τ_E) = C_N / D."""113 return cn["total"] / cn["direct"]114115116def phi(etat: int) -> float:117 return PHI[int(etat)]118119120def age_effectif(age: float, vie: float, etat: int, lam: float = LAMBDA) -> float:121 """Convention de l'ouvrage : a_eff = (1 − λ) a + λ φ(c) L, bornée à [0, L]."""122 a = (1.0 - lam) * age + lam * phi(etat) * vie123 return float(min(max(a, 0.0), vie))124125126def fiche(devis: pd.DataFrame, cn: dict, lam: float = LAMBDA, annee: int = ANNEE_INSPECTION) -> pd.DataFrame:127 g = facteur_majoration(cn)128 rows = []129 for o in OBSERVATIONS:130 direct = float(devis.loc[devis["poste"].isin(o.postes), "montant"].sum())131 assert devis["poste"].isin(o.postes).sum() == len(o.postes), f"poste manquant : {o.postes}"132 age = annee - o.annee133 aeff = age_effectif(age, o.vie_utile, o.etat, lam)134 rows.append({"composante": o.composante, "annee": o.annee, "age": age, "vie_utile": o.vie_utile, "etat": o.etat,135 "phi": phi(o.etat), "age_effectif": aeff, "ratio": aeff / o.vie_utile, "vie_restante": ct.vie_restante(o.vie_utile, aeff),136 "cout_direct": direct, "cout_neuf": direct * g, "longue_duree": o.longue_duree, "constat": o.constat})137 f = pd.DataFrame(rows)138 assert abs(f["cout_direct"].sum() - cn["direct"]) < 0.01, "les composantes doivent couvrir exactement les 33 postes directs"139 assert abs(f["cout_neuf"].sum() - cn["total"]) < 0.01140 return f141142143def indice_etat(f: pd.DataFrame) -> float:144 """Indice d'état pondéré par les coûts neufs : I_E = Σ w_i c_i, w_i = C_i / Σ C_j."""145 return float((f["cout_neuf"] * f["etat"]).sum() / f["cout_neuf"].sum())146147148def travaux(devis: pd.DataFrame, cn: dict) -> pd.DataFrame:149 g = facteur_majoration(cn)150 rows = []151 for nom, poste, frac, motif in TRAVAUX:152 base = float(devis.loc[devis["poste"] == poste, "montant"].iloc[0])153 rows.append({"travaux": nom, "poste_reference": poste, "fraction": frac, "cout_direct": base * frac, "cout": base * frac * g, "motif": motif})154 return pd.DataFrame(rows)155156157def depreciation(f: pd.DataFrame, trav: pd.DataFrame, cn: dict, lam: float = LAMBDA, etat_structure: int | None = None) -> dict:158 """Dépréciation physique ventilée du sujet : curable (travaux) + courte durée (composantes) + longue durée (structure, âge-vie).159 L'entrée asphaltée, remplacée intégralement (travaux), est retirée des composantes de courte durée pour éviter le double comptage."""160 curable = float(trav["cout"].sum())161 courtes = f[(~f["longue_duree"]) & (f["composante"] != "Entrée asphaltée")]162 comps = [ct.Composante(r.composante, r.cout_neuf, age_effectif(r.age, r.vie_utile, r.etat, lam), r.vie_utile) for r in courtes.itertuples()]163 s = f[f["longue_duree"]].iloc[0]164 etat_s = int(s.etat if etat_structure is None else etat_structure)165 a_struct = age_effectif(s.age, VIE_ECONOMIQUE, etat_s, lam)166 # base de longue durée = C_N − curable − Σ coût neuf des composantes de courte durée (l'entrée, remplacée, est entièrement dans le curable)167 dv = ct.depreciation_ventilee(cn["total"], curable, comps, a_struct, VIE_ECONOMIQUE)168 dv["age_effectif_structure"] = a_struct169 dv["vie_restante_structure"] = ct.vie_restante(VIE_ECONOMIQUE, a_struct)170 dv["taux"] = dv["total"] / cn["total"]171 dv["cout_neuf"] = cn["total"]172 dv["age_vie_simple"] = ct.depreciation_age_vie(cn["total"], a_struct, VIE_ECONOMIQUE)173 dv["contribution_batiment"] = cn["total"] - dv["total"]174 return dv175176177def sensibilite(f, trav, cn, lams=(0.0, 0.25, 0.5, 0.75, 1.0), etats=(1, 2, 3, 4, 5)) -> pd.DataFrame:178 rows = []179 for lam in lams:180 for e in etats:181 dv = depreciation(f, trav, cn, lam, e)182 rows.append({"lambda": lam, "etat_structure": e, "age_effectif_structure": dv["age_effectif_structure"],183 "depreciation": dv["total"], "taux": dv["taux"], "longue_duree": dv["longue_duree"]})184 return pd.DataFrame(rows)185186187def valeur_information(p: float, cout_correction: float, cout_test: float) -> dict:188 """Décision de recommander un test : perte espérée sans test = p·C ; le test (coût t) vaut la peine si t < p·C.189 Avec un test parfait, la valeur espérée de l'information est p·C − t (si positive)."""190 return {"perte_esperee": p * cout_correction, "valeur_info": p * cout_correction - cout_test, "recommander": cout_test < p * cout_correction}191192193def main() -> dict:194 devis, sujet, cn = charger()195 g = facteur_majoration(cn)196 f = fiche(devis, cn)197 trav = travaux(devis, cn)198 dv = depreciation(f, trav, cn)199 fm = lambda x: f"{x:,.2f}".replace(",", " ")200 f0 = lambda x: f"{x:,.0f}".replace(",", " ")201 print("== DONNÉES SYNTHÉTIQUES — sujet cas 1 :", sujet["type"], sujet["annee_construction"], sujet["superficie_habitable_m2"], "m² hab.,", sujet["secteur"], "; inspection", sujet["date_evaluation"])202 print(f"== Coût neuf C_N (ch. 21) = {fm(cn['total'])} $ ; directs D = {fm(cn['direct'])} $ ; facteur de majoration C_N/D = {g:.5f}")203 print(f"== Convention âge effectif : a_eff = (1−λ) a + λ φ(c) L, λ = {LAMBDA}, φ = {PHI}, vie économique L_B = {VIE_ECONOMIQUE:.0f} ans")204 print("== Fiche d'état (19 composantes + structure) :")205 print(f" {'composante':58s} {'an':>4s} {'âge':>4s} {'L':>3s} {'c':>2s} {'a_eff':>6s} {'a/L':>5s} {'C direct':>11s} {'C neuf':>11s}")206 for r in f.itertuples():207 print(f" {r.composante:58s} {r.annee:4d} {r.age:4d} {r.vie_utile:3.0f} {r.etat:2d} {r.age_effectif:6.2f} {r.ratio:5.3f} {fm(r.cout_direct):>11s} {fm(r.cout_neuf):>11s}")208 courtes = f[~f["longue_duree"]]209 print(f" Σ courte/moyenne durée : direct {fm(courtes['cout_direct'].sum())} $, neuf {fm(courtes['cout_neuf'].sum())} $ ({courtes['cout_neuf'].sum()/cn['total']*100:.1f} % de C_N)")210 s = f[f["longue_duree"]].iloc[0]211 print(f" Structure et longue durée : direct {fm(s.cout_direct)} $, neuf {fm(s.cout_neuf)} $ ({s.cout_neuf/cn['total']*100:.1f} % de C_N) ; âge effectif {s.age_effectif:.1f} ans, vie restante {s.vie_restante:.1f} ans")212 print(f"== Indice d'état pondéré I_E = {indice_etat(f):.3f} ; médiane des notes {f['etat'].median():.0f} ; nombre de composantes par note : {f['etat'].value_counts().sort_index().to_dict()}")213 print(f" Âge effectif moyen pondéré (ratio a_eff/L) = {(f['cout_neuf']*f['ratio']).sum()/f['cout_neuf'].sum():.3f} → équivalent {((f['cout_neuf']*f['ratio']).sum()/f['cout_neuf'].sum())*VIE_ECONOMIQUE:.1f} ans sur 60")214 print("== Travaux immédiats (curables) :")215 for r in trav.itertuples():216 print(f" {r.travaux:75s} {r.fraction*100:5.1f} % de « {r.poste_reference} » = {fm(r.cout_direct)} $ direct → {fm(r.cout)} $ majoré ; {r.motif}")217 print(f" Total curable = {fm(trav['cout'].sum())} $ ({trav['cout'].sum()/cn['total']*100:.2f} % de C_N)")218 print("== Dépréciation ventilée (traite.cout.depreciation_ventilee) :")219 print(f" curable {fm(dv['curable'])} $")220 for k, v in dv["courte_duree"].items():221 print(f" courte durée — {k:50s} {fm(v):>11s} $")222 print(f" Σ courte durée {fm(sum(dv['courte_duree'].values()))} $")223 print(f" base longue durée {fm(dv['base_longue'])} $ × {dv['age_effectif_structure']:.2f}/{VIE_ECONOMIQUE:.0f} = {fm(dv['longue_duree'])} $")224 print(f" TOTAL {fm(dv['total'])} $ = {dv['taux']*100:.2f} % de C_N ; contribution du bâtiment déprécié {fm(dv['contribution_batiment'])} $")225 print(f" Âge-vie simple sur C_N : {fm(dv['age_vie_simple'])} $ ({dv['age_vie_simple']/cn['total']*100:.2f} %) ; écart ventilée − âge-vie = {fm(dv['total']-dv['age_vie_simple'])} $")226 print(f" Âge-vie simple avec âge chronologique 28 : {fm(ct.depreciation_age_vie(cn['total'], 28, VIE_ECONOMIQUE))} $ ({28/VIE_ECONOMIQUE*100:.2f} %)")227 print("== Sensibilité (λ × état de la structure) — dépréciation totale en % de C_N :")228 sens = sensibilite(f, trav, cn)229 piv = sens.pivot(index="lambda", columns="etat_structure", values="taux") * 100230 print(piv.round(2).to_string())231 piva = sens.pivot(index="lambda", columns="etat_structure", values="age_effectif_structure")232 print(" âge effectif de la structure :"); print(piva.round(1).to_string())233 print(f" Étendue à λ = 0,5 : {piv.loc[0.5].min():.2f} % – {piv.loc[0.5].max():.2f} % ; à état 3 : {piv[3].min():.2f} % – {piv[3].max():.2f} %")234 print("== Valeur de l'information d'un test (illustration, paramètres hypothétiques) :")235 for p, C, t, nom in [(0.10, 3000.0, 50.0, "radon : p = 10 %, correction 3 000 $, dosimètre 50 $"),236 (0.05, 25000.0, 1200.0, "pyrite : p = 5 %, excavation 25 000 $, test 1 200 $"),237 (0.02, 25000.0, 1200.0, "pyrite : p = 2 %")]:238 v = valeur_information(p, C, t)239 print(f" {nom:60s} perte espérée {f0(v['perte_esperee'])} $ ; valeur de l'information {f0(v['valeur_info'])} $ ; recommander : {v['recommander']}")240 return {"devis": devis, "sujet": sujet, "cn": cn, "fiche": f, "travaux": trav, "depreciation": dv, "sensibilite": sens}241242243if __name__ == "__main__":244 main()245