#!/usr/bin/env python3 """Chapitre 11 — inspection synthétique du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES) : fiche d'état par composante, âge effectif (convention déclarée), liste des travaux, lien avec la dépréciation ventilée (traite.cout). Imprime tous les chiffres cités dans le chapitre 11 : * fiche d'état : 19 composantes de courte/moyenne durée + bloc « structure et ouvrages de longue durée », chacune avec son coût neuf (postes du devis cas1_gatineau_couts.csv, majorés des indirects et de l'incitatif du ch. 21), son année d'installation, son âge chronologique, sa vie utile (hypothèse pédagogique), sa note d'état (1-5) et son âge effectif selon la convention a_eff = (1 − λ)·a + λ·φ(c)·L, bornée à [0, L] ; * indice d'état pondéré par les coûts ; âge effectif de la structure ; vie économique restante ; * travaux immédiats (dépréciation physique curable) ; * dépréciation ventilée (traite.cout.depreciation_ventilee) et comparaison avec l'âge-vie simple ; * sensibilité à λ et à la note d'état de la structure ; * valeur de l'information d'un test (décision de recommander un test de radon ou de pyrite). Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch11_inspection.py Toutes les formules de dépréciation proviennent de analysis/python/traite/cout.py (aucune réécriture).""" from __future__ import annotations import sys, json, pathlib from dataclasses import dataclass import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from traite import cout as ct import ch21_cout_neuf as c21 ROOT = pathlib.Path(__file__).resolve().parents[3] D = ROOT / "data/synthetic" ANNEE_INSPECTION = 2026 # date d'évaluation du cas 1 : 2026-09-01 VIE_ECONOMIQUE = 60.0 # vie économique totale du bâtiment (hypothèse pédagogique, voir texte) LAMBDA = 0.5 # poids de l'observation dans l'âge effectif (convention de l'ouvrage) PHI = {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'état ETATS = {1: "Excellent — neuf ou remis à neuf, aucune usure visible", 2: "Bon — usure légère, entretien à jour, aucune réparation requise", 3: "Moyen — usure normale pour l'âge, entretien différé mineur", 4: "Passable — usure avancée, réparations requises à court terme", 5: "Mauvais — fin de vie, remplacement ou réfection nécessaire"} @dataclass class Observation: composante: str postes: tuple[str, ...] # postes du devis (cas1_gatineau_couts.csv) qui forment la composante annee: int # année d'installation (origine 1998 ou remplacement) vie_utile: float # vie utile typique (hypothèse pédagogique) etat: int # note d'état 1-5 constatée à l'inspection (synthétique) constat: str # constat d'inspection (fictif) longue_duree: bool = False # True : fait partie du bloc structure (âge-vie sur la base restante) # Fiche d'état synthétique du sujet (cottage 1998, Hull) — constats FICTIFS construits pour le traité OBSERVATIONS: list[Observation] = [ Observation("Couverture (bardeaux d'asphalte)", ("Couverture bardeaux d'asphalte",), 2012, 20, 3, "Refaite en 2012 ; perte de granules localisée au versant sud, aucun relèvement de bardeaux"), Observation("Gouttières et solins", ("Gouttières et solins",), 1998, 25, 4, "D'origine ; solin de cheminée oxydé, gouttière arrière désolidarisée de la bordure"), Observation("Fenêtres (PVC, double vitrage)", ("Fenêtres (PVC double vitrage)",), 1998, 30, 3, "D'origine ; deux thermos embués (chambre nord-est, salle à manger), calfeutrage sec"), Observation("Portes extérieures et porte de garage", ("Portes extérieures", "Porte de garage"), 1998, 30, 3, "D'origine ; coupe-froid usés, porte de garage fonctionnelle"), Observation("Revêtement extérieur (brique et fibrociment)", ("Revêtement extérieur (brique 40 % / fibrociment 60 %)",), 1998, 40, 2, "Brique saine, joints intacts ; fibrociment repeint en 2018, aucune fissure"), Observation("Peinture intérieure", ("Peinture intérieure",), 2021, 10, 2, "Refaite en 2021, bon état général"), Observation("Planchers de bois franc", ("Planchers bois franc",), 1998, 40, 3, "D'origine ; vernis usé dans les aires de passage"), Observation("Planchers de céramique", ("Planchers céramique",), 1998, 35, 2, "D'origine ; joints sains"), Observation("Planchers flottants (sous-sol)", ("Planchers flottants (sous-sol)",), 2009, 20, 4, "Posés en 2009 ; gondolement au pied du mur nord-est (voir fondations)"), Observation("Armoires de cuisine et comptoirs", ("Armoires de cuisine et comptoirs",), 1998, 30, 3, "D'origine ; mélamine, quelques portes désajustées, comptoir stratifié usé"), 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"), Observation("Plomberie (cuivre et PEX)", ("Plomberie brute et finition",), 1998, 50, 2, "Cuivre d'origine, modifications en PEX (2015) ; aucune fuite, pression adéquate"), Observation("Chauffe-eau électrique", ("Chauffe-eau",), 2019, 12, 2, "Remplacé en 2019 (plaque signalétique)"), Observation("Électricité (entrée 200 A, filage cuivre, éclairage)", ("Électricité (entrée 200 A, filage, appareils)", "Éclairage et finition électrique"), 1998, 50, 2, "Panneau à disjoncteurs 200 A, filage de cuivre ; aucune anomalie visible"), Observation("Ventilateur récupérateur de chaleur (VRC)", ("Ventilation (VRC) et conduits",), 1998, 20, 4, "D'origine ; fonctionnel mais bruyant, noyau et filtres encrassés"), Observation("Thermopompe centrale et plinthes", ("Thermopompe centrale et plinthes",), 2016, 15, 3, "Thermopompe de 2016 ; plinthes d'origine fonctionnelles"), Observation("Balcon et perron", ("Balcon et perron",), 1998, 25, 4, "Bois traité d'origine ; garde-corps lâche, trois planches pourries"), Observation("Entrée asphaltée", ("Entrée asphaltée",), 1998, 20, 5, "D'origine ; fissuration généralisée et affaissement"), Observation("Aménagement paysager", ("Aménagement paysager de base",), 1998, 20, 3, "Entretenu ; pente négative côté nord-est"), # Bloc de longue durée (structure et ouvrages de base) Observation("Structure, fondations, dalle, drain, isolation, cloisons", ( "Excavation et remblai", "Fondations béton (semelles, murs)", "Dalle de sous-sol", "Drain français et imperméabilisation", "Charpente murs et planchers", "Fermes de toit", "Escaliers", "Isolation et pare-vapeur", "Gypse et tirage de joints", "Portes intérieures et moulures", "Raccordements aqueduc/égout", "Nettoyage et conteneurs"), 1998, VIE_ECONOMIQUE, 3, "Fissure de retrait verticale au mur nord-est (0,5 mm) avec efflorescence ; charpente droite ; drain non vérifié", True), ] # 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 neuf TRAVAUX = [ ("Réfection de l'entrée asphaltée", "Entrée asphaltée", 1.00, "fin de vie (état 5) : remplacement complet"), ("Colmatage de la fissure de fondation (injection) et correction de la pente", "Drain français et imperméabilisation", 0.25, "fissure active avec efflorescence"), ("Remplacement de deux thermos embués et calfeutrage", "Fenêtres (PVC double vitrage)", 0.125, "2 unités sur 16"), ("Réparation du balcon (garde-corps, planches)", "Balcon et perron", 0.30, "sécurité des personnes"), ("Réfection du solin de cheminée et refixation de la gouttière", "Gouttières et solins", 0.20, "infiltration potentielle"), ] def charger(): devis = pd.read_csv(D / "cas1_gatineau_couts.csv") sujet = json.loads((D / "cas1_gatineau_sujet.json").read_text()) cn = c21.cout_neuf_sujet(devis) # coût neuf du ch. 21 (397 567,33 $) return devis, sujet, cn def facteur_majoration(cn: dict) -> float: """Coût neuf d'une composante = coût direct du devis × (1 + τ_I)(1 + τ_E) = C_N / D.""" return cn["total"] / cn["direct"] def phi(etat: int) -> float: return PHI[int(etat)] def age_effectif(age: float, vie: float, etat: int, lam: float = LAMBDA) -> float: """Convention de l'ouvrage : a_eff = (1 − λ) a + λ φ(c) L, bornée à [0, L].""" a = (1.0 - lam) * age + lam * phi(etat) * vie return float(min(max(a, 0.0), vie)) def fiche(devis: pd.DataFrame, cn: dict, lam: float = LAMBDA, annee: int = ANNEE_INSPECTION) -> pd.DataFrame: g = facteur_majoration(cn) rows = [] for o in OBSERVATIONS: direct = float(devis.loc[devis["poste"].isin(o.postes), "montant"].sum()) assert devis["poste"].isin(o.postes).sum() == len(o.postes), f"poste manquant : {o.postes}" age = annee - o.annee aeff = age_effectif(age, o.vie_utile, o.etat, lam) rows.append({"composante": o.composante, "annee": o.annee, "age": age, "vie_utile": o.vie_utile, "etat": o.etat, "phi": phi(o.etat), "age_effectif": aeff, "ratio": aeff / o.vie_utile, "vie_restante": ct.vie_restante(o.vie_utile, aeff), "cout_direct": direct, "cout_neuf": direct * g, "longue_duree": o.longue_duree, "constat": o.constat}) f = pd.DataFrame(rows) assert abs(f["cout_direct"].sum() - cn["direct"]) < 0.01, "les composantes doivent couvrir exactement les 33 postes directs" assert abs(f["cout_neuf"].sum() - cn["total"]) < 0.01 return f def indice_etat(f: pd.DataFrame) -> float: """Indice d'état pondéré par les coûts neufs : I_E = Σ w_i c_i, w_i = C_i / Σ C_j.""" return float((f["cout_neuf"] * f["etat"]).sum() / f["cout_neuf"].sum()) def travaux(devis: pd.DataFrame, cn: dict) -> pd.DataFrame: g = facteur_majoration(cn) rows = [] for nom, poste, frac, motif in TRAVAUX: base = float(devis.loc[devis["poste"] == poste, "montant"].iloc[0]) rows.append({"travaux": nom, "poste_reference": poste, "fraction": frac, "cout_direct": base * frac, "cout": base * frac * g, "motif": motif}) return pd.DataFrame(rows) def depreciation(f: pd.DataFrame, trav: pd.DataFrame, cn: dict, lam: float = LAMBDA, etat_structure: int | None = None) -> dict: """Dépréciation physique ventilée du sujet : curable (travaux) + courte durée (composantes) + longue durée (structure, âge-vie). L'entrée asphaltée, remplacée intégralement (travaux), est retirée des composantes de courte durée pour éviter le double comptage.""" curable = float(trav["cout"].sum()) courtes = f[(~f["longue_duree"]) & (f["composante"] != "Entrée asphaltée")] 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()] s = f[f["longue_duree"]].iloc[0] etat_s = int(s.etat if etat_structure is None else etat_structure) a_struct = age_effectif(s.age, VIE_ECONOMIQUE, etat_s, lam) # 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) dv = ct.depreciation_ventilee(cn["total"], curable, comps, a_struct, VIE_ECONOMIQUE) dv["age_effectif_structure"] = a_struct dv["vie_restante_structure"] = ct.vie_restante(VIE_ECONOMIQUE, a_struct) dv["taux"] = dv["total"] / cn["total"] dv["cout_neuf"] = cn["total"] dv["age_vie_simple"] = ct.depreciation_age_vie(cn["total"], a_struct, VIE_ECONOMIQUE) dv["contribution_batiment"] = cn["total"] - dv["total"] return dv def sensibilite(f, trav, cn, lams=(0.0, 0.25, 0.5, 0.75, 1.0), etats=(1, 2, 3, 4, 5)) -> pd.DataFrame: rows = [] for lam in lams: for e in etats: dv = depreciation(f, trav, cn, lam, e) rows.append({"lambda": lam, "etat_structure": e, "age_effectif_structure": dv["age_effectif_structure"], "depreciation": dv["total"], "taux": dv["taux"], "longue_duree": dv["longue_duree"]}) return pd.DataFrame(rows) def valeur_information(p: float, cout_correction: float, cout_test: float) -> dict: """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. Avec un test parfait, la valeur espérée de l'information est p·C − t (si positive).""" return {"perte_esperee": p * cout_correction, "valeur_info": p * cout_correction - cout_test, "recommander": cout_test < p * cout_correction} def main() -> dict: devis, sujet, cn = charger() g = facteur_majoration(cn) f = fiche(devis, cn) trav = travaux(devis, cn) dv = depreciation(f, trav, cn) fm = lambda x: f"{x:,.2f}".replace(",", " ") f0 = lambda x: f"{x:,.0f}".replace(",", " ") 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"]) 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}") print(f"== Convention âge effectif : a_eff = (1−λ) a + λ φ(c) L, λ = {LAMBDA}, φ = {PHI}, vie économique L_B = {VIE_ECONOMIQUE:.0f} ans") print("== Fiche d'état (19 composantes + structure) :") print(f" {'composante':58s} {'an':>4s} {'âge':>4s} {'L':>3s} {'c':>2s} {'a_eff':>6s} {'a/L':>5s} {'C direct':>11s} {'C neuf':>11s}") for r in f.itertuples(): 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}") courtes = f[~f["longue_duree"]] 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)") s = f[f["longue_duree"]].iloc[0] 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") 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()}") 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") print("== Travaux immédiats (curables) :") for r in trav.itertuples(): 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}") print(f" Total curable = {fm(trav['cout'].sum())} $ ({trav['cout'].sum()/cn['total']*100:.2f} % de C_N)") print("== Dépréciation ventilée (traite.cout.depreciation_ventilee) :") print(f" curable {fm(dv['curable'])} $") for k, v in dv["courte_duree"].items(): print(f" courte durée — {k:50s} {fm(v):>11s} $") print(f" Σ courte durée {fm(sum(dv['courte_duree'].values()))} $") print(f" base longue durée {fm(dv['base_longue'])} $ × {dv['age_effectif_structure']:.2f}/{VIE_ECONOMIQUE:.0f} = {fm(dv['longue_duree'])} $") print(f" TOTAL {fm(dv['total'])} $ = {dv['taux']*100:.2f} % de C_N ; contribution du bâtiment déprécié {fm(dv['contribution_batiment'])} $") 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'])} $") print(f" Âge-vie simple avec âge chronologique 28 : {fm(ct.depreciation_age_vie(cn['total'], 28, VIE_ECONOMIQUE))} $ ({28/VIE_ECONOMIQUE*100:.2f} %)") print("== Sensibilité (λ × état de la structure) — dépréciation totale en % de C_N :") sens = sensibilite(f, trav, cn) piv = sens.pivot(index="lambda", columns="etat_structure", values="taux") * 100 print(piv.round(2).to_string()) piva = sens.pivot(index="lambda", columns="etat_structure", values="age_effectif_structure") print(" âge effectif de la structure :"); print(piva.round(1).to_string()) 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} %") print("== Valeur de l'information d'un test (illustration, paramètres hypothétiques) :") for p, C, t, nom in [(0.10, 3000.0, 50.0, "radon : p = 10 %, correction 3 000 $, dosimètre 50 $"), (0.05, 25000.0, 1200.0, "pyrite : p = 5 %, excavation 25 000 $, test 1 200 $"), (0.02, 25000.0, 1200.0, "pyrite : p = 2 %")]: v = valeur_information(p, C, t) print(f" {nom:60s} perte espérée {f0(v['perte_esperee'])} $ ; valeur de l'information {f0(v['valeur_info'])} $ ; recommander : {v['recommander']}") return {"devis": devis, "sujet": sujet, "cn": cn, "fiche": f, "travaux": trav, "depreciation": dv, "sensibilite": sens} if __name__ == "__main__": main()