SPB Git
17.4 KB · 245 lines python
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
Files · 031fae1bd368de12dc60ff542f1be9a8fd73e699
Menu

Theme