SPB Git

Ch. 2 livré et validé (compilation isolée, tests, fusion des registres)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 31, 2026) parent e03a77e Browse files

51 changed files +2,907 −130

51 changed files
modified analysis/python/cases/ch04_marches.py +49 −1
@@ -332,5 +332,53 @@ def main(verbose: bool = True) -> dict:
332 print(f"\n=== Sensibilité au taux : paiement de référence {pmt_ref:.2f} $/mois (prêt 400 000 $, 4,12 %, 25 ans)"); print(sens.to_string(float_format=lambda v: f"{v:.4f}")) 332 print(f"\n=== Sensibilité au taux : paiement de référence {pmt_ref:.2f} $/mois (prêt 400 000 $, 4,12 %, 25 ans)"); print(sens.to_string(float_format=lambda v: f"{v:.4f}"))
333 return res 333 return res
334 334
335+
336+# ----------------------------------------------------------------------------- Exercices (solutions/ch04.tex)
337+def exercices() -> dict:
338+ """Chiffres des solutions des exercices 4.1 à 4.6 (l'exercice 4.7 est rédactionnel)."""
339+ sp = structure_parc(); rt = recensement_types(); tot = rt[rt["type_id"] == 1].set_index("geo")
340+ ex = {}
341+ # 4.1 : types attenants et appartements = tous sauf maison individuelle (2) et mobile (10)
342+ sel = [k for k in sp.index if k not in (2, 10)]
343+ ex["ex1"] = {"qc_part": sp.loc[sel, "qc_menages"].sum() / tot.loc["Québec", "total"],
344+ "qc_taux_prop": sp.loc[sel, "qc_proprietaires"].sum() / sp.loc[sel, "qc_menages"].sum(),
345+ "ca_part": sp.loc[sel, "ca_menages"].sum() / tot.loc["Canada", "total"],
346+ "ca_taux_prop": (sp.loc[sel, "ca_taux_propriete"] * sp.loc[sel, "ca_menages"]).sum() / sp.loc[sel, "ca_menages"].sum(),
347+ "qc_menages": sp.loc[sel, "qc_menages"].sum(), "qc_prop": sp.loc[sel, "qc_proprietaires"].sum()}
348+ # 4.2
349+ ex["ex2"] = {"sherbrooke": 825 / (569 / 3), "trois_rivieres": 447 / (372 / 3), "sherbrooke_plus30": 825 * 1.3 / (569 / 3)}
350+ # 4.3
351+ n_rest, n_new, l0, g, l_new = 19500, 1500, 1000.0, 0.06, 1700.0
352+ l_2025 = (n_rest * l0 * (1 + g) + n_new * l_new) / (n_rest + n_new)
353+ ex["ex3"] = {"loyer_2025": l_2025, "var_brute": l_2025 / l0 - 1, "var_meme": g, "univers_2025": n_rest + n_new}
354+ # 4.4
355+ bar = BAREMES_2026["Gatineau (dès le 25 février 2026)"]
356+ for prix in (620000, 560000):
357+ base = max(prix, 590000 * 1.02)
358+ ex[f"ex4_{prix}"] = {"base": base, "droit": droits_mutation(base, bar), "taux_eff_prix": droits_mutation(base, bar) / prix, "taux_eff_base": droits_mutation(base, bar) / base}
359+ # 4.5
360+ g5 = (903607 / 840396) ** 0.2 - 1
361+ ex["ex5"] = {"tcac": g5, "pop_2035": 903607 * (1 + g5) ** 10, "tcac_isq": 1.16 ** (1 / 26) - 1, "pop_2051_isq": 903607 * 1.16, "pop_2051_tendance": 903607 * (1 + g5) ** 26}
362+ # 4.6
363+ def pmax(taux, pmt=2400.0, mdf=90000.0):
364+ return pmt * fin.facteur_va_annuite(fin.taux_mensuel_canadien(taux), 300) + mdf
365+ def elast(taux, h=0.001):
366+ """Élasticité ∂ln P*/∂ln i par différences finies centrées (h = 10 pb) ; le paiement de référence n'influe pas sur le résultat."""
367+ return (math.log(prix_soutenable(2130.18, taux + h)) - math.log(prix_soutenable(2130.18, taux - h))) / (math.log(taux + h) - math.log(taux - h))
368+ def semi(taux, h=0.001):
369+ """Semi-élasticité ∂ln P*/∂i (par unité de taux ; ÷100 pour « par point de pourcentage »)."""
370+ return (math.log(prix_soutenable(2130.18, taux + h)) - math.log(prix_soutenable(2130.18, taux - h))) / (2 * h)
371+ ex["ex6"] = {"prix_412": pmax(0.0412), "prix_619": pmax(0.0619), "capital_412": pmax(0.0412) - 90000, "capital_619": pmax(0.0619) - 90000,
372+ "elast_3": elast(0.03), "elast_6": elast(0.06), "semi_3_par_point": semi(0.03) / 100, "semi_6_par_point": semi(0.06) / 100,
373+ "pente_3_35_par_point": math.log(prix_soutenable(2130.18, 0.035) / prix_soutenable(2130.18, 0.03)) / 0.5,
374+ "pente_619_7_par_point": math.log(prix_soutenable(2130.18, 0.07) / prix_soutenable(2130.18, 0.0619)) / 0.81}
375+ return ex
376+
377+def imprimer_exercices():
378+ ex = exercices()
379+ print("\n=== Exercices")
380+ for k, v in ex.items():
381+ print(f" {k} :", {a: (round(b, 6) if isinstance(b, float) else b) for a, b in v.items()})
382+
335 if __name__ == "__main__": 383 if __name__ == "__main__":
336− main() 384+ main(); imprimer_exercices()
modified analysis/python/cases/ch06_demembrements.py +13 −2
@@ -117,6 +117,15 @@ def usufruit_viager(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) ->
117 "esperance_vie": esp + 0.5, "annuite_viagere": U / L} 117 "esperance_vie": esp + 0.5, "annuite_viagere": U / L}
118 118
119 119
120+def approximation_terme_espere(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> dict:
121+ """Approximation courante : remplacer la durée aléatoire par la durée espérée du droit, n_eff = E[nombre de fruits annuels]
122+ = somme des (t-1)p_x (= e_x + 1/2 sous la convention de décompte), puis appliquer la formule à terme fixe avec n_eff réel.
123+ Par concavité de l'annuité en n (Jensen), U_approx >= U_viager et NP_approx <= NP_viager."""
124+ n_eff = sum(survie(x, t - 1) for t in range(1, OMEGA - x + 1))
125+ NP = V * ((1 + g) / (1 + r)) ** n_eff
126+ return {"n_eff": n_eff, "U": V - NP, "NP": NP, "part_NP": NP / V}
127+
128+
120 def rente_viagere_equivalente(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> float: 129 def rente_viagere_equivalente(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> float:
121 """Rente annuelle constante, versée tant que l'usufruitier vit, de même valeur actuelle que l'usufruit (art. 1171).""" 130 """Rente annuelle constante, versée tant que l'usufruitier vit, de même valeur actuelle que l'usufruit (art. 1171)."""
122 u = usufruit_viager(V, x, r, g) 131 u = usufruit_viager(V, x, r, g)
@@ -200,7 +209,7 @@ def droit_mutation(base: float, tranches=TRANCHES_2026, taux_municipal: list[tup
200 borne_sup = min([s for s, _ in taux_municipal if s > seuil] + [float("inf")]) 209 borne_sup = min([s for s, _ in taux_municipal if s > seuil] + [float("inf")])
201 supplement += (min(base, borne_sup) - seuil) * (taux - 0.015) 210 supplement += (min(base, borne_sup) - seuil) * (taux - 0.015)
202 return {"base": base, "detail": detail, "droit_base": total, "supplement_municipal": supplement, "total": total + supplement, 211 return {"base": base, "detail": detail, "droit_base": total, "supplement_municipal": supplement, "total": total + supplement,
203− "taux_effectif": (total + supplement) / base} 212+ "taux_effectif": (total + supplement) / base if base > 0 else 0.0}
204 213
205 214
206 def quantum_vice(prix: float, cout_reparation: float, plus_value: float, prejudice_autre: float = 0.0, vendeur_connaissait: bool = False) -> dict: 215 def quantum_vice(prix: float, cout_reparation: float, plus_value: float, prejudice_autre: float = 0.0, vendeur_connaissait: bool = False) -> dict:
@@ -241,8 +250,10 @@ def main() -> None:
241 print() 250 print()
242 for x in (55, 65, 75, 85): 251 for x in (55, 65, 75, 85):
243 u = usufruit_viager(V, x) 252 u = usufruit_viager(V, x)
253+ a = approximation_terme_espere(V, x)
244 print(f"Usufruit viager, {x} ans : e_x = {u['esperance_vie']:.1f} ; U = {_d(u['U'])} ; NP = {_d(u['NP'])} ({u['part_NP']:.1%}) ; " 254 print(f"Usufruit viager, {x} ans : e_x = {u['esperance_vie']:.1f} ; U = {_d(u['U'])} ; NP = {_d(u['NP'])} ({u['part_NP']:.1%}) ; "
245− f"NP direct = {_d(u['NP_direct'])} ; annuité viagère {u['annuite_viagere']:.3f} ; rente équiv. {_d(rente_viagere_equivalente(V, x))}/an") 255+ f"NP direct = {_d(u['NP_direct'])} ; annuité viagère {u['annuite_viagere']:.3f} ; rente équiv. {_d(rente_viagere_equivalente(V, x))}/an ; "
256+ f"approx. terme espéré n_eff = {a['n_eff']:.2f} → NP = {_d(a['NP'])} ({a['part_NP']:.1%}), écart {_d(u['NP'] - a['NP'])}")
246 print("\nGrille NP/V (âge × r) :"); print(grille_usufruit(V).round(3).to_string(), "\n") 257 print("\nGrille NP/V (âge × r) :"); print(grille_usufruit(V).round(3).to_string(), "\n")
247 258
248 noi = rne_cas3() 259 noi = rne_cas3()
added analysis/python/cases/ch11_inspection.py +244 −0
@@ -0,0 +1,244 @@
1+#!/usr/bin/env python3
2+"""Chapitre 11 — inspection synthétique du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES) : fiche d'état par
3+composante, âge effectif (convention déclarée), liste des travaux, lien avec la dépréciation ventilée (traite.cout).
4+
5+Imprime 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 », chacune
7+ 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) et
9+ 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).
15+
16+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch11_inspection.py
17+Toutes les formules de dépréciation proviennent de analysis/python/traite/cout.py (aucune réécriture)."""
18+from __future__ import annotations
19+import sys, json, pathlib
20+from dataclasses import dataclass
21+import numpy as np, pandas as pd
22+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
23+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
24+from traite import cout as ct
25+import ch21_cout_neuf as c21
26+
27+ROOT = pathlib.Path(__file__).resolve().parents[3]
28+D = ROOT / "data/synthetic"
29+
30+ANNEE_INSPECTION = 2026 # date d'évaluation du cas 1 : 2026-09-01
31+VIE_ECONOMIQUE = 60.0 # vie économique totale du bâtiment (hypothèse pédagogique, voir texte)
32+LAMBDA = 0.5 # poids de l'observation dans l'âge effectif (convention de l'ouvrage)
33+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
34+
35+ETATS = {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"}
40+
41+
42+@dataclass
43+class Observation:
44+ composante: str
45+ postes: tuple[str, ...] # postes du devis (cas1_gatineau_couts.csv) qui forment la composante
46+ 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)
51+
52+
53+# Fiche d'état synthétique du sujet (cottage 1998, Hull) — constats FICTIFS construits pour le traité
54+OBSERVATIONS: 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+]
93+
94+# 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
95+TRAVAUX = [
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+]
102+
103+
104+def 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, cn
109+
110+
111+def 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"]
114+
115+
116+def phi(etat: int) -> float:
117+ return PHI[int(etat)]
118+
119+
120+def 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) * vie
123+ return float(min(max(a, 0.0), vie))
124+
125+
126+def 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.annee
133+ 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.01
140+ return f
141+
142+
143+def 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())
146+
147+
148+def 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)
155+
156+
157+def 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_struct
169+ 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 dv
175+
176+
177+def 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)
185+
186+
187+def 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}
191+
192+
193+def 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") * 100
230+ 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}
241+
242+
243+if __name__ == "__main__":
244+ main()
added analysis/python/cases/ch19_spatial.py +351 −0
@@ -0,0 +1,351 @@
1+#!/usr/bin/env python3
2+"""Chapitre 19 — modèles spatiaux et temporels sur le cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES, graine 20261031).
3+
4+Reproduit tous les chiffres cités dans manuscript/chapters/ch19.tex :
5+ 1. matrices de poids (k plus proches voisins, distance inverse, spatio-temporelle de Dubé–Legros) ;
6+ 2. I de Moran des résidus du modèle hédonique de référence (traite.cas1.hedonique) avec et sans indicatrices de secteur ;
7+ 3. corrélogramme (I de Moran par bande de distance) ;
8+ 4. tests du multiplicateur de Lagrange (LM-erreur, LM-décalage et versions robustes) ;
9+ 5. MCO / SAR / SEM / SDM par maximum de vraisemblance concentrée, effets directs et indirects ;
10+ 6. régression géographiquement pondérée (noyau gaussien fixe, bande passante par validation croisée) ;
11+ 7. effets fixes de quartier (secteurs, grilles) et de temps (tendance, trimestres) ;
12+ 8. variogramme empirique des résidus, modèle exponentiel et krigeage ordinaire ;
13+ 9. décalage spatio-temporel (ventes antérieures seulement) ;
14+ 10. validation croisée et indications pour le sujet ; sensibilité au choix de la matrice de poids.
15+
16+Contrôle croisé (non requis pour l'exécution) : spreg (ML_Lag, ML_Error, LMtests), esda (Moran) et mgwr (GWR) donnent les
17+mêmes estimations à 1e-6 près ; voir --controle.
18+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch19_spatial.py [--controle]"""
19+from __future__ import annotations
20+import sys, pathlib, warnings
21+import numpy as np, pandas as pd
22+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
23+from traite import cas1
24+from traite import hedonique as h
25+
26+ROOT = pathlib.Path(__file__).resolve().parents[3]
27+K_VOISINS = 8 # matrice de référence : 8 plus proches voisins, standardisée en ligne
28+PORTEE_KM = 2.0 # matrice de distance inverse : portée 2 km
29+FENETRE_MOIS = 12 # matrice spatio-temporelle : ventes antérieures de moins de 12 mois
30+BANDES_CORRELOGRAMME = [0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 15]
31+BORNES_VARIOGRAMME = np.arange(0, 16.1, 1.0)
32+VOISINS_KRIGEAGE = 40
33+# primes de secteur du générateur (data/synthetic/README.md : log du rapport des médianes APCIQ T3 2026 à Hull ; Masson-Angers − 0,02)
34+PRIMES_DGP = {"Hull": 0.0, "Gatineau": float(np.log(464900 / 475000)), "Aylmer": float(np.log(530000 / 475000)),
35+ "Buckingham": float(np.log(395000 / 475000)), "Masson-Angers": float(np.log(395000 / 475000)) - 0.02}
36+
37+_CACHE: dict = {}
38+
39+# ----------------------------------------------------------------------------- base
40+
41+def charger_base() -> dict:
42+ """Modèle de référence (traite.cas1.hedonique), spécification « sans secteur », coordonnées en km et sujet."""
43+ d = cas1.charger(); v = d["ventes"]; s = d["sujet"]
44+ m = cas1.hedonique(v)
45+ Xc = m["X"]; y = m["y"]
46+ Xr = Xc.drop(columns=[c for c in Xc.columns if c.startswith("sect_")]) # sans indicatrices de secteur
47+ Xn = Xr.drop(columns=["dist_centre"]) # ni secteur ni gradient de distance
48+ lat0, lon0 = float(v["lat"].mean()), float(v["lon"].mean())
49+ xy = h.coordonnees_km(v["lat"].values, v["lon"].values, lat0, lon0)
50+ cs = h.coordonnees_km([s["lat"]], [s["lon"]], lat0, lon0)
51+ xs_r = cas1.matrice(cas1.sujet_df(s))[Xr.columns].values[0]
52+ mco_r = h.mco(Xr.values, y); mco_n = h.mco(Xn.values, y)
53+ return {"ventes": v, "sujet": s, "modele": m, "Xc": Xc, "Xr": Xr, "Xn": Xn, "y": y, "xy": xy, "cs": cs, "xs_r": xs_r,
54+ "mois": Xc["mois"].values, "mco_r": mco_r, "mco_n": mco_n, "lat0": lat0, "lon0": lon0}
55+
56+def matrices(b: dict) -> dict:
57+ xy, mois = b["xy"], b["mois"]
58+ return {"knn": h.matrice_poids_knn(xy, K_VOISINS), "dist": h.matrice_poids_distance_inverse(xy, PORTEE_KM),
59+ "st": h.matrice_poids_spatio_temporelle(xy, mois, PORTEE_KM, FENETRE_MOIS)}
60+
61+def _loglik_mco(res: np.ndarray) -> float:
62+ n = len(res); return float(-n / 2 * (np.log(2 * np.pi * res @ res / n) + 1))
63+
64+def _aic_mco(res: np.ndarray, k: int) -> float:
65+ return 2 * (k + 1) - 2 * _loglik_mco(res)
66+
67+def _aicc(res: np.ndarray, k_eff: float) -> float:
68+ n = len(res); rss = float(res @ res)
69+ return n * np.log(rss / n) + n * np.log(2 * np.pi) + n * (n + k_eff) / (n - 2 - k_eff)
70+
71+# ----------------------------------------------------------------------------- 2-3. Moran et corrélogramme
72+
73+def tableau_moran(b: dict, Ws: dict) -> pd.DataFrame:
74+ series = {"ln P (sans modèle)": b["y"], "Résidus, sans secteur ni distance": b["mco_n"]["residus"],
75+ "Résidus, sans indicatrices de secteur": b["mco_r"]["residus"], "Résidus, modèle de référence": b["modele"]["residus"]}
76+ rows = []
77+ for nom, x in series.items():
78+ row = {"serie": nom}
79+ for wn, W in Ws.items():
80+ r = h.moran_i(x, W); row[f"I_{wn}"] = r["I"]; row[f"z_{wn}"] = r["z"]; row[f"p_{wn}"] = r["p"]
81+ rows.append(row)
82+ return pd.DataFrame(rows)
83+
84+def correlogramme(b: dict) -> dict:
85+ return {"sans_secteur": h.moran_par_distance(b["mco_r"]["residus"], b["xy"], BANDES_CORRELOGRAMME),
86+ "reference": h.moran_par_distance(b["modele"]["residus"], b["xy"], BANDES_CORRELOGRAMME)}
87+
88+# ----------------------------------------------------------------------------- 4-5. LM, SAR/SEM/SDM, effets
89+
90+def tests_lm(b: dict, W: np.ndarray) -> dict:
91+ return {"reference": h.tests_lm_spatial(b["y"], b["Xc"].values, W), "sans_secteur": h.tests_lm_spatial(b["y"], b["Xr"].values, W)}
92+
93+def modeles_spatiaux(b: dict, W: np.ndarray) -> dict:
94+ y, Xr, Xc = b["y"], b["Xr"].values, b["Xc"].values
95+ out = {"mco_r": b["mco_r"], "sar_r": h.sar_mv(y, Xr, W), "sem_r": h.sem_mv(y, Xr, W), "sdm_r": h.sar_mv(y, Xr, W, durbin=True),
96+ "sar_c": h.sar_mv(y, Xc, W), "sem_c": h.sem_mv(y, Xc, W)}
97+ for cle in ["sar_r", "sem_r", "sdm_r", "sar_c", "sem_c"]:
98+ res = out[cle]["innovations"] if "innovations" in out[cle] else out[cle]["residus"]
99+ out[cle]["moran_residus"] = h.moran_i(res, W)["I"]
100+ out["mco_r"]["loglik"] = _loglik_mco(b["mco_r"]["residus"]); out["mco_r"]["aic"] = _aic_mco(b["mco_r"]["residus"], Xr.shape[1])
101+ out["mco_r"]["moran_residus"] = h.moran_i(b["mco_r"]["residus"], W)["I"]
102+ out["mco_c"] = {"beta": b["modele"]["beta"], "se": b["modele"]["coef"]["se_hc1"].values, "residus": b["modele"]["residus"],
103+ "loglik": _loglik_mco(b["modele"]["residus"]), "aic": _aic_mco(b["modele"]["residus"], Xc.shape[1]),
104+ "sigma2": float(b["modele"]["sigma"] ** 2), "moran_residus": h.moran_i(b["modele"]["residus"], W)["I"]}
105+ return out
106+
107+def effets(mod: dict, W: np.ndarray, colonnes) -> pd.DataFrame:
108+ k = len(colonnes)
109+ e_sar = h.effets_sar(mod["sar_r"]["rho"], mod["sar_r"]["beta"], W)
110+ theta = np.concatenate([[0.0], mod["sdm_r"]["beta"][k:]])
111+ e_sdm = h.effets_sar(mod["sdm_r"]["rho"], mod["sdm_r"]["beta"][:k], W, theta)
112+ df = pd.DataFrame({"variable": colonnes, "beta_mco": mod["mco_r"]["beta"], "beta_sar": mod["sar_r"]["beta"],
113+ "sar_direct": e_sar["direct"], "sar_indirect": e_sar["indirect"], "sar_total": e_sar["total"],
114+ "beta_sdm": mod["sdm_r"]["beta"][:k], "theta_sdm": theta,
115+ "sdm_direct": e_sdm["direct"], "sdm_indirect": e_sdm["indirect"], "sdm_total": e_sdm["total"]})
116+ return df.set_index("variable")
117+
118+# ----------------------------------------------------------------------------- 6. GWR
119+
120+def gwr_resultats(b: dict) -> dict:
121+ y, X, xy = b["y"], b["Xr"].values, b["xy"]
122+ sel = h.gwr_bande_cv(y, X, xy)
123+ g = h.gwr(y, X, xy, sel["bande"])
124+ gs = h.gwr(y, X, xy, sel["bande"], cibles=b["cs"])
125+ cols = list(b["Xr"].columns)
126+ beta = pd.DataFrame(g["beta"], columns=cols)
127+ quart = beta.describe(percentiles=[0.25, 0.5, 0.75]).T[["min", "25%", "50%", "75%", "max"]]
128+ inter = pd.Series(g["beta"][:, 0]).groupby(b["ventes"]["secteur"].values).median()
129+ inter_rel = inter - inter["Hull"]
130+ dummies = {"Hull": 0.0, **{s: float(b["modele"]["coef"].loc[f"sect_{s}", "coef"]) for s in cas1.SECTEURS[1:]}}
131+ comp = pd.DataFrame({"gwr_intercept_rel_hull": inter_rel, "indicatrice_reference": pd.Series(dummies), "prime_generateur": pd.Series(PRIMES_DGP)})
132+ aicc_mco_r = _aicc(b["mco_r"]["residus"], X.shape[1]); aicc_mco_c = _aicc(b["modele"]["residus"], b["Xc"].shape[1])
133+ return {"selection": sel, "gwr": g, "beta": beta, "quartiles": quart, "comparaison_secteurs": comp, "sujet_beta": gs["beta"][0],
134+ "aicc_mco_sans_secteur": aicc_mco_r, "aicc_mco_reference": aicc_mco_c}
135+
136+# ----------------------------------------------------------------------------- 7. effets fixes
137+
138+def effets_fixes(b: dict, W: np.ndarray) -> pd.DataFrame:
139+ y, xy, v = b["y"], b["xy"], b["ventes"]; n = len(y)
140+ def ligne(nom, Xm, k_spatial, k_temps):
141+ r = h.mco(Xm, y); k = Xm.shape[1]
142+ return {"specification": nom, "parametres": k, "effets_spatiaux": k_spatial, "effets_temps": k_temps, "r2": r["r2"],
143+ "r2_ajuste": r["r2_ajuste"], "sigma": float(np.sqrt(r["sigma2"])), "aic": _aic_mco(r["residus"], k), "moran": h.moran_i(r["residus"], W)["I"]}
144+ rows = [ligne("Aucun effet spatial ; tendance linéaire", b["Xr"].values, 0, 1),
145+ ligne("Gradient de distance seulement ; tendance linéaire", b["Xr"].values, 1, 1) if False else None,
146+ ligne("Secteurs (référence) ; tendance linéaire", b["Xc"].values, 4, 1)]
147+ rows = [r for r in rows if r]
148+ for cell in [5.0, 3.0, 2.0]:
149+ gx = np.floor(xy[:, 0] / cell); gy = np.floor(xy[:, 1] / cell); cid = pd.factorize(gx * 1000 + gy)[0]
150+ D = pd.get_dummies(cid, drop_first=True).astype(float).values
151+ rows.append(ligne(f"Grille de {cell:g} km ({cid.max() + 1} cellules) ; tendance linéaire", np.column_stack([b["Xr"].values, D]), D.shape[1], 1))
152+ q = pd.PeriodIndex(v["date_vente"], freq="Q").astype(str)
153+ Dq = pd.get_dummies(q, drop_first=True).astype(float).values
154+ rows.append(ligne(f"Secteurs ; indicatrices trimestrielles ({Dq.shape[1]})", np.column_stack([b["Xc"].drop(columns=["mois"]).values, Dq]), 4, Dq.shape[1]))
155+ rt = h.mco(np.column_stack([b["Xc"].drop(columns=["mois"]).values, Dq]), y)
156+ df = pd.DataFrame(rows)
157+ df.attrs["indice_trimestriel"] = pd.Series(np.concatenate([[0.0], rt["beta"][-Dq.shape[1]:]]), index=sorted(q.unique()))
158+ df.attrs["tendance_mensuelle"] = float(b["modele"]["coef"].loc["mois", "coef"])
159+ return df
160+
161+# ----------------------------------------------------------------------------- 8. variogramme et krigeage
162+
163+def variogramme_krigeage(b: dict) -> dict:
164+ e = b["mco_r"]["residus"]; xy = b["xy"]
165+ vg = h.variogramme_empirique(xy, e, BORNES_VARIOGRAMME)
166+ vg_ref = h.variogramme_empirique(xy, b["modele"]["residus"], BORNES_VARIOGRAMME)
167+ p = h.ajuster_variogramme(vg["h"], vg["gamma"], vg["n"], portee_max=5.0)
168+ loo = h.krigeage_validation_croisee(xy, e, p, VOISINS_KRIGEAGE)
169+ sujet = h.krigeage_ordinaire(xy, e, b["cs"], p, VOISINS_KRIGEAGE)
170+ # surface pour la figure : grille régulière (km)
171+ gx = np.linspace(xy[:, 0].min() - 1, xy[:, 0].max() + 1, 90); gy = np.linspace(xy[:, 1].min() - 1, xy[:, 1].max() + 1, 70)
172+ GX, GY = np.meshgrid(gx, gy); grille = np.column_stack([GX.ravel(), GY.ravel()])
173+ kg = h.krigeage_ordinaire(xy, e, grille, p, VOISINS_KRIGEAGE)
174+ return {"variogramme": vg, "variogramme_reference": vg_ref, "parametres": p, "loo": loo, "sujet": sujet,
175+ "grille": {"x": gx, "y": gy, "z": kg["prediction"].reshape(GX.shape), "var": kg["variance"].reshape(GX.shape)},
176+ "variance_residus": float(e.var()), "sigma2_reference": float(b["modele"]["sigma"] ** 2)}
177+
178+# ----------------------------------------------------------------------------- 9. spatio-temporel
179+
180+def spatio_temporel(b: dict, Ws: dict) -> dict:
181+ y, Xr, Wst = b["y"], b["Xr"].values, Ws["st"]
182+ e = b["mco_r"]["residus"]
183+ ok = Wst.sum(1) > 0 # ventes ayant au moins une vente antérieure voisine
184+ Wy = Wst @ y
185+ # décalage spatio-temporel : prédéterminé (matrice triangulaire après tri par date) → MV = MCO sur [W_st y, X]
186+ Z = np.column_stack([Wy[ok], Xr[ok]]); r = h.mco(Z, y[ok])
187+ sem_st = h.sem_mv(y, Xr, Wst)
188+ return {"n_retenues": int(ok.sum()), "n_sans_voisin": int((~ok).sum()), "voisins_moyen": float((Wst[ok] > 0).sum(1).mean()),
189+ "rho_st": float(r["beta"][0]), "se_rho_st": float(r["se_hc1"][0]), "beta_st": r["beta"][1:], "r2_st": r["r2"],
190+ "moran_e_st": h.moran_i(e, Wst), "moran_e_knn": h.moran_i(e, Ws["knn"]), "moran_e_dist": h.moran_i(e, Ws["dist"]),
191+ "sem_st": sem_st, "max_abs_valeur_propre": float(np.abs(np.linalg.eigvals(Wst)).max()),
192+ "moran_e_apres_lag": h.moran_i(r["residus"], Wst[np.ix_(ok, ok)])["I"]}
193+
194+# ----------------------------------------------------------------------------- 10. validation croisée et sujet
195+
196+def validation_croisee(b: dict, mod: dict, g: dict, vk: dict, W: np.ndarray) -> pd.DataFrame:
197+ y = b["y"]; n = len(y)
198+ def loo_mco(Xm):
199+ r = h.mco(Xm, y); H = Xm @ np.linalg.pinv(Xm.T @ Xm) @ Xm.T; return r["residus"] / (1 - np.diag(H))
200+ sem = mod["sem_r"]; u = sem["residus"]; B = np.eye(n) - sem["lambda"] * W; Q = B.T @ B
201+ Eu = np.array([-(Q[i] @ u - Q[i, i] * u[i]) / Q[i, i] for i in range(n)])
202+ series = {"MCO, modèle de référence (secteurs)": loo_mco(b["Xc"].values),
203+ "MCO, sans indicatrices de secteur": loo_mco(b["Xr"].values),
204+ "MCO sans secteur + krigeage des résidus": b["mco_r"]["residus"] - vk["loo"],
205+ "SEM sans secteur (espérance conditionnelle)": u - Eu,
206+ "SAR sans secteur (prédiction conditionnelle)": y - (mod["sar_r"]["rho"] * (W @ y) + b["Xr"].values @ mod["sar_r"]["beta"]),
207+ "GWR sans secteur (validation croisée)": None}
208+ rows = []
209+ for nom, r in series.items():
210+ if r is None:
211+ rmse = float(np.sqrt(g["gwr"]["cv"] / n)); rows.append({"modele": nom, "rmse_log": rmse, "erreur_abs_moy_pct": np.nan, "moran_erreurs": h.moran_i(g["gwr"]["residus"], W)["I"]})
212+ else:
213+ rows.append({"modele": nom, "rmse_log": float(np.sqrt(np.mean(r ** 2))), "erreur_abs_moy_pct": float(np.mean(np.abs(np.exp(r) - 1))), "moran_erreurs": h.moran_i(r, W)["I"]})
214+ return pd.DataFrame(rows)
215+
216+def predictions_sujet(b: dict, mod: dict, g: dict, vk: dict, W: np.ndarray) -> pd.DataFrame:
217+ s, xs = b["sujet"], b["xs_r"]; y = b["y"]; n = len(y)
218+ duan = lambda res: float(np.mean(np.exp(res)))
219+ ref = float(cas1.predire(b["modele"], cas1.sujet_df(s))[0])
220+ mco_r = float(np.exp(xs @ b["mco_r"]["beta"]) * duan(b["mco_r"]["residus"]))
221+ krig = float(np.exp(xs @ b["mco_r"]["beta"] + vk["sujet"]["prediction"][0]) * duan(b["mco_r"]["residus"] - vk["loo"]))
222+ # SEM : espérance conditionnelle de u au sujet (matrice augmentée : le sujet n'est voisin d'aucune vente)
223+ xy_aug = np.vstack([b["xy"], b["cs"]]); Wa = np.zeros((n + 1, n + 1)); Wa[:n, :n] = W
224+ Wa[n, :n] = h.matrice_poids_knn(xy_aug, K_VOISINS)[n, :n]; Wa[n] /= Wa[n].sum()
225+ sem = mod["sem_r"]; u_s = float(h.sem_esperance_conditionnelle(sem["lambda"], Wa, sem["residus"], [n])[0])
226+ sem_pred = float(np.exp(xs @ sem["beta"] + u_s) * duan(sem["innovations"]))
227+ # SAR : prédiction conditionnelle avec le décalage observé (moyenne des ln P des k ventes voisines)
228+ idx = np.nonzero(Wa[n, :n])[0]; Wy_s = float(Wa[n, :n] @ y)
229+ sar = mod["sar_r"]; sar_pred = float(np.exp(sar["rho"] * Wy_s + xs @ sar["beta"]) * duan(sar["residus"]))
230+ gwr_pred = float(np.exp(xs @ g["sujet_beta"]) * duan(g["gwr"]["residus"]))
231+ df = pd.DataFrame([
232+ {"modele": "MCO, modèle de référence (indicatrices de secteur)", "ln_correction": 0.0, "indication": ref},
233+ {"modele": "MCO sans indicatrices de secteur", "ln_correction": 0.0, "indication": mco_r},
234+ {"modele": "MCO sans secteur + krigeage du résidu au sujet", "ln_correction": float(vk["sujet"]["prediction"][0]), "indication": krig},
235+ {"modele": "SEM sans secteur (espérance conditionnelle du résidu)", "ln_correction": u_s, "indication": sem_pred},
236+ {"modele": "SAR sans secteur (décalage des 8 ventes voisines)", "ln_correction": float(sar["rho"] * Wy_s), "indication": sar_pred},
237+ {"modele": "GWR sans secteur (coefficients locaux au sujet)", "ln_correction": float(g["sujet_beta"][0] - b["mco_r"]["beta"][0]), "indication": gwr_pred}])
238+ df.attrs["voisins_sujet"] = list(b["ventes"]["id_vente"].values[idx]); df.attrs["secteurs_voisins"] = list(b["ventes"]["secteur"].values[idx])
239+ df.attrs["variance_krigeage_sujet"] = float(vk["sujet"]["variance"][0])
240+ return df
241+
242+# ----------------------------------------------------------------------------- 11. sensibilité
243+
244+def sensibilite(b: dict) -> pd.DataFrame:
245+ y, Xr, xy, e = b["y"], b["Xr"].values, b["xy"], b["mco_r"]["residus"]
246+ specs = [("k plus proches voisins", k, h.matrice_poids_knn(xy, k)) for k in [4, 6, 8, 12, 16]]
247+ specs += [("distance inverse", d, h.matrice_poids_distance_inverse(xy, d)) for d in [1.0, 2.0, 4.0]]
248+ rows = []
249+ for fam, par, W in specs:
250+ mi = h.moran_i(e, W); sar = h.sar_mv(y, Xr, W); sem = h.sem_mv(y, Xr, W)
251+ rows.append({"famille": fam, "parametre": par, "voisins_moyen": float((W > 0).sum(1).mean()), "I": mi["I"], "z": mi["z"],
252+ "rho": sar["rho"], "se_rho": sar["se_rho"], "aic_sar": sar["aic"], "lambda": sem["lambda"], "se_lambda": sem["se_lambda"], "aic_sem": sem["aic"]})
253+ return pd.DataFrame(rows)
254+
255+# ----------------------------------------------------------------------------- contrôle croisé facultatif
256+
257+def controle_croise(b: dict, W: np.ndarray, g: dict) -> dict:
258+ """Compare les estimations maison à spreg/esda/mgwr (installés sur la machine de rédaction)."""
259+ import spreg
260+ from libpysal.weights import W as Wcls
261+ from esda.moran import Moran
262+ from mgwr.sel_bw import Sel_BW
263+ from mgwr.gwr import GWR
264+ y, X = b["y"], b["Xr"]
265+ neigh = {i: list(np.nonzero(W[i])[0]) for i in range(len(W))}; wts = {i: [W[i, j] for j in neigh[i]] for i in neigh}
266+ w = Wcls(neigh, wts); w.transform = "r"; Xs = X.drop(columns=["const"]).values; yy = y.reshape(-1, 1)
267+ with warnings.catch_warnings():
268+ warnings.simplefilter("ignore")
269+ ml = spreg.ML_Lag(yy, Xs, w, method="full"); me = spreg.ML_Error(yy, Xs, w, method="full")
270+ ols = spreg.OLS(yy, Xs, w=w, spat_diag=True, moran=True)
271+ mo = Moran(b["mco_r"]["residus"], w, permutations=0)
272+ bw = Sel_BW(b["xy"], yy, Xs, fixed=True, kernel="gaussian").search(criterion="CV")
273+ gm = GWR(b["xy"], yy, Xs, bw, fixed=True, kernel="gaussian").fit()
274+ mine_sar = h.sar_mv(y, X.values, W); mine_sem = h.sem_mv(y, X.values, W); lm = h.tests_lm_spatial(y, X.values, W)
275+ return {"rho_spreg": float(ml.rho), "rho_maison": mine_sar["rho"], "lambda_spreg": float(me.lam), "lambda_maison": mine_sem["lambda"],
276+ "lm_err_spreg": float(ols.lm_error[0]), "lm_err_maison": lm["LM_erreur"][0], "rlm_lag_spreg": float(ols.rlm_lag[0]), "rlm_lag_maison": lm["RLM_decalage"][0],
277+ "moran_esda_z": float(mo.z_norm), "moran_maison_z": h.moran_i(b["mco_r"]["residus"], W)["z"],
278+ "bande_mgwr": float(bw), "bande_maison": g["selection"]["bande"], "aicc_mgwr": float(gm.aicc), "aicc_maison_meme_bande": h.gwr(y, X.values, b["xy"], float(bw))["aicc"]}
279+
280+# ----------------------------------------------------------------------------- main
281+
282+def main(verbose: bool = True, controle: bool = False) -> dict:
283+ if "res" in _CACHE and not controle:
284+ return _CACHE["res"]
285+ b = charger_base(); Ws = matrices(b); W = Ws["knn"]
286+ res = {"base": b, "W": Ws, "moran": tableau_moran(b, Ws), "correlogramme": correlogramme(b), "lm": tests_lm(b, W)}
287+ res["modeles"] = modeles_spatiaux(b, W)
288+ res["effets"] = effets(res["modeles"], W, list(b["Xr"].columns))
289+ res["gwr"] = gwr_resultats(b)
290+ res["effets_fixes"] = effets_fixes(b, W)
291+ res["variogramme"] = variogramme_krigeage(b)
292+ res["spatio_temporel"] = spatio_temporel(b, Ws)
293+ res["validation"] = validation_croisee(b, res["modeles"], res["gwr"], res["variogramme"], W)
294+ res["sujet"] = predictions_sujet(b, res["modeles"], res["gwr"], res["variogramme"], W)
295+ res["sensibilite"] = sensibilite(b)
296+ if controle:
297+ res["controle"] = controle_croise(b, W, res["gwr"])
298+ _CACHE["res"] = res
299+ if verbose:
300+ imprimer(res)
301+ return res
302+
303+def imprimer(res: dict):
304+ pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30)
305+ b = res["base"]; m = b["modele"]
306+ print("== Modèle de référence : R² %.3f, σ %.4f, n %d, k %d" % (m["r2"], m["sigma"], m["n"], m["k"]))
307+ print("== Sans secteur : R² %.3f, σ %.4f ; sans secteur ni distance : R² %.3f" % (b["mco_r"]["r2"], np.sqrt(b["mco_r"]["sigma2"]), b["mco_n"]["r2"]))
308+ print("== Étendue des coordonnées (km) :", b["xy"].min(0).round(1), b["xy"].max(0).round(1), "; sujet :", b["cs"].round(2))
309+ for wn, W in res["W"].items():
310+ print(f" matrice {wn} : voisins moyens {(W > 0).sum(1).mean():.1f}, lignes vides {(W.sum(1) == 0).sum()}")
311+ print("== I de Moran :\n", res["moran"].round(4).to_string(index=False))
312+ print("== Corrélogramme (sans secteur) :", [(c["d_min"], c["d_max"], round(c["I"], 3), round(c["z"], 1), c["paires"]) for c in res["correlogramme"]["sans_secteur"]])
313+ print("== Corrélogramme (référence) :", [(c["d_min"], c["d_max"], round(c["I"], 3), round(c["z"], 1)) for c in res["correlogramme"]["reference"]])
314+ for k, v in res["lm"].items():
315+ print(f"== Tests LM ({k}) :", {a: (round(s, 2), round(p, 4)) for a, (s, p) in v.items()})
316+ mod = res["modeles"]
317+ print("== SAR sans secteur : ρ %.4f (%.4f), logL %.2f, AIC %.1f, I résidus %.3f" % (mod["sar_r"]["rho"], mod["sar_r"]["se_rho"], mod["sar_r"]["loglik"], mod["sar_r"]["aic"], mod["sar_r"]["moran_residus"]))
318+ print("== SEM sans secteur : λ %.4f (%.4f), logL %.2f, AIC %.1f, I innovations %.3f" % (mod["sem_r"]["lambda"], mod["sem_r"]["se_lambda"], mod["sem_r"]["loglik"], mod["sem_r"]["aic"], mod["sem_r"]["moran_residus"]))
319+ print("== SDM sans secteur : ρ %.4f (%.4f), logL %.2f, AIC %.1f, I résidus %.3f" % (mod["sdm_r"]["rho"], mod["sdm_r"]["se_rho"], mod["sdm_r"]["loglik"], mod["sdm_r"]["aic"], mod["sdm_r"]["moran_residus"]))
320+ print("== MCO sans secteur : logL %.2f, AIC %.1f ; MCO référence : logL %.2f, AIC %.1f" % (mod["mco_r"]["loglik"], mod["mco_r"]["aic"], mod["mco_c"]["loglik"], mod["mco_c"]["aic"]))
321+ print("== SAR référence : ρ %.4f (%.4f) ; SEM référence : λ %.4f (%.4f)" % (mod["sar_c"]["rho"], mod["sar_c"]["se_rho"], mod["sem_c"]["lambda"], mod["sem_c"]["se_lambda"]))
322+ cols = list(b["Xr"].columns)
323+ tab = pd.DataFrame({"MCO réf.": pd.Series(m["beta"], index=b["Xc"].columns).reindex(cols), "MCO": mod["mco_r"]["beta"], "SAR": mod["sar_r"]["beta"], "SEM": mod["sem_r"]["beta"], "SDM β": mod["sdm_r"]["beta"][:len(cols)],
324+ "SDM θ": np.concatenate([[np.nan], mod["sdm_r"]["beta"][len(cols):]]),
325+ "se MCO": mod["mco_r"]["se_hc1"], "se SAR": mod["sar_r"]["se"], "se SEM": mod["sem_r"]["se"], "se SDM": mod["sdm_r"]["se"][:len(cols)]}, index=cols)
326+ print("== Coefficients :\n", tab.round(4).to_string())
327+ print("== Effets directs/indirects/totaux :\n", res["effets"].round(4).to_string())
328+ g = res["gwr"]
329+ print("== GWR : bande %.3f km (CV %.4f), tr(S) %.1f, AICc %.1f (MCO sans secteur %.1f ; référence %.1f), R² %.3f, σ %.4f" % (g["selection"]["bande"], g["selection"]["cv"], g["gwr"]["trace_S"], g["gwr"]["aicc"], g["aicc_mco_sans_secteur"], g["aicc_mco_reference"], g["gwr"]["r2"], g["gwr"]["sigma"]))
330+ print(" quartiles des coefficients locaux :\n", g["quartiles"].round(4).to_string())
331+ print(" constante locale médiane par secteur (écart à Hull) vs indicatrice vs générateur :\n", g["comparaison_secteurs"].round(4).to_string())
332+ print(" coefficients locaux au sujet :", dict(zip(cols, g["sujet_beta"].round(4))))
333+ print("== Effets fixes :\n", res["effets_fixes"].round(4).to_string(index=False))
334+ print(" indice trimestriel (log) :", res["effets_fixes"].attrs["indice_trimestriel"].round(3).to_dict())
335+ vk = res["variogramme"]
336+ print("== Variogramme (sans secteur) : h", vk["variogramme"]["h"].round(2), "\n γ", vk["variogramme"]["gamma"].round(5), "\n N", vk["variogramme"]["n"])
337+ print(" référence γ", vk["variogramme_reference"]["gamma"].round(5))
338+ print(" paramètres :", {k: round(v, 5) for k, v in vk["parametres"].items()}, "; var(e) %.5f ; σ² réf %.5f" % (vk["variance_residus"], vk["sigma2_reference"]))
339+ print(" krigeage au sujet : %.4f (variance %.5f)" % (vk["sujet"]["prediction"][0], vk["sujet"]["variance"][0]))
340+ st = res["spatio_temporel"]
341+ print("== Spatio-temporel : %d ventes retenues (%d sans voisin antérieur), %.1f voisins en moyenne, max |valeur propre| %.1e" % (st["n_retenues"], st["n_sans_voisin"], st["voisins_moyen"], st["max_abs_valeur_propre"]))
342+ print(" I(e) : W_st %.3f (z %.1f) ; knn %.3f ; distance %.3f" % (st["moran_e_st"]["I"], st["moran_e_st"]["z"], st["moran_e_knn"]["I"], st["moran_e_dist"]["I"]))
343+ print(" ρ_st (MCO sur ventes antérieures) %.4f (%.4f), R² %.3f, I résidus après décalage %.3f ; SEM-st λ %.4f (%.4f), AIC %.1f" % (st["rho_st"], st["se_rho_st"], st["r2_st"], st["moran_e_apres_lag"], st["sem_st"]["lambda"], st["sem_st"]["se_lambda"], st["sem_st"]["aic"]))
344+ print("== Validation croisée :\n", res["validation"].round(4).to_string(index=False))
345+ print("== Sujet :\n", res["sujet"].round(4).to_string(index=False)); print(" voisins du sujet :", res["sujet"].attrs["voisins_sujet"], res["sujet"].attrs["secteurs_voisins"])
346+ print("== Sensibilité :\n", res["sensibilite"].round(4).to_string(index=False))
347+ if "controle" in res:
348+ print("== Contrôle croisé spreg/esda/mgwr :", {k: round(v, 5) for k, v in res["controle"].items()})
349+
350+if __name__ == "__main__":
351+ main(controle="--controle" in sys.argv)
modified analysis/python/figures/ch06_usufruit.py +2 −5
@@ -23,11 +23,8 @@ def main():
23 ages = np.arange(50, 91) 23 ages = np.arange(50, 91)
24 for i, r in enumerate((0.04, 0.05, 0.06)): 24 for i, r in enumerate((0.04, 0.05, 0.06)):
25 b.plot(ages, [cas.usufruit_viager(V, int(x), r)["part_NP"] for x in ages], color=palette.cat[i], label=f"viager, $r$ = {r*100:.0f} %") 25 b.plot(ages, [cas.usufruit_viager(V, int(x), r)["part_NP"] for x in ages], color=palette.cat[i], label=f"viager, $r$ = {r*100:.0f} %")
26− approx = [] 26+ approx = [cas.approximation_terme_espere(V, int(x), 0.05)["part_NP"] for x in ages]
27− for x in ages: 27+ b.plot(ages, approx, color=palette.ink2, ls="--", lw=1.2, label="terme fixe = durée espérée, $r$ = 5 %")
28− n = int(round(cas.esperance_vie(int(x))))
29− approx.append(cas.usufruit_terme(V, n, 0.05)["part_NP"])
30− b.plot(ages, approx, color=palette.ink2, ls="--", lw=1.2, label="terme fixe = espérance de vie, $r$ = 5 %")
31 b.set_xlabel("Âge de l'usufruitier à la date d'évaluation"); b.set_ylabel("Nue-propriété / pleine propriété") 28 b.set_xlabel("Âge de l'usufruitier à la date d'évaluation"); b.set_ylabel("Nue-propriété / pleine propriété")
32 b.set_ylim(0.2, 1.0); b.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, p: f"{y*100:.0f} %")) 29 b.set_ylim(0.2, 1.0); b.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, p: f"{y*100:.0f} %"))
33 b.legend(loc="upper left", fontsize=7.5); b.set_title("(b) Part de la nue-propriété ($g$ = 1,5 %)", loc="left") 30 b.legend(loc="upper left", fontsize=7.5); b.set_title("(b) Part de la nue-propriété ($g$ = 1,5 %)", loc="left")
added analysis/python/figures/ch11_fiche_etat.py +62 −0
@@ -0,0 +1,62 @@
1+#!/usr/bin/env python3
2+"""Figures ch11 — (1) fiche d'état du sujet du cas 1 : fraction de vie utile consommée a_eff/L par composante, colorée par
3+note d'état, taille ∝ coût neuf ; (2) sensibilité de la dépréciation physique totale à λ et à la note d'état de la structure.
4+DONNÉES SYNTHÉTIQUES (cas 1, graine 20261031 ; constats d'inspection fictifs de analysis/python/cases/ch11_inspection.py).
5+Sorties : assets/figures/ch11/ch11-fig-fiche-etat.pdf, assets/figures/ch11/ch11-fig-sensibilite-age-effectif.pdf"""
6+import sys, pathlib
7+ROOT = pathlib.Path(__file__).resolve().parents[3]
8+sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))
9+import numpy as np
10+import matplotlib.pyplot as plt
11+from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars
12+import ch11_inspection as cas
13+
14+COUL_ETAT = {1: palette.seq[0], 2: palette.seq[1], 3: palette.seq[2], 4: palette.seq[3], 5: palette.seq[4]}
15+
16+
17+def fiche_etat(f):
18+ fig, ax = plt.subplots(figsize=(LARGEUR, 4.2))
19+ f = f.sort_values("ratio", ascending=True).reset_index(drop=True)
20+ y = np.arange(len(f))
21+ ax.hlines(y, f["age"] / f["vie_utile"], f["ratio"], color=palette.grid, lw=1.2, zorder=1)
22+ ax.scatter(f["age"] / f["vie_utile"], y, marker="|", s=60, color=palette.ink2, zorder=2, label="âge chronologique / vie utile")
23+ tailles = 18 + 220 * f["cout_neuf"] / f["cout_neuf"].max()
24+ for e in sorted(f["etat"].unique()):
25+ m = f["etat"] == e
26+ ax.scatter(f.loc[m, "ratio"], y[m], s=tailles[m], color=COUL_ETAT[int(e)], edgecolor=palette.ink2, lw=0.4, zorder=3, label=f"état {int(e)}")
27+ ax.set_yticks(y); ax.set_yticklabels([c.replace(" (", "\n(") if len(c) > 44 else c for c in f["composante"]], fontsize=7)
28+ ax.set_xlim(0, 1.45); ax.axvline(1.0, color=palette.ink2, lw=0.7, ls="--")
29+ ax.set_xlabel("Fraction de la vie utile consommée (repère | : âge chronologique ; disque : âge effectif, taille proportionnelle au coût neuf)")
30+ ax.grid(axis="y", visible=False)
31+ ax.legend(loc="lower right", fontsize=6.8, ncol=2, title="Note d'état", title_fontsize=7)
32+ fig.tight_layout()
33+ return savefig(fig, "ch11/ch11-fig-fiche-etat.pdf")
34+
35+
36+def sensibilite(sens, cn):
37+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))
38+ for i, e in enumerate(sorted(sens["etat_structure"].unique())):
39+ s = sens[sens["etat_structure"] == e].sort_values("lambda")
40+ ax.plot(s["lambda"], s["taux"] * 100, marker="o", ms=4, color=palette.cat[i], label=f"structure en état {int(e)}")
41+ base = sens[(sens["lambda"] == cas.LAMBDA) & (sens["etat_structure"] == 3)]["taux"].iloc[0] * 100
42+ ax.plot([cas.LAMBDA], [base], marker="*", ms=11, color=palette.brand, ls="none", zorder=5)
43+ ax.annotate(f"convention retenue : λ = {cas.LAMBDA}, état 3 → {base:.1f} %", (cas.LAMBDA, base), xytext=(0.52, 40), fontsize=7.5,
44+ arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
45+ ax.set_xlabel("Poids λ de la note d'état dans l'âge effectif (0 = âge chronologique seul ; 1 = note d'état seule)")
46+ ax.set_ylabel("Dépréciation physique totale (% du coût neuf)")
47+ ax.set_xticks([0, 0.25, 0.5, 0.75, 1.0]); ax.set_ylim(20, 72)
48+ ax.legend(fontsize=7, ncol=3, loc="lower left")
49+ fig.tight_layout()
50+ return savefig(fig, "ch11/ch11-fig-sensibilite-age-effectif.pdf")
51+
52+
53+def main():
54+ setup()
55+ devis, sujet, cn = cas.charger()
56+ f = cas.fiche(devis, cn); trav = cas.travaux(devis, cn)
57+ print("OK", fiche_etat(f))
58+ print("OK", sensibilite(cas.sensibilite(f, trav, cn), cn))
59+
60+
61+if __name__ == "__main__":
62+ main()
added analysis/python/figures/ch11_frise_pathologies.py +58 −0
@@ -0,0 +1,58 @@
1+#!/usr/bin/env python3
2+"""Figure ch11 — frise INDICATIVE des pathologies et matériaux à risque selon la période de construction (Québec).
3+Les bornes de périodes sont des repères de connaissances générales du métier (matériaux en usage, dates d'interdiction
4+ou de normalisation) et non des statistiques : elles sont arrondies et signalées comme indicatives dans la légende.
5+Les seules dates vérifiées sur une source publique sont marquées « (v) » dans le libellé : interdiction de la MIUF (déc. 1980,
6+RNCan), interdiction de l'amiante (30 déc. 2018, Canada.ca), vermiculite Libby/Zonolite 1920-1990 (RNCan), fin du Programme
7+pyrrhotite (30 sept. 2026, Québec.ca), Code de construction fondé sur le CNB 2020 modifié Québec (17 avril 2025, RBQ).
8+Sortie : assets/figures/ch11/ch11-fig-frise-pathologies.pdf"""
9+import sys, pathlib
10+ROOT = pathlib.Path(__file__).resolve().parents[3]
11+sys.path.insert(0, str(ROOT / "analysis/python"))
12+import matplotlib.pyplot as plt
13+from traite.style import setup, palette, savefig, LARGEUR
14+
15+# (libellé, début, fin, famille) — période pendant laquelle le matériau ou la pratique à risque était couramment mis en place
16+FRISE = [
17+ ("Plomberie d'alimentation en acier galvanisé", 1900, 1960, "Plomberie"),
18+ ("Entrées d'eau en plomb (petits diamètres)", 1900, 1955, "Plomberie"),
19+ ("Fils électriques à boutons et tubes ; fusibles", 1900, 1950, "Électricité"),
20+ ("Isolants contenant de l'amiante (tuyaux, plâtre, carreaux) — interdiction 2018 (v)", 1920, 1985, "Isolation et air intérieur"),
21+ ("Vermiculite Zonolite (mine de Libby) — vente 1920-1990 (v)", 1920, 1990, "Isolation et air intérieur"),
22+ ("Réservoirs de mazout souterrains ou extérieurs d'origine", 1945, 1980, "Chauffage"),
23+ ("Filage de cuivre sans mise à la terre (deux fils)", 1945, 1962, "Électricité"),
24+ ("Filage d'aluminium (circuits de dérivation)", 1965, 1976, "Électricité"),
25+ ("Mousse isolante d'urée-formaldéhyde (MIUF) — interdite déc. 1980 (v)", 1970, 1980, "Isolation et air intérieur"),
26+ ("Remblai sous dalle susceptible de contenir de la pyrite (région de Montréal)", 1970, 1999, "Fondations et sols"),
27+ ("Tuyauterie de polybutylène (gris)", 1978, 1995, "Plomberie"),
28+ ("Fenêtres à simple vitrage ou à châssis de bois non étanches", 1900, 1980, "Enveloppe"),
29+ ("Toitures et drains français sans membrane ni cheminées de nettoyage (ocre)", 1900, 1995, "Fondations et sols"),
30+ ("Granulats à pyrrhotite dans le béton (Mauricie) — programme SHQ clos le 30 sept. 2026 (v)", 1996, 2008, "Fondations et sols"),
31+ ("Pare-vapeur et pare-air déficients, condensation dans les combles", 1975, 1995, "Enveloppe"),
32+ ("Construction sous le chapitre I fondé sur le CNB 2020 modifié Québec (v)", 2025, 2026, "Normes"),
33+]
34+FAMILLES = ["Fondations et sols", "Enveloppe", "Isolation et air intérieur", "Plomberie", "Électricité", "Chauffage", "Normes"]
35+
36+
37+def main():
38+ setup()
39+ coul = {f: palette.cat[i % len(palette.cat)] for i, f in enumerate(FAMILLES)}
40+ ordre = sorted(FRISE, key=lambda r: (FAMILLES.index(r[3]), r[1]))
41+ fig, ax = plt.subplots(figsize=(LARGEUR, 4.6))
42+ for k, (lib, d, f_, fam) in enumerate(ordre):
43+ ax.barh(k, f_ - d, left=d, height=0.62, color=coul[fam], alpha=0.9)
44+ ax.text(d - 1, k, lib, ha="right", va="center", fontsize=6.6, color=palette.ink)
45+ ax.set_yticks([]); ax.set_ylim(-0.7, len(ordre) - 0.3); ax.invert_yaxis()
46+ ax.set_xlim(1900, 2030); ax.set_xticks(range(1900, 2031, 10))
47+ ax.set_xlabel("Période de construction ou de mise en place (repères indicatifs, arrondis à la décennie sauf dates (v) vérifiées)")
48+ ax.axvline(1998, color=palette.brand, lw=0.9, ls="--"); ax.text(1998.5, -0.45, "sujet cas 1 : 1998", fontsize=7, color=palette.brand, va="center")
49+ ax.grid(axis="y", visible=False)
50+ from matplotlib.patches import Patch
51+ ax.legend(handles=[Patch(color=coul[f], label=f) for f in FAMILLES], loc="lower left", ncol=4, fontsize=6.6, bbox_to_anchor=(-0.95, -0.26), frameon=False)
52+ for s in ("left",): ax.spines[s].set_visible(False)
53+ fig.tight_layout()
54+ out = savefig(fig, "ch11/ch11-fig-frise-pathologies.pdf"); print("OK", out)
55+
56+
57+if __name__ == "__main__":
58+ main()
added analysis/python/figures/ch11_matrice_age_effectif.py +48 −0
@@ -0,0 +1,48 @@
1+#!/usr/bin/env python3
2+"""Figure ch11 — matrice état × âge chronologique → âge effectif (convention déclarée de l'ouvrage, chapitre 11).
3+Panneau gauche : carte de chaleur de a_eff = (1 − λ) a + λ φ(c) L pour L = 60 ans, λ = 0,5 ; panneau droit : profils a_eff(a) par note.
4+Sortie : assets/figures/ch11/ch11-fig-matrice-age-effectif.pdf (schéma de convention, aucune donnée empirique)."""
5+import sys, pathlib
6+ROOT = pathlib.Path(__file__).resolve().parents[3]
7+sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))
8+import numpy as np
9+import matplotlib.pyplot as plt
10+from traite.style import setup, palette, savefig, LARGEUR
11+import ch11_inspection as cas
12+
13+
14+def main():
15+ setup()
16+ L = cas.VIE_ECONOMIQUE
17+ ages = np.arange(0, 61, 1)
18+ etats = [1, 2, 3, 4, 5]
19+ M = np.array([[cas.age_effectif(a, L, e) for a in ages] for e in etats])
20+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.1), gridspec_kw={"width_ratios": [1.35, 1]})
21+ im = ax1.imshow(M, aspect="auto", origin="lower", cmap=palette.cmap_seq(), extent=[ages[0] - 0.5, ages[-1] + 0.5, 0.5, 5.5], vmin=0, vmax=L)
22+ ax1.set_yticks(etats); ax1.set_yticklabels([f"{e}" for e in etats])
23+ ax1.set_xlabel("Âge chronologique $a$ (années)"); ax1.set_ylabel("Note d'état $c$ (1 = excellent, 5 = mauvais)")
24+ ax1.grid(False)
25+ for e in etats:
26+ for a in (0, 15, 30, 45, 60):
27+ v = cas.age_effectif(a, L, e)
28+ ax1.text(a, e, f"{v:.0f}", ha="center", va="center", fontsize=7.5, color=palette.ink if v < 0.55 * L else "white")
29+ cb = fig.colorbar(im, ax=ax1, fraction=0.05, pad=0.03); cb.set_label("Âge effectif $a_{\\mathrm{eff}}$ (années)")
30+ cb.outline.set_visible(False)
31+ for i, e in enumerate(etats):
32+ ax2.plot(ages, M[i], color=palette.cat[i], label=f"état {e} (φ = {cas.PHI[e]:.2f})")
33+ ax2.plot(ages, ages, color=palette.ink2, lw=0.9, ls="--", label="$a_{\\mathrm{eff}} = a$")
34+ ax2.set_xlabel("Âge chronologique $a$ (années)"); ax2.set_ylabel("Âge effectif (années)")
35+ ax2.set_xlim(0, 60); ax2.set_ylim(0, 60)
36+ ax2.legend(fontsize=6.8, loc="upper left", ncol=1)
37+ # repère du sujet du cas 1 : a = 28, état 3 → 29 ans
38+ ax2.plot([28], [cas.age_effectif(28, L, 3)], marker="*", ms=9, color=palette.brand, ls="none")
39+ ax2.annotate("sujet cas 1\n(28 ans, état 3)", (28, cas.age_effectif(28, L, 3)), xytext=(33, 12), fontsize=7, color=palette.ink,
40+ arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
41+ fig.suptitle(f"Convention : $a_{{\\mathrm{{eff}}}} = (1-\\lambda)\\,a + \\lambda\\,\\varphi(c)\\,L$, λ = {cas.LAMBDA}, L = {L:.0f} ans, bornée à [0, L]",
42+ fontsize=8.5, x=0.02, ha="left", color=palette.ink)
43+ fig.tight_layout()
44+ out = savefig(fig, "ch11/ch11-fig-matrice-age-effectif.pdf"); print("OK", out)
45+
46+
47+if __name__ == "__main__":
48+ main()
added analysis/python/tests/test_ch06.py +202 −0
@@ -0,0 +1,202 @@
1+"""Tests du chapitre 6 — démembrements et modalités du droit de propriété (DONNÉES SYNTHÉTIQUES, hypothèses déclarées).
2+Tolérances : 0,5 $ sur les montants arrondis cités dans le texte ; 1e-6 (relatif) sur les identités ; 0,05 point sur les
3+pourcentages cités ; valeurs exactes pour les droits de mutation (calcul en dollars et cents)."""
4+import sys, pathlib, math
5+import numpy as np, pytest
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
7+import ch06_demembrements as cas
8+
9+TOL = 0.5
10+V = cas.V_ARRONDI
11+
12+
13+@pytest.fixture(scope="module")
14+def noi():
15+ return cas.rne_cas3()
16+
17+
18+class TestMortalite:
19+ def test_survie_monotone_et_bornes(self):
20+ for x in (55, 65, 85):
21+ p = [cas.survie(x, t) for t in range(0, cas.OMEGA - x + 1)]
22+ assert p[0] == 1.0 and p[-1] == 0.0
23+ assert all(a >= b for a, b in zip(p, p[1:]))
24+
25+ def test_esperances_citees(self):
26+ tm = cas.table_mortalite()
27+ assert tm.loc[65, "esperance_vie"] == pytest.approx(17.9, abs=0.05)
28+ assert tm.loc[55, "esperance_vie"] == pytest.approx(25.9, abs=0.05)
29+ assert tm.loc[75, "esperance_vie"] == pytest.approx(11.1, abs=0.05)
30+ assert tm.loc[85, "esperance_vie"] == pytest.approx(6.2, abs=0.05)
31+ assert tm.loc[65, "p10"] == pytest.approx(0.789, abs=5e-4) and tm.loc[65, "p20"] == pytest.approx(0.424, abs=5e-4)
32+ assert all(tm["esperance_vie"].diff().dropna() < 0)
33+
34+
35+class TestUsufruit:
36+ def test_valeur_cas1_et_loyer(self):
37+ c = cas.valeur_cas1()
38+ assert c["prediction_hedonique"] == pytest.approx(637952.85, abs=0.01)
39+ assert round(c["prediction_hedonique"], -3) == V
40+ assert cas.loyer_net(V) == pytest.approx(22330.0, abs=1e-6)
41+ assert cas.loyer_net(V) / 12 == pytest.approx(1861, abs=0.5)
42+
43+ @pytest.mark.parametrize("n,U,NP,part", [(10, 183443, 454557, 71.2), (20, 314141, 323859, 50.8), (30, 407260, 230740, 36.2)])
44+ def test_terme_fixe(self, n, U, NP, part):
45+ u = cas.usufruit_terme(V, n)
46+ assert u["U"] == pytest.approx(U, abs=TOL) and u["NP"] == pytest.approx(NP, abs=TOL)
47+ assert u["NP"] == pytest.approx(u["NP_formule"], rel=1e-9) # NP = V((1+g)/(1+r))^n
48+ assert u["U"] + u["NP"] == pytest.approx(V, rel=1e-12)
49+ assert u["part_NP"] * 100 == pytest.approx(part, abs=0.05)
50+
51+ @pytest.mark.parametrize("x,U,NP,part,ann,rente", [(55, 359836, 278164, 43.6, 16.114, 26300), (65, 280357, 357643, 56.1, 12.555, 25335),
52+ (75, 197560, 440440, 69.0, 8.847, 24409), (85, 123863, 514137, 80.6, 5.547, 23599)])
53+ def test_viager(self, x, U, NP, part, ann, rente):
54+ u = cas.usufruit_viager(V, x)
55+ assert u["U"] == pytest.approx(U, abs=TOL) and u["NP"] == pytest.approx(NP, abs=TOL)
56+ assert u["NP_direct"] == pytest.approx(u["NP"], rel=1e-9) # proposition : identité du partage viager
57+ assert u["part_NP"] * 100 == pytest.approx(part, abs=0.05)
58+ assert u["annuite_viagere"] == pytest.approx(ann, abs=5e-4)
59+ assert cas.rente_viagere_equivalente(V, x) == pytest.approx(rente, abs=TOL)
60+
61+ def test_identite_tous_ages_et_taux(self):
62+ for x in range(50, 100, 7):
63+ for r in (0.03, 0.05, 0.08):
64+ u = cas.usufruit_viager(V, x, r, 0.015)
65+ assert u["U"] + u["NP_direct"] == pytest.approx(V, rel=1e-9)
66+
67+ def test_approximation_terme_fixe_sous_estime_np(self):
68+ u = cas.usufruit_viager(V, 65)
69+ t = cas.usufruit_terme(V, 18) # espérance de vie arrondie (17,9 → 18)
70+ assert t["part_NP"] * 100 == pytest.approx(54.3, abs=0.05)
71+ assert u["NP"] - t["NP"] == pytest.approx(11000, abs=600) # « environ 11 000 $ »
72+ for x in (55, 65, 75, 85): # Jensen : NP viager ≥ NP terme fixe e_x
73+ n = int(round(cas.esperance_vie(x)))
74+ assert cas.usufruit_viager(V, x)["NP"] >= cas.usufruit_terme(V, n)["NP"]
75+
76+ def test_grille_sensibilite(self):
77+ g = cas.grille_usufruit(V)
78+ attendu = {55: (54.4, 43.6, 35.4), 60: (59.8, 49.7, 41.6), 65: (65.4, 56.1, 48.4), 70: (71.0, 62.6, 55.6),
79+ 75: (76.3, 69.0, 62.8), 80: (81.2, 75.1, 69.7), 85: (85.5, 80.6, 76.1)}
80+ for age, vals in attendu.items():
81+ assert tuple(round(v * 100, 1) for v in g.loc[age]) == vals
82+ assert (0.654 - 0.484) * V == pytest.approx(108000, abs=600)
83+ assert all(g.diff().dropna().values.ravel() > 0) # NP/V croît avec l'âge
84+ assert all(g.diff(axis=1).dropna(axis=1).values.ravel() < 0) # et décroît avec r
85+
86+
87+class TestEmphyteose:
88+ def test_reference(self, noi):
89+ assert noi == pytest.approx(89955.75, abs=0.01)
90+ e = cas.emphyteose(noi, cas.RENTE_EMPH, cas.N_EMPH)
91+ assert e["V_pleine"] == pytest.approx(1999017, abs=TOL)
92+ assert e["V_emph"] == pytest.approx(1166242, abs=TOL) and e["part_emph"] * 100 == pytest.approx(58.3, abs=0.05)
93+ assert e["V_rentes"] == pytest.approx(388430, abs=TOL) and e["V_reprise"] == pytest.approx(444344, abs=TOL)
94+ assert e["V_prop"] == pytest.approx(832774, abs=TOL)
95+ assert e["somme"] == pytest.approx(e["V_pleine"], rel=1e-9)
96+
97+ def test_identite_toutes_durees(self, noi):
98+ for n in range(1, 101):
99+ e = cas.emphyteose(noi, cas.RENTE_EMPH, n)
100+ assert e["somme"] == pytest.approx(e["V_pleine"], rel=1e-9)
101+ g = cas.grille_emphyteose(noi, cas.RENTE_EMPH)
102+ assert all(g["V_emph"].diff().dropna() < 0) # décroît quand la durée restante baisse
103+
104+ def test_grille_citee(self, noi):
105+ g = cas.grille_emphyteose(noi, cas.RENTE_EMPH)
106+ attendu = {60: (1426059, 421175, 151782, 572958, 71.3), 50: (1351745, 414022, 233250, 647272, 67.6),
107+ 40: (1240623, 399951, 358443, 758394, 62.1), 20: (834713, 317820, 846483, 1164303, 41.8),
108+ 10: (487489, 210707, 1300820, 1511528, 24.4), 5: (263447, 123006, 1612564, 1735570, 13.2)}
109+ for n, (ve, vr, vp, vt, part) in attendu.items():
110+ assert g.loc[n, "V_emph"] == pytest.approx(ve, abs=TOL) and g.loc[n, "V_rentes"] == pytest.approx(vr, abs=TOL)
111+ assert g.loc[n, "V_reprise"] == pytest.approx(vp, abs=TOL) and g.loc[n, "V_prop"] == pytest.approx(vt, abs=TOL)
112+ assert g.loc[n, "part_emph"] * 100 == pytest.approx(part, abs=0.05)
113+
114+ def test_taux_distincts(self, noi):
115+ e = cas.emphyteose(noi, cas.RENTE_EMPH, cas.N_EMPH, r_prop=0.055)
116+ assert e["V_prop"] == pytest.approx(1190025, abs=TOL)
117+ assert e["somme"] == pytest.approx(2356268, abs=TOL)
118+ assert e["somme"] - e["V_pleine"] == pytest.approx(357251, abs=TOL)
119+
120+
121+class TestAutres:
122+ def test_indivision(self):
123+ assert cas.part_indivise(V, 0.5, 0.0)["valeur_part"] == pytest.approx(319000, abs=TOL)
124+ assert cas.part_indivise(V, 0.5, 0.10)["valeur_part"] == pytest.approx(287100, abs=TOL)
125+ assert cas.part_indivise(V, 0.5, 0.20)["valeur_part"] == pytest.approx(255200, abs=TOL)
126+
127+ def test_servitude(self):
128+ c = cas.valeur_cas1()
129+ assert c["elasticite_terrain"] == pytest.approx(0.0937, abs=5e-4)
130+ s = cas.servitude_terrain(V, c["terrain_m2"], 120, c["elasticite_terrain"])
131+ assert s["delta"] == pytest.approx(12725, abs=TOL) and s["delta_pct"] * 100 == pytest.approx(2.0, abs=0.05)
132+
133+ def test_rang(self):
134+ r = cas.rang_hypothecaire(560000, [455000, 90000, 40000])
135+ assert [x["recouvre"] for x in r] == [455000, 90000, 15000, 0]
136+ assert r[2]["taux_recouvrement"] == pytest.approx(0.375)
137+ assert sum(x["recouvre"] for x in r) == 560000
138+
139+ def test_mutation(self):
140+ m = cas.droit_mutation(V)
141+ assert [round(t["montant"], 2) for t in m["detail"]] == [314.50, 2521.00, 4845.00]
142+ assert m["total"] == pytest.approx(7680.50, abs=0.005)
143+ assert m["taux_effectif"] * 100 == pytest.approx(1.204, abs=0.0005)
144+ m2 = cas.droit_mutation(750000, taux_municipal=[(500000, 0.02)])
145+ assert m2["droit_base"] == pytest.approx(9360.50, abs=0.005) and m2["supplement_municipal"] == pytest.approx(1250.0, abs=0.005)
146+ assert cas.droit_mutation(62900)["total"] == pytest.approx(314.50, abs=0.005)
147+ assert cas.droit_mutation(0)["total"] == 0.0
148+
149+ def test_vice_et_registre(self):
150+ q = cas.quantum_vice(520000, 46000, 9000, 6500, False)
151+ assert q["reduction_prix"] == 37000 and q["total"] == 37000 and q["part_prix"] * 100 == pytest.approx(7.1, abs=0.05)
152+ assert cas.quantum_vice(520000, 46000, 9000, 6500, True)["total"] == 43500
153+ assert cas.budget_registre(5)["cout"] == 15.0
154+
155+
156+class TestExercices:
157+ """Chiffres des solutions (solutions/ch06.tex)."""
158+
159+ def test_ex1(self):
160+ assert 612000 - 6500 - 4000 == 601500
161+
162+ def test_ex3(self):
163+ p = cas.part_indivise(900000, 1 / 3, 0.12)
164+ assert p["valeur_proportionnelle"] == pytest.approx(300000, abs=1e-6) and p["valeur_part"] == pytest.approx(264000, abs=1e-6)
165+
166+ def test_ex4(self):
167+ paires = [(548000, 531000), (602000, 586500), (575000, 559000)]
168+ d = [a - b for a, b in paires]; rel = [(a - b) / a for a, b in paires]
169+ assert d == [17000, 15500, 16000] and np.mean(d) == pytest.approx(16166.67, abs=0.01)
170+ assert [round(x * 100, 2) for x in rel] == [3.10, 2.57, 2.78]
171+ assert np.mean(rel) * 100 == pytest.approx(2.82, abs=0.005)
172+ assert np.mean(rel) * 638000 == pytest.approx(17991, abs=TOL)
173+
174+ def test_ex5(self):
175+ assert cas.quantum_vice(520000, 46000, 9000, 6500, False)["prix_ajuste"] == 483000
176+
177+ def test_ex6(self):
178+ Vx, r, g, x = 455000, 0.045, 0.01, 72
179+ assert cas.loyer_net(Vx, r, g) == pytest.approx(15925.0, abs=1e-6)
180+ u = cas.usufruit_viager(Vx, x, r, g)
181+ assert u["U"] == pytest.approx(158934, abs=TOL) and u["NP"] == pytest.approx(296066, abs=TOL)
182+ assert u["part_NP"] * 100 == pytest.approx(65.1, abs=0.05) and u["esperance_vie"] == pytest.approx(13.0, abs=0.05)
183+ t = cas.usufruit_terme(Vx, 13, r, g)
184+ assert t["U"] == pytest.approx(162802, abs=TOL) and t["NP"] == pytest.approx(292198, abs=TOL)
185+ assert u["NP"] - t["NP"] == pytest.approx(3868, abs=TOL)
186+ assert cas.rente_viagere_equivalente(Vx, x, r, g) == pytest.approx(17038, abs=TOL)
187+ t30 = cas.usufruit_terme(Vx, 30, r, g)
188+ assert t30["U"] == pytest.approx(291257, abs=TOL) and t30["NP"] == pytest.approx(163743, abs=TOL)
189+
190+ def test_ex7(self):
191+ e = cas.emphyteose(142000, 24000, 28, 0.07, 0.02)
192+ assert e["V_pleine"] == pytest.approx(2840000, abs=1e-6)
193+ assert e["V_emph"] == pytest.approx(1805044, abs=TOL) and e["V_rentes"] == pytest.approx(291291, abs=TOL)
194+ assert e["V_reprise"] == pytest.approx(743665, abs=TOL) and e["V_prop"] == pytest.approx(1034956, abs=TOL)
195+ assert e["somme"] == pytest.approx(2840000, rel=1e-9)
196+ e2 = cas.emphyteose(142000, 24000, 68, 0.07, 0.02)
197+ assert e2["V_emph"] == pytest.approx(2390930, abs=TOL) and e2["V_prop"] == pytest.approx(449070, abs=TOL)
198+ m = cas.droit_mutation(e["V_emph"])
199+ assert m["total"] == pytest.approx(314.50 + 2521.00 + 0.015 * (e["V_emph"] - 315000), abs=0.01)
200+ assert m["total"] == pytest.approx(25186.16, abs=0.01)
201+ e3 = cas.emphyteose(142000, 24000, 28, 0.07, 0.02, r_prop=0.055)
202+ assert e3["V_prop"] == pytest.approx(1443131, abs=TOL) and e3["somme"] == pytest.approx(3248175, abs=TOL)
modified analysis/python/tests/test_ch07.py +6 −0
@@ -177,6 +177,12 @@ class TestExercices:
177 assert cas.cotisation_seuil(plancher=25000) == pytest.approx(23228, abs=1) 177 assert cas.cotisation_seuil(plancher=25000) == pytest.approx(23228, abs=1)
178 assert cas.cotisation_seuil(solde0=150000) == pytest.approx(17304, abs=1) 178 assert cas.cotisation_seuil(solde0=150000) == pytest.approx(17304, abs=1)
179 assert cas.cotisation_seuil(indexer=False) == pytest.approx(25625, abs=1) 179 assert cas.cotisation_seuil(indexer=False) == pytest.approx(25625, abs=1)
180+ assert cas.trajectoire(cas.cotisation_seuil(indexer=False), indexer=False)["F"][-1] == pytest.approx(125996, abs=1)
181+ # F4 : ajustement −q4 D0 vs valeur actualisée (5 %) de la hausse de cotisation indexée sur 10 ans
182+ q4 = cas.quotes_parts()["q"][3]; h = q4 * (cas.cotisation_seuil() - cas.COTISATION_ACTUELLE)
183+ assert h == pytest.approx(1393, abs=1)
184+ pv = sum(h * 1.025 ** (t - 1) / 1.05 ** t for t in range(1, 11))
185+ assert pv == pytest.approx(11929, abs=1) and sum(h * 1.025 ** (t - 1) for t in range(1, 11)) == pytest.approx(15602, abs=1)
180 186
181 def test_ex7_indivision(self): 187 def test_ex7_indivision(self):
182 V = 1050000.0 188 V = 1050000.0
added analysis/python/tests/test_ch11.py +176 −0
@@ -0,0 +1,176 @@
1+"""Tests du chapitre 11 — inspection synthétique du sujet du cas 1, convention d'âge effectif, dépréciation ventilée,
2+sensibilité et exercices. Tolérances : 0,01 $ sur les montants recalculés ; 0,5 $ sur les montants arrondis cités ;
3+1e-9 sur les identités."""
4+import sys, pathlib
5+import numpy as np, pytest
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
7+import ch11_inspection as cas
8+from traite import cout as ct
9+
10+
11+@pytest.fixture(scope="module")
12+def data():
13+ devis, sujet, cn = cas.charger()
14+ f = cas.fiche(devis, cn); trav = cas.travaux(devis, cn)
15+ return {"devis": devis, "sujet": sujet, "cn": cn, "f": f, "trav": trav, "dv": cas.depreciation(f, trav, cn), "sens": cas.sensibilite(f, trav, cn)}
16+
17+
18+class TestConvention:
19+ def test_bornes_et_monotonie(self):
20+ L = 60.0
21+ for a in np.linspace(0, 80, 17):
22+ prev = -1.0
23+ for c in range(1, 6):
24+ v = cas.age_effectif(a, L, c)
25+ assert 0.0 <= v <= L and v >= prev # croissant avec la note d'état, borné à [0, L]
26+ prev = v
27+ for c in range(1, 6): # croissant avec l'âge chronologique
28+ vals = [cas.age_effectif(a, L, c) for a in range(0, 61)]
29+ assert all(x <= y for x, y in zip(vals, vals[1:]))
30+
31+ def test_cas_limites(self):
32+ assert cas.age_effectif(0, 60, 1) == pytest.approx(1.5) # neuf et excellent : 0,5 × 0,05 × 60
33+ assert cas.age_effectif(60, 60, 5) == 60.0 # en fin de vie et mauvais : borne L
34+ assert cas.age_effectif(28, 60, 3) == pytest.approx(29.0) # sujet : (1 − 0,5) × 28 + 0,5 × 0,5 × 60
35+ assert cas.age_effectif(28, 60, 3, lam=0.0) == 28.0 # λ = 0 : âge chronologique
36+ assert cas.age_effectif(28, 60, 3, lam=1.0) == 30.0 # λ = 1 : φ(3) L
37+ assert cas.age_effectif(28, 20, 4) == 20.0 # VRC : 14 + 7,5 = 21,5 → borné à 20
38+
39+ def test_phi(self):
40+ assert list(cas.PHI.values()) == [0.05, 0.25, 0.50, 0.75, 1.00]
41+
42+
43+class TestFiche:
44+ def test_couverture_du_devis(self, data):
45+ f, cn = data["f"], data["cn"]
46+ assert len(f) == 20 and int(f["longue_duree"].sum()) == 1
47+ assert f["cout_direct"].sum() == pytest.approx(cn["direct"], abs=0.01)
48+ assert f["cout_neuf"].sum() == pytest.approx(cn["total"], abs=0.01)
49+ assert cas.facteur_majoration(cn) == pytest.approx(1.33043, abs=5e-6)
50+ assert cn["total"] == pytest.approx(397567.33, abs=0.01)
51+
52+ def test_valeurs_citees(self, data):
53+ f = data["f"].set_index("composante")
54+ assert f.loc["Couverture (bardeaux d'asphalte)", "age_effectif"] == pytest.approx(12.0)
55+ assert f.loc["Couverture (bardeaux d'asphalte)", "cout_neuf"] == pytest.approx(10217.70, abs=0.01)
56+ assert f.loc["Gouttières et solins", "age_effectif"] == pytest.approx(23.375)
57+ assert f.loc["Fenêtres (PVC, double vitrage)", "age_effectif"] == pytest.approx(21.5)
58+ assert f.loc["Fenêtres (PVC, double vitrage)", "cout_neuf"] == pytest.approx(22311.42, abs=0.01)
59+ assert f.loc["Ventilateur récupérateur de chaleur (VRC)", "ratio"] == pytest.approx(1.0)
60+ assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "cout_neuf"] == pytest.approx(172611.24, abs=0.01)
61+ assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "age_effectif"] == pytest.approx(29.0)
62+ assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "vie_restante"] == pytest.approx(31.0)
63+ courtes = data["f"][~data["f"]["longue_duree"]]
64+ assert courtes["cout_neuf"].sum() == pytest.approx(224956.09, abs=0.01)
65+ assert courtes["cout_neuf"].sum() / data["cn"]["total"] == pytest.approx(0.566, abs=5e-4)
66+
67+ def test_indice_etat(self, data):
68+ f = data["f"]
69+ assert cas.indice_etat(f) == pytest.approx(2.809, abs=5e-4)
70+ assert f["etat"].value_counts().to_dict() == {2: 7, 3: 8, 4: 4, 5: 1}
71+ r = (f["cout_neuf"] * f["ratio"]).sum() / f["cout_neuf"].sum()
72+ assert r == pytest.approx(0.547, abs=5e-4) and r * 60 == pytest.approx(32.8, abs=0.05)
73+
74+
75+class TestTravaux:
76+ def test_total(self, data):
77+ t = data["trav"]
78+ assert len(t) == 5
79+ assert t["cout"].sum() == pytest.approx(10479.98, abs=0.01)
80+ assert t["cout"].sum() / data["cn"]["total"] == pytest.approx(0.0264, abs=5e-5)
81+ assert t.set_index("poste_reference").loc["Entrée asphaltée", "cout"] == pytest.approx(3438.43, abs=0.01)
82+ assert t.set_index("poste_reference").loc["Fenêtres (PVC double vitrage)", "cout"] == pytest.approx(2788.93, abs=0.01)
83+
84+
85+class TestDepreciation:
86+ def test_identites(self, data):
87+ dv, cn = data["dv"], data["cn"]
88+ assert dv["total"] == pytest.approx(dv["curable"] + sum(dv["courte_duree"].values()) + dv["longue_duree"], rel=1e-12)
89+ courtes_hors_entree = data["f"][(~data["f"]["longue_duree"]) & (data["f"]["composante"] != "Entrée asphaltée")]["cout_neuf"].sum()
90+ assert dv["base_longue"] == pytest.approx(cn["total"] - dv["curable"] - courtes_hors_entree, abs=1e-6)
91+ assert dv["longue_duree"] == pytest.approx(dv["base_longue"] * 29.0 / 60.0, rel=1e-12)
92+ assert dv["contribution_batiment"] == pytest.approx(cn["total"] - dv["total"], rel=1e-12)
93+ assert len(dv["courte_duree"]) == 18
94+
95+ def test_valeurs_citees(self, data):
96+ dv = data["dv"]
97+ assert dv["curable"] == pytest.approx(10479.98, abs=0.01)
98+ assert sum(dv["courte_duree"].values()) == pytest.approx(130584.55, abs=0.01)
99+ assert dv["base_longue"] == pytest.approx(165569.69, abs=0.01)
100+ assert dv["longue_duree"] == pytest.approx(80025.35, abs=0.01)
101+ assert dv["total"] == pytest.approx(221089.88, abs=0.01)
102+ assert dv["taux"] == pytest.approx(0.5561, abs=5e-5)
103+ assert dv["contribution_batiment"] == pytest.approx(176477.45, abs=0.01)
104+ assert dv["age_vie_simple"] == pytest.approx(192157.55, abs=0.01)
105+ assert dv["age_vie_simple"] / dv["cout_neuf"] == pytest.approx(0.4833, abs=5e-5)
106+ assert dv["total"] - dv["age_vie_simple"] == pytest.approx(28932.34, abs=0.01)
107+ assert ct.depreciation_age_vie(dv["cout_neuf"], 28, 60) == pytest.approx(185531.42, abs=0.01)
108+ assert dv["courte_duree"]["Ventilateur récupérateur de chaleur (VRC)"] == pytest.approx(7501.27, abs=0.01)
109+ assert dv["courte_duree"]["Fenêtres (PVC, double vitrage)"] == pytest.approx(15989.85, abs=0.01)
110+
111+
112+class TestSensibilite:
113+ def test_grille(self, data):
114+ s = data["sens"].set_index(["lambda", "etat_structure"])
115+ assert s.loc[(0.5, 3), "taux"] == pytest.approx(0.5561, abs=5e-5)
116+ assert s.loc[(0.0, 3), "taux"] == pytest.approx(0.6368, abs=5e-5)
117+ assert s.loc[(1.0, 1), "taux"] == pytest.approx(0.2738, abs=5e-5)
118+ assert s.loc[(1.0, 5), "taux"] == pytest.approx(0.6694, abs=5e-5)
119+ assert s.loc[(0.5, 1), "taux"] == pytest.approx(0.4624, abs=5e-5)
120+ assert s.loc[(0.5, 5), "taux"] == pytest.approx(0.6602, abs=5e-5)
121+ for lam in (0.0, 0.25, 0.5, 0.75, 1.0):
122+ t = [s.loc[(lam, e), "taux"] for e in range(1, 6)]
123+ assert all(x <= y + 1e-12 for x, y in zip(t, t[1:])) # croissante avec la note d'état
124+ # à λ = 0 la note d'état n'intervient pas
125+ assert len({round(s.loc[(0.0, e), "taux"], 9) for e in range(1, 6)}) == 1
126+ assert s.loc[(1.0, 5), "age_effectif_structure"] == 60.0 and s.loc[(1.0, 1), "age_effectif_structure"] == pytest.approx(3.0)
127+
128+
129+class TestValeurInformation:
130+ def test_regle(self):
131+ v = cas.valeur_information(0.10, 3000.0, 50.0)
132+ assert v["perte_esperee"] == 300.0 and v["valeur_info"] == 250.0 and v["recommander"]
133+ v = cas.valeur_information(0.05, 25000.0, 1200.0)
134+ assert v["valeur_info"] == pytest.approx(50.0) and v["recommander"]
135+ v = cas.valeur_information(0.02, 25000.0, 1200.0)
136+ assert v["valeur_info"] == pytest.approx(-700.0) and not v["recommander"]
137+ # seuil de probabilité : p* = t / C
138+ assert 1200.0 / 25000.0 == pytest.approx(0.048)
139+
140+
141+class TestExercices:
142+ """Chiffres des solutions (solutions/ch11.tex)."""
143+
144+ def test_ex3_ages_effectifs(self):
145+ # toiture 12 ans / L 20 / état 4 ; chauffe-eau 9 / 12 / état 2 ; fenêtres 35 / 30 / état 2 ; structure 45 / 60 / état 2
146+ assert cas.age_effectif(12, 20, 4) == pytest.approx(13.5)
147+ assert cas.age_effectif(9, 12, 2) == pytest.approx(6.0)
148+ assert cas.age_effectif(35, 30, 2) == pytest.approx(21.25)
149+ assert cas.age_effectif(45, 60, 2) == pytest.approx(30.0)
150+ assert ct.vie_restante(60, 30.0) == 30.0
151+
152+ def test_ex4_ventilee(self):
153+ comps = [ct.Composante("toiture", 12000.0, 13.5, 20), ct.Composante("chauffe-eau", 1500.0, 6.0, 12), ct.Composante("fenêtres", 24000.0, 21.25, 30)]
154+ dv = ct.depreciation_ventilee(300000.0, 4500.0, comps, 30.0, 60.0)
155+ assert dv["courte_duree"]["toiture"] == pytest.approx(8100.0)
156+ assert dv["courte_duree"]["chauffe-eau"] == pytest.approx(750.0)
157+ assert dv["courte_duree"]["fenêtres"] == pytest.approx(17000.0)
158+ assert dv["base_longue"] == pytest.approx(258000.0)
159+ assert dv["longue_duree"] == pytest.approx(129000.0)
160+ assert dv["total"] == pytest.approx(159350.0)
161+ assert dv["total"] / 300000.0 == pytest.approx(0.53117, abs=5e-5)
162+ assert ct.depreciation_age_vie(300000.0, 30.0, 60.0) == pytest.approx(150000.0)
163+
164+ def test_ex5_valeur_information(self):
165+ v = cas.valeur_information(0.08, 20000.0, 900.0)
166+ assert v["perte_esperee"] == pytest.approx(1600.0) and v["valeur_info"] == pytest.approx(700.0)
167+ assert 900.0 / 20000.0 == pytest.approx(0.045)
168+ # dosimètre radon : 50 $ ; seuil p* pour une correction de 2 500 $
169+ assert 50.0 / 2500.0 == pytest.approx(0.02)
170+
171+ def test_ex6_variante_lambda(self, data):
172+ s = data["sens"].set_index(["lambda", "etat_structure"])
173+ assert s.loc[(0.75, 3), "taux"] == pytest.approx(0.5093, abs=5e-5)
174+ assert s.loc[(0.25, 3), "taux"] == pytest.approx(0.5977, abs=5e-5)
175+ assert s.loc[(0.75, 3), "age_effectif_structure"] == pytest.approx(29.5)
176+ assert s.loc[(0.25, 3), "age_effectif_structure"] == pytest.approx(28.5)
modified assets/figures/ch02/ch02-fig-decomposition-droits.pdf +0 −0

Binary file not shown.

modified assets/figures/ch02/ch02-fig-ecart-logements.pdf +0 −0

Binary file not shown.

modified assets/figures/ch06/ch06-fig-usufruit.pdf +0 −0

Binary file not shown.

added assets/figures/ch11/ch11-fig-fiche-etat.pdf +0 −0

Binary file not shown.

added assets/figures/ch11/ch11-fig-frise-pathologies.pdf +0 −0

Binary file not shown.

added assets/figures/ch11/ch11-fig-matrice-age-effectif.pdf +0 −0

Binary file not shown.

added assets/figures/ch11/ch11-fig-sensibilite-age-effectif.pdf +0 −0

Binary file not shown.

added manuscript/_wrap_ch07.tex +12 −0
@@ -0,0 +1,12 @@
1+\providecommand{\BUILDVARIANT}{uqo}\providecommand{\BUILDMEDIA}{numerique}
2+\documentclass[10pt,twoside,openright]{book}
3+\newif\ifLogoAutorise\LogoAutorisefalse\def\PALETTE{uqo}
4+\usepackage{traite-qc}
5+\input{../manuscript/frontmatter/metadata.tex}
6+\input{/Users/simon-pierreboucher/Desktop/UQO/Traite-Evaluation-QC/build/ch07/_labels.tex}
7+\begin{document}
8+\setcounter{chapter}{6}
9+\input{chapters/ch07.tex}
10+\appendix\chapter{Solutions du chapitre}\input{../solutions/ch07.tex}
11+\printbibliography
12+\end{document}
modified manuscript/appendices/formulaires-inspection.tex +479 −1
@@ -1,2 +1,480 @@
1 \chapter{Formulaires d'inspection}\label{app:formulaires-inspection} 1 \chapter{Formulaires d'inspection}\label{app:formulaires-inspection}
2−\stubchapter 2+
3+L'inspection est le moment où l'évaluateur cesse de raisonner sur des documents pour regarder l'immeuble. Elle fonde l'identification physique du bien, la cote d'état qui entre dans les ajustements de la méthode de comparaison, l'âge effectif qui gouverne la dépréciation de la méthode du coût et la liste des dépenses en capital que la méthode du revenu doit projeter. La présente annexe fournit trois formulaires structurés, conçus pour être imprimés ou reproduits dans une tablette : l'un pour la maison unifamiliale, le second pour les immeubles de deux à cinq logements, le troisième pour les immeubles de six logements et plus. Elle en explique le mode d'emploi, précise ce que l'inspection d'évaluation est et n'est pas, rappelle les pathologies propres au parc québécois que l'évaluateur doit savoir reconnaître sans prétendre les diagnostiquer, et se termine par un exemple rempli, entièrement synthétique, pour le sujet du premier dossier de cas. Les formulaires reprennent la structure et le vocabulaire du \cref{ch:11}, auquel le lecteur se reportera pour la description détaillée des composantes et des désordres, et ils alimentent directement les calculs du \cref{ch:24}.
4+
5+\providecommand{\caseA}{$\square$}
6+\providecommand{\cinq}{$\square\,\square\,\square\,\square\,\square$}
7+\providecommand{\inc}{$\square$\,{\footnotesize$+$}\ $\square$\,{\footnotesize$-$}\ $\square$\,{\footnotesize$0$}}
8+
9+\section{Objet et limites de l'inspection d'évaluation}\label{sec:appB:objet}
10+
11+Les Normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec font de l'inspection une obligation : tout immeuble évalué doit être inspecté, et seules des circonstances exceptionnelles, expliquées dans le rapport, autorisent à s'en dispenser \parencite[règle 2.4]{oeaqNormes}. La règle précise ce que l'inspection doit au minimum permettre : décrire l'immeuble, fonder la conclusion sur l'usage le meilleur et le plus profitable et soutenir les comparaisons avec les ventes retenues. Elle ne demande pas à l'évaluateur de se substituer à l'inspecteur en bâtiment, à l'ingénieur ou au laboratoire ; elle lui demande de voir ce qu'un observateur attentif et formé peut voir, de le consigner, et de dire ce qu'il n'a pas pu voir. Le Code de déontologie ajoute que le rapport fait état de la méthodologie et de l'étendue des recherches \parencite[art.~41]{deontOEAQ}, ce qui inclut la description de l'inspection elle-même : date, durée, conditions météorologiques, parties visitées, parties inaccessibles, personnes présentes.
12+
13+Il faut distinguer nettement trois démarches qui se ressemblent sur le terrain. L'\motcle{inspection d'évaluation} est un relevé visuel, non destructif, orienté vers la valeur : elle cherche les caractéristiques qui différencient le sujet de ses comparables, les signes d'usure qui déterminent l'âge effectif et les désordres dont le marché tiendrait compte. L'\motcle{inspection préachat} est un service distinct, rendu à un acheteur en vertu d'un contrat de service, dont l'objet est de révéler à un acheteur prudent et diligent l'état d'un immeuble et dont la portée est définie par la convention de service de l'inspecteur ; elle est plus longue, plus systématique, et elle emporte ses propres responsabilités. L'\motcle{expertise} enfin (structure, pyrite, amiante, radon, sol) relève de professionnels spécialisés, procède souvent par essais ou prélèvements et fournit un diagnostic. L'évaluateur qui constate un signe d'alerte recommande l'expertise ; il ne la remplace pas, et il traite le désordre soupçonné soit par une réserve, soit par une hypothèse extraordinaire dont il indique l'incidence sur la valeur \parencite[règle 2.1~c) et règle 2.3, élément 7]{oeaqNormes}. La notion de vice caché du Code civil éclaire la frontière : est apparent le vice qu'un acheteur prudent et diligent peut constater sans recourir à un expert \parencite[art.~1726]{ccq} ; l'inspection d'évaluation se situe au niveau de cet acheteur prudent et diligent, non à celui de l'expert.
14+
15+\begin{attention}[Ce que le formulaire n'est pas]
16+Les formulaires de cette annexe ne constituent ni un rapport d'inspection préachat, ni une attestation de conformité au Code de construction, ni un diagnostic. Une case cochée « 2 » à la rubrique « fondations » signifie que l'évaluateur a observé des signes d'un état inférieur à la normale pour l'âge, rien de plus. La conclusion qui en découle dans le rapport est une opinion d'évaluateur sur l'incidence de cet état sur la valeur, assortie, si nécessaire, de la recommandation d'une expertise et d'une hypothèse extraordinaire. Le Code de déontologie interdit d'exclure sa responsabilité civile par une clause générale \parencite[art.~32]{deontOEAQ} ; la protection de l'évaluateur ne vient pas d'une réserve passe-partout, mais de la description précise de ce qu'il a fait et constaté.
17+\end{attention}
18+
19+\section{Mode d'emploi}\label{sec:appB:mode}
20+
21+\subsection{Avant la visite}
22+
23+La visite se prépare au bureau, et la qualité de la préparation détermine celle du relevé. L'évaluateur réunit d'abord les documents qui décrivent l'immeuble indépendamment de lui : la fiche du rôle d'évaluation et, lorsqu'elle est accessible, la matrice graphique de la municipalité ; le certificat de localisation, qui révèle les empiètements, les servitudes et les irrégularités du lot (\cref{ch:7}) ; les permis de construction et de rénovation délivrés, qui datent les travaux et signalent ceux qui ont été faits sans permis lorsque l'immeuble montre des transformations qui ne figurent nulle part ; la grille de zonage et les règlements applicables (\cref{ch:8}) ; la cartographie des zones inondables et de mobilité en vigueur, qui classe désormais les secteurs selon l'intensité du risque \parencite{inondcartes,zonesInond2026} ; et, pour un immeuble à revenus, les baux, l'état locatif et les trois derniers exercices de dépenses (\cref{ch:26,ch:28}). Lorsque l'immeuble est en vente par l'intermédiaire d'un courtier, le formulaire obligatoire de déclarations du vendeur, exigé depuis 2012 pour la vente par une personne physique d'un immeuble résidentiel de moins de cinq logements, renseigne sur les sinistres, les infiltrations, les présences connues de pyrite, d'ocre ferreuse, d'amiante, de réservoirs de mazout, et sur les loyers et les dossiers portés devant le Tribunal administratif du logement \parencite{oaciqFormulaireDV,oaciqContexteDV}. Il ne remplace pas l'inspection, mais il oriente le regard : une déclaration d'infiltration au sous-sol en 2019 appelle une attention particulière à la jonction du mur et de la dalle.
24+
25+La préparation comprend aussi le choix du formulaire et l'impression, ou le chargement, des pages nécessaires ; la vérification de l'équipement (appareil photo ou tablette, télémètre laser, ruban de 30~m, lampe, niveau, humidimètre si l'évaluateur en possède un, gabarit de fissures, tournevis pour sonder les bois extérieurs, gants) ; et la prise de rendez-vous, avec la mention explicite des parties à visiter (tous les logements, le toit si accessible, le vide sanitaire, la salle mécanique). Pour un immeuble locatif, l'accès aux logements est soumis au préavis que le Code civil impose au locateur \parencite[art.~1931]{ccq} ; l'évaluateur demande au propriétaire de l'avoir donné et note les logements auxquels l'accès lui est refusé, puisqu'ils feront l'objet d'une hypothèse déclarée dans le rapport.
26+
27+\subsection{Pendant la visite}
28+
29+Le relevé suit toujours le même ordre, parce que l'ordre est ce qui empêche l'oubli : le site et le terrain depuis la rue, puis le tour complet de l'enveloppe à distance puis de près, puis l'intérieur du haut vers le bas ou du bas vers le haut selon l'accès, en terminant par les locaux techniques. À chaque rubrique du formulaire, l'évaluateur consigne quatre choses. La \emph{cote d'état}, de 1 à 5, est définie au \cref{tab:appB:echelle} ; elle s'apprécie par rapport à l'âge de la composante, un revêtement de 25~ans en fin de vie mais sans désordre étant coté 2 ou 3 selon l'urgence du remplacement, non 1. L'\emph{âge apparent} est l'âge que la composante paraît avoir compte tenu de son état, qu'il faut distinguer de l'âge chronologique connu par les permis ou les déclarations : un toit refait en 2012 qui cloque a un âge apparent supérieur à ses quatorze ans. Les \emph{observations} sont des faits, pas des interprétations : « fissure verticale de 3~mm, mur ouest, du haut de la fondation jusqu'à la dalle, traces d'efflorescence » plutôt que « problème de fondation ». L'\emph{incidence sur la valeur} est codée en trois signes : positive ($+$) lorsque la composante place le sujet au-dessus de ses comparables, négative ($-$) lorsqu'elle appelle une dépense ou une décote, nulle (0) lorsqu'elle est conforme à la norme du marché ; le montant, lui, n'est pas estimé sur place, il résulte de l'analyse au bureau (\cref{ch:17,ch:24}).
30+
31+La photographie est la mémoire du dossier. La règle d'usage est une vue de chaque façade, une vue de chaque pièce depuis la porte, une vue de chaque appareil mécanique avec sa plaque signalétique lisible, et une vue rapprochée de chaque désordre consigné, accompagnée d'un objet d'échelle (gabarit, ruban, pièce de monnaie). Les photographies sont numérotées dans l'ordre du formulaire et leur numéro est inscrit dans la colonne des observations, de sorte que chaque constat renvoie à une image. Le mesurage suit les conventions du \cref{ch:12} : périmètre extérieur de chaque niveau, hauteur des plafonds, superficies des pièces principales si le rapport doit les donner, et dimensions du terrain confrontées au certificat de localisation. L'évaluateur résiste à deux tentations : celle d'ouvrir ce qui est fermé (panneau électrique sous tension, trappe de vide sanitaire scellée, isolant de l'entretoit) lorsqu'il n'en a ni la compétence ni l'autorisation, et celle de qualifier devant le propriétaire ou le locataire ce qu'il constate. Ce qui est dit sur place est répété, déformé, et finit parfois devant un tribunal.
32+
33+\subsection{Après la visite}
34+
35+Le formulaire rempli est relu le jour même, pendant que la mémoire est fraîche, et complété : cotes hésitantes tranchées, photographies associées, mesures recopiées. Trois synthèses en sont tirées. La première est la \emph{cote d'état générale}, qui n'est pas la moyenne arithmétique des cotes, mais un jugement pondéré par l'importance de chaque composante dans le coût de remplacement, dont la \cref{sec:appB:age} donne la formalisation ; c'est cette cote qui entre comme variable d'état dans les grilles d'ajustement et dans les modèles hédoniques (\cref{ch:17,ch:18}). La deuxième est l'\emph{âge effectif} de l'immeuble, qui alimente la dépréciation physique de la méthode du coût (\cref{ch:24}). La troisième est la \emph{liste des travaux}, classée en travaux immédiats (dépréciation physique curable, ou dépenses immédiatement après l'achat dans la méthode de comparaison), travaux à moyen terme (dépenses en capital à projeter dans les flux actualisés d'un immeuble à revenus, \cref{ch:30}) et désordres à expertiser (hypothèses extraordinaires ou réserves du rapport). Le formulaire, les photographies et les notes sont conservés au dossier, que le règlement sur la tenue des dossiers oblige à garder au moins cinq ans après le dernier service rendu \parencite[art.~6]{tenuedossiersoeaq} ; ils constituent la preuve de l'étendue du travail que la règle 2.3 oblige à décrire.
36+
37+\section{Échelle d'état et âge effectif}\label{sec:appB:age}
38+
39+\begin{table}[htbp]\centering
40+\caption{Échelle de cotation de l'état des composantes utilisée dans les formulaires}\label{tab:appB:echelle}
41+\small\begin{tabularx}{\textwidth}{@{}cL{2.6cm}YL{3.1cm}@{}}
42+\toprule
43+Cote & Qualificatif & Définition opérationnelle & Conséquence habituelle \\
44+\midrule
45+5 & Neuf ou comme neuf & Composante neuve ou récemment remplacée, sans usure visible & Âge apparent proche de zéro ; ajustement positif si les comparables sont dans la norme \\
46+4 & Bon & Usure normale pour l'âge, entretien régulier visible, aucun désordre & Âge apparent égal ou inférieur à l'âge chronologique \\
47+3 & Moyen & Usure conforme à l'âge, entretien différé mineur, fin de vie économique en vue mais non atteinte & Âge apparent égal à l'âge chronologique ; réserve de remplacement à prévoir \\
48+2 & Passable & Fin de vie atteinte ou désordre fonctionnel ; remplacement ou réparation requis à court terme & Dépréciation curable ou dépense en capital immédiate \\
49+1 & Mauvais & Composante défaillante, dangereuse ou absente ; dommages consécutifs à d'autres composantes & Travaux urgents ; expertise souvent requise ; hypothèse extraordinaire possible \\
50+\bottomrule
51+\end{tabularx}
52+\tabsource{convention de l'ouvrage, inspirée des pratiques décrites au \cref{ch:11} et dans \textcite{appraisal2026} ; l'échelle est celle des variables \texttt{qualite\_1a5} et \texttt{etat\_1a5} des dossiers synthétiques.}
53+\end{table}
54+
55+L'échelle du \cref{tab:appB:echelle} est volontairement courte. Les échelles longues (sur 10 ou 100) donnent une illusion de précision que l'observation visuelle ne justifie pas, et elles rendent les cotes de deux inspecteurs moins comparables. Cinq niveaux suffisent à distinguer ce que le marché distingue, et la littérature sur la dépréciation montre que c'est l'écart entre l'immeuble entretenu et l'immeuble négligé, plus que les nuances fines, qui pèse sur les prix : \textcite{hardingRosenthalSirmans2007} estiment la dépréciation nette d'entretien autour de \pct{2.5} par an et montrent que l'entretien compense environ un point de pourcentage par an, de sorte que deux maisons du même âge peuvent différer de plusieurs dizaines de points de valeur selon leur cote d'état. La typologie de \textcite{baum1991}, qui distingue la détérioration physique de la désuétude fonctionnelle et esthétique, rappelle que la cote d'état mesure la première ; les secondes sont consignées dans les observations et traitées séparément au \cref{ch:24}.
56+
57+La cote d'état générale $E$ et l'âge effectif $\bar a$ de l'immeuble s'obtiennent en pondérant les composantes par leur part $w_i$ dans le coût de remplacement neuf, parts dont le \cref{ch:22,ch:23} donnent les ordres de grandeur par type d'immeuble :
58+\begin{equation}\label{eq:appB:cote}
59+E = \sum_{i=1}^{n} w_i\, e_i, \qquad \bar a = \sum_{i=1}^{n} w_i\, a_i, \qquad \sum_{i=1}^{n} w_i = 1,
60+\end{equation}
61+où $e_i$ est la cote et $a_i$ l'âge apparent de la composante $i$. La pondération par le coût évite qu'un robinet qui fuit pèse autant qu'une toiture ; elle exprime aussi la logique de la méthode âge-vie, dans laquelle la dépréciation physique incurable d'une composante de longue durée est le rapport de son âge effectif à sa durée de vie économique $L_i$. En agrégeant,
62+\begin{equation}\label{eq:appB:agevie}
63+\frac{D_{\text{phys}}}{C_N} \approx \sum_{i=1}^{n} w_i\, \min\!\left(\frac{a_i}{L_i},\, 1\right),
64+\end{equation}
65+ce qui montre que l'âge effectif pondéré est une statistique suffisante de la dépréciation physique seulement si toutes les composantes ont la même durée de vie, hypothèse que le \cref{ch:24} discute et que la décomposition par composante permet d'abandonner. Le formulaire est construit pour fournir les $e_i$ et $a_i$ ; les $w_i$ et $L_i$ viennent de la méthode du coût. L'évaluateur qui remplit le formulaire ne calcule donc rien sur place : il observe, et la formule attend au bureau.
66+
67+\section{Formulaire 1 : maison unifamiliale}\label{sec:appB:f1}
68+
69+Le premier formulaire couvre la maison isolée, jumelée ou en rangée, avec ou sans garage, et la partie privative d'une copropriété divise horizontale lorsque le mandat porte sur une fraction. Il se lit de haut en bas dans l'ordre de la visite. Les rubriques en italique dans la colonne des observations indiquent les constats à rechercher ; les pathologies de la \cref{sec:appB:patho} y sont rappelées par leur nom lorsque la composante est le lieu habituel de leur manifestation.
70+
71+{\small\setlength{\tabcolsep}{4pt}\renewcommand{\arraystretch}{1.45}
72+\begin{longtable}{@{}L{3.1cm}c>{\centering\arraybackslash}p{1.1cm}L{4.1cm}c@{}}
73+\caption{Formulaire 1 — Inspection d'évaluation d'une maison unifamiliale}\label{tab:appB:f1}\\
74+\toprule
75+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
76+\midrule
77+\endfirsthead
78+\caption*{Tableau~\ref{tab:appB:f1} (suite)}\\
79+\toprule
80+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
81+\midrule
82+\endhead
83+\midrule\multicolumn{5}{r@{}}{\footnotesize\itshape suite à la page suivante}\\
84+\endfoot
85+\bottomrule
86+\endlastfoot
87+\multicolumn{5}{@{}l}{\textbf{A. Identification et conditions de la visite}}\\
88+Adresse, lot, date et heure & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
89+Personnes présentes, météo, température & \multicolumn{4}{l@{}}{} \\
90+Parties non visitées et motif & \multicolumn{4}{l@{}}{} \\
91+Documents consultés avant la visite & \multicolumn{4}{l@{}}{\caseA~rôle \caseA~certificat de localisation \caseA~permis \caseA~zonage \caseA~zones inondables \caseA~DV} \\
92+\midrule
93+\multicolumn{5}{@{}l}{\textbf{B. Site et terrain}}\\
94+Topographie, drainage de surface & \cinq & & \emph{pente vers la maison, eau stagnante, affaissements} & \inc \\
95+Aménagement paysager, arbres & \cinq & & \emph{arbres matures près des fondations, racines} & \inc \\
96+Entrée, stationnement, trottoirs & \cinq & & \emph{asphalte, pavé, béton ; fissures, soulèvements} & \inc \\
97+Clôtures, murets, remises, piscine & \cinq & & \emph{conformité apparente ; piscine : clôture, filtration} & \inc \\
98+Services (aqueduc, égout, puits, fosse) & \cinq & & \emph{municipal ou autonome ; puits : débit déclaré ; fosse : vidange} & \inc \\
99+Nuisances et servitudes visibles & \cinq & & \emph{bruit, lignes électriques, voie ferrée, droit de passage} & \inc \\
100+Zone inondable ou de mobilité & \caseA~non \caseA~oui & & \emph{classe d'intensité selon la carte en vigueur ; marques de crue} & \inc \\
101+\midrule
102+\multicolumn{5}{@{}l}{\textbf{C. Fondations et structure}}\\
103+Type et matériau des fondations & \multicolumn{4}{l@{}}{\caseA~béton coulé \caseA~blocs \caseA~pierre \caseA~pieux \caseA~dalle sur sol \caseA~vide sanitaire} \\
104+Murs de fondation (ext. et int.) & \cinq & & \emph{fissures (largeur, orientation, actives), efflorescence, déversement} & \inc \\
105+Dalle du sous-sol et du garage & \cinq & & \emph{soulèvement, fissures en étoile, poudre blanche : \textbf{pyrite} ; dalle du garage d'abord} & \inc \\
106+Drainage des fondations, puisard & \cinq & & \emph{pompe, clapet, cheminées de nettoyage ; dépôt gélatineux rouille : \textbf{ocre ferreuse}} & \inc \\
107+Traces d'humidité et d'infiltration & \cinq & & \emph{jonction mur-dalle, cernes, odeur, moisissures, matériaux gondolés} & \inc \\
108+Charpente visible, solives, poutres, colonnes & \cinq & & \emph{flèche, entailles, pourriture, colonnes ajustables} & \inc \\
109+Éclatement du béton des fondations & \cinq & & \emph{fissures en carte, gonflement du béton lui-même : \textbf{pyrrhotite} (régions touchées)} & \inc \\
110+\midrule
111+\multicolumn{5}{@{}l}{\textbf{D. Enveloppe extérieure}}\\
112+Revêtement principal & \multicolumn{4}{l@{}}{\caseA~brique \caseA~pierre \caseA~vinyle \caseA~aluminium \caseA~fibrociment \caseA~bois \caseA~acrylique/stuc \caseA~autre} \\
113+État du revêtement & \cinq & & \emph{joints de mortier, briques éclatées, linteaux rouillés, chantepleures, fixations} & \inc \\
114+Solins, scellants, jonctions & \cinq & & \emph{autour des ouvertures, au toit, aux balcons} & \inc \\
115+Soffites, fascias, corniches & \cinq & & \emph{ventilation de l'entretoit, pourriture, nids} & \inc \\
116+Balcons, galeries, escaliers extérieurs & \cinq & & \emph{garde-corps, ancrages, bois en contact avec le sol} & \inc \\
117+Portes extérieures et de garage & \cinq & & \emph{étanchéité, quincaillerie, ouvre-porte} & \inc \\
118+Isolation des murs (si visible ou déclarée) & \multicolumn{4}{l@{}}{\caseA~inconnue \caseA~laine \caseA~cellulose \caseA~mousse \caseA~\textbf{vermiculite} \caseA~\textbf{MIUF} déclarée} \\
119+\midrule
120+\multicolumn{5}{@{}l}{\textbf{E. Toiture}}\\
121+Type et matériau & \multicolumn{4}{l@{}}{\caseA~bardeau d'asphalte \caseA~membrane élastomère \caseA~gravier \caseA~tôle \caseA~autre ; pente \caseA~forte \caseA~faible \caseA~plate} \\
122+État du couvrement & \cinq & & \emph{granules perdus, bardeaux relevés, cloques, joints ouverts, mousse} & \inc \\
123+Solins, cheminées, évents, drains & \cinq & & \emph{rouille, scellant fissuré, drain obstrué} & \inc \\
124+Gouttières et descentes & \cinq & & \emph{présence, pente, rejet à distance des fondations} & \inc \\
125+Entretoit (si accessible) & \cinq & & \emph{isolant (type, épaisseur), ventilation, taches, givre, \textbf{vermiculite}} & \inc \\
126+Année de réfection déclarée & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
127+\midrule
128+\multicolumn{5}{@{}l}{\textbf{F. Fenestration}}\\
129+Type et matériau & \multicolumn{4}{l@{}}{\caseA~bois \caseA~PVC \caseA~aluminium \caseA~hybride ; vitrage \caseA~simple \caseA~double \caseA~triple} \\
130+État des fenêtres & \cinq & & \emph{condensation entre les verres, mécanismes, pourriture, calfeutrage} & \inc \\
131+Puits de lumière, portes-fenêtres & \cinq & & \emph{infiltration, scellants} & \inc \\
132+Année de remplacement déclarée & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
133+\midrule
134+\multicolumn{5}{@{}l}{\textbf{G. Intérieur}}\\
135+Planchers (par niveau) & \cinq & & \emph{matériau, niveau, grincement, dénivellation} & \inc \\
136+Murs et plafonds & \cinq & & \emph{fissures, taches, réparations récentes (peinture fraîche localisée)} & \inc \\
137+Cuisine : armoires, comptoirs, évier & \cinq & & \emph{année de rénovation, fonctionnalité} & \inc \\
138+Salles de bain (nombre, état) & \cinq & & \emph{étanchéité, ventilation, moisissures, joints} & \inc \\
139+Portes, boiseries, escaliers, rampes & \cinq & & \emph{conformité apparente des garde-corps} & \inc \\
140+Sous-sol fini : qualité, hauteur libre & \cinq & & \emph{hauteur sous poutre, fenêtres, issue} & \inc \\
141+Foyer, poêle, cheminée & \cinq & & \emph{type (bois, gaz, granules), certification visible, état du conduit} & \inc \\
142+Odeurs, qualité de l'air, animaux & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
143+\midrule
144+\multicolumn{5}{@{}l}{\textbf{H. Électricité}}\\
145+Entrée électrique et panneau & \multicolumn{4}{l@{}}{\caseA~60~A \caseA~100~A \caseA~200~A \caseA~autre ; \caseA~disjoncteurs \caseA~fusibles} \\
146+État apparent du panneau et du câblage visible & \cinq & & \emph{fils \textbf{aluminium} (gaine marquée AL), gaines textiles, boîtes ouvertes, épissures} & \inc \\
147+Prises, interrupteurs, DDFT, détecteurs & \cinq & & \emph{DDFT près de l'eau, détecteurs de fumée et de CO présents et datés} & \inc \\
148+\midrule
149+\multicolumn{5}{@{}l}{\textbf{I. Plomberie}}\\
150+Entrée d'eau et distribution & \multicolumn{4}{l@{}}{\caseA~cuivre \caseA~PEX \caseA~\textbf{galvanisé} \caseA~plomb (entrée) \caseA~inconnu} \\
151+Évacuation et ventilation & \multicolumn{4}{l@{}}{\caseA~ABS/PVC \caseA~fonte \caseA~\textbf{galvanisé} ; \emph{odeurs, écoulement lent}} \\
152+Chauffe-eau (type, année, capacité) & \cinq & & \emph{plaque signalétique, corrosion, bac, âge supérieur à 10~ans} & \inc \\
153+Appareils et robinetterie & \cinq & & \emph{débit, fuites, pression} & \inc \\
154+Clapet antiretour, pompe de puisard & \cinq & & \emph{présence, fonctionnement déclaré} & \inc \\
155+\midrule
156+\multicolumn{5}{@{}l}{\textbf{J. Chauffage, ventilation, climatisation}}\\
157+Système principal et énergie & \multicolumn{4}{l@{}}{\caseA~plinthes électriques \caseA~air pulsé \caseA~eau chaude \caseA~thermopompe \caseA~\textbf{mazout} \caseA~gaz \caseA~bois} \\
158+État de l'appareil principal & \cinq & & \emph{plaque signalétique, année, entretien, bruit, odeur} & \inc \\
159+Réservoir de mazout (int./ext., année) & \cinq & & \emph{plaque, corrosion, âge ; déclaration de retrait d'un ancien réservoir} & \inc \\
160+Distribution (conduits, radiateurs) & \cinq & & \emph{isolation des conduits, équilibrage} & \inc \\
161+Ventilation (VRC, hottes, salles de bain) & \cinq & & \emph{présence, rejet à l'extérieur} & \inc \\
162+Climatisation, thermopompe murale & \cinq & & \emph{année, état de l'unité extérieure} & \inc \\
163+\midrule
164+\multicolumn{5}{@{}l}{\textbf{K. Environnement intérieur et déclarations}}\\
165+Test de radon (déclaré) & \multicolumn{4}{l@{}}{\caseA~aucun \caseA~fait, résultat : \rule{2cm}{0.4pt} Bq/m\textsuperscript{3} ; \caseA~système d'atténuation présent} \\
166+Matériaux susceptibles de contenir de l'amiante & \multicolumn{4}{l@{}}{\caseA~vermiculite \caseA~isolant de tuyaux \caseA~tuiles \caseA~plâtre \caseA~aucun signe} \\
167+Déclarations du vendeur ou du propriétaire & \multicolumn{4}{l@{}}{\emph{sinistres, infiltrations, réclamations, travaux sans permis, litiges}} \\
168+\midrule
169+\multicolumn{5}{@{}l}{\textbf{L. Synthèse}}\\
170+Cote d'état générale (pondérée) & \cinq & & \emph{justification en une phrase} & \\
171+Âge chronologique / âge effectif estimé & \multicolumn{4}{l@{}}{\rule{2cm}{0.4pt} ans / \rule{2cm}{0.4pt} ans} \\
172+Travaux immédiats (curables) & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
173+Travaux à moyen terme & \multicolumn{4}{l@{}}{} \\
174+Désordres à expertiser (hypothèses extraordinaires) & \multicolumn{4}{l@{}}{} \\
175+Nombre de photographies ; mesures prises & \multicolumn{4}{l@{}}{} \\
176+\end{longtable}
177+\tabsource{conception de l'auteur ; rubriques cohérentes avec le \cref{ch:11} ; aucune donnée.}
178+}
179+
180+\section{Formulaire 2 : duplex à quintuplex}\label{sec:appB:f2}
181+
182+Le second formulaire s'emploie \emph{en complément} du premier : les sections B à K du formulaire 1 sont remplies pour le bâtiment dans son ensemble, et le formulaire 2 ajoute ce qui distingue un petit immeuble locatif d'une maison : la description de chaque logement, le lien entre l'état observé et le bail, les aires communes et les équipements partagés. Le \cref{ch:32} explique pourquoi ces éléments comptent : dans un plex, la valeur dépend autant de la structure des baux et du potentiel de rattrapage des loyers que de l'état physique, et un logement rénové occupé par un locataire de longue date dont le loyer est très inférieur au marché n'a pas la même incidence qu'un logement identique loué au prix du marché depuis un an. Les règles du Code civil sur la reconduction des baux et sur la fixation des loyers (\cref{ch:26,ch:34}) font de chaque fiche de logement une pièce de l'analyse du droit évalué, non un simple inventaire.
183+
184+{\small\setlength{\tabcolsep}{4pt}\renewcommand{\arraystretch}{1.45}
185+\begin{longtable}{@{}L{3.1cm}c>{\centering\arraybackslash}p{1.1cm}L{4.1cm}c@{}}
186+\caption{Formulaire 2 — Compléments pour un immeuble de deux à cinq logements (une section M par logement)}\label{tab:appB:f2}\\
187+\toprule
188+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
189+\midrule
190+\endfirsthead
191+\caption*{Tableau~\ref{tab:appB:f2} (suite)}\\
192+\toprule
193+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
194+\midrule
195+\endhead
196+\midrule\multicolumn{5}{r@{}}{\footnotesize\itshape suite à la page suivante}\\
197+\endfoot
198+\bottomrule
199+\endlastfoot
200+\multicolumn{5}{@{}l}{\textbf{M. Fiche de logement (reproduire pour chaque logement)}}\\
201+Numéro, niveau, typologie & \multicolumn{4}{l@{}}{n\textsuperscript{o} \rule{1cm}{0.4pt} ; niveau \rule{1cm}{0.4pt} ; \caseA~studio \caseA~3½ \caseA~4½ \caseA~5½ \caseA~6½ et plus ; superficie \rule{1.2cm}{0.4pt} m\textsuperscript{2}} \\
202+Occupation et accès & \multicolumn{4}{l@{}}{\caseA~visité \caseA~non visité (motif) ; \caseA~locataire \caseA~propriétaire-occupant \caseA~vacant} \\
203+Bail : loyer mensuel, échéance, inclusions & \multicolumn{4}{l@{}}{\rule{1.5cm}{0.4pt} \$/mois ; échéance \rule{2cm}{0.4pt} ; \caseA~chauffage \caseA~électricité \caseA~eau chaude \caseA~stationnement \caseA~électroménagers} \\
204+Ancienneté du locataire, avis en cours & \multicolumn{4}{l@{}}{depuis \rule{1.5cm}{0.4pt} ; \caseA~avis d'augmentation \caseA~dossier TAL \caseA~avis de reprise ou d'éviction} \\
205+Cuisine & \cinq & & \emph{année de rénovation, électroménagers fournis} & \inc \\
206+Salle(s) de bain & \cinq & & \emph{ventilation, étanchéité, moisissures} & \inc \\
207+Planchers, murs, plafonds & \cinq & & \emph{dénivellation, fissures, taches au plafond (fuite du logement supérieur)} & \inc \\
208+Fenêtres et portes du logement & \cinq & & \emph{voir section F ; différences entre logements} & \inc \\
209+Chauffage et compteur & \multicolumn{4}{l@{}}{\caseA~compteur distinct \caseA~compteur commun ; \caseA~plinthes \caseA~central ; état \cinq} \\
210+Électricité du logement & \cinq & & \emph{panneau distinct, ampérage, prises DDFT} & \inc \\
211+Détecteurs de fumée et de CO & \caseA~oui \caseA~non & & \emph{présence dans chaque logement} & \inc \\
212+Issues, escaliers de secours & \cinq & & \emph{accès, garde-corps, dégagement} & \inc \\
213+État général du logement & \cinq & & \emph{rattrapable à la rotation ? travaux estimés} & \inc \\
214+\midrule
215+\multicolumn{5}{@{}l}{\textbf{N. Aires et équipements communs}}\\
216+Hall, corridors, escaliers intérieurs & \cinq & & \emph{éclairage, revêtements, propreté} & \inc \\
217+Sous-sol commun, rangements, buanderie & \cinq & & \emph{appareils fournis, humidité, accès} & \inc \\
218+Entrée électrique commune et compteurs & \cinq & & \emph{nombre de compteurs, capacité, espace commun chauffé par qui ?} & \inc \\
219+Chauffe-eau (par logement ou commun) & \cinq & & \emph{nombre, années} & \inc \\
220+Escaliers extérieurs, balcons, galeries & \cinq & & \emph{acier : corrosion aux ancrages ; bois : pourriture ; garde-corps} & \inc \\
221+Stationnements, remises, cour & \cinq & & \emph{nombre de cases, attribution par bail} & \inc \\
222+Ordures, recyclage, déneigement & \multicolumn{4}{l@{}}{\emph{organisation, contrats déclarés}} \\
223+\midrule
224+\multicolumn{5}{@{}l}{\textbf{O. Synthèse locative}}\\
225+Logements visités / total & \multicolumn{4}{l@{}}{\rule{1cm}{0.4pt} / \rule{1cm}{0.4pt}} \\
226+Loyers mensuels totaux (contractuels) & \multicolumn{4}{l@{}}{\rule{2cm}{0.4pt} \$ ; écart apparent au marché \caseA~faible \caseA~moyen \caseA~fort} \\
227+Logements à rénover à la rotation & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
228+Dépenses en capital communes à prévoir & \multicolumn{4}{l@{}}{} \\
229+\end{longtable}
230+\tabsource{conception de l'auteur ; cohérent avec les \cref{ch:26,ch:32}.}
231+}
232+
233+\section{Formulaire 3 : immeubles de six logements et plus}\label{sec:appB:f3}
234+
235+Le troisième formulaire complète les deux premiers pour les immeubles de six logements et plus, dont le \cref{ch:33} traite l'évaluation. La différence n'est pas seulement d'échelle. À partir de ce seuil, les systèmes deviennent centraux (chauffage, eau chaude, ventilation des corridors), la sécurité incendie est organisée au niveau de l'immeuble, la réglementation de construction et de sécurité s'applique différemment, et l'exploitation suppose une gestion, des contrats d'entretien et des réserves que l'évaluateur doit retrouver dans les dépenses normalisées (\cref{ch:28}). La Loi sur le bâtiment exclut de l'application du chapitre « Bâtiment » du Code de sécurité les immeubles totalement résidentiels de moins de trois étages ou de moins de neuf logements \parencite[art.~29]{loibatiment} ; au-delà, l'immeuble est assujetti et l'évaluateur demande les documents qui en attestent (registre d'entretien du système d'alarme, rapports d'inspection des façades et des stationnements étagés lorsqu'ils s'appliquent, plan de sécurité incendie). Il ne vérifie pas lui-même la conformité ; il consigne l'existence ou l'absence des documents, ce qui, en soi, informe sur la gestion.
236+
237+{\small\setlength{\tabcolsep}{4pt}\renewcommand{\arraystretch}{1.45}
238+\begin{longtable}{@{}L{3.1cm}c>{\centering\arraybackslash}p{1.1cm}L{4.1cm}c@{}}
239+\caption{Formulaire 3 — Compléments pour un immeuble de six logements et plus}\label{tab:appB:f3}\\
240+\toprule
241+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
242+\midrule
243+\endfirsthead
244+\caption*{Tableau~\ref{tab:appB:f3} (suite)}\\
245+\toprule
246+Élément & {État 1--5} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
247+\midrule
248+\endhead
249+\midrule\multicolumn{5}{r@{}}{\footnotesize\itshape suite à la page suivante}\\
250+\endfoot
251+\bottomrule
252+\endlastfoot
253+\multicolumn{5}{@{}l}{\textbf{P. Systèmes centraux}}\\
254+Chauffage central (chaudière, énergie, année) & \cinq & & \emph{plaque, capacité, redondance, contrat d'entretien ; \textbf{mazout} : réservoir, âge} & \inc \\
255+Production d'eau chaude centrale & \cinq & & \emph{réservoirs, année, recirculation} & \inc \\
256+Ventilation des corridors et des salles de bain & \cinq & & \emph{VRC, ventilateurs de toit, filtres} & \inc \\
257+Distribution électrique (entrée, compteurs, panneaux) & \cinq & & \emph{capacité, espace réservé, mise à la terre, câblage \textbf{aluminium}} & \inc \\
258+Plomberie verticale (colonnes, pluviaux) & \cinq & & \emph{\textbf{galvanisé} ou fonte, remplacement partiel, pression aux étages} & \inc \\
259+Ascenseur (si présent) & \cinq & & \emph{certificat affiché, année, contrat} & \inc \\
260+Interphone, contrôle d'accès, caméras & \cinq & & \emph{fonctionnement déclaré} & \inc \\
261+\midrule
262+\multicolumn{5}{@{}l}{\textbf{Q. Sécurité incendie}}\\
263+Système d'alarme et panneau & \cinq & & \emph{registre d'entretien, dernière inspection, affichage} & \inc \\
264+Détecteurs, avertisseurs, éclairage d'urgence & \cinq & & \emph{présence dans les corridors et les logements} & \inc \\
265+Séparations coupe-feu, portes à fermeture automatique & \cinq & & \emph{portes calées ouvertes, percements non scellés} & \inc \\
266+Extincteurs, gicleurs, cabinets d'incendie & \cinq & & \emph{étiquettes d'inspection, dates} & \inc \\
267+Issues, escaliers de secours, dégagements & \cinq & & \emph{encombrement, état des escaliers extérieurs d'acier} & \inc \\
268+Plan de sécurité incendie, documents disponibles & \multicolumn{4}{l@{}}{\caseA~plan \caseA~registre d'alarme \caseA~rapports d'inspection \caseA~aucun document fourni} \\
269+\midrule
270+\multicolumn{5}{@{}l}{\textbf{R. Accessibilité et aires communes}}\\
271+Entrée principale, rampe, seuils & \cinq & & \emph{accessibilité sans obstacle, ouvre-porte} & \inc \\
272+Corridors, éclairage, revêtements & \cinq & & \emph{largeur, usure, propreté} & \inc \\
273+Buanderie commune, rangements, salle communautaire & \cinq & & \emph{appareils, revenus accessoires déclarés} & \inc \\
274+Local des ordures, chute à déchets & \cinq & & \emph{odeurs, vermine, conformité apparente} & \inc \\
275+Local technique, atelier, bureau du concierge & \cinq & & \emph{propreté, entreposage de produits} & \inc \\
276+\midrule
277+\multicolumn{5}{@{}l}{\textbf{S. Stationnement et extérieur}}\\
278+Stationnement de surface (cases, revêtement) & \cinq & & \emph{nombre, marquage, drainage, revenus par case} & \inc \\
279+Stationnement intérieur ou étagé & \cinq & & \emph{dalle, infiltrations, ventilation, inspection périodique si assujetti} & \inc \\
280+Bornes de recharge, éclairage extérieur & \cinq & & \emph{nombre, alimentation} & \inc \\
281+Aménagement, murs de soutènement, clôtures & \cinq & & \emph{stabilité, drainage} & \inc \\
282+Toiture : accès, équipements en toiture & \cinq & & \emph{unités mécaniques, ancrages, membrane autour des bases} & \inc \\
283+\midrule
284+\multicolumn{5}{@{}l}{\textbf{T. Gestion et documents}}\\
285+État locatif fourni, baux consultés & \multicolumn{4}{l@{}}{\caseA~état locatif daté \caseA~copies des baux \caseA~registre des avis TAL \caseA~liste des travaux réalisés} \\
286+Contrats d'entretien et de service & \multicolumn{4}{l@{}}{\caseA~chauffage \caseA~alarme \caseA~ascenseur \caseA~déneigement \caseA~extermination \caseA~conciergerie} \\
287+Dépenses des trois derniers exercices & \multicolumn{4}{l@{}}{\caseA~fournies \caseA~partielles \caseA~refusées} \\
288+Travaux majeurs réalisés (10~ans) et prévus & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
289+\midrule
290+\multicolumn{5}{@{}l}{\textbf{U. Synthèse}}\\
291+Logements visités / total ; échantillon représentatif ? & \multicolumn{4}{l@{}}{\rule{1cm}{0.4pt} / \rule{1cm}{0.4pt} ; \caseA~oui \caseA~non (hypothèse à déclarer)} \\
292+Dépenses en capital à projeter (composante, année, ordre de grandeur) & \multicolumn{4}{l@{}}{\rule{0pt}{10pt}} \\
293+Désordres à expertiser & \multicolumn{4}{l@{}}{} \\
294+\end{longtable}
295+\tabsource{conception de l'auteur ; cohérent avec les \cref{ch:28,ch:33} et avec la fiche d'inspection du dossier de cas du \cref{ch:47}.}
296+}
297+
298+\section{Pathologies québécoises à rechercher}\label{sec:appB:patho}
299+
300+Le parc résidentiel québécois présente une série de désordres qui tiennent à sa géologie, à son climat, à ses matériaux et à ses époques de construction. L'évaluateur ne les diagnostique pas ; il doit en connaître les signes, les contextes où ils sont probables et la conduite à tenir lorsqu'il les soupçonne. Le \cref{tab:appB:patho} les résume ; les paragraphes qui suivent précisent les sources et les conséquences pour le rapport. Le \cref{ch:11} décrit chaque phénomène plus complètement et le \cref{ch:24} en traite la mesure en dépréciation ; le \cref{ch:43} couvre les risques environnementaux de site (inondation, contamination), qui ne sont pas repris ici.
301+
302+{\small\setlength{\tabcolsep}{4pt}
303+\begin{longtable}{@{}L{2.2cm}L{2.9cm}L{4.3cm}L{3.3cm}@{}}
304+\caption{Pathologies à rechercher lors de l'inspection : contexte, signes, conduite à tenir}\label{tab:appB:patho}\\
305+\toprule
306+Pathologie & Contexte probable & Signes observables & Conduite et renvoi \\
307+\midrule
308+\endfirsthead
309+\caption*{Tableau~\ref{tab:appB:patho} (suite)}\\
310+\toprule
311+Pathologie & Contexte probable & Signes observables & Conduite et renvoi \\
312+\midrule
313+\endhead
314+\midrule\multicolumn{4}{r@{}}{\footnotesize\itshape suite à la page suivante}\\
315+\endfoot
316+\bottomrule
317+\endlastfoot
318+Pyrite (remblai gonflant) & Remblai de pierre concassée sous les dalles ; constructions antérieures à la certification des remblais ; secteurs de la Montérégie et de la vallée du Saint-Laurent & Soulèvement de la dalle du garage puis du sous-sol, fissures en étoile ou en croix, poudre blanche, portes qui coincent, murs de fondation poussés vers l'extérieur ; apparition souvent 10 à 25~ans après la construction & Expertise selon le protocole CTQ-M200 (indice IPPG du remblai) ; hypothèse extraordinaire ou réserve ; décote de marché documentée ; \cref{ch:11,ch:24} \\
319+Ocre ferreuse & Sols riches en fer ou en ferrobactéries, nappe phréatique haute, secteurs identifiés par la municipalité & Dépôt gélatineux rouille dans le puisard et les cheminées de nettoyage, eau rougeâtre en surface, odeur de soufre, drain colmaté, infiltrations à la jonction mur-dalle & Vérifier cheminées de nettoyage et fréquence d'entretien déclarée ; coût récurrent dans les dépenses ; réserve ; \cref{ch:11,ch:28} \\
320+Pyrrhotite (béton des fondations) & Granulats de certaines carrières ; constructions des années 1990 et 2000 en Mauricie et dans les régions voisines & Fissuration en carte des murs de fondation, éclatement du béton lui-même (non de la dalle seule), gonflement progressif & Expertise par carottage (teneur en pyrrhotite) ; hypothèse extraordinaire ; valeur parfois limitée au terrain moins démolition ; \cref{ch:24,ch:25} \\
321+Vermiculite et amiante & Isolant en vrac de l'entretoit et des murs posé jusqu'au début des années 1990 ; isolants de tuyaux, tuiles et plâtres anciens & Granules brun doré ou gris argenté dans l'entretoit ; gaines blanchâtres sur les tuyaux de chauffage ; ne pas remuer & Présence consignée, non testée ; coût de désamiantage à l'ouverture des murs ; réserve ; \cref{ch:11,ch:44} \\
322+MIUF (mousse d'urée-formaldéhyde) & Isolation injectée dans les murs à la fin des années 1970 ; interdite au Canada depuis 1980 & Mousse friable jaune-brun aux ouvertures murales, déclaration du vendeur, mention dans les titres & Déclaration dans le rapport ; décote de marché à documenter par comparables ; \cref{ch:11,ch:17} \\
323+Radon & Gaz du sol ; concentration très variable d'une maison à l'autre ; ligne directrice de 200~Bq/m\textsuperscript{3} & Aucun signe visible ; seul un test de longue durée informe ; vérifier la présence d'un système d'atténuation (tuyau et ventilateur au sous-sol) & Demander le résultat du test s'il existe ; coût d'atténuation en dépréciation curable ; réserve type sinon ; \cref{ch:11,ch:43} \\
324+Câblage en aluminium & Résidences câblées à la fin des années 1960 et au début des années 1970 & Gaine marquée « AL » ou « ALUMINIUM » dans le panneau, connexions chaudes ou décolorées, prises marquées CO/ALR & Noter sans manipuler ; vérification par maître électricien ; coût de correction des connexions ; incidence sur l'assurabilité ; \cref{ch:11} \\
325+Plomberie galvanisée & Distribution et évacuation d'avant les années 1960, souvent remplacée partiellement & Tuyaux gris mats filetés, pression faible aux étages, eau brune au premier jet, raccords cuivre-galvanisé corrodés & Coût de remplacement en dépréciation curable ou incurable selon l'accès ; \cref{ch:24} \\
326+Chauffage au mazout et réservoirs & Maisons d'avant 1980 non converties ; réservoirs intérieurs ou enfouis & Réservoir d'acier (plaque, année, corrosion au fond), odeur, taches au sol, conduite de remplissage abandonnée (réservoir enfoui) & Règlement sur les appareils de chauffage au mazout : remplacement par combustible fossile interdit ; coût de conversion et de retrait ; sol à vérifier si enfoui ; \cref{ch:43,ch:44} \\
327+Fondations de pierre ou de blocs ; drain absent & Bâtiments antérieurs aux années 1950, plex des quartiers centraux & Mortier désagrégé, humidité chronique, absence de drain français, sous-sol bas & Travaux en dépréciation incurable ou curable ; hauteur libre et usage du sous-sol ; \cref{ch:24,ch:32} \\
328+Humidité, moisissures, mérule & Sous-sols mal ventilés, infiltrations récurrentes, bois en contact avec le sol & Odeur, taches, matériaux gondolés, mycélium blanc ou brun sur le bois & Expertise en cas de signes étendus ; hypothèse extraordinaire ; \cref{ch:11} \\
329+\end{longtable}
330+\tabsource{synthèse de l'auteur d'après les sources citées dans le texte de la \cref{sec:appB:patho} (ACQC, RBQ, SHQ, Santé Canada, RNCan, Québec.ca ; consultées le 31~octobre 2026) ; le contexte « probable » est indicatif et ne dispense d'aucune vérification.}
331+}
332+
333+La \motcle{pyrite} désigne, dans l'usage québécois, le gonflement de certains remblais de pierre concassée placés sous les dalles de béton : en présence d'humidité et d'oxygène, la pyrite contenue dans la pierre s'oxyde, les produits de la réaction occupent plus de volume, la dalle se soulève et se fissure, et le béton lui-même peut être attaqué. L'Association des consommateurs pour la qualité dans la construction indique que les dommages apparaissent généralement une dizaine d'années après la construction, parfois quinze, vingt ou plus de vingt-cinq ans, qu'ils se manifestent d'abord à la dalle du garage parce que l'épaisseur de remblai y est plus grande, et que seule une analyse en laboratoire d'échantillons du remblai permet de conclure ; le protocole d'expertise des bâtiments existants est la norme CTQ-M200 et les remblais neufs sont certifiés non gonflants (« DB ») ; un programme d'aide de la Société d'habitation du Québec a existé de 2001 à 2011\footnote{ACQC, « La pyrite », \url{https://www.acqc.ca/fr/info/pyrite}, consulté le 31~octobre 2026 ; la page ne porte pas de date de mise à jour. La méthode d'essai de l'indice pétrographique du potentiel de gonflement sulfatique (IPPG) fait l'objet de la norme BNQ 2560-500/2022 du Bureau de normalisation du Québec (sommaire consulté le 31~octobre 2026).}. Pour l'évaluateur, la pyrite est l'exemple type du désordre qui exige une hypothèse extraordinaire : s'il n'a pas de rapport d'expertise, il évalue sous l'hypothèse que le remblai n'est pas gonflant et il indique l'incidence, qui est au minimum le coût de remplacement de la dalle et du remblai et, dans les secteurs où le marché est sensibilisé, une décote qui dépasse ce coût.
334+
335+L'\motcle{ocre ferreuse} est un dépôt, d'origine chimique ou bactérienne, qui colmate les drains de fondation. La Régie du bâtiment du Québec décrit ses signes : eau rougeâtre dans les fossés après la pluie, boue gluante de couleur ocre dans le bassin de captation, dépôts rougeâtres sur la dalle avec une odeur de soufre, humidité élevée au sous-sol ; elle précise que le fer étant naturellement présent dans le sol, on ne peut empêcher la formation du dépôt, de sorte que les solutions sont des aménagements (cheminées de nettoyage, modification du drainage, étanchéisation, parfois condamnation du sous-sol)\footnote{Régie du bâtiment du Québec, « Dépôt d'ocre », \url{https://www.rbq.gouv.qc.ca/vous-etes/citoyen/problemes-avec-votre-residence/depot-docre}, consulté le 31~octobre 2026. L'ACQC signale que la norme BNQ 3661-500, publiée en 2012, encadre la conception des drains dans les sols propices aux dépôts d'ocre (\url{https://acqc.ca/fr/ocre-ferreuse}, consulté le 31~octobre 2026).}. Au contraire de la pyrite, l'ocre ferreuse n'est pas un désordre que l'on corrige une fois pour toutes ; c'est une charge récurrente d'entretien, qui entre dans les dépenses normalisées d'un immeuble à revenus (\cref{ch:28}) et qui, pour une maison, se traduit par une décote que les comparables du même secteur révèlent lorsqu'ils sont bien choisis (\cref{ch:16}).
336+
337+La \motcle{pyrrhotite} est un minéral présent dans les granulats de certaines carrières, qui, incorporé au béton des fondations, provoque le gonflement et l'éclatement du béton lui-même. Le phénomène a touché des milliers de résidences construites dans les années 1990 et 2000 en Mauricie et dans les régions voisines. La Société d'habitation du Québec a administré de 2011 à 2026 un programme d'aide dont le volet travaux exigeait notamment que les fondations présentent un taux minimal de \pct{0.23} de pyrrhotite ; le programme a pris fin le 30~septembre 2026\footnote{Gouvernement du Québec, « Programme pour les résidences endommagées par la pyrrhotite », \url{https://www.quebec.ca/habitation-territoire/construction-renovation/renovation-residentielle/programme-residences-endommagees-pyrrhotite}, page mise à jour le 8~octobre 2026, consultée le 31~octobre 2026 ; normes du programme (SHQ, PDF) consultées le même jour.}. La signature visuelle est la fissuration en carte des murs de fondation avec éclatement du béton, à ne pas confondre avec le soulèvement de dalle de la pyrite ; la confirmation passe par le carottage et l'analyse. Lorsque la teneur est élevée, la correction consiste à remplacer les fondations, ce qui peut ramener la valeur de l'immeuble à celle du terrain diminuée des coûts de démolition (\cref{ch:25}) ; l'évaluateur qui soupçonne le phénomène sans expertise ne peut conclure qu'au moyen d'une hypothèse extraordinaire explicite.
338+
339+L'isolant de \motcle{vermiculite} peut contenir de l'amiante amphibolique ; Santé Canada a avisé les Canadiens dès 2004 que cet isolant, vendu notamment sous la marque Zonolite, pouvait être nocif s'il était remué, et le gouvernement fédéral interdit depuis 2018 la vente de produits de consommation contenant de l'amiante\footnote{Santé Canada, avis du 3~avril 2004 « Santé Canada avise les Canadiens que l'isolant de vermiculite peut contenir de l'amiante », archives de Canada.ca, consulté le 31~octobre 2026 ; Ressources naturelles Canada, \emph{Emprisonnons la chaleur}, section 3, page modifiée le 31~janvier 2025, consultée le 31~octobre 2026.}. La \motcle{mousse isolante d'urée-formaldéhyde} (MIUF, \emph{UFFI}), injectée dans les murs de nombreuses maisons à la fin des années 1970, est interdite au Canada depuis 1980\footnote{Ressources naturelles Canada, \emph{Emprisonnons la chaleur}, section 3, \url{https://ressources-naturelles.canada.ca/efficacite-energetique/efficacite-energetique-maison/emprisonnons-chaleur-section-3}, consulté le 31~octobre 2026.}. Pour l'un comme pour l'autre, l'évaluateur consigne la présence observée ou déclarée, ne prélève rien, et traite la question au plan de la valeur : l'incidence d'une vermiculite non dérangée dans un entretoit est le surcoût de tout travail futur qui l'exposera ; celle d'une MIUF déclarée est essentiellement une décote de marché, à documenter par des ventes comparables, puisque le risque sanitaire d'une mousse ancienne et sèche est aujourd'hui considéré comme faible mais que l'information doit être divulguée et pèse sur l'acheteur.
340+
341+Le \motcle{radon} est un gaz radioactif d'origine naturelle qui s'accumule dans les sous-sols. La ligne directrice canadienne est de 200~becquerels par mètre cube en moyenne annuelle dans les aires normalement occupées, et Santé Canada recommande d'agir dans l'année lorsque ce seuil est dépassé, d'autant plus vite que la concentration est élevée\footnote{Santé Canada, « Lignes directrices sur le radon », \url{https://www.canada.ca/fr/sante-canada/services/securite-et-risque-pour-sante/radiation/radon/lignes-directrices-gouvernement-canada.html}, page modifiée le 15~octobre 2025, consultée le 31~octobre 2026.}. Rien ne se voit à l'inspection, sinon un système d'atténuation déjà installé ; l'évaluateur demande le résultat d'un test s'il en existe un, et son rapport contient, à défaut, une réserve type indiquant que la concentration n'a pas été mesurée. L'incidence est modeste lorsque le marché local connaît mal le phénomène et que le coût d'atténuation est faible par rapport à la valeur ; elle est réelle dans les secteurs où les tests sont devenus usuels dans les promesses d'achat.
342+
343+Le \motcle{câblage en aluminium} équipe de nombreuses résidences câblées à la fin des années 1960 et au début des années 1970, période de forte hausse du prix du cuivre ; le risque tient aux connexions, où la dilatation et l'oxydation du métal desserrent les contacts et échauffent les boîtes, non au conducteur lui-même. L'évaluateur repère la gaine marquée du mot « aluminium » ou de l'abréviation « AL » dans le panneau et note la présence de dispositifs CO/ALR ou de connecteurs de transition ; il ne manipule rien sous tension. L'incidence passe par le coût de correction des connexions par un maître électricien et, souvent, par les conditions d'assurance, que les comparables ne révèlent qu'imparfaitement ; le rapport la déclare et la chiffre comme dépréciation curable. La \motcle{plomberie galvanisée}, courante avant les années 1960, se reconnaît à ses tuyaux gris filetés et à ses symptômes (pression faible, eau colorée au premier jet, corrosion aux raccords avec le cuivre) ; son remplacement, selon qu'il exige d'ouvrir des murs finis ou non, est une dépréciation curable ou incurable (\cref{ch:24}). Ces deux affirmations relèvent de la pratique courante du bâtiment et non d'une source officielle vérifiée pour les besoins de l'ouvrage ; elles sont consignées comme telles dans le registre des affirmations de l'annexe.
344+
345+Le \motcle{chauffage au mazout} enfin est en voie de disparition réglementée. Le Règlement sur les appareils de chauffage au mazout interdit l'installation d'un appareil au mazout dans les constructions résidentielles neuves et, dans les bâtiments existants d'au plus 600~m\textsuperscript{2} et trois étages, le remplacement d'un appareil au mazout par un appareil fonctionnant à un combustible fossile ; l'entretien demeure permis tant qu'il se limite à l'entretien normal d'un appareil de moins de vingt ans (dix ans pour un chauffe-eau), les réparations majeures étant interdites ; le retrait d'un réservoir doit être déclaré et réalisé selon les normes\footnote{Gouvernement du Québec, « Chauffage au mazout résidentiel », \url{https://www.quebec.ca/habitation-territoire/chauffage-consommation-energie/chauffage-mazout}, page mise à jour le 8~juin 2026, consultée le 31~octobre 2026 ; communiqué d'édiction du règlement (interdictions à compter du 31~décembre 2021 pour le neuf et du 31~décembre 2023 pour les remplacements), Québec.ca, consulté le même jour. Le texte réglementaire lui-même n'a pas été relu : les seuils cités proviennent de la page gouvernementale.}. Pour l'évaluateur, un appareil au mazout proche de vingt ans n'est plus une composante dépréciée : c'est une conversion certaine à court terme, dont le coût (appareil, entrée électrique souvent à augmenter, retrait du réservoir) est une dépréciation curable de la méthode du coût et une dépense immédiate dans la méthode de comparaison, les programmes d'aide, lorsqu'ils existent, venant en déduction (\cref{ch:44}). Un réservoir enfoui ajoute un risque de contamination du sol qui relève du \cref{ch:43}.
346+
347+\begin{quebec}[Garanties et déclarations : ce que l'inspection peut présumer]
348+Trois cadres québécois conditionnent la lecture d'un formulaire d'inspection. Pour un bâtiment neuf visé par le Plan de garantie des bâtiments résidentiels neufs (unifamiliales isolées, jumelées ou en rangée, duplex à quintuplex, petites copropriétés), les malfaçons non apparentes sont couvertes un an après la réception, les vices cachés trois ans et les vices de conception, de construction ou du sol cinq ans, sous des plafonds de \dollars{300000} par unifamiliale et de \dollars{200000} par logement pour un multifamilial \parencite[art.~10, 13, 14]{reglgarantie}, la garantie étant transférable à l'acquéreur \parencite{gcrprotections} ; l'évaluateur d'un immeuble récent consigne l'existence de la garantie et des réclamations en cours. Pour un immeuble vendu par courtier, les déclarations du vendeur constituent une source d'information que l'évaluateur cite comme telle, sans la vérifier, mais dont il relève les contradictions avec ses observations \parencite{oaciqFormulaireDV}. Pour les travaux récents, le Code de construction en vigueur repose sur le Code national du bâtiment 2020 modifié pour le Québec depuis le 17~avril 2025, avec une période de transition pendant laquelle les dispositions antérieures peuvent s'appliquer aux travaux commencés avant le 17~octobre 2027 \parencite{codeConstruction} ; l'évaluateur n'atteste pas la conformité, mais il sait qu'un bâtiment ancien n'a pas à satisfaire au code actuel et qu'une non-conformité apparente n'est une désuétude fonctionnelle que si le marché la sanctionne (\cref{ch:24}). Ces textes ont été vérifiés dans leur version à jour au 1\textsuperscript{er} juillet 2026 (règlements) ou au 15~octobre 2026 (lois).
349+\end{quebec}
350+
351+\section{Exemple rempli : le sujet du cas 1}\label{sec:appB:exemple}
352+
353+\begin{synthetique}
354+L'exemple qui suit est entièrement fictif. Il porte sur la maison du dossier de cas 1 décrite dans \texttt{data/synthetic/cas1\_gatineau\_sujet.json} (maison à étages du secteur Hull, construite en 1998, 165~m\textsuperscript{2} habitables, terrain de 620~m\textsuperscript{2}, quatre chambres, deux salles de bain, garage simple, sous-sol fini, cote de qualité 3) et utilisée aux \cref{ch:16,ch:17,ch:21,ch:24,ch:45}. Les observations, les âges apparents et les cotes de composantes ont été rédigés pour l'annexe afin d'être cohérents avec la cote d'état générale de 3 du dossier ; ils ne proviennent d'aucune inspection réelle et aucun montant n'y figure, les chiffres du dossier étant calculés par les scripts de \texttt{analysis/python/traite/cas1.py} et des chapitres cités.
355+\end{synthetique}
356+
357+Le \cref{tab:appB:exemple} montre comment le formulaire 1 se remplit sur le terrain. On y voit le passage des faits aux codes : la toiture de 2014, cotée 3 avec un âge apparent de douze ans, n'appelle aucune incidence immédiate mais entre dans l'âge effectif ; le chauffe-eau de 2013 est en fin de vie et reçoit une incidence négative ; la fissure du mur ouest, de faible largeur et sans déversement, est consignée avec sa mesure, photographiée, et renvoyée à une réserve plutôt qu'à une hypothèse extraordinaire parce que rien n'indique un désordre structural ; le sous-sol fini de bonne qualité, le garage et la thermopompe récente donnent au sujet un avantage sur les comparables qui n'en ont pas, ce que la grille d'ajustements du \cref{ch:17} traduira en dollars. La synthèse, enfin, aboutit à une cote générale de 3 et à un âge effectif inférieur à l'âge chronologique, parce que plusieurs composantes de courte durée ont été renouvelées ; c'est ce chiffre que le \cref{ch:24} utilise.
358+
359+{\small\setlength{\tabcolsep}{4pt}
360+\begin{longtable}{@{}L{2.7cm}c>{\centering\arraybackslash}p{0.9cm}L{5.2cm}L{2.5cm}@{}}
361+\caption{Formulaire 1 rempli pour le sujet du cas 1 (inspection fictive du 20~août 2026 ; données synthétiques)}\label{tab:appB:exemple}\\
362+\toprule
363+Élément & {État} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
364+\midrule
365+\endfirsthead
366+\caption*{Tableau~\ref{tab:appB:exemple} (suite)}\\
367+\toprule
368+Élément & {État} & {Âge app.} & Observations (n\textsuperscript{o} photo) & Incidence \\
369+\midrule
370+\endhead
371+\midrule\multicolumn{5}{r@{}}{\footnotesize\itshape suite à la page suivante}\\
372+\endfoot
373+\bottomrule
374+\endlastfoot
375+\multicolumn{5}{@{}l}{\textbf{A. Identification}}\\
376+Adresse, lot, date & \multicolumn{4}{l@{}}{123, rue des Érables (fictive), Hull ; lot fictif ; 20~août 2026, 9~h~30 à 11~h~15 ; propriétaire présent ; ensoleillé, 24~°C} \\
377+Parties non visitées & \multicolumn{4}{l@{}}{Entretoit observé depuis la trappe seulement ; remise verrouillée} \\
378+Documents consultés & \multicolumn{4}{l@{}}{Rôle 2025, certificat de localisation (2019), permis (garage 1998, toiture 2014), zonage résidentiel, hors zone inondable selon la carte en vigueur, DV du vendeur} \\
379+\midrule
380+\multicolumn{5}{@{}l}{\textbf{B. Site et terrain}}\\
381+Topographie, drainage & 4 & — & Terrain plat, pente légère vers la rue ; aucune eau stagnante (ph.~1--2) & 0 \\
382+Aménagement, arbres & 3 & — & Érable mature à 4~m du mur est ; haie de cèdres & 0 \\
383+Entrée, stationnement & 3 & 14 & Asphalte 2012, fissures fines, deux cases devant le garage (ph.~3) & 0 \\
384+Remise, clôture & 3 & 20 & Remise de bois 8~pi~×~10~pi, clôture de mailles ; pas de piscine & 0 \\
385+Services & 5 & — & Aqueduc et égout municipaux & 0 \\
386+Zone inondable & non & — & Carte consultée le 19~août 2026 & 0 \\
387+\midrule
388+\multicolumn{5}{@{}l}{\textbf{C. Fondations et structure}}\\
389+Type & \multicolumn{4}{l@{}}{Béton coulé, sous-sol pleine hauteur (2,35~m libre)} \\
390+Murs de fondation & 3 & 28 & Fissure verticale de 1~mm au mur ouest, du haut jusqu'à 40~cm du sol, sèche, sans déversement ; efflorescence légère (ph.~7--8) & 0 (réserve) \\
391+Dalle sous-sol et garage & 4 & 28 & Aucun soulèvement ni fissure en étoile ; dalle du garage plane ; aucun signe de pyrite (ph.~9) & 0 \\
392+Drainage, puisard & 4 & 28 & Pompe de puisard 2021, clapet ; aucun dépôt d'ocre ; eau claire & 0 \\
393+Humidité, infiltrations & 4 & — & Aucune trace ; DV : aucune infiltration déclarée & 0 \\
394+Charpente visible & 4 & 28 & Solives de bois d'ingénierie, aucune flèche visible & 0 \\
395+\midrule
396+\multicolumn{5}{@{}l}{\textbf{D. Enveloppe}}\\
397+Revêtement & \multicolumn{4}{l@{}}{Brique en façade et pignons, vinyle sur les côtés et l'arrière} \\
398+État du revêtement & 3 & 28 & Mortier sain ; deux linteaux d'acier avec rouille de surface au-dessus des fenêtres arrière (ph.~12) & $-$ (curable mineur) \\
399+Solins, scellants & 3 & 10 & Scellants durcis au pourtour de trois fenêtres & $-$ (curable mineur) \\
400+Soffites, fascias & 4 & 12 & Aluminium 2014, ventilés & 0 \\
401+Galerie arrière & 3 & 15 & Bois traité, garde-corps conforme en apparence, surface à teindre & 0 \\
402+Porte de garage & 4 & 8 & Isolée, ouvre-porte 2018 & $+$ (garage vs comparables sans garage) \\
403+Isolation des murs & \multicolumn{4}{l@{}}{Laine minérale visible à une ouverture du sous-sol ; aucune vermiculite ni MIUF déclarée} \\
404+\midrule
405+\multicolumn{5}{@{}l}{\textbf{E. Toiture}}\\
406+Type & \multicolumn{4}{l@{}}{Bardeau d'asphalte, pente forte, refait en 2014 (permis)} \\
407+Couvrement & 3 & 12 & Granules intacts, aucun bardeau relevé ; mousse légère côté nord (ph.~14) & 0 \\
408+Solins, évents & 4 & 12 & Évents de plastique, solins de la cheminée scellés & 0 \\
409+Gouttières & 3 & 12 & Descentes rejetant à 1~m des fondations ; rallonges recommandées & 0 \\
410+Entretoit & 3 & 28 & Vu de la trappe : laine soufflée, env. 25~cm, ventilation apparente, aucune tache ; aucune vermiculite visible & 0 \\
411+\midrule
412+\multicolumn{5}{@{}l}{\textbf{F. Fenestration}}\\
413+Type & \multicolumn{4}{l@{}}{PVC, vitrage double, d'origine (1998) sauf deux fenêtres du sous-sol (2016)} \\
414+État & 3 & 28 & Un verre embué (chambre 3, ph.~16) ; mécanismes fonctionnels & $-$ (curable mineur) \\
415+\midrule
416+\multicolumn{5}{@{}l}{\textbf{G. Intérieur}}\\
417+Planchers & 4 & 10 & Bois franc au rez-de-chaussée (sablé 2016), stratifié à l'étage, céramique aux pièces d'eau & 0 \\
418+Murs, plafonds & 4 & — & Aucune fissure significative ; peinture récente & 0 \\
419+Cuisine & 3 & 14 & Armoires de mélamine 2012, comptoir stratifié, dosseret de céramique (ph.~18) & 0 \\
420+Salles de bain (2) & 3 & 14 / 28 & Étage : rénovée 2012, ventilateur ; sous-sol : d'origine, joints à refaire & 0 \\
421+Sous-sol fini & 4 & 10 & Salle familiale, chambre, salle de bain ; plafond suspendu ; hauteur 2,35~m & $+$ (vs comparables non finis) \\
422+Foyer & 3 & 28 & Foyer au bois d'origine, conduit ramoné selon le propriétaire ; aucun certificat vu & 0 \\
423+\midrule
424+\multicolumn{5}{@{}l}{\textbf{H. Électricité}}\\
425+Entrée et panneau & \multicolumn{4}{l@{}}{200~A, disjoncteurs, cuivre ; espace libre pour 6 circuits} \\
426+Câblage visible & 4 & 28 & Aucun aluminium ; boîtes fermées & 0 \\
427+Prises, détecteurs & 4 & — & DDFT aux salles de bain et à la cuisine ; détecteurs de fumée à chaque niveau, CO au sous-sol & 0 \\
428+\midrule
429+\multicolumn{5}{@{}l}{\textbf{I. Plomberie}}\\
430+Distribution, évacuation & \multicolumn{4}{l@{}}{Cuivre et ABS d'origine ; aucun galvanisé} \\
431+Chauffe-eau & 2 & 13 & Électrique 60~gal, 2013, corrosion au raccord supérieur, sans bac (ph.~21) & $-$ (remplacement) \\
432+Appareils & 4 & — & Pression normale, aucune fuite & 0 \\
433+\midrule
434+\multicolumn{5}{@{}l}{\textbf{J. Chauffage, ventilation}}\\
435+Système & \multicolumn{4}{l@{}}{Plinthes électriques d'origine + thermopompe murale (2022) au rez-de-chaussée ; aucun mazout} \\
436+Appareils & 4 & 4 / 28 & Thermopompe 2022 en bon état ; plinthes fonctionnelles & $+$ (vs comparables sans thermopompe) \\
437+Ventilation & 3 & 28 & Ventilateurs de salles de bain vers l'extérieur ; aucun VRC & 0 \\
438+\midrule
439+\multicolumn{5}{@{}l}{\textbf{K. Environnement intérieur}}\\
440+Radon & \multicolumn{4}{l@{}}{Aucun test déclaré ; aucun système d'atténuation ; réserve type au rapport} \\
441+Amiante & \multicolumn{4}{l@{}}{Aucun signe (construction 1998)} \\
442+Déclarations & \multicolumn{4}{l@{}}{DV : aucun sinistre, aucune réclamation, aucun travail sans permis déclaré} \\
443+\midrule
444+\multicolumn{5}{@{}l}{\textbf{L. Synthèse}}\\
445+Cote générale & 3 & — & Entretien normal pour l'âge ; composantes de courte durée partiellement renouvelées ; aucun désordre structural apparent & \\
446+Âges & \multicolumn{4}{l@{}}{Chronologique 28~ans ; effectif estimé 22~ans (toiture, revêtements de sol, cuisine et salle de bain de l'étage renouvelés ; fenêtres et salle de bain du sous-sol d'origine)} \\
447+Travaux immédiats & \multicolumn{4}{l@{}}{Chauffe-eau ; scellants ; verre embué ; linteaux à nettoyer et peindre} \\
448+Moyen terme & \multicolumn{4}{l@{}}{Fenêtres d'origine (5 à 10~ans) ; salle de bain du sous-sol ; asphalte} \\
449+À expertiser & \multicolumn{4}{l@{}}{Aucun ; fissure du mur ouest consignée en réserve avec recommandation de suivi} \\
450+Photographies, mesures & \multicolumn{4}{l@{}}{24 photographies ; périmètre extérieur mesuré au télémètre, conforme au certificat de localisation} \\
451+\end{longtable}
452+\tabsource{\synth{exemple rédigé pour l'annexe à partir de \texttt{cas1\_gatineau\_sujet.json} ; aucun montant ; la cote 3 et l'âge effectif sont ceux que les \cref{ch:21,ch:24} utilisent.}}
453+}
454+
455+\begin{erreurs}
456+\begin{itemize}
457+\item Coter l'état par rapport au neuf plutôt que par rapport à l'âge : une maison de 1998 bien entretenue est un « 4 », non un « 2 » parce qu'elle n'est pas neuve ; l'âge entre par l'âge effectif, pas deux fois.
458+\item Écrire des interprétations au lieu de faits (« fondation problématique ») : seule la description mesurée, photographiée et datée résiste à la contestation.
459+\item Omettre de consigner les parties non visitées et les refus d'accès, puis conclure comme si tout avait été vu : la règle 2.4 exige que l'exception soit expliquée, et l'hypothèse qui en découle doit être déclarée.
460+\item Transformer un signe d'alerte en diagnostic (« présence de pyrite ») ou, à l'inverse, le taire pour ne pas inquiéter : la conduite correcte est la description du signe, la recommandation d'expertise et l'hypothèse extraordinaire avec son incidence.
461+\item Ouvrir, sonder ou déplacer ce que l'on n'est pas autorisé ni formé à manipuler (panneau sous tension, isolant suspect, trappe scellée).
462+\item Compter deux fois le même désordre : une fois comme dépense immédiate dans la comparaison, une fois comme dépréciation curable dans le coût, puis encore dans la cote d'état qui sert à l'ajustement.
463+\item Confondre l'inspection d'évaluation avec une inspection préachat et laisser le client croire que le rapport d'évaluation en tient lieu.
464+\end{itemize}
465+\end{erreurs}
466+
467+\begin{limites}
468+Les formulaires de cette annexe sont une convention de l'ouvrage, non un modèle imposé par l'Ordre ; les Normes de pratique professionnelle n'imposent aucun formulaire d'inspection et se bornent à exiger que l'inspection ait lieu et qu'elle soutienne la description, l'analyse de l'usage et les comparaisons. L'échelle de cotation à cinq niveaux et la pondération par le coût sont des choix pédagogiques cohérents avec les dossiers synthétiques du livre ; d'autres cabinets emploient d'autres échelles, et la comparabilité entre inspecteurs n'est jamais acquise sans étalonnage. Les contextes « probables » du \cref{tab:appB:patho} (époques, régions) sont des repères issus de sources d'associations et d'organismes publics consultées le 31~octobre 2026 ; les affirmations relatives à la période d'installation du câblage en aluminium et à la plomberie galvanisée relèvent de la pratique du bâtiment et n'ont pas été vérifiées dans une source officielle. Les seuils du Règlement sur les appareils de chauffage au mazout ont été lus sur la page gouvernementale et non dans le texte réglementaire. L'exemple rempli est fictif et ne saurait servir de référence pour une maison réelle du même âge.
469+\end{limites}
470+
471+\begin{synthese}
472+\begin{itemize}
473+\item L'inspection est obligatoire (règle 2.4 des NPP) ; elle est visuelle, orientée vers la valeur, et se distingue de l'inspection préachat et de l'expertise, vers lesquelles elle renvoie en cas de signe d'alerte.
474+\item Quatre colonnes suffisent : cote d'état de 1 à 5 appréciée par rapport à l'âge, âge apparent, observations factuelles photographiées, incidence codée ($+$, $-$, 0) ; les montants se calculent au bureau.
475+\item La cote générale et l'âge effectif sont des moyennes pondérées par la part de chaque composante dans le coût neuf (\cref{eq:appB:cote}) ; ils alimentent la méthode du coût (\cref{eq:appB:agevie}) et la variable d'état des ajustements.
476+\item Trois formulaires emboîtés : la maison ; le plex (fiche par logement, baux, aires communes) ; l'immeuble de six logements et plus (systèmes centraux, sécurité incendie, accessibilité, stationnement, documents de gestion).
477+\item Les pathologies québécoises (pyrite, ocre ferreuse, pyrrhotite, vermiculite, MIUF, radon, aluminium, galvanisé, mazout) se repèrent par des signes et des contextes ; elles se traitent par la réserve, l'hypothèse extraordinaire avec incidence ou la dépréciation curable, jamais par un diagnostic de l'évaluateur.
478+\item Le formulaire, les photographies et les mesures restent au dossier au moins cinq ans ; ils sont la preuve de l'étendue du travail décrite au rapport (annexe~\ref{app:modeles-rapport} pour les textes-types).
479+\end{itemize}
480+\end{synthese}
modified manuscript/chapters/ch01.tex +6 −6
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch02.tex +11 −11
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch04.tex +30 −15
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch05.tex +14 −14
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch07.tex +23 −21
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch08.tex +40 −40
Large diff (127 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch09.tex +584 −1
Large diff (586 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch10.tex +9 −9
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch16.tex +3 −3
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
modified manuscript/chapters/ch21.tex +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch02.bib +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added references/lib-ch08.bib +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
modified references/library.bib +9 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch04.tex +45 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch05.tex +51 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
modified solutions/ch06.tex +75 −0
Large diff (76 lines) is not loaded by default to keep the page fast.Load diff
modified sources/registry.csv +3 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
modified verification/calculations.csv +24 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch02-claims.csv +30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch02-registry.csv +1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch02.md +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch07-calculations.csv +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch07-claims.csv +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch07-figures.csv +6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch07-registry.csv +3 −0
Large diff (3 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch08-calculations.csv +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch08-claims.csv +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch08-figures.csv +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added verification/chapters/ch08-registry.csv +2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
modified verification/claims.csv +80 −0
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
modified verification/figures.csv +11 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme