SPB Git

Ch. 16 rédigé (modèle) : texte, schéma, 4 figures, solutions, tests ; cleveref sans article ; compilation isolée avec étiquettes inter-chapitres

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

54 changed files +3,024 −37

54 changed files
Unified Split
modified CONVENTIONS.md +1 −0
@@ -41,6 +41,7 @@ Cible : **9 000 à 14 000 mots** par chapitre (prose), 3-6 figures, 4-8 tableaux
41 - Sigles français : RNE (NOI), RCD (DSCR), RPV (LTV), TRI (IRR), VAN (NPV), REB/RBE (revenu brut effectif), RBP (revenu brut potentiel), PHMU (plus haute et meilleure utilisation), MRB (multiplicateur du revenu brut), taux global d'actualisation (TGA) = taux de capitalisation global. 41 - Sigles français : RNE (NOI), RCD (DSCR), RPV (LTV), TRI (IRR), VAN (NPV), REB/RBE (revenu brut effectif), RBP (revenu brut potentiel), PHMU (plus haute et meilleure utilisation), MRB (multiplicateur du revenu brut), taux global d'actualisation (TGA) = taux de capitalisation global.
42 - Distinguer toujours : **prix** (observé), **valeur marchande** (estimation), **valeur au rôle** (fiscale, date de référence), **valeur d'investissement** (propre à un investisseur), **coût**. Ne jamais écrire « la valeur est de… » pour un prix observé. 42 - Distinguer toujours : **prix** (observé), **valeur marchande** (estimation), **valeur au rôle** (fiscale, date de référence), **valeur d'investissement** (propre à un investisseur), **coût**. Ne jamais écrire « la valeur est de… » pour un prix observé.
43 - Distinguer **exigence professionnelle** (norme OEAQ/IVS/CUSPAP), **droit applicable** (loi, règlement, jurisprudence), **pratique courante** et **modèle expérimental**. Formules : « la norme exige », « la loi prévoit », « il est d'usage », « on peut modéliser ». 43 - Distinguer **exigence professionnelle** (norme OEAQ/IVS/CUSPAP), **droit applicable** (loi, règlement, jurisprudence), **pratique courante** et **modèle expérimental**. Formules : « la norme exige », « la loi prévoit », « il est d'usage », « on peut modéliser ».
44+- **Renvois** : `\cref{}` produit le nom SANS article (« chapitre 17 », « figure 16.2 », « tableau 16.4 », « équation (16.3) », « exercice 16.5 ») ; l'auteur écrit l'article et la préposition : « le \cref{ch:17} », « au \cref{ch:17} », « à la \cref{fig:16:nuage} », « l'\cref{eq:16:local} », « du \cref{tab:16:grille} ». En début de phrase, `\Cref{}` (majuscule, sans article) ou « Le \cref{} ».
44 - Toute affirmation juridique cite l'article (ex. « art. 1955 C.c.Q. ») et la source bibliographique, avec date de consultation si c'est une page web ; toute statistique cite l'organisme, le tableau et la période. 45 - Toute affirmation juridique cite l'article (ex. « art. 1955 C.c.Q. ») et la source bibliographique, avec date de consultation si c'est une page web ; toute statistique cite l'organisme, le tableau et la période.
45 - **Rien d'inventé.** Pas de référence, de loi, de norme, de chiffre « plausible ». En cas de doute : écrire la chose comme non vérifiée dans `limites` et l'inscrire dans `verification/claims.csv` avec `statut=non_verifie`. 46 - **Rien d'inventé.** Pas de référence, de loi, de norme, de chiffre « plausible ». En cas de doute : écrire la chose comme non vérifiée dans `limites` et l'inscrire dans `verification/claims.csv` avec `statut=non_verifie`.
46 47
added analysis/python/cases/ch21_cout_neuf.py +230 −0
@@ -0,0 +1,230 @@
1+#!/usr/bin/env python3
2+"""Chapitre 21 — coût neuf (remplacement) du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES) et indexation IPCB (données réelles).
3+
4+Imprime tous les chiffres cités dans le chapitre 21 :
5+ * devis par composantes (data/synthetic/cas1_gatineau_couts.csv, 38 postes) regroupé par corps de métier ;
6+ * coûts directs, indirects, incitatif de l'entrepreneur (traite.cout.cout_neuf), coût neuf total, $/m² et $/pi² ;
7+ * comparaison unitaire avec la fourchette Altus 2026 (155-215 $/pi², Montréal, unifamiliale) ;
8+ * indexation par ratio d'indices IPCB Montréal (Statistique Canada 18-10-0289-01, données réelles dans data/raw) ;
9+ * ajustement de taille (élasticité) ;
10+ * analyse de sensibilité (taux d'indirects, incitatif, indice, élasticité).
11+
12+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch21_cout_neuf.py
13+Toutes les fonctions financières proviennent de analysis/python/traite/cout.py (aucune réécriture)."""
14+from __future__ import annotations
15+import sys, json, csv, pathlib
16+import numpy as np, pandas as pd
17+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
18+from traite import cout as ct
19+
20+ROOT = pathlib.Path(__file__).resolve().parents[3]
21+D = ROOT / "data/synthetic"
22+RAW = ROOT / "data/raw/statcan_18100289/18100289.csv"
23+
24+PI2_PAR_M2 = 10.7639
25+TAUX_INCITATIF = 0.10 # hypothèse pédagogique (voir texte, §21.6) ; assiette : directs + indirects
26+ALTUS_MIN, ALTUS_MAX = 155.0, 215.0 # $/pi², Altus Canadian Cost Guide 2026, Montréal, unifamiliale (tel que relayé)
27+
28+# Regroupement des 38 postes du devis en sept corps de métier (convention du chapitre)
29+CORPS = {
30+ "Excavation et remblai": "Fondations et site", "Fondations béton (semelles, murs)": "Fondations et site",
31+ "Dalle de sous-sol": "Fondations et site", "Drain français et imperméabilisation": "Fondations et site",
32+ "Raccordements aqueduc/égout": "Fondations et site",
33+ "Charpente murs et planchers": "Structure et charpente", "Fermes de toit": "Structure et charpente",
34+ "Escaliers": "Structure et charpente",
35+ "Couverture bardeaux d'asphalte": "Enveloppe", "Revêtement extérieur (brique 40 % / fibrociment 60 %)": "Enveloppe",
36+ "Isolation et pare-vapeur": "Enveloppe", "Fenêtres (PVC double vitrage)": "Enveloppe", "Portes extérieures": "Enveloppe",
37+ "Porte de garage": "Enveloppe", "Gouttières et solins": "Enveloppe",
38+ "Gypse et tirage de joints": "Finition intérieure", "Peinture intérieure": "Finition intérieure",
39+ "Planchers bois franc": "Finition intérieure", "Planchers céramique": "Finition intérieure",
40+ "Planchers flottants (sous-sol)": "Finition intérieure", "Portes intérieures et moulures": "Finition intérieure",
41+ "Armoires de cuisine et comptoirs": "Finition intérieure", "Vanités et accessoires (2 sdb)": "Finition intérieure",
42+ "Plomberie brute et finition": "Mécanique et électricité", "Chauffe-eau": "Mécanique et électricité",
43+ "Électricité (entrée 200 A, filage, appareils)": "Mécanique et électricité", "Ventilation (VRC) et conduits": "Mécanique et électricité",
44+ "Thermopompe centrale et plinthes": "Mécanique et électricité", "Éclairage et finition électrique": "Mécanique et électricité",
45+ "Balcon et perron": "Aménagements extérieurs", "Entrée asphaltée": "Aménagements extérieurs",
46+ "Aménagement paysager de base": "Aménagements extérieurs", "Nettoyage et conteneurs": "Aménagements extérieurs",
47+ "Permis et études (arpentage, sol)": "Coûts indirects", "Supervision de chantier (salaires)": "Coûts indirects",
48+ "Assurances chantier et cautionnement": "Coûts indirects", "Plans et honoraires professionnels": "Coûts indirects",
49+ "Financement intérimaire": "Coûts indirects",
50+}
51+ORDRE_CORPS = ["Fondations et site", "Structure et charpente", "Enveloppe", "Finition intérieure",
52+ "Mécanique et électricité", "Aménagements extérieurs", "Coûts indirects"]
53+
54+
55+def charger() -> tuple[pd.DataFrame, dict]:
56+ devis = pd.read_csv(D / "cas1_gatineau_couts.csv")
57+ devis["corps"] = devis["poste"].map(CORPS)
58+ assert devis["corps"].notna().all(), "poste sans corps de métier"
59+ sujet = json.loads((D / "cas1_gatineau_sujet.json").read_text())
60+ return devis, sujet
61+
62+
63+def regrouper(devis: pd.DataFrame) -> pd.DataFrame:
64+ g = devis.groupby("corps", sort=False)["montant"].agg(["sum", "count"]).reindex(ORDRE_CORPS)
65+ g.columns = ["montant", "postes"]
66+ g["part_direct_indirect"] = g["montant"] / g["montant"].sum()
67+ return g
68+
69+
70+def cout_neuf_sujet(devis: pd.DataFrame, taux_incitatif: float = TAUX_INCITATIF, base: str = "direct+indirect") -> dict:
71+ """Coût neuf du sujet : les indirects viennent du devis (5 postes), l'incitatif est appliqué par traite.cout.cout_neuf."""
72+ direct = float(devis.loc[~devis["indirect"], "montant"].sum())
73+ indirects = float(devis.loc[devis["indirect"], "montant"].sum())
74+ taux_ind = indirects / direct
75+ cn = ct.cout_neuf(direct, taux_ind, taux_incitatif, base)
76+ cn["taux_indirects"] = taux_ind
77+ cn["taux_incitatif"] = taux_incitatif
78+ return cn
79+
80+
81+def superficies(sujet: dict, devis: pd.DataFrame) -> dict:
82+ """Superficie habitable (JSON du sujet) et aire brute des étages (hypothèse : empreinte = aire de la dalle de sous-sol × 2 étages)."""
83+ hab = float(sujet["superficie_habitable_m2"])
84+ empreinte = float(devis.loc[devis["poste"] == "Dalle de sous-sol", "quantite"].iloc[0])
85+ brute = 2.0 * empreinte # cottage à deux étages hors sol (type « à étages »)
86+ return {"habitable_m2": hab, "habitable_pi2": hab * PI2_PAR_M2, "empreinte_m2": empreinte,
87+ "brute_etages_m2": brute, "brute_etages_pi2": brute * PI2_PAR_M2}
88+
89+
90+# ----------------------------------------------------------------------------- IPCB (données réelles)
91+
92+def ipcb_montreal(types: tuple[str, ...] = ("Bâtiments résidentiels [621]", "Maison individuelle [621112]",
93+ "Immeuble à hauteur restreinte (moins de cinq étages) [62111]",
94+ "Tour d'habitations (cinq étages ou plus) [621111]",
95+ "Bâtiments non résidentiels [622]")) -> pd.DataFrame:
96+ """Séries trimestrielles IPCB (2023 = 100), Montréal, agrégat des divisions, lues dans le CSV brut de Statistique Canada."""
97+ rows = []
98+ with open(RAW, encoding="utf-8-sig") as f:
99+ for r in csv.DictReader(f, delimiter=";"):
100+ if r["GÉO"] == "Montréal, Québec" and r["Division"] == "Agrégat des divisions" and r["Type d'immeuble"] in types and r["VALEUR"]:
101+ rows.append({"periode": r["PÉRIODE DE RÉFÉRENCE"], "type": r["Type d'immeuble"], "indice": float(r["VALEUR"]), "vecteur": r["VECTEUR"]})
102+ df = pd.DataFrame(rows)
103+ df["date"] = pd.PeriodIndex(df["periode"].str.replace("-01", "Q1").str.replace("-04", "Q2").str.replace("-07", "Q3").str.replace("-10", "Q4"), freq="Q").to_timestamp()
104+ return df.sort_values(["type", "date"]).reset_index(drop=True)
105+
106+
107+def indice(df: pd.DataFrame, type_: str, periode: str) -> float:
108+ v = df[(df["type"] == type_) & (df["periode"] == periode)]["indice"]
109+ if v.empty:
110+ raise KeyError(f"{type_} {periode} absent")
111+ return float(v.iloc[0])
112+
113+
114+# ----------------------------------------------------------------------------- sensibilité
115+
116+def grille_sensibilite(direct: float, taux_ind: np.ndarray, taux_inc: np.ndarray) -> np.ndarray:
117+ """Coût neuf (assiette directs + indirects) pour chaque couple (taux d'indirects, taux d'incitatif)."""
118+ return np.array([[ct.cout_neuf(direct, ti, tc)["total"] for tc in taux_inc] for ti in taux_ind])
119+
120+
121+def tornade(direct: float, taux_ind0: float, taux_inc0: float, ratio_idx: tuple[float, float], elast: tuple[float, float],
122+ d_ind: float = 0.05, d_inc: float = 0.05) -> list[dict]:
123+ """Variation du coût neuf quand un seul paramètre bouge (bas / haut), les autres restant à leur valeur centrale."""
124+ base = ct.cout_neuf(direct, taux_ind0, taux_inc0)["total"]
125+ out = [
126+ {"param": "Taux d'indirects", "bas": ct.cout_neuf(direct, taux_ind0 - d_ind, taux_inc0)["total"], "haut": ct.cout_neuf(direct, taux_ind0 + d_ind, taux_inc0)["total"],
127+ "etiquette": f"{(taux_ind0-d_ind)*100:.1f} % → {(taux_ind0+d_ind)*100:.1f} %"},
128+ {"param": "Taux d'incitatif", "bas": ct.cout_neuf(direct, taux_ind0, taux_inc0 - d_inc)["total"], "haut": ct.cout_neuf(direct, taux_ind0, taux_inc0 + d_inc)["total"],
129+ "etiquette": f"{(taux_inc0-d_inc)*100:.0f} % → {(taux_inc0+d_inc)*100:.0f} %"},
130+ {"param": "Indice de coûts (date)", "bas": ct.indexer_cout(base, 1.0, ratio_idx[0]), "haut": ct.indexer_cout(base, 1.0, ratio_idx[1]),
131+ "etiquette": f"×{ratio_idx[0]:.3f} → ×{ratio_idx[1]:.3f}"},
132+ {"param": "Élasticité de taille", "bas": base * ct.cout_par_superficie_ajuste(1.0, 140.0, 165.0, elast[0]) / ct.cout_par_superficie_ajuste(1.0, 140.0, 165.0, -0.10),
133+ "haut": base * ct.cout_par_superficie_ajuste(1.0, 140.0, 165.0, elast[1]) / ct.cout_par_superficie_ajuste(1.0, 140.0, 165.0, -0.10),
134+ "etiquette": f"{elast[0]:.2f} → {elast[1]:.2f}"},
135+ ]
136+ for o in out:
137+ o["base"] = base
138+ return out
139+
140+
141+def main() -> dict:
142+ devis, sujet = charger()
143+ g = regrouper(devis)
144+ cn = cout_neuf_sujet(devis)
145+ sup = superficies(sujet, devis)
146+ f = lambda x: f"{x:,.2f}".replace(",", " ")
147+ print("== DONNÉES SYNTHÉTIQUES — sujet cas 1 :", sujet["type"], sujet["annee_construction"], sujet["superficie_habitable_m2"], "m² hab.,", sujet["secteur"])
148+ print(f"== Devis : {len(devis)} postes ; {int((~devis['indirect']).sum())} directs, {int(devis['indirect'].sum())} indirects")
149+ print("== Regroupement par corps de métier ($) :")
150+ for k, r in g.iterrows():
151+ print(f" {k:28s} {int(r['postes']):2d} postes {f(r['montant']):>14s} {r['part_direct_indirect']*100:5.1f} %")
152+ print(f"== Coûts directs D = {f(cn['direct'])} $")
153+ print(f"== Coûts indirects I = {f(cn['indirects'])} $ (I/D = {cn['taux_indirects']*100:.2f} % ; I/(D+I) = {cn['indirects']/(cn['direct']+cn['indirects'])*100:.2f} %)")
154+ print(f"== Sous-total D + I = {f(cn['direct']+cn['indirects'])} $")
155+ print(f"== Incitatif {TAUX_INCITATIF*100:.0f} % (D+I) = {f(cn['incitatif'])} $")
156+ print(f"== COÛT NEUF C_N = {f(cn['total'])} $ (arrondi {round(cn['total']/1000)*1000:,.0f} $)".replace(",", " "))
157+ print(f" part directs {cn['direct']/cn['total']*100:.1f} %, indirects {cn['indirects']/cn['total']*100:.1f} %, incitatif {cn['incitatif']/cn['total']*100:.1f} %")
158+ alt = cout_neuf_sujet(devis, TAUX_INCITATIF, "direct")
159+ print(f"== Variante assiette 'direct' : incitatif {f(alt['incitatif'])} $, C_N {f(alt['total'])} $ (écart {f(cn['total']-alt['total'])} $)")
160+ print(f"== Superficies : habitable {sup['habitable_m2']:.0f} m² = {sup['habitable_pi2']:.0f} pi² ; empreinte {sup['empreinte_m2']:.0f} m² ; aire brute des étages {sup['brute_etages_m2']:.0f} m² = {sup['brute_etages_pi2']:.0f} pi²")
161+ um_hab = cn["total"] / sup["habitable_m2"]; um_brut = cn["total"] / sup["brute_etages_m2"]
162+ print(f"== C_N / m² habitable = {um_hab:,.0f} $/m² = {um_hab/PI2_PAR_M2:,.1f} $/pi²".replace(",", " "))
163+ print(f"== C_N / m² brut = {um_brut:,.0f} $/m² = {um_brut/PI2_PAR_M2:,.1f} $/pi²".replace(",", " "))
164+ di = cn["direct"] + cn["indirects"]
165+ print(f"== (D+I) / m² habitable = {di/sup['habitable_m2']:,.0f} $/m² = {di/sup['habitable_m2']/PI2_PAR_M2:,.1f} $/pi² ; (D+I)/m² brut = {di/sup['brute_etages_m2']:,.0f} $/m² = {di/sup['brute_etages_m2']/PI2_PAR_M2:,.1f} $/pi²".replace(",", " "))
166+ # comparaison unitaire Altus
167+ alt_mid = 0.5 * (ALTUS_MIN + ALTUS_MAX)
168+ print(f"== Comparaison unitaire Altus 2026 ({ALTUS_MIN:.0f}-{ALTUS_MAX:.0f} $/pi², milieu {alt_mid:.0f}) :")
169+ for nom, s_pi2 in [("aire brute des étages", sup["brute_etages_pi2"]), ("superficie habitable", sup["habitable_pi2"])]:
170+ print(f" sur {nom} ({s_pi2:.0f} pi²) : {ALTUS_MIN*s_pi2:,.0f} – {ALTUS_MAX*s_pi2:,.0f} $, milieu {alt_mid*s_pi2:,.0f} $".replace(",", " "))
171+ print(f" milieu Altus en $/m² : {alt_mid*PI2_PAR_M2:,.0f} $/m²".replace(",", " "))
172+ # indexation IPCB
173+ ipcb = ipcb_montreal()
174+ res, mi = "Bâtiments résidentiels [621]", "Maison individuelle [621112]"
175+ print("== IPCB Montréal (2023 = 100), données réelles Statistique Canada 18-10-0289-01 :")
176+ for t in ipcb["type"].unique():
177+ s = ipcb[ipcb["type"] == t]
178+ print(f" {t:60s} {len(s)} trimestres, {s['periode'].iloc[0]} → {s['periode'].iloc[-1]} ; premier {s['indice'].iloc[0]}, dernier {s['indice'].iloc[-1]} ({s['vecteur'].iloc[0]})")
179+ pts = {p: indice(ipcb, mi, p) for p in ["2017-01", "2019-07", "2021-07", "2023-07", "2025-07", "2026-04", "2026-07"]}
180+ print(" Maison individuelle :", pts)
181+ ptr = {p: indice(ipcb, res, p) for p in ["2017-01", "2019-07", "2021-07", "2023-07", "2025-07", "2026-07"]}
182+ print(" Bâtiments résidentiels :", ptr)
183+ var_an = indice(ipcb, res, "2026-07") / indice(ipcb, res, "2025-07") - 1
184+ var_an_mi = indice(ipcb, mi, "2026-07") / indice(ipcb, mi, "2025-07") - 1
185+ print(f" Variation annuelle T3 2025 → T3 2026 : résidentiel {var_an*100:.2f} %, maison individuelle {var_an_mi*100:.2f} %")
186+ r_2017 = indice(ipcb, mi, "2026-07") / indice(ipcb, mi, "2017-01")
187+ r_2019 = indice(ipcb, mi, "2026-07") / indice(ipcb, mi, "2019-07")
188+ r_2021 = indice(ipcb, mi, "2026-07") / indice(ipcb, mi, "2021-07")
189+ print(f" Ratios maison individuelle : T3 2026 / T1 2017 = {r_2017:.4f} ; / T3 2019 = {r_2019:.4f} ; / T3 2021 = {r_2021:.4f}")
190+ # taux annuel composé 2017T1→2026T3 (9,5 ans)
191+ n_ans = (2026 + 6 / 12) - (2017 + 0 / 12)
192+ print(f" Croissance annuelle composée T1 2017 → T3 2026 ({n_ans:.1f} ans) : {(r_2017**(1/n_ans)-1)*100:.2f} %/an")
193+ # exemple d'indexation : coût du sujet exprimé en dollars du T3 2019 et du T3 2021 (rétropolation)
194+ c_2019 = ct.indexer_cout(cn["total"], indice(ipcb, mi, "2026-07"), indice(ipcb, mi, "2019-07"))
195+ c_2021 = ct.indexer_cout(cn["total"], indice(ipcb, mi, "2026-07"), indice(ipcb, mi, "2021-07"))
196+ print(f" C_N du sujet en dollars du T3 2019 : {c_2019:,.0f} $ ; du T3 2021 : {c_2021:,.0f} $".replace(",", " "))
197+ # exemple direct : un devis de 2019T3 de 290 000 $ porté au T3 2026
198+ c0 = 290_000.0
199+ print(f" Exemple indexation : {c0:,.0f} $ (T3 2019) × {indice(ipcb, mi, '2026-07')}/{indice(ipcb, mi, '2019-07')} = {ct.indexer_cout(c0, indice(ipcb, mi, '2019-07'), indice(ipcb, mi, '2026-07')):,.0f} $".replace(",", " "))
200+ # erreur de série : non résidentiel et tours comme mauvais substituts
201+ for t in ["Bâtiments non résidentiels [622]", "Tour d'habitations (cinq étages ou plus) [621111]", "Immeuble à hauteur restreinte (moins de cinq étages) [62111]"]:
202+ r = indice(ipcb, t, "2026-07") / indice(ipcb, t, "2019-07")
203+ print(f" Ratio T3 2026 / T3 2019 avec {t} : {r:.4f} (écart vs maison individuelle {(r/r_2019-1)*100:+.1f} %)")
204+ # ajustement de taille
205+ print("== Ajustement de taille (élasticité) :")
206+ for e in [-0.05, -0.10, -0.15, -0.20]:
207+ u = ct.cout_par_superficie_ajuste(um_hab, 165.0, 140.0, e) # coût unitaire d'une maison de 140 m² déduit du sujet
208+ u2 = ct.cout_par_superficie_ajuste(um_hab, 165.0, 200.0, e)
209+ print(f" e = {e:+.2f} : 140 m² → {u:,.0f} $/m² ({u*140:,.0f} $) ; 200 m² → {u2:,.0f} $/m² ({u2*200:,.0f} $)".replace(",", " "))
210+ # exemple du texte : coût unitaire de référence 2 300 $/m² observé sur 200 m², appliqué à 165 m²
211+ u_ref = 2300.0
212+ for e in [0.0, -0.10]:
213+ u = ct.cout_par_superficie_ajuste(u_ref, 200.0, 165.0, e)
214+ print(f" Référence {u_ref:.0f} $/m² (200 m²) → 165 m² avec e = {e:+.2f} : {u:,.1f} $/m², coût {u*165:,.0f} $".replace(",", " "))
215+ # sensibilité
216+ print("== Sensibilité (assiette directs + indirects) :")
217+ ti = np.array([0.12, 0.15, 0.18, 0.2095, 0.24, 0.27]); tc = np.array([0.05, 0.08, 0.10, 0.12, 0.15])
218+ G = grille_sensibilite(cn["direct"], ti, tc)
219+ print(" indirects \\ incitatif", [f"{x*100:.0f} %" for x in tc])
220+ for i, x in enumerate(ti):
221+ print(f" {x*100:5.1f} % ", [f"{v:,.0f}".replace(",", " ") for v in G[i]])
222+ print(f" Étendue de la grille : {G.min():,.0f} – {G.max():,.0f} $ ({(G.max()/G.min()-1)*100:.1f} %)".replace(",", " "))
223+ torn = tornade(cn["direct"], cn["taux_indirects"], TAUX_INCITATIF, (1 / (1 + var_an_mi), 1 + var_an_mi), (-0.05, -0.20))
224+ for o in torn:
225+ print(f" {o['param']:24s} {o['etiquette']:22s} bas {o['bas']:,.0f} haut {o['haut']:,.0f} (base {o['base']:,.0f}) ; étendue {(o['haut']-o['bas'])/o['base']*100:+.1f} %".replace(",", " "))
226+ return {"devis": devis, "sujet": sujet, "groupes": g, "cout_neuf": cn, "superficies": sup, "ipcb": ipcb, "grille": (ti, tc, G), "tornade": torn}
227+
228+
229+if __name__ == "__main__":
230+ main()
added analysis/python/cases/ch29_capitalisation.py +227 −0
@@ -0,0 +1,227 @@
1+#!/usr/bin/env python3
2+"""Chapitre 29 — Capitalisation directe : calculs du cas 3 (12 logements, Rosemont ; DONNÉES SYNTHÉTIQUES).
3+
4+Tous les chiffres cités dans manuscript/chapters/ch29.tex et solutions/ch29.tex sortent d'ici. Les calculs partagés
5+(RNE normalisé, taux extraits, valeur par capitalisation) viennent de traite.cas3 afin de coïncider avec les
6+chapitres 30 et 47 ; les formules financières viennent de traite.finance (aucune n'est réécrite ici).
7+
8+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch29_capitalisation.py
9+"""
10+from __future__ import annotations
11+import sys, pathlib
12+import numpy as np, pandas as pd
13+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
14+from traite import cas3, finance as fin
15+
16+ROOT = pathlib.Path(__file__).resolve().parents[3]
17+
18+# Barème 2026 des droits sur les mutations immobilières, Ville de Montréal (sources/notes-B.md §, clé mtlmutations2026) :
19+# bornes supérieures des tranches ($) et taux marginaux. Facteur comparatif 2026 = 1,00 (LDMI art. 1.1 ; rôle 2026-2028).
20+MUTATIONS_MTL_2026 = [(62_900, 0.005), (315_000, 0.01), (552_300, 0.015), (1_104_700, 0.02), (2_136_500, 0.025), (3_113_000, 0.035), (float("inf"), 0.04)]
21+FACTEUR_COMPARATIF_MTL_2026 = 1.00
22+
23+
24+def droits_mutation(base: float, bareme=MUTATIONS_MTL_2026) -> float:
25+ """Droits progressifs par tranches (LDMI art. 2 ; barème municipal)."""
26+ total, bas = 0.0, 0.0
27+ for haut, taux in bareme:
28+ if base <= bas:
29+ break
30+ total += (min(base, haut) - bas) * taux
31+ bas = haut
32+ return total
33+
34+
35+def comparables_proches(c: pd.DataFrame, sujet: dict, k: int = 10, date_eval="2026-09-01") -> pd.DataFrame:
36+ """Les k ventes les plus proches du sujet : taille, état, âge, quartier, ancienneté de la vente (distance pédagogique)."""
37+ c = c.copy()
38+ mois = (pd.Timestamp(date_eval) - c["date_vente"]).dt.days / 30.4375
39+ c["mois"] = mois
40+ c["distance"] = (abs(c["nb_logements"] - sujet["nb_logements"]) / 6 + abs(c["etat_1a5"] - sujet["etat_1a5"])
41+ + abs(c["annee_construction"] - sujet["annee_construction"]) / 20 + 0.5 * (c["quartier"] != "Rosemont") + mois / 24)
42+ return c.sort_values("distance").head(k)
43+
44+
45+def calculer() -> dict:
46+ d = cas3.charger()
47+ etat, dep, hyp, sujet, comps = d["etat"], d["dep"], d["hyp"], d["sujet"], d["comps"]
48+ base = cas3.rne(etat, dep, hyp)
49+ tx = cas3.taux_extraits(comps)
50+ c = tx["comparables"]
51+ R = tx["mediane"]
52+ V = fin.capitalisation_directe(base["rne"], R)
53+ proches = comparables_proches(c, sujet)
54+ # régression : résidus
55+ X = np.column_stack([np.ones(len(c)), c["etat_1a5"] - 3, c["nb_logements"] - 12])
56+ beta = np.array([tx["regression"]["constante"], tx["regression"]["etat"], tx["regression"]["taille"]])
57+ resid = c["taux_global"].values - X @ beta
58+ reg = {**tx["regression"], "sigma_resid": float(resid.std(ddof=3)), "sigma_brut": float(c["taux_global"].std(ddof=1)),
59+ "r2": float(1 - resid.var(ddof=0) / c["taux_global"].var(ddof=0)), "prevision_sujet": float(beta[0])}
60+ V_reg = fin.capitalisation_directe(base["rne"], reg["prevision_sujet"])
61+
62+ # --- construction du taux -------------------------------------------------------------------------------------
63+ f_ = hyp["financement"]
64+ h1 = fin.Hypotheque(1.0, f_["taux"], f_["amortissement_mois"]) # prêt unitaire : constante hypothécaire
65+ f = h1.constante_hypothecaire
66+ M = f_["rpv"]
67+ re_implicite = (R - M * f) / (1 - M) # rendement courant des fonds propres impliqué par le marché
68+ grille_re = {re: fin.taux_bande_investissement(M, f, re) for re in (0.04, 0.05, 0.06, 0.07, 0.08)}
69+ R_rcd = fin.taux_par_rcd(f_["rcd_min"], M, f)
70+ V_rcd = fin.capitalisation_directe(base["rne"], R_rcd)
71+ r = hyp["taux_actualisation"]; Rt = hyp["taux_terminal"]
72+ g_terminal = r - Rt # croissance impliquée par le taux terminal du cas 3
73+ gordon = {"r": r, "g_terminal": g_terminal, "R_g_terminal": fin.taux_gordon(r, g_terminal),
74+ "g_tal": hyp["croissance_loyers_contrat"], "R_g_tal": fin.taux_gordon(r, hyp["croissance_loyers_contrat"]),
75+ "g_implicite_marche": r - R}
76+ n = hyp["horizon_ans"]
77+ P = 1 - h1.solde(12 * n) # part du capital remboursée en n ans
78+ sff = fin.facteur_fonds_amortissement(r, n)
79+ delta = (1 + g_terminal) ** n - 1 # appréciation cohérente avec g_terminal
80+ ellwood = {"Y": r, "M": M, "f": f, "P": P, "n": n, "SFF": sff, "delta": delta,
81+ "R_delta0": fin.taux_ellwood(r, M, f, P, n, 0.0), "R_delta": fin.taux_ellwood(r, M, f, P, n, delta),
82+ "R_akerson": fin.taux_akerson(r, M, f, P, n, delta), "R_sans_dette": fin.taux_ellwood(r, 0.0, f, P, n, delta),
83+ "R_sans_dette_delta0": fin.taux_ellwood(r, 0.0, f, P, n, 0.0),
84+ "composantes": {"M f": M * f, "(1-M) Y": (1 - M) * r, "-M P SFF": -M * P * sff, "-delta SFF": -delta * sff}}
85+ ellwood["V_delta"] = fin.capitalisation_directe(base["rne"], ellwood["R_delta"])
86+ # lecture inverse : appréciation Δ cohérente avec le taux de marché (Y, M, f fixés) et rendement Y cohérent avec Δ fixé
87+ ellwood["delta_implicite"] = (ellwood["R_delta0"] - R) / sff
88+ lo, hi = 0.02, 0.30
89+ for _ in range(200):
90+ mid = 0.5 * (lo + hi); Pm = 1 - h1.solde(12 * n)
91+ if fin.taux_ellwood(mid, M, f, Pm, n, delta) < R: lo = mid
92+ else: hi = mid
93+ ellwood["Y_implicite"] = 0.5 * (lo + hi)
94+ ellwood["composantes_marche"] = {"M f": M * f, "(1-M) Y": (1 - M) * r, "-M P SFF": -M * P * sff, "-delta SFF": -ellwood["delta_implicite"] * sff}
95+
96+ # --- multiplicateurs -------------------------------------------------------------------------------------------
97+ mrb = {"mediane": float(c["mrb"].median()), "moyenne": float(c["mrb"].mean()), "min": float(c["mrb"].min()), "max": float(c["mrb"].max()),
98+ "cv": float(c["mrb"].std(ddof=1) / c["mrb"].mean()), "cv_taux": float(c["taux_global"].std(ddof=1) / c["taux_global"].mean())}
99+ mrb["V_contrat"] = mrb["mediane"] * base["rbp_contrat"]; mrb["V_marche"] = mrb["mediane"] * base["rbp_marche"]
100+ mrb["mrn"] = 1 / R; mrb["mrb_sujet_implicite"] = V / base["rbp_contrat"]
101+ mrb["ratio_dep_median_comps"] = float(c["ratio_depenses"].median()); mrb["ratio_dep_sujet_rbe"] = base["depenses_totales"] / base["rbe"]
102+
103+ # --- cohérence RNE–taux ----------------------------------------------------------------------------------------
104+ taux_taxes = hyp["taux_taxe_fonciere"] + hyp["taux_taxe_scolaire"]
105+ taxes_role = hyp["valeur_role_2026"] * taux_taxes
106+ taxes_valeur = V * taux_taxes
107+ coherence = {"taxes_role": taxes_role, "taxes_sur_valeur": taxes_valeur, "ecart_taxes": taxes_role - taxes_valeur,
108+ "effet_valeur": (taxes_role - taxes_valeur) / R, "valeur_role": hyp["valeur_role_2026"], "ratio_valeur_role": V / hyp["valeur_role_2026"],
109+ "reserve": base["reserve"], "reserve_par_logement": base["reserve"] / len(etat),
110+ "R_avant_reserve_equiv": (base["rne"] + base["reserve"]) / V, "gestion": 0.04 * base["rbe"],
111+ "V_si_taux_avant_reserve_sur_rne_apres": fin.capitalisation_directe(base["rne"], (base["rne"] + base["reserve"]) / V)}
112+ # loyers de marché capitalisés à tort (baux en place)
113+ rbe_m = fin.revenu_brut_effectif(base["rbp_marche"], base["taux_vacance_pertes"], base["autres_revenus"])
114+ de_m = base["depenses_hors_reserve"] - 0.04 * base["rbe"] + 0.04 * rbe_m
115+ rne_m = fin.rne(rbe_m, de_m, base["reserve"])
116+ coherence["rne_marche"] = rne_m; coherence["V_marche"] = fin.capitalisation_directe(rne_m, R); coherence["surevaluation_marche"] = coherence["V_marche"] / V - 1
117+ # droits de mutation (encadré Québec)
118+ base_droits = max(V, hyp["valeur_role_2026"] * FACTEUR_COMPARATIF_MTL_2026)
119+ mut = {"base": base_droits, "droits": droits_mutation(base_droits), "droits_sur_prix": droits_mutation(V)}
120+ mut["pct_prix"] = mut["droits"] / V
121+
122+ # --- sensibilité -----------------------------------------------------------------------------------------------
123+ grille_R = np.round(np.arange(0.0375, 0.0526, 0.0025), 4)
124+ sens = pd.DataFrame({"R": grille_R})
125+ for k, m in (("rne_m5", 0.95), ("rne", 1.0), ("rne_p5", 1.05)):
126+ sens[k] = [fin.capitalisation_directe(base["rne"] * m, x) for x in grille_R]
127+ sens["var_vs_base"] = sens["rne"] / V - 1
128+ derive = {"dV_dR": -V / R, "dV_25pb": -V / R * 0.0025, "V_plus25": fin.capitalisation_directe(base["rne"], R + 0.0025),
129+ "V_moins25": fin.capitalisation_directe(base["rne"], R - 0.0025), "elasticite_R": -1.0, "elasticite_rne": 1.0}
130+ derive["var_plus25_exacte"] = derive["V_plus25"] / V - 1; derive["var_plus25_lineaire"] = -0.0025 / R
131+
132+ return {"base": base, "taux": tx, "R": R, "V": V, "V_arrondie": round(V / 5000) * 5000, "proches": proches, "mediane_proches": float(proches["taux_global"].median()),
133+ "V_proches": fin.capitalisation_directe(base["rne"], float(proches["taux_global"].median())), "V_q1": fin.capitalisation_directe(base["rne"], tx["q3"]), "V_q3": fin.capitalisation_directe(base["rne"], tx["q1"]),
134+ "regression": reg, "V_reg": V_reg, "f": f, "M": M, "re_implicite": re_implicite, "grille_re": grille_re, "R_rcd": R_rcd, "V_rcd": V_rcd,
135+ "gordon": gordon, "ellwood": ellwood, "mrb": mrb, "coherence": coherence, "mutations": mut, "sensibilite": sens, "derive": derive,
136+ "hyp": hyp, "sujet": sujet, "comps": comps}
137+
138+
139+def exercices(res: dict | None = None) -> dict:
140+ """Chiffres des exercices 29.1 à 29.8 (énoncés dans ch29.tex, solutions dans solutions/ch29.tex)."""
141+ res = res or calculer(); c = res["comps"].set_index("id_vente"); b = res["base"]
142+ ex = {}
143+ # 29.1 — capitalisation simple : RNE de 61 200 $ (énoncé), R = 4,75 % ; +25 pb
144+ ex["1"] = {"rne": 61_200.0, "R": 0.0475, "V": fin.capitalisation_directe(61_200.0, 0.0475), "V_plus25": fin.capitalisation_directe(61_200.0, 0.05)}
145+ ex["1"]["var"] = ex["1"]["V_plus25"] / ex["1"]["V"] - 1; ex["1"]["lineaire"] = -0.0025 / 0.0475
146+ # 29.2 — extraction sur cinq ventes du fichier cas 3
147+ ids = ["M008", "M011", "M022", "M034", "M040"]; s = c.loc[ids]
148+ taux = s["rne"] / s["prix_vente"]
149+ ex["2"] = {"ids": ids, "taux": taux.to_dict(), "mediane": float(taux.median()), "moyenne": float(taux.mean()),
150+ "ponderee": float((s["rne"].sum()) / s["prix_vente"].sum()), "V_mediane": fin.capitalisation_directe(b["rne"], float(taux.median()))}
151+ # 29.3 — convention de la réserve : un comparable de 18 logements (énoncé) ; réserve 675 $/logement
152+ rne_apres, reserve, prix = 150_000.0, 18 * 675.0, 3_400_000.0
153+ R_apres = fin.taux_global_extrait(rne_apres, prix); R_avant = fin.taux_global_extrait(rne_apres + reserve, prix)
154+ ex["3"] = {"R_apres": R_apres, "R_avant": R_avant, "V_correct": fin.capitalisation_directe(b["rne"], R_apres),
155+ "V_errone": fin.capitalisation_directe(b["rne"], R_avant), "V_coherent_avant": fin.capitalisation_directe(b["rne"] + b["reserve"], R_avant)}
156+ ex["3"]["erreur"] = ex["3"]["V_errone"] / ex["3"]["V_correct"] - 1
157+ # 29.4 — bande d'investissement et RCD (R_E = 6 % et RCD = 1,25 : hypothèses de l'énoncé)
158+ f = res["f"]; M = res["M"]
159+ ex["4"] = {"f": f, "R_bande": fin.taux_bande_investissement(M, f, 0.06), "R_rcd": fin.taux_par_rcd(1.25, M, f),
160+ "V_bande": fin.capitalisation_directe(b["rne"], fin.taux_bande_investissement(M, f, 0.06)),
161+ "V_rcd": fin.capitalisation_directe(b["rne"], fin.taux_par_rcd(1.25, M, f)), "re_implicite": res["re_implicite"]}
162+ # 29.5 — Gordon
163+ g = res["gordon"]
164+ ex["5"] = {"R_225": g["R_g_terminal"], "R_310": g["R_g_tal"], "g_implicite": g["g_implicite_marche"],
165+ "V_225": fin.capitalisation_directe(b["rne"], g["R_g_terminal"]), "V_310": fin.capitalisation_directe(b["rne"], g["R_g_tal"]),
166+ "R_g4": fin.taux_gordon(g["r"], 0.04), "V_g4": fin.capitalisation_directe(b["rne"], fin.taux_gordon(g["r"], 0.04))}
167+ # 29.6 — Ellwood / Akerson (Δ = 25 % sur 10 ans, énoncé)
168+ e = res["ellwood"]
169+ ex["6"] = {"P": e["P"], "SFF": e["SFF"], "R_ellwood": fin.taux_ellwood(e["Y"], e["M"], e["f"], e["P"], e["n"], 0.25),
170+ "R_akerson": fin.taux_akerson(e["Y"], e["M"], e["f"], e["P"], e["n"], 0.25), "R_sans_dette": fin.taux_ellwood(e["Y"], 0.0, e["f"], e["P"], e["n"], 0.25),
171+ "R_delta0": e["R_delta0"], "g_equiv": 1.25 ** (1 / e["n"]) - 1}
172+ ex["6"]["R_gordon_equiv"] = fin.taux_gordon(e["Y"], ex["6"]["g_equiv"]); ex["6"]["V"] = fin.capitalisation_directe(b["rne"], ex["6"]["R_ellwood"])
173+ # 29.7 — loyers de marché vs baux en place ; MRB
174+ co = res["coherence"]; m = res["mrb"]
175+ ex["7"] = {"rne_marche": co["rne_marche"], "V_marche": co["V_marche"], "surevaluation": co["surevaluation_marche"], "V_contrat": res["V"],
176+ "V_mrb_contrat": m["V_contrat"], "V_mrb_marche": m["V_marche"], "ecart_rbp": b["ecart_marche"], "v": b["taux_vacance_pertes"],
177+ "ecart_rne": co["rne_marche"] - b["rne"], "V_mrb_vs_cap": m["V_contrat"] / res["V"] - 1}
178+ # 29.8 — régression et sensibilité croisée
179+ r = res["regression"]
180+ R24 = r["constante"] + r["etat"] * (2 - 3) + r["taille"] * (24 - 12)
181+ ex["8"] = {"R_24_etat2": R24, "R_6_etat4": r["constante"] + r["etat"] * (4 - 3) + r["taille"] * (6 - 12), "sigma_resid": r["sigma_resid"],
182+ "V_pire": fin.capitalisation_directe(b["rne"] * 0.95, res["R"] + 0.0025), "V_meilleur": fin.capitalisation_directe(b["rne"] * 1.05, res["R"] - 0.0025)}
183+ ex["8"]["var_pire"] = ex["8"]["V_pire"] / res["V"] - 1; ex["8"]["var_meilleur"] = ex["8"]["V_meilleur"] / res["V"] - 1
184+ ex["8"]["approx_pire"] = -0.05 - 0.0025 / res["R"]; ex["8"]["approx_meilleur"] = 0.05 + 0.0025 / res["R"]
185+ return ex
186+
187+
188+def _p(x, dec=0):
189+ return f"{x:,.{dec}f}".replace(",", " ")
190+
191+
192+def main():
193+ res = calculer(); b = res["base"]; tx = res["taux"]
194+ pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30)
195+ print("=== Cas 3 (synthétique) — revenus et RNE normalisé (traite.cas3) ===")
196+ for k in ["rbp_contrat", "rbp_marche", "ecart_marche", "vacance_pertes", "autres_revenus", "rbe", "depenses_hors_reserve", "reserve", "depenses_totales", "rne", "rne_par_logement"]:
197+ print(f" {k:24s} {_p(b[k], 2)}")
198+ print(f" ratio dépenses/RBE {b['ratio_depenses']:.4f}")
199+ print(b["detail"].round(2).to_string(index=False))
200+ print("\n=== Extraction du taux (40 ventes, 6-30 logements, 24 mois) ===")
201+ for k in ["n", "mediane", "moyenne", "ponderee_rne", "q1", "q3", "min", "max"]:
202+ print(f" {k:14s} {tx[k]}")
203+ print(" régression :", {k: round(v, 6) for k, v in res["regression"].items()})
204+ print(f" V (RNE / médiane) = {_p(b['rne'], 2)} / {res['R']:.5f} = {_p(res['V'], 2)} (arrondie {_p(res['V_arrondie'])}) ; V au taux de régression = {_p(res['V_reg'])}")
205+ print(f" 10 comparables les plus proches (médiane {res['mediane_proches']:.5f}) :")
206+ cols = ["id_vente", "date_vente", "quartier", "nb_logements", "annee_construction", "etat_1a5", "rne", "prix_vente", "taux_global", "mrb", "distance"]
207+ pr = res["proches"][cols].copy(); pr["date_vente"] = pr["date_vente"].dt.date; print(pr.round(3).to_string(index=False))
208+ print("\n=== Construction du taux ===")
209+ print(f" constante hypothécaire f (4,12 %, 480 mois, composition semestrielle) = {res['f']:.6f} ; M = {res['M']}")
210+ print(f" bande d'investissement : R_E impliqué par R = {res['R']:.5f} → {res['re_implicite']:.5f} ; grille R(R_E) = {{ {', '.join(f'{k:.2f}: {v:.5f}' for k, v in res['grille_re'].items())} }}")
211+ print(f" formule RCD : R = 1,20 × 0,75 × f = {res['R_rcd']:.5f} → V = {_p(res['V_rcd'])}")
212+ print(" Gordon :", {k: round(v, 5) for k, v in res["gordon"].items()})
213+ e = res["ellwood"]; print(" Ellwood/Akerson :", {k: (round(v, 6) if isinstance(v, float) else v) for k, v in e.items() if not k.startswith("composantes")})
214+ print(" composantes (Δ du cas) :", {k: round(v, 6) for k, v in e["composantes"].items()}); print(" composantes (Δ implicite) :", {k: round(v, 6) for k, v in e["composantes_marche"].items()})
215+ print(f" V (médiane des 10 proches {res['mediane_proches']:.5f}) = {_p(res['V_proches'])} ; V(Q3 des taux) = {_p(res['V_q1'])} ; V(Q1 des taux) = {_p(res['V_q3'])}")
216+ print("\n=== Multiplicateurs ===", {k: round(v, 4) for k, v in res["mrb"].items()})
217+ print("\n=== Cohérence RNE–taux ===", {k: round(v, 4) for k, v in res["coherence"].items()})
218+ print("=== Droits de mutation Montréal 2026 ===", {k: round(v, 4) for k, v in res["mutations"].items()})
219+ print("\n=== Sensibilité ===")
220+ print(res["sensibilite"].round(4).to_string(index=False)); print(" dérivée :", {k: round(v, 6) for k, v in res["derive"].items()})
221+ print("\n=== Exercices ===")
222+ for k, v in exercices(res).items():
223+ print(f" 29.{k} :", {kk: (round(vv, 6) if isinstance(vv, float) else vv) for kk, vv in v.items()})
224+
225+
226+if __name__ == "__main__":
227+ main()
added analysis/python/cases/ch30_dcf.py +297 −0
@@ -0,0 +1,297 @@
1+#!/usr/bin/env python3
2+"""Chapitre 30 — Flux actualisés et valeur terminale : exemple intégral du cas 3 (12 logements, Montréal,
3+DONNÉES SYNTHÉTIQUES). Tous les chiffres cités dans manuscript/chapters/ch30.tex et solutions/ch30.tex
4+proviennent de ce script, qui n'appelle que la bibliothèque commune (traite.cas3, traite.finance).
5+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch30_dcf.py
6+Les fonctions sont importées par les scripts de figures (analysis/python/figures/ch30_*.py) et par
7+analysis/python/tests/test_ch30.py."""
8+from __future__ import annotations
9+import sys, copy, pathlib
10+import numpy as np, pandas as pd
11+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
12+from traite import cas3, finance as fin
13+
14+ROOT = pathlib.Path(__file__).resolve().parents[3]
15+
16+# ----------------------------------------------------------------------------- cœur du cas 3
17+
18+def base() -> dict:
19+ """Tout le cas 3 (RNE normalisé, taux extraits, projection 11 ans, DCF, capitalisation directe, financement)."""
20+ return cas3.tout()
21+
22+def tableau_dcf(r: dict) -> pd.DataFrame:
23+ """Tableau DCF complet : projection + facteur d'actualisation + valeur actuelle + VA cumulée (années 1..T, puis T+1)."""
24+ h = r["hyp"]; T = h["horizon_ans"]; taux = h["taux_actualisation"]
25+ p = r["projection"].copy()
26+ p["facteur"] = [fin.facteur_va(taux, int(t)) if t <= T else np.nan for t in p["annee"]]
27+ p["va"] = p["flux_avant_dette"] * p["facteur"]
28+ p["va_cumulee"] = p["va"].cumsum()
29+ return p
30+
31+def dcf_sans_levier(r: dict) -> dict:
32+ """Flux de l'investisseur non endetté qui paie V_0 : TRI, VAN au taux d'actualisation, délai de récupération, MIRR."""
33+ d = r["dcf"]; h = r["hyp"]
34+ flux = [-d["valeur"]] + list(d["flux"]); flux[-1] += d["vt_nette"]
35+ taux_r = h["taux_actualisation"]
36+ return {"flux": flux, "tri": fin.tri(flux), "van_au_taux": fin.van(flux, taux_r),
37+ "delai_recuperation": fin.delai_recuperation(flux),
38+ "mirr": fin.tri_modifie(flux, taux_r, taux_r)}
39+
40+def taux_implicites(r: dict) -> dict:
41+ """Lecture croisée DCF / capitalisation directe : taux implicite du DCF, croissance implicite, TRI si l'on paie la
42+ valeur par capitalisation directe, croissance annualisée du RNE sur l'horizon."""
43+ d = r["dcf"]; h = r["hyp"]; p = r["projection"]; T = h["horizon_ans"]
44+ rne1 = float(p.loc[p["annee"] == 1, "rne"].iloc[0]); rneT1 = float(p.loc[p["annee"] == T + 1, "rne"].iloc[0])
45+ R_med = r["taux"]["mediane"]; taux_r = h["taux_actualisation"]; Rt = h["taux_terminal"]
46+ V_cap = r["valeur_capitalisation"]
47+ flux_cap = [-V_cap] + list(d["flux"]); flux_cap[-1] += d["vt_nette"]
48+ return {"R_implicite_dcf": rne1 / d["valeur"], "g_implicite_gordon_R_med": taux_r - R_med, "g_implicite_terminal": taux_r - Rt,
49+ "croissance_rne_annualisee": (rneT1 / rne1) ** (1 / T) - 1, "croissance_rbp_annualisee": (float(p.loc[p["annee"] == T + 1, "rbp"].iloc[0]) / float(p.loc[p["annee"] == 1, "rbp"].iloc[0])) ** (1 / T) - 1,
50+ "tri_si_paye_V_cap": fin.tri(flux_cap), "ecart_dcf_cap": d["valeur"] - V_cap, "ecart_dcf_cap_pct": d["valeur"] / V_cap - 1,
51+ "V_gordon_g_marche": fin.capitalisation_directe(rne1, fin.taux_gordon(taux_r, h["croissance_loyers_marche"])),
52+ "V_cap_au_taux_terminal": fin.capitalisation_directe(rne1, Rt)}
53+
54+def flux_levier(r: dict) -> dict:
55+ """Flux après service de la dette (financement de traite.cas3.financement) et TRI des fonds propres avec levier.
56+ Les CAPEX sont supportés par les fonds propres : la série passée à rendement_fonds_propres_levier est le flux avant dette (RNE − CAPEX)."""
57+ d = r["dcf"]; f = r["financement"]; h = r["hyp"]; T = h["horizon_ans"]
58+ hyp = f["hypotheque"]
59+ res = fin.rendement_fonds_propres_levier(d["flux"], d["valeur"], hyp, d["vt_nette"])
60+ apres_dette = fin.flux_apres_financement(d["flux"], hyp)
61+ rne = r["projection"].loc[r["projection"]["annee"] <= T, "rne"].tolist()
62+ tab = pd.DataFrame({"annee": range(1, T + 1), "rne": rne, "capex": [c for c in (r["projection"].loc[r["projection"]["annee"] <= T, "capex"])],
63+ "flux_avant_dette": d["flux"], "service_dette": [hyp.service_dette_annuel] * T, "flux_apres_dette": apres_dette,
64+ "rcd": [x / hyp.service_dette_annuel for x in rne]})
65+ tab["flux_fonds_propres"] = res["flux"][1:]
66+ signes = int(sum(1 for a, b in zip(res["flux"][:-1], res["flux"][1:]) if a * b < 0))
67+ mirr = fin.tri_modifie(res["flux"], h["financement"]["taux"], h["taux_actualisation"])
68+ return {**res, "tableau": tab, "service_dette": hyp.service_dette_annuel, "changements_signe": signes, "mirr": mirr,
69+ "produit_net_revente": d["vt_nette"] - res["solde_final"], "cash_on_cash_1": fin.rendement_cash_on_cash(apres_dette[0], res["mise_de_fonds"]),
70+ "rcd_1": fin.rcd(rne[0], hyp.service_dette_annuel), "rcd_min_horizon": min(tab["rcd"]), "taux_effectif_annuel_hyp": fin.taux_effectif_annuel(h["financement"]["taux"], 2),
71+ "taux_mensuel": hyp.taux_periodique, "taux_mensuel_si_composition_mensuelle": h["financement"]["taux"] / 12,
72+ "levier": "positif" if res["tri"] > r["hyp"]["taux_actualisation"] else "négatif"}
73+
74+# ----------------------------------------------------------------------------- sensibilité
75+
76+def valeur_avec(hyp_mod: dict, r: dict) -> dict:
77+ """Recalcule projection + DCF avec un dictionnaire d'hypothèses modifié (les données d'état locatif/dépenses sont inchangées)."""
78+ proj = cas3.projection_dcf(r["etat"], r["dep"], hyp_mod)
79+ return cas3.valeur_dcf(proj, hyp_mod) | {"projection": proj}
80+
81+def modifier(hyp: dict, **kw) -> dict:
82+ h = copy.deepcopy(hyp)
83+ for k, v in kw.items():
84+ if k == "capex_facteur":
85+ h["capex"] = {a: m * v for a, m in h["capex"].items()}
86+ else:
87+ h[k] = v
88+ return h
89+
90+def sensibilite_taux_terminal(r: dict, grille=(0.0425, 0.045, 0.0475, 0.05, 0.0525, 0.055, 0.0575)) -> pd.DataFrame:
91+ rows = []
92+ for Rt in grille:
93+ v = valeur_avec(modifier(r["hyp"], taux_terminal=Rt), r)
94+ rows.append({"taux_terminal": Rt, "vt_brute": v["vt_brute"], "vt_nette": v["vt_nette"], "va_terminale": v["va_terminale"], "valeur": v["valeur"],
95+ "part_terminale": v["part_terminale"], "ecart_pct_base": v["valeur"] / r["dcf"]["valeur"] - 1})
96+ return pd.DataFrame(rows)
97+
98+def surface_r_Rt(r: dict, taux=(0.0625, 0.065, 0.0675, 0.07, 0.0725, 0.075, 0.0775, 0.08, 0.0825), Rt=(0.0425, 0.045, 0.0475, 0.05, 0.0525, 0.055, 0.0575)) -> pd.DataFrame:
99+ """Surface V(r, R_T) ; lignes = r, colonnes = R_T."""
100+ M = np.zeros((len(taux), len(Rt)))
101+ for i, ri in enumerate(taux):
102+ for j, Rj in enumerate(Rt):
103+ M[i, j] = valeur_avec(modifier(r["hyp"], taux_actualisation=ri, taux_terminal=Rj), r)["valeur"]
104+ return pd.DataFrame(M, index=list(taux), columns=list(Rt))
105+
106+def surface_r_g(r: dict, taux=(0.0625, 0.065, 0.0675, 0.07, 0.0725, 0.075, 0.0775, 0.08, 0.0825), g=(0.02, 0.025, 0.03, 0.035, 0.04, 0.045, 0.05)) -> pd.DataFrame:
107+ """Surface V(r, g) où g est la croissance des loyers de marché (R_T inchangé)."""
108+ M = np.zeros((len(taux), len(g)))
109+ for i, ri in enumerate(taux):
110+ for j, gj in enumerate(g):
111+ M[i, j] = valeur_avec(modifier(r["hyp"], taux_actualisation=ri, croissance_loyers_marche=gj), r)["valeur"]
112+ return pd.DataFrame(M, index=list(taux), columns=list(g))
113+
114+def tornade(r: dict) -> pd.DataFrame:
115+ """Variation de la valeur pour un choc bas/haut de chaque intrant, les autres étant à leur valeur de base."""
116+ h = r["hyp"]; V0 = r["dcf"]["valeur"]
117+ chocs = [
118+ ("Taux d'actualisation r (−50 / +50 pb)", {"taux_actualisation": h["taux_actualisation"] - 0.005}, {"taux_actualisation": h["taux_actualisation"] + 0.005}),
119+ ("Taux terminal R_T (−25 / +25 pb)", {"taux_terminal": h["taux_terminal"] - 0.0025}, {"taux_terminal": h["taux_terminal"] + 0.0025}),
120+ ("Croissance des loyers de marché (2,5 / 4,5 %)", {"croissance_loyers_marche": 0.025}, {"croissance_loyers_marche": 0.045}),
121+ ("Croissance des loyers contractuels (2,0 / 4,0 %)", {"croissance_loyers_contrat": 0.020}, {"croissance_loyers_contrat": 0.040}),
122+ ("Rotation annuelle (5 / 25 %)", {"rotation_annuelle": 0.05}, {"rotation_annuelle": 0.25}),
123+ ("Inoccupation et pertes (2,9 / 4,9 %)", {"taux_inoccupation_pertes": 0.029}, {"taux_inoccupation_pertes": 0.049}),
124+ ("CAPEX (−25 / +25 %)", {"capex_facteur": 0.75}, {"capex_facteur": 1.25}),
125+ ("Croissance des dépenses (2,0 / 4,0 %)", {"croissance_depenses": 0.020}, {"croissance_depenses": 0.040}),
126+ ("Coûts de vente (1,5 / 3,5 %)", {"couts_vente": 0.015}, {"couts_vente": 0.035}),
127+ ]
128+ rows = []
129+ for nom, bas, haut in chocs:
130+ vb = valeur_avec(modifier(h, **bas), r)["valeur"]; vh = valeur_avec(modifier(h, **haut), r)["valeur"]
131+ rows.append({"intrant": nom, "valeur_bas": vb, "valeur_haut": vh, "delta_bas": vb - V0, "delta_haut": vh - V0, "amplitude": abs(vh - vb)})
132+ return pd.DataFrame(rows).sort_values("amplitude", ascending=False).reset_index(drop=True)
133+
134+def scenarios(r: dict) -> pd.DataFrame:
135+ """Trois scénarios narratifs : base ; encadrement serré des loyers ; hausse des taux.
136+ Deux lectures : (i) la valeur que conclurait l'évaluateur sous le scénario ; (ii) le sort de l'investisseur qui a payé la
137+ valeur de base et financé avec le prêt de base (TRI sans levier et TRI des fonds propres réalisés si le scénario se matérialise).
138+ Dans « Hausse des taux », le prêt de base est supposé renouvelé au taux initial + 100 pb dès l'origine (approximation du renouvellement, cf. ch. 31)."""
139+ h = r["hyp"]; V_base = r["dcf"]["valeur"]; f_base = r["financement"]
140+ defs = {
141+ "Base": ({}, 0.0),
142+ "Encadrement serré": ({"croissance_loyers_contrat": 0.020, "rotation_annuelle": 0.08, "croissance_loyers_marche": 0.030}, 0.0),
143+ "Hausse des taux": ({"taux_actualisation": h["taux_actualisation"] + 0.0075, "taux_terminal": h["taux_terminal"] + 0.005}, 0.01),
144+ }
145+ rows = []
146+ for nom, (kw, choc_taux_hyp) in defs.items():
147+ hm = modifier(h, **kw); v = valeur_avec(hm, r); p = v["projection"]
148+ hyp_pret = fin.Hypotheque(f_base["pret"], h["financement"]["taux"] + choc_taux_hyp, h["financement"]["amortissement_mois"])
149+ flux_sl = [-V_base] + list(v["flux"]); flux_sl[-1] += v["vt_nette"]
150+ lev = fin.rendement_fonds_propres_levier(v["flux"], V_base, hyp_pret, v["vt_nette"])
151+ rows.append({"scenario": nom, "r": hm["taux_actualisation"], "R_T": hm["taux_terminal"], "g_contrat": hm["croissance_loyers_contrat"], "g_marche": hm["croissance_loyers_marche"],
152+ "rotation": hm["rotation_annuelle"], "taux_hyp": hyp_pret.taux_nominal, "rne_1": float(p.loc[p["annee"] == 1, "rne"].iloc[0]),
153+ "rne_T1": v["rne_T1"], "vt_nette": v["vt_nette"], "valeur": v["valeur"], "part_terminale": v["part_terminale"],
154+ "ecart_pct_base": v["valeur"] / V_base - 1, "service_dette": hyp_pret.service_dette_annuel, "rcd_min": min(x / hyp_pret.service_dette_annuel for x in p.loc[p["annee"] <= hm["horizon_ans"], "rne"]),
155+ "tri_sans_levier_prix_base": fin.tri(flux_sl), "tri_fonds_propres_prix_base": lev["tri"], "flux_fp_min": min(lev["flux"][1:-1])})
156+ return pd.DataFrame(rows)
157+
158+# ----------------------------------------------------------------------------- vérification de la proposition DCF = capitalisation directe (Gordon)
159+
160+def verification_gordon(noi1: float = 100_000.0, r: float = 0.0725, g: float = 0.035, T: int = 10, couts_vente: float = 0.0) -> dict:
161+ """Sous croissance constante g et R_T = r − g, le DCF à horizon T redonne exactement NOI_1/(r − g), quel que soit T."""
162+ noi = fin.projeter_croissance(noi1, g, T + 1)
163+ res = fin.dcf(noi[:T], r, noi_suivant=noi[T], taux_terminal=fin.taux_gordon(r, g), couts_vente=couts_vente)
164+ return {"V_dcf": res.valeur, "V_gordon": fin.capitalisation_directe(noi1, fin.taux_gordon(r, g)), "part_terminale": res.va_terminale / res.valeur, "T": T}
165+
166+# ----------------------------------------------------------------------------- exercices (solutions/ch30.tex)
167+
168+def exercices() -> dict:
169+ out = {}
170+ # 30.1 Gordon
171+ out["ex1"] = {"R": fin.taux_gordon(0.0725, 0.035), "V": fin.capitalisation_directe(100_000, fin.taux_gordon(0.0725, 0.035)), "g_implicite": 0.0725 - 0.04415, "r_implicite": 0.04415 + 0.035}
172+ # 30.2 valeur terminale
173+ vtb, vtn = fin.valeur_terminale(159_658.38, 0.05, 0.025)
174+ out["ex2"] = {"vt_brute": vtb, "vt_nette": vtn, "va_10": vtn * fin.facteur_va(0.0725, 10), "va_11_erronee": vtn * fin.facteur_va(0.0725, 11), "ecart": vtn * fin.facteur_va(0.0725, 10) - vtn * fin.facteur_va(0.0725, 11),
175+ "vt_nette_55": fin.valeur_terminale(159_658.38, 0.055, 0.025)[1]}
176+ # 30.3 DCF 5 ans
177+ flux5 = [62_000, 64_000, 20_000, 68_000, 70_000]; noi6 = 72_000
178+ res5 = fin.dcf(flux5, 0.07, noi_suivant=noi6, taux_terminal=0.05, couts_vente=0.03)
179+ prix = 1_300_000
180+ f5 = [-prix] + flux5; f5[-1] += res5.valeur_terminale_nette
181+ out["ex3"] = {"va_flux": res5.va_flux, "vt_brute": res5.valeur_terminale_brute, "vt_nette": res5.valeur_terminale_nette, "va_vt": res5.va_terminale, "valeur": res5.valeur,
182+ "van_prix": fin.van(f5, 0.07), "tri_prix": fin.tri(f5), "part_terminale": res5.va_terminale / res5.valeur}
183+ # 30.4 taux hypothécaire canadien
184+ i = 0.0412
185+ out["ex4"] = {"i_m_can": fin.taux_mensuel_canadien(i), "i_m_us": i / 12, "eff_can": fin.taux_effectif_annuel(i, 2), "eff_us": fin.taux_effectif_annuel(i, 12),
186+ "paiement_can": fin.Hypotheque(1_000_000, i, 300).paiement, "paiement_us": fin.Hypotheque(1_000_000, i, 300, composition=12).paiement}
187+ out["ex4"]["ecart_annuel"] = (out["ex4"]["paiement_us"] - out["ex4"]["paiement_can"]) * 12
188+ # 30.5 cohérence Gordon
189+ out["ex5"] = {T: verification_gordon(90_000, 0.0725, 0.035, T) for T in (5, 10, 20)}
190+ out["ex5_cv"] = verification_gordon(90_000, 0.0725, 0.035, 10, couts_vente=0.025)
191+ # 30.6 variante du cas 3
192+ r = base()
193+ v6 = valeur_avec(modifier(r["hyp"], taux_actualisation=0.0775, taux_terminal=0.055), r)
194+ out["ex6"] = {"valeur": v6["valeur"], "va_flux": v6["va_flux"], "va_terminale": v6["va_terminale"], "vt_nette": v6["vt_nette"], "part_terminale": v6["part_terminale"], "ecart_pct": v6["valeur"] / r["dcf"]["valeur"] - 1,
195+ "R_implicite": float(v6["projection"].loc[0, "rne"]) / v6["valeur"]}
196+ # 30.7 TRI avec levier (données du cas 3)
197+ lev = flux_levier(r)
198+ out["ex7"] = {"flux": lev["flux"], "tri": lev["tri"], "mirr": lev["mirr"], "changements_signe": lev["changements_signe"], "mise": lev["mise_de_fonds"], "solde_final": lev["solde_final"],
199+ "van_8": fin.van(lev["flux"], 0.08), "van_12": fin.van(lev["flux"], 0.12)}
200+ # 30.8 écart DCF / capitalisation directe
201+ out["ex8"] = taux_implicites(r)
202+ return out
203+
204+
205+# ----------------------------------------------------------------------------- corps de tableaux LaTeX (collés dans ch30.tex)
206+
207+def _n(x, dec=0):
208+ """Nombre brut pour une colonne S de siunitx (point décimal, pas de séparateur)."""
209+ return f"{x:.{dec}f}"
210+
211+def latex_tables():
212+ r = base(); h = r["hyp"]; T = h["horizon_ans"]; t = tableau_dcf(r); d = r["dcf"]
213+ print("% --- tab:30:projection (revenus et dépenses, années 1-11)")
214+ for _, x in t.iterrows():
215+ print(f"{int(x.annee)} & {int(x.exercice)} & {_n(x.rbp)} & {_n(x.rbp_marche)} & {_n(x.rbe)} & {_n(x.depenses)} & {_n(x.reserve)} & {_n(x.rne)} \\\\")
216+ print("% --- tab:30:dcf (flux, facteur, VA)")
217+ for _, x in t[t.annee <= T].iterrows():
218+ print(f"{int(x.annee)} & {_n(x.rne)} & {_n(x.capex)} & {_n(x.flux_avant_dette)} & {_n(x.facteur, 6)} & {_n(x.va)} & {_n(x.va_cumulee)} \\\\")
219+ print("% --- tab:30:levier")
220+ lv = flux_levier(r)
221+ for _, x in lv["tableau"].iterrows():
222+ print(f"{int(x.annee)} & {_n(x.rne)} & {_n(x.flux_avant_dette)} & {_n(x.service_dette)} & {_n(x.flux_apres_dette)} & {_n(x.rcd, 2)} & {_n(x.flux_fonds_propres)} \\\\")
223+ print("% --- tab:30:sens-rt")
224+ for _, x in sensibilite_taux_terminal(r).iterrows():
225+ print(f"{_n(x.taux_terminal*100, 2)} & {_n(x.vt_brute)} & {_n(x.vt_nette)} & {_n(x.va_terminale)} & {_n(x.valeur)} & {_n(x.part_terminale*100, 1)} & {_n(x.ecart_pct_base*100, 1)} \\\\")
226+ print("% --- tab:30:tornade")
227+ for _, x in tornade(r).iterrows():
228+ print(f"{x.intrant} & {_n(x.valeur_bas)} & {_n(x.valeur_haut)} & {_n(x.delta_bas)} & {_n(x.delta_haut)} & {_n(x.amplitude)} \\\\")
229+ print("% --- tab:30:scenarios (colonnes = scénarios)")
230+ s = scenarios(r).set_index("scenario")
231+ lignes = [("Taux d'actualisation $r$ (\\%)", "r", 2, 100), ("Taux terminal $R_T$ (\\%)", "R_T", 2, 100), ("Croissance des loyers contractuels (\\%)", "g_contrat", 1, 100),
232+ ("Croissance des loyers de marché (\\%)", "g_marche", 1, 100), ("Rotation annuelle (\\%)", "rotation", 0, 100), ("Taux hypothécaire (\\%)", "taux_hyp", 2, 100),
233+ ("$\\NOI_{11}$ (\\$)", "rne_T1", 0, 1), ("Valeur terminale nette (\\$)", "vt_nette", 0, 1), ("Valeur $V_0$ (\\$)", "valeur", 0, 1), ("Écart à la base (\\%)", "ecart_pct_base", 1, 100),
234+ ("Part de la valeur terminale (\\%)", "part_terminale", 1, 100), ("Service de la dette (\\$)", "service_dette", 0, 1), ("RCD minimal sur l'horizon", "rcd_min", 2, 1),
235+ ("TRI sans levier si payé \\dollars{2208566} (\\%)", "tri_sans_levier_prix_base", 2, 100), ("TRI des fonds propres si payé \\dollars{2208566} (\\%)", "tri_fonds_propres_prix_base", 2, 100), ("Flux des fonds propres le plus bas (\\$)", "flux_fp_min", 0, 1)]
236+ for lab, col, dec, mult in lignes:
237+ print(lab + " & " + " & ".join(_n(s.loc[sc, col] * mult, dec) for sc in ["Base", "Encadrement serré", "Hausse des taux"]) + " \\\\")
238+ print("% --- surface V(r,R_T) en milliers")
239+ S = surface_r_Rt(r)
240+ for ri, row in S.iterrows():
241+ print(_n(ri*100, 2) + " & " + " & ".join(_n(v/1000, 0) for v in row.values) + " \\\\")
242+
243+# ----------------------------------------------------------------------------- impression
244+
245+def _d(x): return f"{x:,.0f}".replace(",", " ") + " $"
246+def _p(x, d=2): return f"{x*100:.{d}f}".replace(".", ",") + " %"
247+
248+def main():
249+ pd.set_option("display.width", 250); pd.set_option("display.max_columns", 40); pd.set_option("display.float_format", lambda x: f"{x:,.4f}")
250+ r = base(); h = r["hyp"]; b = r["base"]; tx = r["taux"]; d = r["dcf"]
251+ print("=" * 100); print("CHAPITRE 30 — CAS 3 (DONNÉES SYNTHÉTIQUES) : FLUX ACTUALISÉS"); print("=" * 100)
252+ print("\n== Hypothèses (data/synthetic/cas3_montreal_hypotheses_dcf.json)")
253+ for k in ["date_evaluation", "horizon_ans", "taux_inoccupation_pertes", "croissance_loyers_contrat", "croissance_loyers_marche", "rotation_annuelle", "croissance_depenses", "taux_actualisation", "taux_terminal", "couts_vente", "capex", "autres_revenus_annuels"]:
254+ print(f" {k:32s} {h[k]}")
255+ print(f" financement : {h['financement']}")
256+ print("\n== Année 1 (RNE normalisé, cf. ch. 27-28)")
257+ print(f" RBP contractuel {_d(b['rbp_contrat'])} ; RBP marché {_d(b['rbp_marche'])} ; écart {_d(b['ecart_marche'])} ({_p(b['ecart_marche']/b['rbp_marche'],1)} sous le marché)")
258+ print(f" Inoccupation et pertes {_p(b['taux_vacance_pertes'],1)} = {_d(b['vacance_pertes'])} ; autres revenus {_d(b['autres_revenus'])} ; RBE {_d(b['rbe'])}")
259+ print(f" Dépenses hors réserve {_d(b['depenses_hors_reserve'])} ; réserve {_d(b['reserve'])} ; RNE {_d(b['rne'])} ({_d(b['rne_par_logement'])}/log.) ; ratio dépenses {_p(b['ratio_depenses'],1)}")
260+ print("\n== Taux extraits des 40 comparables (ch. 29)")
261+ print(f" n={tx['n']} ; médiane {_p(tx['mediane'],3)} ; moyenne {_p(tx['moyenne'],3)} ; pondérée RNE {_p(tx['ponderee_rne'],3)} ; Q1 {_p(tx['q1'],3)} ; Q3 {_p(tx['q3'],3)} ; min {_p(tx['min'],2)} ; max {_p(tx['max'],2)}")
262+ print(f" Valeur par capitalisation directe au taux médian : {_d(r['valeur_capitalisation'])}")
263+ print("\n== Tableau DCF complet (années 1-10 + année 11 pour la VT)")
264+ t = tableau_dcf(r)
265+ print(t[["annee", "exercice", "rbp", "rbp_marche", "rbe", "depenses", "reserve", "rne", "capex", "flux_avant_dette", "facteur", "va", "va_cumulee"]].to_string(index=False))
266+ print("\n== Valeur terminale et valeur")
267+ print(f" RNE_11 {_d(d['rne_T1'])} ; R_T {_p(h['taux_terminal'])} ; VT brute {_d(d['vt_brute'])} ; coûts de vente {_p(h['couts_vente'],1)} = {_d(d['vt_brute']-d['vt_nette'])} ; VT nette {_d(d['vt_nette'])}")
268+ print(f" Facteur (1+r)^-10 = {fin.facteur_va(h['taux_actualisation'], 10):.6f} ; VA de la VT {_d(d['va_terminale'])} ; VA des flux {_d(d['va_flux'])}")
269+ print(f" VALEUR DCF = {_d(d['valeur'])} ; part de la VT {_p(d['part_terminale'],1)} ; par logement {_d(d['valeur']/12)}")
270+ ti = taux_implicites(r)
271+ print("\n== Lecture croisée DCF / capitalisation directe")
272+ for k, v in ti.items(): print(f" {k:32s} {(_p(v,3) if abs(v) < 1 else _d(v))}")
273+ sl = dcf_sans_levier(r)
274+ print("\n== Investisseur non endetté payant V_0")
275+ print(f" TRI {_p(sl['tri'],4)} (= r par construction) ; VAN(r) {sl['van_au_taux']:.6f} $ ; délai de récupération {sl['delai_recuperation']:.2f} ans ; MIRR(r,r) {_p(sl['mirr'],4)}")
276+ lv = flux_levier(r); f = r["financement"]
277+ print("\n== Financement (traite.cas3.financement) et flux après dette")
278+ print(f" Prêt max RPV 75 % {_d(f['pret_rpv'])} ; prêt max RCD 1,20 {_d(f['pret_rcd'])} ; prêt retenu {_d(f['pret'])} (contrainte {f['contrainte']}) ; RPV effectif {_p(f['rpv'])}")
279+ print(f" Taux nominal {_p(h['financement']['taux'])} comp. semestrielle → mensuel {_p(lv['taux_mensuel'],5)} (vs {_p(lv['taux_mensuel_si_composition_mensuelle'],5)} si mensuelle) ; effectif annuel {_p(lv['taux_effectif_annuel_hyp'],4)}")
280+ print(f" Paiement mensuel {_d(f['hypotheque'].paiement)} ; service de la dette {_d(lv['service_dette'])} ; constante {_p(f['constante'],3)} ; RCD année 1 {lv['rcd_1']:.3f} ; RCD min sur l'horizon {lv['rcd_min_horizon']:.3f}")
281+ print(f" Mise de fonds {_d(lv['mise_de_fonds'])} ; solde après 120 paiements {_d(lv['solde_final'])} ; produit net de revente {_d(lv['produit_net_revente'])}")
282+ print(lv["tableau"].to_string(index=False))
283+ print(f" Flux des fonds propres : {[round(x) for x in lv['flux']]}")
284+ print(f" Changements de signe {lv['changements_signe']} ; TRI fonds propres {_p(lv['tri'],3)} ; MIRR (fin. {_p(h['financement']['taux'])}, réinv. {_p(h['taux_actualisation'])}) {_p(lv['mirr'],3)} ; cash-on-cash an 1 {_p(lv['cash_on_cash_1'],2)} ; levier {lv['levier']}")
285+ print("\n== Sensibilité au taux terminal")
286+ print(sensibilite_taux_terminal(r).to_string(index=False))
287+ print("\n== Surface V(r, R_T)"); print(surface_r_Rt(r).round(0).to_string())
288+ print("\n== Surface V(r, g_marché)"); print(surface_r_g(r).round(0).to_string())
289+ print("\n== Tornade"); print(tornade(r).to_string(index=False))
290+ print("\n== Scénarios"); print(scenarios(r).to_string(index=False))
291+ print("\n== Vérification de la proposition (croissance constante, R_T = r − g)")
292+ for T in (5, 10, 30): print(f" T={T:2d} : {verification_gordon(T=T)}")
293+ print("\n== Exercices")
294+ for k, v in exercices().items(): print(f" {k}: {v}")
295+
296+if __name__ == "__main__":
297+ latex_tables() if "--latex" in sys.argv else main()
added analysis/python/cases/ch47_dossier.py +417 −0
@@ -0,0 +1,417 @@
1+#!/usr/bin/env python3
2+"""Chapitre 47 — Cas 3 : immeuble de 12 logements à Montréal (DONNÉES SYNTHÉTIQUES, immeuble fictif).
3+
4+Dossier complet : revenus, dépenses normalisées, RNE, extraction du taux global, capitalisation directe, DCF 10 ans,
5+comparaison par unité (prix par logement, MRB), financement (RPV / RCD, APH Select), scénarios de taux au renouvellement,
6+Monte-Carlo (10 000 tirages, graine 20261031), tornade, réconciliation, droits de mutation, exercices.
7+
8+Les calculs de base viennent de `traite.cas3` (partagés avec les chapitres 26-31, 33, 34, 40) ; ce script n'y ajoute que les
9+calculs propres au dossier. Il IMPRIME tous les chiffres cités dans manuscript/chapters/ch47.tex.
10+
11+Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch47_dossier.py [--n 10000]
12+"""
13+from __future__ import annotations
14+import sys, pathlib, argparse, math
15+import numpy as np, pandas as pd
16+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
17+from traite import cas3, finance as fin, comparables as cp
18+
19+ROOT = pathlib.Path(__file__).resolve().parents[3]
20+GRAINE = 20261031
21+DATE_EVAL = pd.Timestamp("2026-09-01")
22+
23+# ----------------------------------------------------------------------------- hypothèses propres au chapitre 47
24+# Comparaison par unité (ajustements déclarés ; voir §47.10 du chapitre)
25+AJ_ETAT_PAR_POINT = 0.03 # +3 % du prix par point d'état (hypothèse pédagogique, cohérente avec la loi de génération du jeu)
26+AJ_TEMPS_ANNUEL = 0.0 # aucun ajustement temporel (effet non significatif sur l'échantillon ; marché stable)
27+ECHELLES_SEL = {"nb_logements": 6.0, "annee_construction": 15.0, "mois": 12.0, "etat_1a5": 1.0}
28+POIDS_SEL = {"nb_logements": 2.0, "annee_construction": 1.0, "mois": 1.0, "etat_1a5": 1.0}
29+K_COMPARABLES = 10
30+
31+# Réconciliation (pondérations raisonnées, §47.13)
32+POIDS_RECONCILIATION = {"capitalisation": 0.45, "dcf": 0.35, "comparaison_unite": 0.20}
33+ARRONDI_VALEUR = 10_000
34+
35+# Droits sur les mutations immobilières, Ville de Montréal, 2026 (bornes supérieures des tranches, taux)
36+TRANCHES_MUTATION_MTL_2026 = [(62_900, 0.005), (315_000, 0.01), (552_300, 0.015), (1_104_700, 0.02), (2_136_500, 0.025), (3_113_000, 0.035), (math.inf, 0.04)]
37+
38+# Monte-Carlo : lois déclarées (§47.12)
39+MC_LOIS = {
40+ "inoccupation_pertes": ("lognormale", {"mediane": 0.039, "sigma_log": 0.30}),
41+ "croissance_loyers_marche": ("normale", {"mu": 0.035, "sigma": 0.010}),
42+ "rotation_annuelle": ("triangulaire", {"min": 0.08, "mode": 0.15, "max": 0.25}),
43+ "croissance_depenses": ("normale", {"mu": 0.030, "sigma": 0.0075}),
44+ "taux_actualisation": ("normale", {"mu": 0.0725, "sigma": 0.005}),
45+ "ecart_terminal": ("normale", {"mu": 0.0225, "sigma": 0.004}), # R_T = r − écart ; plancher R_T ≥ 3,5 %
46+ "facteur_capex": ("lognormale", {"mediane": 1.0, "sigma_log": 0.20}), # un facteur indépendant par événement CAPEX
47+}
48+R_T_PLANCHER = 0.035
49+
50+# ----------------------------------------------------------------------------- utilitaires
51+
52+def droits_mutation_montreal(base: float, tranches=TRANCHES_MUTATION_MTL_2026) -> float:
53+ """Droit sur les mutations immobilières (barème progressif par tranches, Ville de Montréal 2026)."""
54+ droit, bas = 0.0, 0.0
55+ for haut, taux in tranches:
56+ if base <= bas: break
57+ droit += (min(base, haut) - bas) * taux
58+ bas = haut
59+ return droit
60+
61+def arrondir(x: float, pas: int = ARRONDI_VALEUR) -> float:
62+ return round(x / pas) * pas
63+
64+# ----------------------------------------------------------------------------- comparaison par unité
65+
66+def ols_prix_par_logement(comps: pd.DataFrame) -> dict:
67+ """Régression auxiliaire ln(prix par logement) sur état, taille, niveau de loyer, temps et quartier (extraction d'ajustements)."""
68+ c = comps.copy()
69+ c["mois"] = (DATE_EVAL - c["date_vente"]).dt.days / 30.4375
70+ Q = pd.get_dummies(c["quartier"]).astype(float).drop(columns=["Rosemont"])
71+ X = np.column_stack([np.ones(len(c)), c["etat_1a5"] - 3, c["nb_logements"] - 12, np.log(c["loyer_mensuel_moyen"] / 1150.0), c["mois"] / 12.0, Q.values])
72+ noms = ["constante", "etat", "taille", "ln_loyer", "annees"] + list(Q.columns)
73+ y = np.log(c["prix_par_logement"].values)
74+ b, *_ = np.linalg.lstsq(X, y, rcond=None)
75+ e = y - X @ b; k = X.shape[1]; s2 = float(e @ e) / (len(c) - k)
76+ se = np.sqrt(np.diag(s2 * np.linalg.inv(X.T @ X)))
77+ r2 = 1 - float(e @ e) / float(((y - y.mean()) ** 2).sum())
78+ return {"coef": dict(zip(noms, b)), "se": dict(zip(noms, se)), "r2": r2, "sigma": math.sqrt(s2), "n": len(c)}
79+
80+def comparaison_par_unite(d: dict, base: dict) -> dict:
81+ comps = d["comps"].copy(); sujet = d["sujet"]
82+ comps["mois"] = (DATE_EVAL - comps["date_vente"]).dt.days / 30.4375
83+ ols = ols_prix_par_logement(comps)
84+ cf = ols["coef"]
85+ suj = {"nb_logements": sujet["nb_logements"], "annee_construction": sujet["annee_construction"], "mois": 0.0, "etat_1a5": sujet["etat_1a5"]}
86+ comps["distance"] = [cp.distance_attributs(suj, r, ECHELLES_SEL, POIDS_SEL) for r in comps.to_dict("records")]
87+ sel = comps.sort_values("distance").head(K_COMPARABLES).copy()
88+ loyer_sujet = base["rbp_contrat"] / (12 * sujet["nb_logements"])
89+ objets = []; lignes = []
90+ for r in sel.to_dict("records"):
91+ p = r["prix_par_logement"]
92+ # ajustement de transaction (marché/temps) en pourcentage, séquentiel
93+ base_tx = cp.prix_ajuste_multiplicatif(p, [(1 + AJ_TEMPS_ANNUEL) ** (r["mois"] / 12.0) - 1])
94+ # ajustements de propriété en dollars par logement, signe du point de vue du sujet
95+ a_loc = base_tx * (math.exp(-cf.get(r["quartier"], 0.0)) - 1) # sujet à Rosemont (référence)
96+ a_taille = base_tx * (math.exp(cf["taille"] * (sujet["nb_logements"] - r["nb_logements"])) - 1)
97+ a_etat = base_tx * AJ_ETAT_PAR_POINT * (sujet["etat_1a5"] - r["etat_1a5"])
98+ a_loyer = base_tx * ((loyer_sujet / r["loyer_mensuel_moyen"]) ** cf["ln_loyer"] - 1)
99+ aj = {"aj_localisation": a_loc, "aj_taille": a_taille, "aj_etat": a_etat, "aj_loyer": a_loyer}
100+ o = cp.ComparableAjuste(r["id_vente"], base_tx, aj); objets.append(o)
101+ lignes.append({"id": r["id_vente"], "date": r["date_vente"].date().isoformat(), "quartier": r["quartier"], "n": r["nb_logements"], "annee": r["annee_construction"],
102+ "etat": r["etat_1a5"], "loyer_moyen": r["loyer_mensuel_moyen"], "prix": r["prix_vente"], "prix_par_logement": p, "mrb": r["mrb"], "taux": r["taux_global"],
103+ "distance": r["distance"], **aj, "prix_ajuste": o.prix_ajuste, "brut_pct": o.brut_pct, "net_pct": o.net_pct})
104+ grille = pd.DataFrame(lignes)
105+ w = [float(x) for x in cp.poids_inverse_brut(objets)]
106+ rec = {k: (float(v) if isinstance(v, (int, float, np.floating)) else v) for k, v in cp.reconciliation(objets, w).items()}
107+ indication = float(rec["valeur_ponderee"] * sujet["nb_logements"])
108+ mrb_med = float(sel["mrb"].median()); mrb_q1 = float(sel["mrb"].quantile(0.25)); mrb_q3 = float(sel["mrb"].quantile(0.75))
109+ indication_mrb = mrb_med * base["rbp_contrat"]
110+ return {"ols": ols, "grille": grille, "poids": w, "reconciliation": rec, "prix_par_logement_retenu": rec["valeur_ponderee"], "indication": indication,
111+ "loyer_moyen_sujet": loyer_sujet, "mrb_mediane": mrb_med, "mrb_q1": mrb_q1, "mrb_q3": mrb_q3, "indication_mrb": indication_mrb,
112+ "mrb_sujet_cap": None, "selection": sel}
113+
114+# ----------------------------------------------------------------------------- financement et scénarios de taux
115+
116+def scenarios_renouvellement(r: dict, hausses_pb=(0, 100, 200, 300)) -> pd.DataFrame:
117+ """Au terme de 5 ans, le solde est refinancé au taux initial + hausse, sur l'amortissement résiduel (420 mois)."""
118+ f = r["financement"]; h = f["hypotheque"]; p = r["projection"]; terme = r["hyp"]["financement"]["terme_ans"]
119+ solde = h.solde(terme * 12); rows = []
120+ for pb in hausses_pb:
121+ t = h.taux_nominal + pb / 10_000; h2 = fin.Hypotheque(solde, t, h.amortissement_mois - terme * 12)
122+ sd = h2.service_dette_annuel
123+ rcd = {f"rcd_{a}": float(p.loc[p["annee"] == a, "rne"].iloc[0]) / sd for a in range(terme + 1, r["hyp"]["horizon_ans"] + 1)}
124+ flux6 = float(p.loc[p["annee"] == terme + 1, "rne"].iloc[0]) - sd
125+ rows.append({"hausse_pb": pb, "taux": t, "solde_renouvele": solde, "service_dette": sd, "paiement_mensuel": h2.paiement, "flux_apres_dette_an6": flux6, **rcd})
126+ return pd.DataFrame(rows)
127+
128+def rcd_par_annee(r: dict, sc: pd.DataFrame) -> pd.DataFrame:
129+ """RCD par année 1-10 : années 1-5 au taux initial, années 6-10 selon le scénario de renouvellement."""
130+ p = r["projection"]; f = r["financement"]; terme = r["hyp"]["financement"]["terme_ans"]
131+ out = {"annee": list(range(1, r["hyp"]["horizon_ans"] + 1))}
132+ for _, s in sc.iterrows():
133+ col = []
134+ for a in out["annee"]:
135+ sd = f["service_dette"] if a <= terme else s["service_dette"]
136+ col.append(float(p.loc[p["annee"] == a, "rne"].iloc[0]) / sd)
137+ out[f"+{int(s['hausse_pb'])} pb"] = col
138+ return pd.DataFrame(out)
139+
140+def aph_select(r: dict, valeur: float, rpv_max=0.85, rcd_min=1.10, amortissements=(480, 540, 600), prime_pct=None) -> pd.DataFrame:
141+ """Variante APH Select (SCHL) : RPV 85 % (50 points, existant), RCD 1,10, amortissement 40/45/50 ans, taux assuré 4,12 %."""
142+ f = r["hyp"]["financement"]; noi = r["base"]["rne"]; rows = []
143+ for am in amortissements:
144+ p_rpv = fin.pret_max_par_rpv(valeur, rpv_max); p_rcd = fin.pret_max_par_rcd(noi, rcd_min, f["taux"], am)
145+ pret = min(p_rpv, p_rcd); h = fin.Hypotheque(pret, f["taux"], am)
146+ rpv = pret / valeur
147+ prime = 0.0260 if rpv <= 0.65 else 0.0285 if rpv <= 0.70 else 0.0335 if rpv <= 0.75 else 0.0435 if rpv <= 0.80 else 0.0535 if rpv <= 0.85 else 0.0590
148+ surprime = 0.0025 * max(0, (am / 12 - 25) / 5)
149+ rows.append({"amortissement_ans": am // 12, "pret_rpv": p_rpv, "pret_rcd": p_rcd, "pret": pret, "contrainte": "RPV" if p_rpv <= p_rcd else "RCD", "rpv": rpv,
150+ "service_dette": h.service_dette_annuel, "rcd": noi / h.service_dette_annuel, "mise_de_fonds": valeur - pret, "prime_pct": prime + surprime, "prime": (prime + surprime) * pret})
151+ return pd.DataFrame(rows)
152+
153+# ----------------------------------------------------------------------------- Monte-Carlo
154+
155+def projeter_vectorise(etat: pd.DataFrame, hyp: dict, base: dict, v: np.ndarray, g_m: np.ndarray, rot: np.ndarray, g_d: np.ndarray, fac_capex: np.ndarray, g_c: float | None = None) -> dict:
156+ """Projection vectorisée (N tirages) reproduisant exactement `cas3.projection_dcf` pour les paramètres de base.
157+ fac_capex : tableau (N, nb_événements CAPEX) de facteurs multiplicatifs."""
158+ T = hyp["horizon_ans"]; N = len(v); n_log = len(etat)
159+ g_c = hyp["croissance_loyers_contrat"] if g_c is None else g_c
160+ loyers = np.tile(etat["loyer_mensuel_contrat"].astype(float).values, (N, 1)); marche = np.tile(etat["loyer_mensuel_marche"].astype(float).values, (N, 1))
161+ autres = hyp["autres_revenus_annuels"]; res0 = base["reserve"]
162+ de0_hors_gestion = base["depenses_hors_reserve"] - 0.04 * base["rbe"]
163+ rbe1 = loyers.sum(1) * 12 * (1 - v) + autres
164+ de0 = de0_hors_gestion + 0.04 * rbe1 # gestion imputée = 4 % du RBE de première année
165+ k = np.round(rot * n_log).astype(int)
166+ annee0 = int(hyp["date_evaluation"][:4]); evenements = sorted(hyp["capex"].items())
167+ rne = np.zeros((N, T + 1)); capex = np.zeros((N, T)); rbp = np.zeros((N, T + 1))
168+ for t in range(1, T + 2):
169+ if t > 1:
170+ loyers = loyers * (1 + g_c); marche = marche * (1 + g_m[:, None])
171+ rang = np.argsort(np.argsort(loyers / marche, axis=1), axis=1)
172+ loyers = np.where(rang < k[:, None], marche, loyers)
173+ rbp[:, t - 1] = loyers.sum(1) * 12
174+ rbe = rbp[:, t - 1] * (1 - v) + autres * (1 + g_d) ** (t - 1)
175+ de = de0 * (1 + g_d) ** (t - 1)
176+ rne[:, t - 1] = rbe - de - res0
177+ if t <= T:
178+ for j, (an, montant) in enumerate(evenements):
179+ if int(an) == annee0 + t - 1: capex[:, t - 1] += montant * fac_capex[:, j]
180+ flux = rne[:, :T] - capex
181+ return {"rne": rne, "capex": capex, "flux": flux, "rbp": rbp}
182+
183+def valeur_vectorisee(proj: dict, r: np.ndarray, R_T: np.ndarray, cv: float) -> dict:
184+ T = proj["flux"].shape[1]
185+ t = np.arange(1, T + 1)
186+ disc = (1 + r[:, None]) ** (-t)
187+ va_flux = (proj["flux"] * disc).sum(1)
188+ vt_nette = proj["rne"][:, T] / R_T * (1 - cv)
189+ va_vt = vt_nette / (1 + r) ** T
190+ return {"valeur": va_flux + va_vt, "va_flux": va_flux, "va_terminale": va_vt, "vt_nette": vt_nette}
191+
192+def tirer(n: int, graine: int, nb_capex: int, lois=MC_LOIS) -> dict:
193+ rng = np.random.default_rng(graine)
194+ def tir(nom, taille):
195+ typ, p = lois[nom]
196+ if typ == "lognormale": return np.exp(np.log(p["mediane"]) + p["sigma_log"] * rng.standard_normal(taille))
197+ if typ == "normale": return p["mu"] + p["sigma"] * rng.standard_normal(taille)
198+ if typ == "triangulaire": return rng.triangular(p["min"], p["mode"], p["max"], taille)
199+ raise ValueError(typ)
200+ x = {"inoccupation_pertes": np.clip(tir("inoccupation_pertes", n), 0.0, 0.30), "croissance_loyers_marche": tir("croissance_loyers_marche", n),
201+ "rotation_annuelle": tir("rotation_annuelle", n), "croissance_depenses": tir("croissance_depenses", n), "taux_actualisation": np.clip(tir("taux_actualisation", n), 0.04, None),
202+ "ecart_terminal": tir("ecart_terminal", n), "facteur_capex": tir("facteur_capex", (n, nb_capex))}
203+ x["taux_terminal"] = np.maximum(x["taux_actualisation"] - x["ecart_terminal"], R_T_PLANCHER)
204+ return x
205+
206+def monte_carlo(r: dict, n: int = 10_000, graine: int = GRAINE) -> dict:
207+ etat, hyp, base = r["etat"], r["hyp"], r["base"]
208+ x = tirer(n, graine, len(hyp["capex"]))
209+ proj = projeter_vectorise(etat, hyp, base, x["inoccupation_pertes"], x["croissance_loyers_marche"], x["rotation_annuelle"], x["croissance_depenses"], x["facteur_capex"])
210+ val = valeur_vectorisee(proj, x["taux_actualisation"], x["taux_terminal"], hyp["couts_vente"])
211+ V = val["valeur"]
212+ sd = r["financement"]["service_dette"]; terme = hyp["financement"]["terme_ans"]
213+ rcd = proj["rne"] / sd # RCD par année au prêt de base (service de la dette constant sur le terme)
214+ rcd_min5 = rcd[:, :terme].min(1)
215+ # renouvellement à l'année 6 : taux initial + 200 pb sur le solde résiduel
216+ h = r["financement"]["hypotheque"]; h6 = fin.Hypotheque(h.solde(terme * 12), h.taux_nominal + 0.02, h.amortissement_mois - terme * 12)
217+ rcd6_choc = proj["rne"][:, terme] / h6.service_dette_annuel
218+ q = lambda a, p: float(np.quantile(a, p))
219+ res = {"n": n, "graine": graine, "tirages": x, "valeur": V, "rne": proj["rne"], "rcd": rcd,
220+ "stats": {"moyenne": float(V.mean()), "ecart_type": float(V.std(ddof=1)), "cv": float(V.std(ddof=1) / V.mean()), "mediane": q(V, 0.5),
221+ "q05": q(V, 0.05), "q25": q(V, 0.25), "q75": q(V, 0.75), "q95": q(V, 0.95), "min": float(V.min()), "max": float(V.max())},
222+ "probabilites": {"V_sous_cap": float((V < r["valeur_capitalisation"]).mean()), "V_sous_2M": float((V < 2_000_000).mean()), "V_sous_role": float((V < hyp["valeur_role_2026"]).mean()),
223+ "rcd1_sous_1.20": float((rcd[:, 0] < 1.20).mean()), "rcd1_sous_1.00": float((rcd[:, 0] < 1.00).mean()),
224+ "rcd_min5_sous_1.20": float((rcd_min5 < 1.20).mean()), "rcd_min5_sous_1.00": float((rcd_min5 < 1.00).mean()),
225+ "rcd6_choc200_sous_1.20": float((rcd6_choc < 1.20).mean()), "rcd6_choc200_sous_1.00": float((rcd6_choc < 1.00).mean())},
226+ "rcd1_q05": q(rcd[:, 0], 0.05), "rcd_min5_q05": q(rcd_min5, 0.05), "rcd6_choc200_q05": q(rcd6_choc, 0.05), "rcd6_choc200_mediane": q(rcd6_choc, 0.5),
227+ "sd_renouvele_200pb": h6.service_dette_annuel}
228+ # corrélations (sensibilité globale) : coefficient de corrélation de rang entre chaque intrant et la valeur
229+ rangs = {}
230+ for nom in ["inoccupation_pertes", "croissance_loyers_marche", "rotation_annuelle", "croissance_depenses", "taux_actualisation", "taux_terminal"]:
231+ rangs[nom] = float(pd.Series(x[nom]).corr(pd.Series(V), method="spearman"))
232+ rangs["facteur_capex_moyen"] = float(pd.Series(x["facteur_capex"].mean(1)).corr(pd.Series(V), method="spearman"))
233+ res["correlations_rang"] = rangs
234+ return res
235+
236+def tornade(r: dict) -> pd.DataFrame:
237+ """Sensibilité un-à-la-fois : chaque intrant à son quantile 10 % et 90 % (autres intrants à la base), valeur DCF."""
238+ etat, hyp, base = r["etat"], r["hyp"], r["base"]
239+ from scipy import stats
240+ def q(nom, p):
241+ typ, par = MC_LOIS[nom]
242+ if typ == "lognormale": return float(np.exp(np.log(par["mediane"]) + par["sigma_log"] * stats.norm.ppf(p)))
243+ if typ == "normale": return float(par["mu"] + par["sigma"] * stats.norm.ppf(p))
244+ if typ == "triangulaire": return float(stats.triang.ppf(p, c=(par["mode"] - par["min"]) / (par["max"] - par["min"]), loc=par["min"], scale=par["max"] - par["min"]))
245+ basepar = {"inoccupation_pertes": hyp["taux_inoccupation_pertes"], "croissance_loyers_marche": hyp["croissance_loyers_marche"], "rotation_annuelle": hyp["rotation_annuelle"],
246+ "croissance_depenses": hyp["croissance_depenses"], "taux_actualisation": hyp["taux_actualisation"], "ecart_terminal": hyp["taux_actualisation"] - hyp["taux_terminal"], "facteur_capex": 1.0}
247+ def valeur(par):
248+ one = lambda k: np.array([par[k]])
249+ fac = np.full((1, len(hyp["capex"])), par["facteur_capex"])
250+ proj = projeter_vectorise(etat, hyp, base, one("inoccupation_pertes"), one("croissance_loyers_marche"), one("rotation_annuelle"), one("croissance_depenses"), fac)
251+ R_T = max(par["taux_actualisation"] - par["ecart_terminal"], R_T_PLANCHER)
252+ return float(valeur_vectorisee(proj, one("taux_actualisation"), np.array([R_T]), hyp["couts_vente"])["valeur"][0])
253+ V0 = valeur(basepar); rows = []
254+ for nom in basepar:
255+ lo = dict(basepar); hi = dict(basepar); lo[nom] = q(nom, 0.10); hi[nom] = q(nom, 0.90)
256+ rows.append({"intrant": nom, "bas": lo[nom], "haut": hi[nom], "V_bas": valeur(lo), "V_haut": valeur(hi), "V_base": V0})
257+ t = pd.DataFrame(rows); t["amplitude"] = (t["V_haut"] - t["V_bas"]).abs()
258+ return t.sort_values("amplitude", ascending=False).reset_index(drop=True)
259+
260+# ----------------------------------------------------------------------------- réconciliation
261+
262+def reconciliation(r: dict, cu: dict) -> dict:
263+ ind = {"capitalisation": r["valeur_capitalisation"], "dcf": r["dcf"]["valeur"], "comparaison_unite": cu["indication"]}
264+ w = POIDS_RECONCILIATION
265+ vp = sum(w[k] * ind[k] for k in ind)
266+ V = arrondir(vp)
267+ hyp = r["hyp"]; sujet = r["sujet"]; base = r["base"]
268+ return {"indications": ind, "mrb_indication": cu["indication_mrb"], "poids": w, "valeur_ponderee": vp, "valeur_finale": V,
269+ "fourchette": (min(ind.values()), max(ind.values())), "ratio_valeur_role": V / hyp["valeur_role_2026"], "prix_par_logement": V / sujet["nb_logements"],
270+ "mrb_contrat": V / base["rbp_contrat"], "mrb_marche": V / base["rbp_marche"], "taux_implicite": base["rne"] / V, "droits_mutation": droits_mutation_montreal(V),
271+ "prix_par_m2_locatif": V / sujet["superficie_locative_m2"]}
272+
273+def valeur_libre_de_baux(r: dict) -> dict:
274+ """RNE et valeur par capitalisation si tous les logements étaient loués au loyer de marché (hypothèse du rôle, art. 45.1 LFM)."""
275+ b, hyp, dep = r["base"], r["hyp"], r["dep"]
276+ rbe_m = fin.revenu_brut_effectif(b["rbp_marche"], hyp["taux_inoccupation_pertes"], hyp["autres_revenus_annuels"])
277+ dn = cas3.depenses_normalisees(dep, hyp, rbe_m)
278+ de = float(dn.loc[dn["type"] != "réserve", "normalise"].sum()); res = float(dn.loc[dn["type"] == "réserve", "normalise"].sum())
279+ rne_m = fin.rne(rbe_m, de, res)
280+ return {"rbe": rbe_m, "depenses": de + res, "rne": rne_m, "valeur": fin.capitalisation_directe(rne_m, r["taux"]["mediane"]), "ecart_rne": rne_m - b["rne"]}
281+
282+def financement_final(r: dict, V: float) -> dict:
283+ f = cas3.financement(V, r["base"]["rne"], r["hyp"])
284+ p = r["projection"]; flux_ad = p.loc[p["annee"] <= r["hyp"]["horizon_ans"], "flux_avant_dette"].tolist()
285+ lev = fin.rendement_fonds_propres_levier(flux_ad, V, f["hypotheque"], r["dcf"]["vt_nette"])
286+ f["tri_fonds_propres"] = lev["tri"]; f["flux_fonds_propres"] = lev["flux"]; f["solde_final"] = lev["solde_final"]
287+ f["cash_on_cash_1"] = fin.rendement_cash_on_cash(flux_ad[0] - f["service_dette"], f["mise_de_fonds"])
288+ f["point_mort"] = fin.ratio_point_mort(r["base"]["depenses_totales"], f["service_dette"], r["base"]["rbp_contrat"])
289+ f["point_mort_hors_reserve"] = fin.ratio_point_mort(r["base"]["depenses_hors_reserve"], f["service_dette"], r["base"]["rbp_contrat"])
290+ tri_sans = fin.tri([-V] + flux_ad[:-1] + [flux_ad[-1] + r["dcf"]["vt_nette"]])
291+ f["tri_sans_levier"] = tri_sans
292+ return f
293+
294+# ----------------------------------------------------------------------------- exercices
295+
296+def exercices(r: dict, cu: dict, rec: dict) -> dict:
297+ """Solutions chiffrées des exercices du chapitre 47 (solutions/ch47.tex)."""
298+ b = r["base"]; hyp = r["hyp"]; tx = r["taux"]; out = {}
299+ # Ex. 1 : vacance économique 5 % au lieu de 3,9 %
300+ rbe5 = fin.revenu_brut_effectif(b["rbp_contrat"], 0.05, b["autres_revenus"]); dn5 = cas3.depenses_normalisees(r["dep"], hyp, rbe5)
301+ de5 = float(dn5.loc[dn5["type"] != "réserve", "normalise"].sum()); res5 = float(dn5.loc[dn5["type"] == "réserve", "normalise"].sum())
302+ rne5 = fin.rne(rbe5, de5, res5)
303+ out["ex1"] = {"rbe": rbe5, "depenses_hors_reserve": de5, "rne": rne5, "ecart_rne": rne5 - b["rne"], "valeur": fin.capitalisation_directe(rne5, tx["mediane"]), "ecart_valeur": fin.capitalisation_directe(rne5, tx["mediane"]) - r["valeur_capitalisation"]}
304+ # Ex. 2 : taxes municipales = historique 2025 × 1,053 (hausse Rosemont 2026) au lieu du recalcul au taux général
305+ taxes_alt = 10631 * 1.053; taxes_norm = hyp["valeur_role_2026"] * hyp["taux_taxe_fonciere"]
306+ rne2 = b["rne"] - (taxes_alt - taxes_norm)
307+ out["ex2"] = {"taxes_alt": taxes_alt, "taxes_norm": taxes_norm, "ecart": taxes_alt - taxes_norm, "rne": rne2, "valeur": fin.capitalisation_directe(rne2, tx["mediane"]), "ecart_valeur": fin.capitalisation_directe(rne2, tx["mediane"]) - r["valeur_capitalisation"]}
308+ # Ex. 3 : fourchette Q1-Q3 du taux et taux par régression (état 3, 12 logements)
309+ out["ex3"] = {"V_q1": fin.capitalisation_directe(b["rne"], tx["q1"]), "V_q3": fin.capitalisation_directe(b["rne"], tx["q3"]), "taux_regression": tx["regression"]["constante"],
310+ "V_regression": fin.capitalisation_directe(b["rne"], tx["regression"]["constante"]), "V_ponderee": fin.capitalisation_directe(b["rne"], tx["ponderee_rne"])}
311+ # Ex. 4 : rotation 10 % (au lieu de 15 %) et 25 %
312+ for nom, rot in [("rot10", 0.10), ("rot25", 0.25)]:
313+ h2 = dict(hyp); h2["rotation_annuelle"] = rot
314+ p2 = cas3.projection_dcf(r["etat"], r["dep"], h2); v2 = cas3.valeur_dcf(p2, h2)
315+ out[f"ex4_{nom}"] = {"valeur": v2["valeur"], "rne_T1": v2["rne_T1"], "ecart": v2["valeur"] - r["dcf"]["valeur"], "rbp_an10": float(p2.loc[p2["annee"] == 10, "rbp"].iloc[0])}
316+ # Ex. 5 : taux 5,12 % (+100 pb) : prêt max RCD 1,20, prêt RPV 75 % de la valeur finale, RCD, mise de fonds
317+ V = rec["valeur_finale"]; f5 = dict(hyp["financement"]); f5["taux"] = 0.0512
318+ h5 = dict(hyp); h5["financement"] = f5; fi5 = cas3.financement(V, b["rne"], h5)
319+ out["ex5"] = {k: fi5[k] for k in ["pret_rpv", "pret_rcd", "pret", "contrainte", "service_dette", "rcd", "rpv", "mise_de_fonds", "constante"]}
320+ out["ex5"]["ecart_pret"] = fi5["pret"] - cas3.financement(V, b["rne"], hyp)["pret"]
321+ # Ex. 6 : APH Select 50 points (RPV 85 %, RCD 1,10, 40 ans) sur la valeur finale, prime
322+ aph = aph_select(r, V, amortissements=(480,)); out["ex6"] = aph.iloc[0].to_dict()
323+ out["ex6"]["pret_conventionnel"] = cas3.financement(V, b["rne"], hyp)["pret"]
324+ # Ex. 7 : Monte-Carlo avec inoccupation médiane 6 % (marché détendu) — 10 000 tirages, même graine
325+ lois = dict(MC_LOIS); lois["inoccupation_pertes"] = ("lognormale", {"mediane": 0.06, "sigma_log": 0.30})
326+ x = tirer(10_000, GRAINE, len(hyp["capex"]), lois)
327+ proj = projeter_vectorise(r["etat"], hyp, b, x["inoccupation_pertes"], x["croissance_loyers_marche"], x["rotation_annuelle"], x["croissance_depenses"], x["facteur_capex"])
328+ val = valeur_vectorisee(proj, x["taux_actualisation"], x["taux_terminal"], hyp["couts_vente"])["valeur"]
329+ sd = r["financement"]["service_dette"]; rcd1 = proj["rne"][:, 0] / sd
330+ out["ex7"] = {"mediane": float(np.median(val)), "q05": float(np.quantile(val, 0.05)), "q95": float(np.quantile(val, 0.95)), "p_rcd1_sous_1.20": float((rcd1 < 1.2).mean()), "p_rcd1_sous_1.00": float((rcd1 < 1.0).mean()),
331+ "rcd1_mediane": float(np.median(rcd1))}
332+ return out
333+
334+# ----------------------------------------------------------------------------- dossier complet
335+
336+def dossier(n_mc: int = 10_000, graine: int = GRAINE) -> dict:
337+ r = cas3.tout()
338+ cu = comparaison_par_unite(r, r["base"])
339+ sc = scenarios_renouvellement(r); rcd_an = rcd_par_annee(r, sc)
340+ mc = monte_carlo(r, n_mc, graine); torn = tornade(r)
341+ rec = reconciliation(r, cu); fi = financement_final(r, rec["valeur_finale"]); fi_dcf = financement_final(r, r["dcf"]["valeur"])
342+ aph = aph_select(r, rec["valeur_finale"])
343+ return {**r, "comparaison_unite": cu, "libre_de_baux": valeur_libre_de_baux(r), "scenarios": sc, "rcd_par_annee": rcd_an, "mc": mc, "tornade": torn, "reconciliation": rec,
344+ "financement_final": fi, "financement_dcf": fi_dcf, "aph": aph}
345+
346+def _d(x): return f"{x:,.0f}".replace(",", " ") + " $"
347+def _p(x, d=2): return f"{100*x:.{d}f}".replace(".", ",") + " %"
348+
349+def imprimer(D: dict) -> None:
350+ pd.set_option("display.width", 220); pd.set_option("display.max_columns", 40); pd.set_option("display.float_format", lambda v: f"{v:,.4f}")
351+ b, hyp, tx, p = D["base"], D["hyp"], D["taux"], D["projection"]
352+ print("=" * 100 + "\nCH. 47 — CAS 3 : 12 LOGEMENTS, ROSEMONT (DONNÉES SYNTHÉTIQUES, immeuble fictif) — date d'évaluation", hyp["date_evaluation"])
353+ print("\n--- 47.4-47.5 État locatif et revenus ---")
354+ e = D["etat"].copy(); e["ecart"] = e["loyer_mensuel_marche"] - e["loyer_mensuel_contrat"]; e["ecart_pct"] = e["ecart"] / e["loyer_mensuel_marche"]; e["anciennete"] = 2026 - e["locataire_depuis"]
355+ print(e[["logement", "pieces", "superficie_m2", "loyer_mensuel_contrat", "loyer_mensuel_marche", "ecart", "ecart_pct", "locataire_depuis", "anciennete", "stationnement"]].to_string(index=False))
356+ print("loyer mensuel total contrat", _d(e["loyer_mensuel_contrat"].sum()), "marché", _d(e["loyer_mensuel_marche"].sum()), "écart moyen", _p(e["ecart"].sum() / e["loyer_mensuel_marche"].sum()))
357+ print("écart moyen par logement (mois)", _d(e["ecart"].mean()), "ancienneté moyenne", f"{e['anciennete'].mean():.1f} ans", "loyer/m² contrat", f"{(e.loyer_mensuel_contrat/e.superficie_m2).mean():.2f}")
358+ for k in ["rbp_contrat", "rbp_marche", "ecart_marche", "autres_revenus", "vacance_pertes", "rbe"]: print(f"{k:22s} {_d(b[k])}")
359+ print("taux vacance/pertes", _p(b["taux_vacance_pertes"], 1), "écart au marché", _p(b["ecart_marche"] / b["rbp_marche"], 1), "autres revenus / logement / mois", f"{b['autres_revenus']/144:.0f} $")
360+ print("\n--- 47.6 Dépenses (historique 2023-2025 et normalisation) ---")
361+ hist = D["dep"].pivot(index="poste", columns="annee", values="montant"); print(hist.to_string()); print("totaux", hist.sum().to_dict())
362+ print(b["detail"].to_string(index=False))
363+ print("dépenses hors réserve", _d(b["depenses_hors_reserve"]), "réserve", _d(b["reserve"]), f"({b['reserve']/12:.2f} $/logement)", "total", _d(b["depenses_totales"]), "ratio/RBE", _p(b["ratio_depenses"], 1),
364+ "ratio/RBP", _p(b["depenses_totales"] / b["rbp_contrat"], 1), "dépenses/logement", _d(b["depenses_totales"] / 12))
365+ print("taxes recalculées", _d(hyp["valeur_role_2026"] * hyp["taux_taxe_fonciere"]), "scolaire", _d(hyp["valeur_role_2026"] * hyp["taux_taxe_scolaire"]), "gestion 4 %", _d(0.04 * b["rbe"]))
366+ print("\n--- 47.7 RNE normalisé ---"); print("RNE", f"{b['rne']:.2f}", "par logement", f"{b['rne_par_logement']:.2f}", "RNE/RBE", _p(b["rne"] / b["rbe"], 1))
367+ print("\n--- 47.8 Taux extraits (n = %d) ---" % tx["n"])
368+ for k in ["mediane", "moyenne", "ponderee_rne", "q1", "q3", "min", "max"]: print(f"{k:14s} {_p(tx[k], 3)}")
369+ print("régression", {k: round(v, 6) for k, v in tx["regression"].items()}, "→ taux prédit état 3 / 12 log. =", _p(tx["regression"]["constante"], 3))
370+ c = tx["comparables"]; print("écart-type", _p(c["taux_global"].std(ddof=1), 3), "par quartier (médiane) :", c.groupby("quartier")["taux_global"].median().round(5).to_dict(), "par état :", c.groupby("etat_1a5")["taux_global"].median().round(5).to_dict())
371+ print("tranche 6-14 logements : n =", int(c["nb_logements"].between(6, 14).sum()), "médiane", _p(c.loc[c["nb_logements"].between(6, 14), "taux_global"].median(), 3), "; 12 derniers mois : n =", int((c["date_vente"] >= DATE_EVAL - pd.DateOffset(months=12)).sum()), "médiane", _p(c.loc[c["date_vente"] >= DATE_EVAL - pd.DateOffset(months=12), "taux_global"].median(), 3))
372+ print("Valeur par capitalisation (RNE_1 / médiane)", f"{D['valeur_capitalisation']:.2f}", "→ par logement", _d(D["valeur_capitalisation"] / 12))
373+ print("sensibilité ±25 pb :", _d(b["rne"] / (tx["mediane"] - 0.0025)), "/", _d(b["rne"] / (tx["mediane"] + 0.0025)), "; Q1/Q3 :", _d(b["rne"] / tx["q1"]), "/", _d(b["rne"] / tx["q3"]))
374+ print("\n--- 47.9 DCF 10 ans ---"); print(p.to_string(index=False))
375+ d = D["dcf"]
376+ for k in ["valeur", "va_flux", "va_terminale", "vt_brute", "vt_nette", "rne_T1", "part_terminale"]: print(f"{k:16s} {d[k]:.4f}")
377+ print("croissance RNE an 1→10 (TCAC)", _p((p.loc[9, "rne"] / p.loc[0, "rne"]) ** (1 / 9) - 1), "RBP an 10 / marché an 10", _p(p.loc[9, "rbp"] / p.loc[9, "rbp_marche"], 1), "CAPEX total", _d(p["capex"].sum()))
378+ print("taux implicite V_dcf : RNE_1/V =", _p(b["rne"] / d["valeur"], 3), "; TRI non levier (achat à V_dcf) =", _p(D["financement_dcf"]["tri_sans_levier"], 3))
379+ print("écart DCF − capitalisation", _d(d["valeur"] - D["valeur_capitalisation"]), _p(d["valeur"] / D["valeur_capitalisation"] - 1, 1))
380+ print("\n--- 47.10 Comparaison par unité ---")
381+ cu = D["comparaison_unite"]; o = cu["ols"]
382+ print("OLS ln(prix/logement) : n =", o["n"], "R² =", round(o["r2"], 3), "σ =", round(o["sigma"], 4))
383+ for k in o["coef"]: print(f" {k:12s} {o['coef'][k]:+.4f} (é.-t. {o['se'][k]:.4f})")
384+ print("loyer moyen sujet (contrat)", f"{cu['loyer_moyen_sujet']:.2f}")
385+ print(cu["grille"].to_string(index=False)); print("poids", [round(w, 3) for w in cu["poids"]])
386+ rc = cu["reconciliation"]; print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in rc.items() if k != "poids"})
387+ print("prix/logement retenu", f"{cu['prix_par_logement_retenu']:.2f}", "→ indication", f"{cu['indication']:.2f}")
388+ print("MRB sélection : médiane", cu["mrb_mediane"], "Q1", cu["mrb_q1"], "Q3", cu["mrb_q3"], "→ indication MRB × RBP contrat", f"{cu['indication_mrb']:.2f}")
389+ print("\n--- 47.11 Financement (base : valeur DCF, comme au ch. 31) ---")
390+ f = D["financement"]
391+ for k in ["pret_rpv", "pret_rcd", "pret", "contrainte", "service_dette", "rcd", "rpv", "mise_de_fonds", "constante", "taux_mensuel"]: print(f"{k:14s} {f[k]}")
392+ print("paiement mensuel", f"{f['hypotheque'].paiement:.2f}", "solde après 60 mois", f"{f['hypotheque'].solde(60):.2f}", "après 120", f"{f['hypotheque'].solde(120):.2f}")
393+ fd = D["financement_dcf"]; print("TRI fonds propres (achat à V_dcf)", _p(fd["tri_fonds_propres"], 2), "TRI sans levier", _p(fd["tri_sans_levier"], 2), "cash-on-cash an 1", _p(fd["cash_on_cash_1"], 2), "point mort", _p(fd["point_mort"], 1), "hors réserve", _p(fd["point_mort_hors_reserve"], 1))
394+ print("flux fonds propres", [round(x) for x in fd["flux_fonds_propres"]])
395+ print("levier : constante", _p(f["constante"], 3), "vs taux de capitalisation", _p(tx["mediane"], 3), "→ levier", "négatif" if f["constante"] > tx["mediane"] else "positif", "; intérêts an 1", _d(f["hypotheque"].interets_annee(1)), "capital an 1", _d(f["hypotheque"].capital_annee(1)))
396+ print("\nScénarios de renouvellement (an 6) :"); print(D["scenarios"].to_string(index=False)); print(D["rcd_par_annee"].to_string(index=False))
397+ print("\nAPH Select (valeur finale) :"); print(D["aph"].to_string(index=False))
398+ print("\n--- 47.12 Monte-Carlo ---")
399+ mc = D["mc"]; print("n =", mc["n"], "graine", mc["graine"]); print({k: round(v, 2) for k, v in mc["stats"].items()}); print({k: round(v, 4) for k, v in mc["probabilites"].items()})
400+ print("RCD an 1 q05", round(mc["rcd1_q05"], 3), "RCD min 1-5 q05", round(mc["rcd_min5_q05"], 3), "RCD an 6 (+200 pb) médiane", round(mc["rcd6_choc200_mediane"], 3), "q05", round(mc["rcd6_choc200_q05"], 3), "SD renouvelé +200 pb", _d(mc["sd_renouvele_200pb"]))
401+ print("corrélations de rang", {k: round(v, 3) for k, v in mc["correlations_rang"].items()})
402+ for nom in ["inoccupation_pertes", "croissance_loyers_marche", "rotation_annuelle", "croissance_depenses", "taux_actualisation", "taux_terminal"]:
403+ x = mc["tirages"][nom]; print(f" {nom:26s} moyenne {x.mean():.4f} q05 {np.quantile(x,0.05):.4f} q95 {np.quantile(x,0.95):.4f}")
404+ print("tornade :"); print(D["tornade"].to_string(index=False))
405+ print("\n--- 47.13 Réconciliation ---")
406+ print("valeur libre de baux (loyers de marché, taux médian) :", {k: round(v, 2) for k, v in D["libre_de_baux"].items()}, "→ ratio rôle/valeur libre", round(hyp["valeur_role_2026"] / D["libre_de_baux"]["valeur"], 4))
407+ rec = D["reconciliation"]
408+ for k, v in rec.items(): print(f"{k:20s} {v}")
409+ ff = D["financement_final"]
410+ print("financement à la valeur finale :", {k: (round(v, 4) if isinstance(v, float) else v) for k, v in ff.items() if k in ["pret_rpv", "pret_rcd", "pret", "contrainte", "service_dette", "rcd", "rpv", "mise_de_fonds", "tri_fonds_propres", "tri_sans_levier", "cash_on_cash_1", "point_mort"]})
411+ print("coût d'acquisition : droits de mutation", _d(rec["droits_mutation"]), "; mise de fonds + droits", _d(ff["mise_de_fonds"] + rec["droits_mutation"]))
412+ print("\n--- Exercices ---")
413+ for k, v in exercices(D, D["comparaison_unite"], rec).items(): print(k, {kk: (round(vv, 4) if isinstance(vv, float) else vv) for kk, vv in v.items()})
414+
415+if __name__ == "__main__":
416+ ap = argparse.ArgumentParser(); ap.add_argument("--n", type=int, default=10_000); a = ap.parse_args()
417+ imprimer(dossier(a.n))
added analysis/python/figures/ch21_decomposition.py +37 −0
@@ -0,0 +1,37 @@
1+#!/usr/bin/env python3
2+"""Figure ch21 — décomposition du coût neuf du sujet du cas 1 par corps de métier (DONNÉES SYNTHÉTIQUES, graine 20261031).
3+Sortie : assets/figures/ch21/ch21-fig-decomposition-corps.pdf"""
4+import sys, pathlib
5+ROOT = pathlib.Path(__file__).resolve().parents[3]
6+sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))
7+import matplotlib.pyplot as plt
8+from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars
9+import ch21_cout_neuf as cas
10+
11+def main():
12+ setup()
13+ devis, sujet = cas.charger(); g = cas.regrouper(devis); cn = cas.cout_neuf_sujet(devis)
14+ noms = list(g.index) + ["Incitatif de l'entrepreneur (10 %)"]
15+ vals = list(g["montant"]) + [cn["incitatif"]]
16+ couleurs = [palette.cat[0]] * 6 + [palette.cat[1], palette.cat[3]]
17+ ordre = sorted(range(len(vals)), key=lambda i: vals[i])
18+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))
19+ y = range(len(ordre))
20+ ax.barh(list(y), [vals[i] for i in ordre], color=[couleurs[i] for i in ordre], height=0.68)
21+ ax.set_yticks(list(y)); ax.set_yticklabels([noms[i] for i in ordre])
22+ tot = cn["total"]
23+ for k, i in enumerate(ordre):
24+ ax.text(vals[i] + 1200, k, f"{fmt_dollars(vals[i])} ({vals[i]/tot*100:.1f} %)", va="center", ha="left", fontsize=8, color=palette.ink)
25+ ax.set_xlim(0, max(vals) * 1.42)
26+ ax.xaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars))
27+ ax.set_xlabel("Montant (dollars de 2026)")
28+ ax.grid(axis="y", visible=False)
29+ ax.set_title(f"Coût neuf total : {fmt_dollars(tot)} — directs {cn['direct']/tot*100:.1f} %, indirects {cn['indirects']/tot*100:.1f} %, incitatif {cn['incitatif']/tot*100:.1f} %",
30+ loc="left", fontsize=8.5, color=palette.ink)
31+ from matplotlib.patches import Patch
32+ ax.legend(handles=[Patch(color=palette.cat[0], label="Coûts directs (six corps de métier)"), Patch(color=palette.cat[1], label="Coûts indirects (cinq postes)"),
33+ Patch(color=palette.cat[3], label="Incitatif de l'entrepreneur")], loc="upper center", bbox_to_anchor=(0.42, -0.16), ncol=3, fontsize=7.5)
34+ out = savefig(fig, "ch21/ch21-fig-decomposition-corps.pdf"); print("OK", out)
35+
36+if __name__ == "__main__":
37+ main()
added analysis/python/figures/ch21_ipcb.py +36 −0
@@ -0,0 +1,36 @@
1+#!/usr/bin/env python3
2+"""Figure ch21 — IPCB Montréal (Statistique Canada, tableau 18-10-0289-01, base 2023 = 100), données RÉELLES lues dans data/raw.
3+Séries résidentielles depuis le début de la série (T1 2017) jusqu'au T3 2026 ; non résidentiel en comparaison.
4+Sortie : assets/figures/ch21/ch21-fig-ipcb-montreal.pdf"""
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 matplotlib.pyplot as plt, matplotlib.dates as mdates
9+from traite.style import setup, palette, savefig, LARGEUR
10+import ch21_cout_neuf as cas
11+
12+ETIQ = {"Bâtiments résidentiels [621]": "Bâtiments résidentiels", "Maison individuelle [621112]": "Maison individuelle",
13+ "Immeuble à hauteur restreinte (moins de cinq étages) [62111]": "Immeuble < 5 étages",
14+ "Tour d'habitations (cinq étages ou plus) [621111]": "Tour d'habitation (≥ 5 étages)", "Bâtiments non résidentiels [622]": "Non résidentiel"}
15+
16+def main():
17+ setup()
18+ df = cas.ipcb_montreal(); df = df[df["date"] >= "2017-01-01"]
19+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.5))
20+ for i, (t, lab) in enumerate(ETIQ.items()):
21+ s = df[df["type"] == t]
22+ lw = 2.2 if t.startswith("Maison") else 1.4
23+ ls = "--" if t.startswith("Bâtiments non") else "-"
24+ ax.plot(s["date"], s["indice"], color=palette.cat[i], lw=lw, ls=ls, label=lab)
25+ ax.axhline(100, color=palette.ink2, lw=0.6, ls=":"); ax.text(df["date"].min(), 101, "2023 = 100", fontsize=7.5, color=palette.ink2)
26+ ax.set_ylabel("Indice (2023 = 100)"); ax.set_xlim(df["date"].min(), df["date"].max() + __import__("pandas").Timedelta(days=120))
27+ ax.legend(loc="upper left", bbox_to_anchor=(0.0, 0.98), fontsize=7.5)
28+ mi = df[df["type"] == "Maison individuelle [621112]"]
29+ ax.annotate(f"T3 2026 : {mi['indice'].iloc[-1]:.1f}", xy=(mi["date"].iloc[-1], mi["indice"].iloc[-1]), xytext=(-70, 14), textcoords="offset points", fontsize=7.5,
30+ arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6), color=palette.ink)
31+ ax.xaxis.set_major_locator(mdates.YearLocator()); ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
32+ ax.set_ylim(60, 125)
33+ out = savefig(fig, "ch21/ch21-fig-ipcb-montreal.pdf"); print("OK", out)
34+
35+if __name__ == "__main__":
36+ main()
added analysis/python/figures/ch21_sensibilite.py +40 −0
@@ -0,0 +1,40 @@
1+#!/usr/bin/env python3
2+"""Figure ch21 — sensibilité du coût neuf du sujet du cas 1 (DONNÉES SYNTHÉTIQUES) : (a) grille taux d'indirects × taux d'incitatif ;
3+(b) tornade (indirects, incitatif, indice de coûts). Sortie : assets/figures/ch21/ch21-fig-sensibilite.pdf"""
4+import sys, pathlib
5+ROOT = pathlib.Path(__file__).resolve().parents[3]
6+sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))
7+import numpy as np, matplotlib.pyplot as plt
8+from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars
9+import ch21_cout_neuf as cas
10+
11+def main():
12+ setup()
13+ devis, sujet = cas.charger(); cn = cas.cout_neuf_sujet(devis)
14+ ti = np.array([0.12, 0.15, 0.18, cn["taux_indirects"], 0.24, 0.27]); tc = np.array([0.05, 0.08, 0.10, 0.12, 0.15])
15+ G = cas.grille_sensibilite(cn["direct"], ti, tc)
16+ ipcb = cas.ipcb_montreal(); mi = "Maison individuelle [621112]"
17+ var = cas.indice(ipcb, mi, "2026-07") / cas.indice(ipcb, mi, "2025-07") - 1
18+ torn = cas.tornade(cn["direct"], cn["taux_indirects"], cas.TAUX_INCITATIF, (1 / (1 + var), 1 + var), (-0.05, -0.20))[:3]
19+ fig, (a, b) = plt.subplots(1, 2, figsize=(LARGEUR, 3.3), gridspec_kw={"width_ratios": [1.15, 1]})
20+ im = a.imshow(G / 1000, cmap=palette.cmap_seq(), aspect="auto", origin="lower")
21+ a.set_xticks(range(len(tc))); a.set_xticklabels([f"{x*100:.0f} %" for x in tc]); a.set_yticks(range(len(ti))); a.set_yticklabels([f"{x*100:.1f} %" for x in ti])
22+ a.set_xlabel("Taux d'incitatif (sur directs + indirects)"); a.set_ylabel("Taux d'indirects (sur directs)"); a.grid(False)
23+ for i in range(len(ti)):
24+ for j in range(len(tc)):
25+ a.text(j, i, f"{G[i, j]/1000:.0f}", ha="center", va="center", fontsize=7.5, color="white" if G[i, j] > G.mean() else palette.ink)
26+ a.set_title("(a) Coût neuf (milliers de $)", loc="left")
27+ base = torn[0]["base"]
28+ for k, o in enumerate(torn):
29+ lo, hi = min(o["bas"], o["haut"]), max(o["bas"], o["haut"])
30+ b.barh(k, lo - base, left=base, color=palette.cat[0], height=0.55); b.barh(k, hi - base, left=base, color=palette.cat[1], height=0.55)
31+ b.text(lo - 1500, k, fmt_dollars(lo), ha="right", va="center", fontsize=7.2); b.text(hi + 1500, k, fmt_dollars(hi), ha="left", va="center", fontsize=7.2)
32+ b.set_yticks(range(len(torn))); b.set_yticklabels([f"{o['param']}\n({o['etiquette']})" for o in torn], fontsize=7.5)
33+ b.axvline(base, color=palette.ink, lw=0.8); b.set_xlim(base - 62000, base + 62000)
34+ b.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f"{x/1000:.0f} k$")); b.grid(axis="y", visible=False)
35+ b.set_title("(b) Tornade autour de " + fmt_dollars(base), loc="left")
36+ fig.tight_layout(w_pad=2.0)
37+ out = savefig(fig, "ch21/ch21-fig-sensibilite.pdf"); print("OK", out)
38+
39+if __name__ == "__main__":
40+ main()
added analysis/python/figures/ch29_figures.py +146 −0
@@ -0,0 +1,146 @@
1+#!/usr/bin/env python3
2+"""Chapitre 29 — figures (5) : taux extraits (histogramme), taux vs état/taille (nuage), surface V(RNE, R) (carte de
3+chaleur), décomposition du taux (barres), rendements des obligations du GdC et taux hypothécaire affiché (série réelle,
4+Banque du Canada). Idempotent ; écrit dans assets/figures/ch29/ch29-fig-<slug>.pdf.
5+Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch29_figures.py"""
6+from __future__ import annotations
7+import sys, pathlib
8+import numpy as np, pandas as pd
9+import matplotlib.pyplot as plt
10+from matplotlib.ticker import FuncFormatter, PercentFormatter
11+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
12+from traite.style import setup, palette, savefig, LARGEUR, DEMI, fmt_dollars
13+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
14+from ch29_capitalisation import calculer
15+
16+ROOT = pathlib.Path(__file__).resolve().parents[3]
17+RAW = ROOT / "data/raw/bdc_valet"
18+pct = FuncFormatter(lambda x, p: f"{x*100:.2f}".replace(".", ",") + " %")
19+pct1 = FuncFormatter(lambda x, p: f"{x*100:.1f}".replace(".", ",") + " %")
20+
21+
22+def fig_taux_extraits(res):
23+ c = res["taux"]["comparables"]; R = res["R"]
24+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))
25+ bins = np.arange(0.0365, 0.0566, 0.0025)
26+ ax.hist(c["taux_global"], bins=bins, color=palette.cat[0], alpha=0.85, edgecolor=palette.surface, linewidth=0.8)
27+ ymax = ax.get_ylim()[1] * 1.12; ax.set_ylim(0, ymax)
28+ for x, lab, ls, h, ha in ((res["taux"]["q1"], "Q1", ":", 0.97, "right"), (R, "médiane", "-", 0.88, "right"), (res["taux"]["q3"], "Q3", ":", 0.97, "left")):
29+ ax.axvline(x, color=palette.ink, linestyle=ls, linewidth=1.1)
30+ ax.text(x + (0.0002 if ha == "left" else -0.0002), ymax * h, f"{lab} {x*100:.2f} %".replace(".", ","), ha=ha, va="top", fontsize=8)
31+ xr = res["regression"]["prevision_sujet"]
32+ ax.axvline(xr, color=palette.cat[1], linestyle="--", linewidth=1.1)
33+ ax.text(xr + 0.0002, ymax * 0.70, f"régression (état 3, 12 log.)\n{xr*100:.2f} %".replace(".", ","), color=palette.ink, fontsize=7.5, ha="left", va="top")
34+ ax.xaxis.set_major_formatter(pct)
35+ ax.set_xlabel("Taux global extrait (RNE après réserve / prix)"); ax.set_ylabel("Nombre de ventes")
36+ ax.set_title(f"Distribution des taux extraits — {len(c)} ventes synthétiques, 6 à 30 logements, 2024-09 à 2026-08", loc="left")
37+ ax.text(0.99, -0.30, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)
38+ return savefig(fig, "ch29/ch29-fig-taux-extraits.pdf")
39+
40+
41+def fig_taux_etat_taille(res):
42+ c = res["taux"]["comparables"]; reg = res["regression"]
43+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))
44+ marqueurs = {2: "s", 3: "o", 4: "^"}
45+ for i, et in enumerate([2, 3, 4]):
46+ s = c[c["etat_1a5"] == et]
47+ ax.scatter(s["nb_logements"], s["taux_global"], color=palette.cat[i], marker=marqueurs[et], s=28, label=f"état {et} (n = {len(s)})", zorder=3)
48+ xs = np.array([6, 30]); ax.plot(xs, reg["constante"] + reg["etat"] * (et - 3) + reg["taille"] * (xs - 12), color=palette.cat[i], linewidth=1.0, alpha=0.8)
49+ ax.scatter([12], [reg["prevision_sujet"]], marker="*", s=120, color=palette.ink, zorder=4)
50+ ax.annotate("sujet (12 log., état 3)\nprévision de la régression", (12, reg["prevision_sujet"]), xytext=(16.5, 0.0535), fontsize=7.5,
51+ arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
52+ ax.yaxis.set_major_formatter(pct); ax.set_xlabel("Nombre de logements"); ax.set_ylabel("Taux global extrait")
53+ ax.set_title("Taux extraits selon la taille et l'état : droites de la régression $R = a + b_1(\\mathrm{état}-3) + b_2(n-12)$", loc="left")
54+ ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.22), ncol=3)
55+ ax.text(0.99, -0.42, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)
56+ return savefig(fig, "ch29/ch29-fig-taux-etat-taille.pdf")
57+
58+
59+def fig_surface(res):
60+ rne0 = res["base"]["rne"]; R0 = res["R"]
61+ Rs = np.array(sorted(set(np.round(np.arange(0.0375, 0.0526, 0.0025), 4)) | {round(R0, 5)}))
62+ mult = np.array([0.90, 0.95, 1.00, 1.05, 1.10])
63+ V = np.array([[rne0 * m / r for r in Rs] for m in mult])
64+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))
65+ im = ax.imshow(V / 1e6, cmap=palette.cmap_seq(), aspect="auto", origin="lower")
66+ ax.set_xticks(range(len(Rs))); ax.set_xticklabels([f"{r*100:.2f}".replace(".", ",") for r in Rs])
67+ ax.set_yticks(range(len(mult))); ax.set_yticklabels([f"{m*100-100:+.0f} %\n{rne0*m/1000:,.1f} k$".replace(",", " ").replace(".", ",") for m in mult])
68+ for i in range(len(mult)):
69+ for j in range(len(Rs)):
70+ v = V[i, j]; coul = "white" if v / 1e6 > np.percentile(V / 1e6, 55) else palette.ink
71+ ax.text(j, i, f"{v/1e6:.2f}".replace(".", ","), ha="center", va="center", fontsize=7.5, color=coul)
72+ j0 = int(np.argmin(abs(Rs - R0))); ax.add_patch(plt.Rectangle((j0 - 0.5, 2 - 0.5), 1, 1, fill=False, edgecolor=palette.cat[1], linewidth=1.6))
73+ ax.set_xlabel("Taux global $R$ (%)"); ax.set_ylabel("RNE normalisé (écart au cas de base ; k$)")
74+ rne_txt = f"{rne0:,.0f}".replace(",", " "); r_txt = f"{R0*100:.3f}".replace(".", ",")
75+ ax.set_title(f"Valeur indiquée $V = RNE/R$ (M\\$) ; cadre : cas de base (RNE {rne_txt} \\$, R = {r_txt} %)", loc="left")
76+ ax.grid(False)
77+ cb = fig.colorbar(im, ax=ax, pad=0.02); cb.set_label("V (M$)")
78+ ax.text(0.99, -0.28, "DONNÉES SYNTHÉTIQUES", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)
79+ return savefig(fig, "ch29/ch29-fig-surface-v.pdf")
80+
81+
82+def fig_bande(res):
83+ e = res["ellwood"]; M, f, R = res["M"], res["f"], res["R"]
84+ constructions = [
85+ ("Bande\nd'investissement\n(R_E impliqué)", [("M·f", M * f), ("(1−M)·R_E", (1 - M) * res["re_implicite"])]),
86+ ("Couverture\nde la dette\n(RCD 1,20)", [("RCD·M·f", res["R_rcd"])]),
87+ ("Akerson\nΔ implicite 7,7 %\n(Y = 7,25 %)", [("M·f", e["composantes_marche"]["M f"]), ("(1−M)·Y", e["composantes_marche"]["(1-M) Y"]),
88+ ("−M·P·SFF", e["composantes_marche"]["-M P SFF"]), ("−Δ·SFF", e["composantes_marche"]["-delta SFF"])]),
89+ ("Akerson\nΔ = 24,9 %\n(Y = 7,25 %)", [("M·f", e["composantes"]["M f"]), ("(1−M)·Y", e["composantes"]["(1-M) Y"]),
90+ ("−M·P·SFF", e["composantes"]["-M P SFF"]), ("−Δ·SFF", e["composantes"]["-delta SFF"])]),
91+ ]
92+ coul = {"M·f": palette.cat[0], "(1−M)·R_E": palette.cat[2], "RCD·M·f": palette.cat[4], "(1−M)·Y": palette.cat[2], "−M·P·SFF": palette.cat[1], "−Δ·SFF": palette.cat[3]}
93+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))
94+ vus = set()
95+ for k, (nom, comps) in enumerate(constructions):
96+ pos, neg = 0.0, 0.0
97+ for lab, v in comps:
98+ if v >= 0:
99+ ax.bar(k, v, bottom=pos, color=coul[lab], width=0.6, label=lab if lab not in vus else None); pos += v
100+ else:
101+ # composante négative dessinée en retrait depuis le sommet des composantes positives
102+ ax.bar(k + 0.0, v, bottom=pos + neg, color=coul[lab], width=0.6, alpha=0.9, hatch="//", edgecolor=palette.surface, label=lab if lab not in vus else None); neg += v
103+ vus.add(lab)
104+ total = pos + neg
105+ ax.plot([k - 0.38, k + 0.38], [total, total], color=palette.ink, linewidth=1.4)
106+ ax.text(k, max(pos, total) + 0.0012, f"R = {total*100:.2f} %".replace(".", ","), ha="center", va="bottom", fontsize=8, fontweight="bold")
107+ ax.axhline(R, color=palette.ink2, linestyle="--", linewidth=0.9, label=f"médiane du marché ({R*100:.2f} %)".replace(".", ","))
108+ ax.set_xticks(range(len(constructions))); ax.set_xticklabels([c[0] for c in constructions], fontsize=7.5)
109+ ax.yaxis.set_major_formatter(pct1); ax.set_ylabel("Taux global $R$"); ax.set_xlim(-0.5, 3.5); ax.set_ylim(0, 0.066)
110+ ax.set_title("Décomposition du taux global selon la méthode de construction (cas 3 ; M = 75 %, f = 5,08 %)", loc="left")
111+ ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.30), ncol=4, fontsize=7.5)
112+ ax.text(0.99, -0.46, "DONNÉES SYNTHÉTIQUES (paramètres du cas 3)", transform=ax.transAxes, ha="right", va="bottom", fontsize=7, color=palette.ink2)
113+ return savefig(fig, "ch29/ch29-fig-bande-investissement.pdf")
114+
115+
116+def _valet(nom: str) -> pd.Series:
117+ df = pd.read_csv(RAW / f"{nom}.csv", skiprows=8, header=None, names=["date", "v"])
118+ df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce"); df["v"] = pd.to_numeric(df["v"], errors="coerce")
119+ return df.dropna().set_index("date")["v"] / 100
120+
121+
122+def fig_gdc():
123+ s5, s10, hyp = _valet("BD.CDN.5YR.DQ.YLD"), _valet("BD.CDN.10YR.DQ.YLD"), _valet("V80691335")
124+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))
125+ ax.plot(s10.resample("MS").mean(), color=palette.cat[0], label="Obligation du GdC 10 ans (moyenne mensuelle)")
126+ ax.plot(s5.resample("MS").mean(), color=palette.cat[1], label="Obligation du GdC 5 ans (moyenne mensuelle)")
127+ ax.plot(hyp.resample("MS").mean(), color=palette.cat[2], label="Taux hypothécaire affiché 5 ans, banques (conventionnel)")
128+ for s, dy in ((s10, 5), (s5, -5), (hyp, 0)):
129+ ax.annotate(f"{s.iloc[-1]*100:.2f} %".replace(".", ","), (s.index[-1], s.iloc[-1]), xytext=(4, dy), textcoords="offset points", fontsize=7.5, va="center", color=palette.ink)
130+ ax.yaxis.set_major_formatter(pct1); ax.set_ylabel("Taux annuel"); ax.set_xlabel("")
131+ ax.set_title("Taux sans risque et coût de la dette au Canada, 2001-2026 : la base des primes de risque du taux global", loc="left")
132+ ax.legend(loc="upper right", fontsize=7.5); ax.set_xlim(pd.Timestamp("2001-01-01"), pd.Timestamp("2027-06-01"))
133+ return savefig(fig, "ch29/ch29-fig-taux-gdc.pdf"), {"gdc5": float(s5.iloc[-1]), "gdc10": float(s10.iloc[-1]), "hyp": float(hyp.iloc[-1]), "date5": str(s5.index[-1].date()), "datehyp": str(hyp.index[-1].date()),
134+ "gdc10_moy_2001_2026": float(s10.mean()), "gdc10_min": float(s10.min()), "gdc10_min_date": str(s10.idxmin().date())}
135+
136+
137+def main():
138+ setup(); res = calculer()
139+ outs = [fig_taux_extraits(res), fig_taux_etat_taille(res), fig_surface(res), fig_bande(res)]
140+ o, info = fig_gdc(); outs.append(o)
141+ for p in outs: print("OK", p.relative_to(ROOT))
142+ print("Banque du Canada (dernières observations) :", info)
143+
144+
145+if __name__ == "__main__":
146+ main()
added analysis/python/figures/ch30_flux.py +39 −0
@@ -0,0 +1,39 @@
1+#!/usr/bin/env python3
2+"""Ch. 30, figure « flux » : RNE, CAPEX et flux net avant dette (barres) du cas 3 (DONNÉES SYNTHÉTIQUES), puis valeur
3+actuelle cumulée des flux et apport de la valeur terminale. Sortie : assets/figures/ch30/ch30-fig-flux.pdf."""
4+from __future__ import annotations
5+import sys, pathlib
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
7+import numpy as np, matplotlib.pyplot as plt
8+from matplotlib.ticker import FuncFormatter
9+from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers
10+from cases import ch30_dcf as c
11+
12+def main():
13+ setup()
14+ r = c.base(); t = c.tableau_dcf(r); h = r["hyp"]; T = h["horizon_ans"]; d = r["dcf"]
15+ tt = t[t["annee"] <= T]
16+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(LARGEUR, 4.6), sharex=True, gridspec_kw={"height_ratios": [1.15, 1], "hspace": 0.12})
17+ x = tt["annee"].values; w = 0.27
18+ ax1.bar(x - w, tt["rne"] / 1000, w, color=palette.cat[0], label="RNE")
19+ ax1.bar(x, -tt["capex"] / 1000, w, color=palette.cat[1], label="CAPEX (sortie)")
20+ ax1.bar(x + w, tt["flux_avant_dette"] / 1000, w, color=palette.cat[2], label="Flux net avant dette")
21+ ax1.axhline(0, color=palette.ink2, lw=0.8)
22+ ax1.set_ylabel("Milliers de dollars")
23+ ax1.legend(loc="upper left", ncol=3)
24+ # bas : VA cumulée
25+ ax2.plot(x, tt["va_cumulee"] / 1000, marker="o", color=palette.cat[0], label="VA cumulée des flux")
26+ ax2.vlines(T, tt["va_cumulee"].iloc[-1] / 1000, d["valeur"] / 1000, color=palette.cat[3], lw=3, label="VA de la valeur terminale")
27+ ax2.scatter([T], [d["valeur"] / 1000], color=palette.cat[3], zorder=5)
28+ ax2.axhline(d["valeur"] / 1000, color=palette.ink2, lw=0.8, ls="--")
29+ ax2.annotate(f"$V_0$ = {fmt_milliers(d['valeur'])} \\$", xy=(1.2, d["valeur"] / 1000), va="bottom", fontsize=8, color=palette.ink)
30+ ax2.annotate(f"flux : {fmt_milliers(d['va_flux'])} \\$ ({d['va_flux']/d['valeur']*100:.0f} %)", xy=(T - 0.1, tt["va_cumulee"].iloc[-1] / 1000 - 40), ha="right", va="top", fontsize=8)
31+ ax2.annotate(f"VT : {fmt_milliers(d['va_terminale'])} \\$ ({d['part_terminale']*100:.0f} %)", xy=(T - 0.1, (tt["va_cumulee"].iloc[-1] + d["valeur"]) / 2000), ha="right", va="center", fontsize=8)
32+ ax2.set_ylabel("Milliers de dollars"); ax2.set_xlabel("Année de projection")
33+ ax2.set_xticks(x); ax2.set_ylim(0, d["valeur"] / 1000 * 1.12)
34+ ax2.legend(loc="center left")
35+ for ax in (ax1, ax2): ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: fmt_milliers(v)))
36+ savefig(fig, "ch30/ch30-fig-flux.pdf")
37+
38+if __name__ == "__main__":
39+ main()
added analysis/python/figures/ch30_gdc.py +37 −0
@@ -0,0 +1,37 @@
1+#!/usr/bin/env python3
2+"""Ch. 30, figure « gdc » (DONNÉES RÉELLES, Banque du Canada, API Valet, téléchargées le 2026-10-31) : rendements des
3+obligations de référence du gouvernement du Canada à 5 et 10 ans et taux hypothécaire conventionnel 5 ans affiché,
4+moyennes mensuelles 2006-2026. Sortie : ch30-fig-gdc.pdf."""
5+from __future__ import annotations
6+import sys, pathlib, io
7+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
8+import pandas as pd, matplotlib.pyplot as plt
9+from traite.style import setup, palette, savefig, LARGEUR, ROOT
10+
11+RAW = ROOT / "data/raw/bdc_valet"
12+
13+def lire(nom: str) -> pd.Series:
14+ txt = (RAW / f"{nom}.csv").read_text(encoding="utf-8-sig")
15+ obs = txt[txt.index('"OBSERVATIONS"'):].split("\n", 1)[1]
16+ df = pd.read_csv(io.StringIO(obs)); df.columns = ["date", "val"]
17+ df["date"] = pd.to_datetime(df["date"]); df["val"] = pd.to_numeric(df["val"], errors="coerce")
18+ return df.dropna().set_index("date")["val"]
19+
20+def main():
21+ setup()
22+ s10 = lire("BD.CDN.10YR.DQ.YLD"); s5 = lire("BD.CDN.5YR.DQ.YLD"); hyp = lire("V80691335")
23+ deb = "2006-01-01"
24+ m10 = s10[deb:].resample("MS").mean(); m5 = s5[deb:].resample("MS").mean(); mh = hyp[deb:].resample("MS").mean()
25+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))
26+ ax.plot(mh.index, mh.values, color=palette.cat[1], lw=1.4, label="Taux hypothécaire conventionnel 5 ans affiché (V80691335)")
27+ ax.plot(m10.index, m10.values, color=palette.cat[0], lw=1.6, label="Obligation GdC 10 ans (BD.CDN.10YR.DQ.YLD)")
28+ ax.plot(m5.index, m5.values, color=palette.cat[2], lw=1.2, label="Obligation GdC 5 ans (BD.CDN.5YR.DQ.YLD)")
29+ last10 = s10.iloc[-1]; lasth = hyp.iloc[-1]
30+ ax.annotate(f"{last10:.2f} % (29 oct. 2026)".replace(".", ","), xy=(s10.index[-1], last10), xytext=(pd.Timestamp("2022-03-01"), 1.0), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
31+ ax.annotate(f"{lasth:.2f} %".replace(".", ","), xy=(hyp.index[-1], lasth), xytext=(pd.Timestamp("2024-09-01"), 7.6), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
32+ ax.set_ylabel("Taux (%)"); ax.set_ylim(0, 9)
33+ ax.legend(loc="upper right", fontsize=7.5)
34+ savefig(fig, "ch30/ch30-fig-gdc.pdf")
35+
36+if __name__ == "__main__":
37+ main()
added analysis/python/figures/ch30_loyers.py +33 −0
@@ -0,0 +1,33 @@
1+#!/usr/bin/env python3
2+"""Ch. 30, figure « loyers » : trajectoire du revenu brut potentiel contractuel (indexé au taux TAL + relocation de 15 %
3+des logements par an au loyer de marché) et du RBP au marché, cas 3 (DONNÉES SYNTHÉTIQUES). Sortie : ch30-fig-loyers.pdf."""
4+from __future__ import annotations
5+import sys, pathlib
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
7+import matplotlib.pyplot as plt
8+from matplotlib.ticker import FuncFormatter
9+from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers
10+from cases import ch30_dcf as c
11+
12+def main():
13+ setup()
14+ r = c.base(); p = r["projection"]; h = r["hyp"]
15+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))
16+ x = p["exercice"].values
17+ ax.fill_between(x, p["rbp"] / 1000, p["rbp_marche"] / 1000, color=palette.grid, alpha=0.9, label="Écart au marché (rattrapage)")
18+ ax.plot(x, p["rbp_marche"] / 1000, color=palette.cat[1], marker="s", ms=3.5, label=f"RBP au loyer de marché (+{h['croissance_loyers_marche']*100:.1f} %/an)")
19+ ax.plot(x, p["rbp"] / 1000, color=palette.cat[0], marker="o", ms=3.5, label=f"RBP contractuel projeté (TAL {h['croissance_loyers_contrat']*100:.1f} % + rotation {h['rotation_annuelle']*100:.0f} %)")
20+ # contractuel sans rotation (indexation TAL seule) pour référence
21+ sans = [p["rbp"].iloc[0] * (1 + h["croissance_loyers_contrat"]) ** k / 1000 for k in range(len(x))]
22+ ax.plot(x, sans, color=palette.cat[0], ls=":", lw=1.2, label="RBP contractuel sans relocation (indexation TAL seule)")
23+ e0 = (p["rbp_marche"].iloc[0] - p["rbp"].iloc[0]); eT = (p["rbp_marche"].iloc[-1] - p["rbp"].iloc[-1])
24+ ax.annotate(f"écart {fmt_milliers(e0)} $ ({e0/p['rbp_marche'].iloc[0]*100:.1f} %)", xy=(x[0], (p["rbp"].iloc[0] + p["rbp_marche"].iloc[0]) / 2000), xytext=(x[0] + 0.3, 215), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
25+ ax.annotate(f"écart {fmt_milliers(eT)} $ ({eT/p['rbp_marche'].iloc[-1]*100:.1f} %)", xy=(x[-1], (p["rbp"].iloc[-1] + p["rbp_marche"].iloc[-1]) / 2000), xytext=(x[-1] - 3.6, 262), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.7))
26+ ax.set_ylabel("Revenu brut potentiel (milliers de $)"); ax.set_xlabel("Exercice")
27+ ax.set_xticks(x); ax.yaxis.set_major_formatter(FuncFormatter(lambda v, q: fmt_milliers(v)))
28+ ax.set_ylim(top=p["rbp_marche"].max() / 1000 * 1.04)
29+ ax.legend(loc="upper left", fontsize=7.5)
30+ savefig(fig, "ch30/ch30-fig-loyers.pdf")
31+
32+if __name__ == "__main__":
33+ main()
added analysis/python/figures/ch30_surface.py +34 −0
@@ -0,0 +1,34 @@
1+#!/usr/bin/env python3
2+"""Ch. 30, figure « surface » : valeur DCF du cas 3 (DONNÉES SYNTHÉTIQUES) selon le taux d'actualisation r (lignes) et le
3+taux de capitalisation terminal R_T (colonnes). Sortie : ch30-fig-surface.pdf."""
4+from __future__ import annotations
5+import sys, pathlib
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
7+import numpy as np, matplotlib.pyplot as plt
8+from matplotlib.patches import Rectangle
9+from traite.style import setup, palette, savefig, LARGEUR
10+from cases import ch30_dcf as c
11+
12+def main():
13+ setup()
14+ r = c.base(); S = c.surface_r_Rt(r); h = r["hyp"]
15+ M = S.values / 1000
16+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.9))
17+ im = ax.imshow(M, cmap=palette.cmap_seq(), aspect="auto", origin="upper")
18+ ax.set_xticks(range(len(S.columns))); ax.set_xticklabels([f"{x*100:.2f}".replace(".", ",") for x in S.columns])
19+ ax.set_yticks(range(len(S.index))); ax.set_yticklabels([f"{x*100:.2f}".replace(".", ",") for x in S.index])
20+ ax.set_xlabel("Taux de capitalisation terminal $R_T$ (%)"); ax.set_ylabel("Taux d'actualisation $r$ (%)")
21+ ax.grid(False)
22+ vmin, vmax = M.min(), M.max()
23+ for i in range(M.shape[0]):
24+ for j in range(M.shape[1]):
25+ v = M[i, j]; col = "white" if (v - vmin) / (vmax - vmin) > 0.45 else palette.ink
26+ ax.text(j, i, f"{v:,.0f}".replace(",", " "), ha="center", va="center", fontsize=7, color=col)
27+ i0 = int(np.argmin([abs(x - h["taux_actualisation"]) for x in S.index])); j0 = int(np.argmin([abs(x - h["taux_terminal"]) for x in S.columns]))
28+ ax.add_patch(Rectangle((j0 - 0.5, i0 - 0.5), 1, 1, fill=False, ec=palette.cat[1], lw=2))
29+ ax.text(j0, i0 - 0.62, "base", ha="center", va="bottom", fontsize=7.5, color=palette.cat[1], fontweight="bold")
30+ cb = fig.colorbar(im, ax=ax, fraction=0.035, pad=0.02); cb.set_label("Valeur $V_0$ (milliers de \\$)"); cb.outline.set_visible(False)
31+ savefig(fig, "ch30/ch30-fig-surface.pdf")
32+
33+if __name__ == "__main__":
34+ main()
added analysis/python/figures/ch30_tornade.py +33 −0
@@ -0,0 +1,33 @@
1+#!/usr/bin/env python3
2+"""Ch. 30, figure « tornade » : variation de la valeur DCF du cas 3 (DONNÉES SYNTHÉTIQUES) pour un choc bas/haut de chaque
3+intrant, les autres étant fixés à leur valeur de base. Sortie : ch30-fig-tornade.pdf."""
4+from __future__ import annotations
5+import sys, pathlib
6+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
7+import numpy as np, matplotlib.pyplot as plt
8+from matplotlib.ticker import FuncFormatter
9+from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers
10+from cases import ch30_dcf as c
11+
12+def main():
13+ setup()
14+ r = c.base(); t = c.tornade(r); V0 = r["dcf"]["valeur"]
15+ t = t.iloc[::-1].reset_index(drop=True) # plus grande amplitude en haut
16+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.6))
17+ y = np.arange(len(t))
18+ ax.barh(y, t["delta_bas"] / 1000, color=palette.cat[0], label="Borne basse de l'intrant")
19+ ax.barh(y, t["delta_haut"] / 1000, color=palette.cat[1], label="Borne haute de l'intrant")
20+ ax.axvline(0, color=palette.ink, lw=0.9)
21+ ax.set_yticks(y); ax.set_yticklabels(t["intrant"], fontsize=7.8)
22+ for yi, (db, dh) in enumerate(zip(t["delta_bas"], t["delta_haut"])):
23+ for dv in (db, dh):
24+ ax.text(dv / 1000 + (6 if dv >= 0 else -6), yi, f"{dv/1000:+,.0f}".replace(",", " "), va="center", ha="left" if dv >= 0 else "right", fontsize=7, color=palette.ink2)
25+ ax.set_xlabel(f"Variation de la valeur par rapport à la base ({fmt_milliers(V0)} \\$), en milliers de \\$")
26+ lim = max(abs(t["delta_bas"].min()), abs(t["delta_haut"].max())) / 1000 * 1.25
27+ ax.set_xlim(-lim, lim)
28+ ax.xaxis.set_major_formatter(FuncFormatter(lambda v, q: f"{v:+,.0f}".replace(",", " ")))
29+ ax.legend(loc="lower right")
30+ savefig(fig, "ch30/ch30-fig-tornade.pdf")
31+
32+if __name__ == "__main__":
33+ main()
added analysis/python/figures/ch47_dossier_figures.py +117 −0
@@ -0,0 +1,117 @@
1+#!/usr/bin/env python3
2+"""Chapitre 47 — figures du dossier « 12 logements à Montréal » (DONNÉES SYNTHÉTIQUES, graine 20261031).
3+Produit assets/figures/ch47/ch47-fig-{etat-locatif,depenses,taux-extraits,flux-dcf,montecarlo,tornade,rcd-scenarios}.pdf
4+Usage (racine) : PYTHONPATH=analysis/python python3 analysis/python/figures/ch47_dossier_figures.py"""
5+from __future__ import annotations
6+import sys, pathlib
7+import numpy as np, pandas as pd
8+import matplotlib.pyplot as plt
9+from matplotlib.ticker import FuncFormatter, PercentFormatter
10+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
11+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
12+from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars
13+import ch47_dossier as ch
14+
15+ETIQ = {"inoccupation_pertes": "Inoccupation et pertes", "croissance_loyers_marche": "Croissance des loyers de marché", "rotation_annuelle": "Rotation annuelle",
16+ "croissance_depenses": "Croissance des dépenses", "taux_actualisation": "Taux d'actualisation", "ecart_terminal": "Écart terminal (r − R_T)", "facteur_capex": "Facteur CAPEX"}
17+
18+def fig_etat_locatif(D):
19+ e = D["etat"]; x = np.arange(len(e)); w = 0.38
20+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.1))
21+ ax.bar(x - w / 2, e["loyer_mensuel_contrat"], w, color=palette.cat[0], label="Loyer contractuel (bail 2026-2027)")
22+ ax.bar(x + w / 2, e["loyer_mensuel_marche"], w, color=palette.cat[1], label="Loyer de marché estimé")
23+ for i, r in e.iterrows():
24+ ecart = (r["loyer_mensuel_marche"] - r["loyer_mensuel_contrat"]) / r["loyer_mensuel_marche"]
25+ ax.text(x[i], r["loyer_mensuel_marche"] + 25, f"−{ecart*100:.0f} %".replace(".", ","), ha="center", va="bottom", fontsize=7, color=palette.ink)
26+ ax.set_xticks(x); ax.set_xticklabels([f"{l}\n{p}" for l, p in zip(e["logement"], e["pieces"])], fontsize=7.5)
27+ ax.set_ylabel("Loyer mensuel ($)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_ylim(0, 1900)
28+ ax.legend(loc="upper left", ncol=2); ax.grid(axis="x", visible=False)
29+ savefig(fig, "ch47/ch47-fig-etat-locatif.pdf")
30+
31+def fig_depenses(D):
32+ d = D["base"]["detail"].copy(); d = d[d["normalise"] > 0].sort_values("normalise")
33+ coul = {"fixe": palette.cat[0], "variable": palette.cat[1], "réserve": palette.cat[2]}
34+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.4))
35+ noms = [p.replace(" (par logement)", "").replace(" (aires communes)", " (communs)").replace(" (aires communes, gaz)", " (communs, gaz)") for p in d["poste"]]
36+ ax.barh(noms, d["normalise"], color=[coul[t] for t in d["type"]])
37+ ax.scatter(d["historique_2025"], noms, marker="|", s=90, color=palette.ink, zorder=3, label="Historique 2025 (non normalisé)")
38+ for n, v, h in zip(noms, d["normalise"], d["historique_2025"].fillna(0)): ax.text(max(v, h) + 180, n, fmt_dollars(v), va="center", fontsize=7.5, color=palette.ink)
39+ from matplotlib.patches import Patch
40+ h = [Patch(color=coul["fixe"], label="Fixes (taxes, assurances)"), Patch(color=coul["variable"], label="Variables et gestion imputée"), Patch(color=coul["réserve"], label="Réserve de remplacement")]
41+ h.append(ax.collections[0]); ax.legend(handles=h, loc="lower right", fontsize=7.5)
42+ ax.set_xlabel("Dépense normalisée, première année ($)"); ax.xaxis.set_major_formatter(FuncFormatter(fmt_dollars)); ax.set_xlim(0, 14500); ax.grid(axis="y", visible=False)
43+ savefig(fig, "ch47/ch47-fig-depenses.pdf")
44+
45+def fig_taux_extraits(D):
46+ c = D["taux"]["comparables"]; tx = D["taux"]
47+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))
48+ ax.axhspan(tx["q1"] * 100, tx["q3"] * 100, color=palette.grid, alpha=0.6, lw=0, label="Intervalle interquartile (Q1–Q3)")
49+ for i, et in enumerate([2, 3, 4]):
50+ s = c[c["etat_1a5"] == et]
51+ ax.scatter(s["nb_logements"], s["taux_global"] * 100, color=palette.cat[i], s=28, label=f"État {et}/5 (n = {len(s)})", edgecolor="white", linewidth=0.5)
52+ ax.axhline(tx["mediane"] * 100, color=palette.ink, lw=1.2, ls="--")
53+ ax.text(30.3, tx["mediane"] * 100, f"médiane {tx['mediane']*100:.3f} %".replace(".", ","), va="center", fontsize=7.5)
54+ ax.scatter([12], [tx["mediane"] * 100], marker="D", s=70, facecolor="white", edgecolor=palette.ink, linewidth=1.4, zorder=5)
55+ ax.annotate("sujet (12 log., taux retenu)", (12, tx["mediane"] * 100), xytext=(13.2, 3.78), fontsize=7.5, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6))
56+ ax.set_xlabel("Nombre de logements du comparable"); ax.set_ylabel("Taux global extrait (%)"); ax.set_xlim(4, 34); ax.set_ylim(3.5, 5.7)
57+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ",")))
58+ ax.legend(loc="upper right", fontsize=7.5, ncol=2)
59+ savefig(fig, "ch47/ch47-fig-taux-extraits.pdf")
60+
61+def fig_flux_dcf(D):
62+ p = D["projection"]; p10 = p[p["annee"] <= 10]; sd = D["financement"]["service_dette"]
63+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2)); x = p10["annee"].values
64+ ax.bar(x, p10["rne"], 0.6, color=palette.cat[0], label="RNE (après réserve)")
65+ ax.bar(x, -p10["capex"], 0.6, color=palette.cat[1], label="CAPEX (dépenses en capital ponctuelles)")
66+ ax.plot(x, p10["flux_avant_dette"], color=palette.ink, marker="o", ms=4, lw=1.3, label="Flux avant dette (RNE − CAPEX)")
67+ ax.axhline(sd, color=palette.cat[3], ls="--", lw=1.2); ax.text(10.45, sd, "service de la dette\n(prêt RCD 1,20)", va="center", fontsize=7.2, color=palette.ink)
68+ ax.axhline(0, color=palette.ink2, lw=0.6)
69+ ax.set_xticks(x); ax.set_xticklabels([f"{a}\n{e}" for a, e in zip(p10["annee"], p10["exercice"])], fontsize=7.5)
70+ ax.set_xlabel("Année de projection (exercice)"); ax.set_ylabel("Montant annuel ($)"); ax.yaxis.set_major_formatter(FuncFormatter(fmt_dollars))
71+ ax.set_ylim(-135000, 175000); ax.set_xlim(0.3, 12.2); ax.legend(loc="lower left", fontsize=7.5, ncol=1); ax.grid(axis="x", visible=False)
72+ savefig(fig, "ch47/ch47-fig-flux-dcf.pdf")
73+
74+def fig_montecarlo(D):
75+ mc = D["mc"]; V = mc["valeur"] / 1e6; st = mc["stats"]
76+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.2))
77+ ax.hist(V, bins=80, range=(1.2, 3.8), color=palette.cat[0], alpha=0.85, edgecolor="white", linewidth=0.3)
78+ ymax = ax.get_ylim()[1]
79+ for q, lab in [(st["q05"], "q05"), (st["mediane"], "médiane"), (st["q95"], "q95")]:
80+ ax.axvline(q / 1e6, color=palette.ink, lw=1.0, ls="--"); ax.text(q / 1e6, ymax * 0.97, f"{lab}\n{q/1e6:.2f} M$".replace(".", ","), ha="center", va="top", fontsize=7.2)
81+ ax.axvline(D["dcf"]["valeur"] / 1e6, color=palette.cat[1], lw=1.6); ax.text(D["dcf"]["valeur"] / 1e6 + 0.03, ymax * 0.36, "DCF déterministe\n2,21 M$", fontsize=7.2, color=palette.ink)
82+ ax.axvline(D["valeur_capitalisation"] / 1e6, color=palette.cat[2], lw=1.6); ax.text(D["valeur_capitalisation"] / 1e6 - 0.03, ymax * 0.52, "capitalisation\ndirecte 2,04 M$", fontsize=7.2, color=palette.ink, ha="right")
83+ ax.set_xlabel("Valeur simulée (millions de dollars)"); ax.set_ylabel("Nombre de tirages (sur 10 000)")
84+ ax.xaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ","))); ax.set_xlim(1.2, 3.8)
85+ savefig(fig, "ch47/ch47-fig-montecarlo.pdf")
86+
87+def fig_tornade(D):
88+ t = D["tornade"].iloc[::-1]; V0 = t["V_base"].iloc[0] / 1e6
89+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))
90+ y = np.arange(len(t))
91+ lo = np.minimum(t["V_bas"], t["V_haut"]) / 1e6; hi = np.maximum(t["V_bas"], t["V_haut"]) / 1e6
92+ ax.barh(y, t["V_bas"] / 1e6 - V0, left=V0, color=palette.cat[0], height=0.6, label="Intrant au quantile 10 %")
93+ ax.barh(y, t["V_haut"] / 1e6 - V0, left=V0, color=palette.cat[1], height=0.6, label="Intrant au quantile 90 %")
94+ ax.axvline(V0, color=palette.ink, lw=1.0)
95+ for yi, l, h in zip(y, lo, hi):
96+ ax.text(l - 0.02, yi, f"{l:.2f}".replace(".", ","), va="center", ha="right", fontsize=7.2); ax.text(h + 0.02, yi, f"{h:.2f}".replace(".", ","), va="center", ha="left", fontsize=7.2)
97+ ax.set_yticks(y); ax.set_yticklabels([ETIQ[i] for i in t["intrant"]], fontsize=8)
98+ ax.set_xlabel("Valeur DCF (millions de dollars), autres intrants à leur valeur de base"); ax.set_xlim(1.75, 2.75)
99+ ax.xaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.2f}".replace(".", ","))); ax.legend(loc="lower right", fontsize=7.5); ax.grid(axis="y", visible=False)
100+ savefig(fig, "ch47/ch47-fig-tornade.pdf")
101+
102+def fig_rcd_scenarios(D):
103+ r = D["rcd_par_annee"]; cols = [c for c in r.columns if c != "annee"]
104+ fig, ax = plt.subplots(figsize=(LARGEUR, 3.0))
105+ for i, c in enumerate(cols):
106+ ax.plot(r["annee"], r[c], marker="o", ms=3.5, color=palette.cat[i], label=f"Renouvellement à l'an 6 : {c}")
107+ ax.axhline(1.20, color=palette.ink, ls="--", lw=1.0); ax.text(10.15, 1.20, "RCD 1,20 (prêteur)", va="center", fontsize=7.5)
108+ ax.axhline(1.00, color=palette.cat[5], ls=":", lw=1.2); ax.text(10.15, 1.00, "RCD 1,00 (défaut technique)", va="center", fontsize=7.5)
109+ ax.axvspan(5.5, 10.5, color=palette.grid, alpha=0.5, lw=0); ax.text(8, 2.12, "second terme (taux renouvelé)", ha="center", fontsize=7.5, color=palette.ink2)
110+ ax.set_xticks(r["annee"]); ax.set_xlim(0.7, 12.4); ax.set_ylim(0.9, 2.2); ax.set_xlabel("Année de projection"); ax.set_ylabel("Ratio de couverture de la dette (RNE / SD)")
111+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, p: f"{v:.1f}".replace(".", ","))); ax.legend(loc="upper left", fontsize=7.5)
112+ savefig(fig, "ch47/ch47-fig-rcd-scenarios.pdf")
113+
114+if __name__ == "__main__":
115+ setup(); D = ch.dossier()
116+ for f in (fig_etat_locatif, fig_depenses, fig_taux_extraits, fig_flux_dcf, fig_montecarlo, fig_tornade, fig_rcd_scenarios):
117+ f(D); print("OK", f.__name__)
added analysis/python/tests/test_ch16.py +59 −0
@@ -0,0 +1,59 @@
1+"""Chapitre 16 — vérification des chiffres cités dans le texte et les solutions."""
2+import sys, pathlib, math
3+import numpy as np, pytest
4+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
5+import ch16_selection as c16
6+from traite import comparables as cp
7+
8+@pytest.fixture(scope="module")
9+def res():
10+ return c16.main(figures=False)
11+
12+def test_entonnoir_et_comparables(res):
13+ e = res["selection"]["etapes"]
14+ assert e == {"candidats_total": 600, "apres_delai": 236, "apres_rayon": 40, "apres_type": 27, "apres_superficie": 16}
15+ top = res["selection"]["comparables"]
16+ assert list(top["id_vente"]) == ["G0574", "G0404", "G0468", "G0477", "G0575"]
17+ assert (top["secteur"] == "Hull").all() and (top["mois"] <= 18).all() and (top["dist_km"] <= 3).all()
18+ assert list(top["prix_vente"]) == [598000, 506500, 624500, 538000, 513500]
19+
20+def test_indication_brute(res):
21+ top = res["selection"]["comparables"]; s = res["sujet"]
22+ ind = top["prix_m2"].median() * s["superficie_habitable_m2"]
23+ assert ind == pytest.approx(551029, abs=1)
24+ assert top["prix_vente"].min() == 506500 and top["prix_vente"].max() == 624500
25+ assert top["prix_vente"].mean() == pytest.approx(556100) and top["prix_vente"].median() == 538000
26+ cand = res["selection"]["candidats"]
27+ assert cand["prix_m2"].median() == pytest.approx(3286, abs=1) and cand["prix_m2"].min() == pytest.approx(2361, abs=1) and cand["prix_m2"].max() == pytest.approx(4140, abs=1)
28+
29+def test_sensibilite(res):
30+ s = res["sensibilite"]
31+ assert len(s) == 48
32+ assert s.indication_m2.min() == pytest.approx(525768, abs=1) and s.indication_m2.max() == pytest.approx(608014, abs=1)
33+ assert s.indication_m2.std() == pytest.approx(24069, abs=2)
34+ g = s.groupby("k").indication_m2.agg(["min", "median", "max"])
35+ assert g.loc[4, "median"] == pytest.approx(579522, abs=1) and g.loc[10, "max"] == pytest.approx(596921, abs=1)
36+
37+def test_solutions_exercices():
38+ # ex. 16.1
39+ pu = [525000 / 152, 489000 / 141, 561000 / 171]
40+ assert sorted(pu)[1] * 165 == pytest.approx(569902, abs=1)
41+ # ex. 16.3
42+ e = {"s": 25, "t": 150, "a": 10, "q": 1}; w = {"s": 2, "t": 0.7, "a": 1, "q": 1}
43+ suj = {"s": 165, "t": 620, "a": 28, "q": 3}
44+ dA = cp.distance_attributs(suj, {"s": 158, "t": 600, "a": 25, "q": 3}, e, w)
45+ dB = cp.distance_attributs(suj, {"s": 190, "t": 650, "a": 10, "q": 4}, e, w)
46+ dC = cp.distance_attributs(suj, {"s": 150, "t": 400, "a": 30, "q": 2}, e, w)
47+ assert dA == pytest.approx(0.509, abs=0.001) and dB == pytest.approx(2.504, abs=0.001) and dC == pytest.approx(1.807, abs=0.001)
48+ # ex. 16.4
49+ assert 506500 * (1.0443 ** (15 / 12) - 1) == pytest.approx(28200, rel=0.01)
50+ # ex. 16.5
51+ inv = [1 / 20**2, 1 / 30**2, 1 / 40**2]; S = sum(inv)
52+ assert [x / S for x in inv] == pytest.approx([0.590, 0.262, 0.148], abs=0.001)
53+ assert math.sqrt(1 / S) == pytest.approx(15.4, abs=0.05) and math.sqrt((400 + 900 + 1600) / 9) == pytest.approx(17.95, abs=0.01)
54+ # ex. 16.7
55+ k = (25000**2 / (2 * 2000**2)) ** (1 / 3)
56+ assert k == pytest.approx(4.27, abs=0.01)
57+ assert (25000**2 / (2 * 500**2)) ** (1 / 3) == pytest.approx(10.8, abs=0.05)
58+ eqm = 2000**2 * 16 + 25000**2 / 4
59+ assert math.sqrt(eqm) == pytest.approx(14800, rel=0.01)
added analysis/python/tests/test_ch29.py +196 −0
@@ -0,0 +1,196 @@
1+"""Tests du chapitre 29 — capitalisation directe (cas 3, données synthétiques).
2+Identités financières + valeurs citées dans manuscript/chapters/ch29.tex et solutions/ch29.tex (tolérances déclarées)."""
3+import sys, pathlib
4+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
5+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
6+import pytest
7+from traite import cas3, finance as fin
8+from ch29_capitalisation import calculer, exercices, droits_mutation
9+
10+ROOT = pathlib.Path(__file__).resolve().parents[3]
11+
12+
13+@pytest.fixture(scope="module")
14+def res():
15+ return calculer()
16+
17+
18+@pytest.fixture(scope="module")
19+def ex(res):
20+ return exercices(res)
21+
22+
23+# ----------------------------------------------------------------------------- identités
24+def test_identite_capitalisation(res):
25+ assert res["V"] == pytest.approx(res["base"]["rne"] / res["R"], rel=1e-12)
26+ assert fin.taux_global_extrait(res["base"]["rne"], res["V"]) == pytest.approx(res["R"], rel=1e-12)
27+
28+
29+def test_coherence_avec_cas3_partage(res):
30+ """Les chiffres du chapitre 29 coïncident avec ceux que les chapitres 30 et 47 tirent de traite.cas3.tout()."""
31+ t = cas3.tout()
32+ assert res["V"] == pytest.approx(t["valeur_capitalisation"], rel=1e-12)
33+ assert res["base"]["rne"] == pytest.approx(t["base"]["rne"], rel=1e-12)
34+ assert res["R"] == pytest.approx(t["taux"]["mediane"], rel=1e-12)
35+
36+
37+def test_ellwood_egal_akerson(res):
38+ e = res["ellwood"]
39+ assert e["R_delta"] == pytest.approx(e["R_akerson"], abs=1e-12)
40+ # somme des composantes d'Akerson = taux
41+ assert sum(e["composantes"].values()) == pytest.approx(e["R_delta"], abs=1e-12)
42+ assert sum(e["composantes_marche"].values()) == pytest.approx(res["R"], abs=1e-9)
43+ # sans dette ni variation de valeur : R = Y
44+ assert e["R_sans_dette_delta0"] == pytest.approx(e["Y"], abs=1e-12)
45+ # Y implicite : réinjecté dans Ellwood, redonne le taux de marché
46+ h = fin.Hypotheque(1.0, res["hyp"]["financement"]["taux"], res["hyp"]["financement"]["amortissement_mois"])
47+ assert fin.taux_ellwood(e["Y_implicite"], e["M"], e["f"], 1 - h.solde(120), e["n"], e["delta"]) == pytest.approx(res["R"], abs=1e-8)
48+
49+
50+def test_bande_et_rcd(res):
51+ assert fin.taux_bande_investissement(res["M"], res["f"], res["re_implicite"]) == pytest.approx(res["R"], abs=1e-12)
52+ assert res["R_rcd"] == pytest.approx(1.20 * 0.75 * res["f"], rel=1e-12)
53+ # constante hypothécaire : 4,12 % composé semestriellement, 480 mois
54+ i_m = (1 + 0.0412 / 2) ** (1 / 6) - 1
55+ assert res["f"] == pytest.approx(12 * i_m / (1 - (1 + i_m) ** -480), rel=1e-12)
56+
57+
58+def test_gordon(res):
59+ g = res["gordon"]
60+ assert g["R_g_terminal"] == pytest.approx(g["r"] - g["g_terminal"], abs=1e-12)
61+ assert g["R_g_terminal"] == pytest.approx(res["hyp"]["taux_terminal"], abs=1e-12)
62+ with pytest.raises(ValueError):
63+ fin.taux_gordon(0.05, 0.05)
64+
65+
66+def test_derivee_sensibilite(res):
67+ d = res["derive"]; V, R = res["V"], res["R"]
68+ assert d["dV_dR"] == pytest.approx(-V / R, rel=1e-12)
69+ # approximation linéaire vs variation exacte pour +25 pb : la linéarisation surestime la baisse
70+ assert d["var_plus25_exacte"] > d["var_plus25_lineaire"]
71+ assert d["var_plus25_exacte"] == pytest.approx(-0.0025 / (R + 0.0025), rel=1e-12)
72+ s = res["sensibilite"]
73+ assert (s["rne_p5"] / s["rne"] - 1).round(10).eq(0.05).all()
74+ assert (s["rne_m5"] / s["rne"] - 1).round(10).eq(-0.05).all()
75+
76+
77+def test_droits_mutation_bareme():
78+ assert droits_mutation(50_000) == pytest.approx(250.0)
79+ assert droits_mutation(62_900) == pytest.approx(314.5)
80+ assert droits_mutation(315_000) == pytest.approx(314.5 + 2521.0)
81+ assert droits_mutation(0) == 0.0
82+
83+
84+# ----------------------------------------------------------------------------- valeurs citées dans le texte
85+def test_valeurs_citees_revenus(res):
86+ b = res["base"]
87+ assert b["rbp_contrat"] == pytest.approx(165_540, abs=0.5)
88+ assert b["rbp_marche"] == pytest.approx(186_036, abs=0.5)
89+ assert b["vacance_pertes"] == pytest.approx(6_456.06, abs=0.01)
90+ assert b["rbe"] == pytest.approx(168_443.94, abs=0.01)
91+ assert b["depenses_hors_reserve"] == pytest.approx(70_385.19, abs=0.01)
92+ assert b["reserve"] == pytest.approx(8_103, abs=0.01)
93+ assert b["rne"] == pytest.approx(89_955.75, abs=0.01)
94+ assert b["ratio_depenses"] == pytest.approx(0.4660, abs=5e-4)
95+ assert b["rne_par_logement"] == pytest.approx(7_496.31, abs=0.01)
96+
97+
98+def test_valeurs_citees_taux(res):
99+ t = res["taux"]
100+ assert t["n"] == 40
101+ assert t["mediane"] == pytest.approx(0.04415, abs=1e-6)
102+ assert t["moyenne"] == pytest.approx(0.044038, abs=1e-6)
103+ assert t["ponderee_rne"] == pytest.approx(0.043319, abs=1e-6)
104+ assert (t["q1"], t["q3"], t["min"], t["max"]) == pytest.approx((0.041525, 0.046375, 0.0372, 0.0547), abs=1e-6)
105+ r = res["regression"]
106+ assert r["constante"] == pytest.approx(0.04567, abs=1e-5)
107+ assert r["etat"] == pytest.approx(-0.000441, abs=1e-6)
108+ assert r["taille"] == pytest.approx(-0.000227, abs=1e-6)
109+ assert r["r2"] == pytest.approx(0.156, abs=1e-3)
110+ assert r["sigma_resid"] == pytest.approx(0.003825, abs=1e-6)
111+ assert res["mediane_proches"] == pytest.approx(0.0452, abs=1e-6)
112+ assert len(res["proches"]) == 10 and res["proches"].iloc[0]["id_vente"] == "M040"
113+
114+
115+def test_valeurs_citees_valeur(res):
116+ assert res["V"] == pytest.approx(2_037_502.89, abs=0.01)
117+ assert res["V_arrondie"] == 2_040_000
118+ assert res["V_reg"] == pytest.approx(1_969_694, abs=1)
119+ assert res["V_proches"] == pytest.approx(1_990_172, abs=1)
120+ assert res["V_q1"] == pytest.approx(1_939_747, abs=1) and res["V_q3"] == pytest.approx(2_166_303, abs=1)
121+
122+
123+def test_valeurs_citees_construction(res):
124+ assert res["f"] == pytest.approx(0.050789, abs=1e-6)
125+ assert res["re_implicite"] == pytest.approx(0.02423, abs=1e-5)
126+ assert res["grille_re"][0.06] == pytest.approx(0.05309, abs=1e-5)
127+ assert res["R_rcd"] == pytest.approx(0.04571, abs=1e-5)
128+ assert res["V_rcd"] == pytest.approx(1_967_946, abs=1)
129+ g = res["gordon"]
130+ assert g["g_terminal"] == pytest.approx(0.0225, abs=1e-9) and g["R_g_tal"] == pytest.approx(0.0415, abs=1e-9)
131+ assert g["g_implicite_marche"] == pytest.approx(0.02835, abs=1e-6)
132+ e = res["ellwood"]
133+ assert e["P"] == pytest.approx(0.122504, abs=1e-6)
134+ assert e["SFF"] == pytest.approx(0.071527, abs=1e-6)
135+ assert e["delta"] == pytest.approx(0.249203, abs=1e-6)
136+ assert e["R_delta0"] == pytest.approx(0.049645, abs=1e-6)
137+ assert e["R_delta"] == pytest.approx(0.03182, abs=1e-5)
138+ assert e["R_sans_dette"] == pytest.approx(0.054675, abs=1e-6)
139+ assert e["delta_implicite"] == pytest.approx(0.076828, abs=1e-6)
140+ assert e["Y_implicite"] == pytest.approx(0.1070, abs=1e-4)
141+ assert e["V_delta"] == pytest.approx(2_826_982, abs=1)
142+
143+
144+def test_valeurs_citees_multiplicateurs_coherence(res):
145+ m = res["mrb"]
146+ assert m["mediane"] == pytest.approx(13.54, abs=1e-6) and m["moyenne"] == pytest.approx(13.372, abs=1e-3)
147+ assert m["V_contrat"] == pytest.approx(2_241_412, abs=1) and m["V_marche"] == pytest.approx(2_518_927, abs=1)
148+ assert m["mrn"] == pytest.approx(22.650, abs=1e-3) and m["mrb_sujet_implicite"] == pytest.approx(12.308, abs=1e-3)
149+ assert m["cv"] > m["cv_taux"]
150+ c = res["coherence"]
151+ assert c["taxes_role"] == pytest.approx(11_925.98, abs=0.01)
152+ assert c["ecart_taxes"] == pytest.approx(880.88, abs=0.01) and c["effet_valeur"] == pytest.approx(19_952, abs=1)
153+ assert c["R_avant_reserve_equiv"] == pytest.approx(0.0481, abs=1e-4)
154+ assert c["V_si_taux_avant_reserve_sur_rne_apres"] == pytest.approx(1_869_136, abs=1)
155+ assert c["rne_marche"] == pytest.approx(108_864.54, abs=0.01)
156+ assert c["V_marche"] == pytest.approx(2_465_788, abs=1) and c["surevaluation_marche"] == pytest.approx(0.2102, abs=1e-4)
157+ mu = res["mutations"]
158+ assert mu["base"] == 2_200_000 and mu["droits"] == pytest.approx(45_460.5, abs=0.01)
159+ assert mu["droits_sur_prix"] == pytest.approx(40_763.07, abs=0.01) and mu["pct_prix"] == pytest.approx(0.0223, abs=1e-4)
160+
161+
162+def test_valeurs_citees_sensibilite(res):
163+ s = res["sensibilite"].set_index("R")
164+ assert s.loc[0.0375, "rne"] == pytest.approx(2_398_820, abs=1)
165+ assert s.loc[0.0525, "rne"] == pytest.approx(1_713_443, abs=1)
166+ assert s.loc[0.045, "rne_p5"] == pytest.approx(2_098_968, abs=1)
167+ d = res["derive"]
168+ assert d["dV_25pb"] == pytest.approx(-115_374, abs=1)
169+ assert d["V_plus25"] == pytest.approx(1_928_312, abs=1) and d["V_moins25"] == pytest.approx(2_159_802, abs=1)
170+ assert d["var_plus25_exacte"] == pytest.approx(-0.0536, abs=1e-4) and d["var_plus25_lineaire"] == pytest.approx(-0.0566, abs=1e-4)
171+
172+
173+def test_exercices(ex):
174+ assert ex["1"]["V"] == pytest.approx(1_288_421.05, abs=0.01) and ex["1"]["V_plus25"] == pytest.approx(1_224_000, abs=0.01)
175+ assert ex["1"]["var"] == pytest.approx(-0.05, abs=1e-9)
176+ assert ex["2"]["mediane"] == pytest.approx(0.044519, abs=1e-6) and ex["2"]["moyenne"] == pytest.approx(0.045395, abs=1e-6)
177+ assert ex["2"]["ponderee"] == pytest.approx(0.045277, abs=1e-6) and ex["2"]["V_mediane"] == pytest.approx(2_020_629, abs=1)
178+ assert ex["3"]["R_apres"] == pytest.approx(0.044118, abs=1e-6) and ex["3"]["R_avant"] == pytest.approx(0.047691, abs=1e-6)
179+ assert ex["3"]["V_errone"] == pytest.approx(1_886_214, abs=1) and ex["3"]["erreur"] == pytest.approx(-0.0749, abs=1e-4)
180+ assert ex["3"]["V_coherent_avant"] == pytest.approx(2_056_119, abs=1)
181+ assert ex["4"]["R_bande"] == pytest.approx(0.053092, abs=1e-6) and ex["4"]["R_rcd"] == pytest.approx(0.047615, abs=1e-6)
182+ assert ex["4"]["V_bande"] == pytest.approx(1_694_335, abs=1) and ex["4"]["V_rcd"] == pytest.approx(1_889_228, abs=1)
183+ assert ex["5"]["V_225"] == pytest.approx(1_799_115, abs=1) and ex["5"]["V_310"] == pytest.approx(2_167_608, abs=1) and ex["5"]["V_g4"] == pytest.approx(2_767_869, abs=1)
184+ assert ex["6"]["R_ellwood"] == pytest.approx(ex["6"]["R_akerson"], abs=1e-12)
185+ assert ex["6"]["R_ellwood"] == pytest.approx(0.031763, abs=1e-6) and ex["6"]["R_sans_dette"] == pytest.approx(0.054618, abs=1e-6)
186+ assert ex["6"]["g_equiv"] == pytest.approx(0.022565, abs=1e-6) and ex["6"]["R_gordon_equiv"] == pytest.approx(0.049935, abs=1e-6)
187+ assert ex["6"]["V"] == pytest.approx(2_832_053, abs=1)
188+ assert ex["7"]["ecart_rne"] == pytest.approx(18_908.79, abs=0.01) and ex["7"]["V_mrb_vs_cap"] == pytest.approx(0.1001, abs=1e-4)
189+ assert ex["8"]["R_24_etat2"] == pytest.approx(0.043381, abs=1e-6) and ex["8"]["R_6_etat4"] == pytest.approx(0.046594, abs=1e-6)
190+ assert ex["8"]["V_pire"] == pytest.approx(1_831_896, abs=1) and ex["8"]["V_meilleur"] == pytest.approx(2_267_792, abs=1)
191+ assert ex["8"]["var_pire"] == pytest.approx(-0.1009, abs=1e-4) and ex["8"]["var_meilleur"] == pytest.approx(0.1130, abs=1e-4)
192+
193+
194+def test_figures_presentes():
195+ for slug in ("taux-extraits", "taux-etat-taille", "surface-v", "bande-investissement", "taux-gdc"):
196+ assert (ROOT / f"assets/figures/ch29/ch29-fig-{slug}.pdf").exists(), slug
added analysis/python/tests/test_ch30.py +143 −0
@@ -0,0 +1,143 @@
1+"""Tests du chapitre 30 — flux actualisés et valeur terminale (cas 3, données synthétiques).
2+Identités : VAN(TRI) = 0 ; VT = RNE_{T+1}/R_T × (1 − c_v) ; DCF = Σ VA + VA(VT) ; cohérence Gordon (DCF = RNE_1/(r − g)
3+sous croissance constante, quel que soit T) ; flux après dette = flux − SD ; valeurs citées dans manuscript/chapters/ch30.tex."""
4+import math, pytest
5+from traite import finance as fin
6+from cases import ch30_dcf as c
7+
8+@pytest.fixture(scope="module")
9+def R():
10+ return c.base()
11+
12+# ------------------------------------------------------------------ valeurs citées (tolérances déclarées)
13+
14+def test_valeurs_citees_dcf(R):
15+ d = R["dcf"]; h = R["hyp"]
16+ assert d["valeur"] == pytest.approx(2_208_566, abs=1) # valeur DCF
17+ assert d["va_flux"] == pytest.approx(662_410, abs=1)
18+ assert d["va_terminale"] == pytest.approx(1_546_156, abs=1)
19+ assert d["vt_brute"] == pytest.approx(3_193_168, abs=1)
20+ assert d["vt_nette"] == pytest.approx(3_113_338, abs=1)
21+ assert d["rne_T1"] == pytest.approx(159_658, abs=1)
22+ assert d["part_terminale"] == pytest.approx(0.700, abs=0.001)
23+ assert R["valeur_capitalisation"] == pytest.approx(2_037_503, abs=1)
24+ assert R["taux"]["mediane"] == pytest.approx(0.04415, abs=1e-6)
25+ assert R["base"]["rne"] == pytest.approx(89_956, abs=1)
26+ assert h["taux_actualisation"] == 0.0725 and abs(h["taux_terminal"] - 0.05) < 1e-12 and h["couts_vente"] == 0.025 and h["horizon_ans"] == 10
27+
28+def test_tableau_dcf_identites(R):
29+ t = c.tableau_dcf(R); h = R["hyp"]; T = h["horizon_ans"]
30+ tt = t[t["annee"] <= T]
31+ assert len(t) == T + 1
32+ # RNE = RBE − dépenses − réserve ; flux = RNE − CAPEX ; VA = flux × facteur ; Σ VA = va_flux
33+ assert (tt["rne"] - (tt["rbe"] - tt["depenses"] - tt["reserve"])).abs().max() < 1e-6
34+ assert (tt["flux_avant_dette"] - (tt["rne"] - tt["capex"])).abs().max() < 1e-6
35+ assert (tt["va"] - tt["flux_avant_dette"] * (1 + h["taux_actualisation"]) ** (-tt["annee"])).abs().max() < 1e-6
36+ assert tt["va"].sum() == pytest.approx(R["dcf"]["va_flux"], rel=1e-12)
37+ assert tt["va_cumulee"].iloc[-1] == pytest.approx(R["dcf"]["va_flux"], rel=1e-12)
38+ assert tt["capex"].sum() == pytest.approx(85_000 + 40_000 + 120_000)
39+ assert (t["rbp"] <= t["rbp_marche"] + 1e-6).all()
40+
41+def test_valeur_terminale(R):
42+ d = R["dcf"]; h = R["hyp"]
43+ vt_b, vt_n = fin.valeur_terminale(d["rne_T1"], h["taux_terminal"], h["couts_vente"])
44+ assert vt_b == pytest.approx(d["rne_T1"] / h["taux_terminal"]) and vt_n == pytest.approx(vt_b * (1 - h["couts_vente"]))
45+ assert d["va_terminale"] == pytest.approx(vt_n * fin.facteur_va(h["taux_actualisation"], h["horizon_ans"]), rel=1e-12)
46+ # actualiser à T+1 au lieu de T serait une erreur de (1+r)^-1 : on vérifie l'ampleur citée (≈ 104 519 $)
47+ erreur = vt_n * fin.facteur_va(h["taux_actualisation"], h["horizon_ans"]) - vt_n * fin.facteur_va(h["taux_actualisation"], h["horizon_ans"] + 1)
48+ assert erreur == pytest.approx(104_519, abs=1)
49+ assert d["valeur"] == pytest.approx(d["va_flux"] + d["va_terminale"], rel=1e-12)
50+
51+# ------------------------------------------------------------------ TRI / VAN
52+
53+def test_van_au_tri_nulle_sans_levier(R):
54+ sl = c.dcf_sans_levier(R)
55+ assert sl["tri"] == pytest.approx(R["hyp"]["taux_actualisation"], abs=1e-9) # acheter à V_0 rapporte exactement r
56+ assert abs(sl["van_au_taux"]) < 1e-4
57+ assert abs(fin.van(sl["flux"], sl["tri"])) < 1e-4
58+ assert sl["mirr"] == pytest.approx(R["hyp"]["taux_actualisation"], abs=1e-9) # MIRR(r, r) = r quand VAN(r) = 0
59+ assert 9.0 < sl["delai_recuperation"] < 10.0
60+
61+def test_levier(R):
62+ lv = c.flux_levier(R); f = R["financement"]; h = R["hyp"]; T = h["horizon_ans"]
63+ hyp = f["hypotheque"]
64+ assert lv["service_dette"] == pytest.approx(hyp.paiement * 12)
65+ assert lv["mise_de_fonds"] == pytest.approx(R["dcf"]["valeur"] - f["pret"])
66+ assert lv["solde_final"] == pytest.approx(hyp.solde(T * 12))
67+ tab = lv["tableau"]
68+ assert (tab["flux_apres_dette"] - (tab["flux_avant_dette"] - tab["service_dette"])).abs().max() < 1e-6
69+ assert tab["flux_fonds_propres"].iloc[-1] == pytest.approx(tab["flux_apres_dette"].iloc[-1] + R["dcf"]["vt_nette"] - lv["solde_final"])
70+ assert abs(fin.van(lv["flux"], lv["tri"])) < 1e-4 # VAN(TRI) = 0
71+ assert lv["changements_signe"] == 5 # flux non conventionnels (CAPEX 2027, 2032)
72+ assert lv["tri"] == pytest.approx(0.1122, abs=0.0005)
73+ assert lv["mirr"] == pytest.approx(0.1061, abs=0.0005)
74+ assert h["financement"]["taux"] < lv["mirr"] < lv["tri"] # MIRR entre taux de financement et TRI ici
75+ assert lv["tri"] > h["taux_actualisation"] # levier positif
76+ assert lv["mise_de_fonds"] == pytest.approx(732_606, abs=1) and lv["solde_final"] == pytest.approx(1_295_148, abs=1)
77+ assert lv["rcd_1"] == pytest.approx(1.20, abs=1e-6)
78+ # composition semestrielle : i_m = (1 + i/2)^(1/6) − 1 < i/12
79+ assert lv["taux_mensuel"] == pytest.approx((1 + h["financement"]["taux"] / 2) ** (1 / 6) - 1) and lv["taux_mensuel"] < h["financement"]["taux"] / 12
80+ assert lv["taux_effectif_annuel_hyp"] == pytest.approx((1 + h["financement"]["taux"] / 2) ** 2 - 1)
81+
82+# ------------------------------------------------------------------ cohérence DCF ↔ capitalisation directe (Gordon)
83+
84+@pytest.mark.parametrize("T", [1, 5, 10, 25, 60])
85+def test_proposition_gordon(T):
86+ v = c.verification_gordon(noi1=100_000, r=0.0725, g=0.035, T=T)
87+ assert v["V_dcf"] == pytest.approx(v["V_gordon"], rel=1e-10)
88+ # la part de la valeur terminale décroît avec T et vaut ((1+g)/(1+r))^T
89+ assert v["part_terminale"] == pytest.approx(((1 + 0.035) / (1 + 0.0725)) ** T, rel=1e-9)
90+
91+def test_gordon_avec_couts_de_vente_brise_l_egalite():
92+ v = c.verification_gordon(noi1=100_000, r=0.0725, g=0.035, T=10, couts_vente=0.025)
93+ assert v["V_dcf"] < v["V_gordon"]
94+ assert v["V_gordon"] - v["V_dcf"] == pytest.approx(0.025 * v["V_gordon"] * ((1 + 0.035) / (1 + 0.0725)) ** 10, rel=1e-9)
95+
96+def test_taux_implicites(R):
97+ ti = c.taux_implicites(R); h = R["hyp"]
98+ assert ti["R_implicite_dcf"] == pytest.approx(R["base"]["rne"] / R["dcf"]["valeur"])
99+ assert ti["R_implicite_dcf"] == pytest.approx(0.0407, abs=0.0001)
100+ assert ti["g_implicite_terminal"] == pytest.approx(h["taux_actualisation"] - h["taux_terminal"])
101+ assert ti["croissance_rne_annualisee"] == pytest.approx(0.0590, abs=0.0005)
102+ assert ti["tri_si_paye_V_cap"] == pytest.approx(0.0825, abs=0.0005)
103+ assert ti["tri_si_paye_V_cap"] > h["taux_actualisation"] # payer moins que V_0 rapporte plus que r
104+ assert ti["ecart_dcf_cap"] == pytest.approx(171_063, abs=1)
105+ assert fin.taux_gordon(h["taux_actualisation"], h["croissance_loyers_marche"]) == pytest.approx(0.0375)
106+
107+# ------------------------------------------------------------------ sensibilité et scénarios
108+
109+def test_sensibilite_monotone(R):
110+ s = c.sensibilite_taux_terminal(R)
111+ assert (s["valeur"].diff().dropna() < 0).all() # V décroît en R_T
112+ assert s.loc[(s["taux_terminal"] - 0.05).abs() < 1e-9, "valeur"].iloc[0] == pytest.approx(R["dcf"]["valeur"])
113+ assert s["valeur"].iloc[0] == pytest.approx(2_481_417, abs=1) and s["valeur"].iloc[-1] == pytest.approx(2_006_893, abs=1)
114+ S = c.surface_r_Rt(R)
115+ assert (S.diff().dropna() < 0).values.all() and (S.T.diff().dropna() < 0).values.all() # V décroît en r et en R_T
116+ G = c.surface_r_g(R)
117+ assert (G.T.diff().dropna() > 0).values.all() # V croît avec g_marché
118+
119+def test_tornade_et_scenarios(R):
120+ t = c.tornade(R)
121+ assert t["intrant"].iloc[0].startswith("Croissance des loyers de marché")
122+ assert (t["amplitude"].diff().dropna() <= 1e-9).all()
123+ assert t["amplitude"].iloc[0] == pytest.approx(368_235, abs=1)
124+ s = c.scenarios(R).set_index("scenario")
125+ assert s.loc["Base", "valeur"] == pytest.approx(R["dcf"]["valeur"])
126+ assert s.loc["Base", "tri_sans_levier_prix_base"] == pytest.approx(0.0725, abs=1e-9)
127+ assert s.loc["Encadrement serré", "valeur"] == pytest.approx(1_912_046, abs=1)
128+ assert s.loc["Hausse des taux", "valeur"] == pytest.approx(1_947_889, abs=1)
129+ assert s.loc["Encadrement serré", "tri_sans_levier_prix_base"] < 0.0725 and s.loc["Hausse des taux", "tri_sans_levier_prix_base"] < 0.0725
130+ assert s.loc["Hausse des taux", "rcd_min"] < 1.2
131+
132+def test_exercices_coherence():
133+ e = c.exercices()
134+ assert e["ex1"]["R"] == pytest.approx(0.0375)
135+ assert e["ex2"]["va_10"] - e["ex2"]["va_11_erronee"] == pytest.approx(e["ex2"]["ecart"])
136+ f5 = e["ex3"]
137+ assert f5["valeur"] == pytest.approx(f5["va_flux"] + f5["va_vt"])
138+ assert f5["van_prix"] < 0 and f5["tri_prix"] < 0.07
139+ assert e["ex4"]["i_m_can"] < e["ex4"]["i_m_us"] and e["ex4"]["paiement_can"] < e["ex4"]["paiement_us"]
140+ for T, v in e["ex5"].items():
141+ assert v["V_dcf"] == pytest.approx(v["V_gordon"], rel=1e-10)
142+ assert e["ex6"]["valeur"] == pytest.approx(1_986_966, abs=1)
143+ assert abs(fin.van(e["ex7"]["flux"], e["ex7"]["tri"])) < 1e-4
added analysis/python/tests/test_ch47.py +117 −0
@@ -0,0 +1,117 @@
1+"""Tests du chapitre 47 (cas 3, 12 logements à Montréal — données synthétiques) : identités, valeurs citées dans le texte,
2+reproductibilité du Monte-Carlo. Tolérances déclarées : montants ±1 $ (valeurs citées au dollar), taux ±1e-6, MC ±1 $ (même graine)."""
3+import sys, pathlib
4+import numpy as np, pytest
5+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))
6+from traite import cas3, finance as fin
7+import ch47_dossier as ch
8+
9+@pytest.fixture(scope="module")
10+def D():
11+ return ch.dossier(n_mc=10_000, graine=ch.GRAINE)
12+
13+# ----------------------------------------------------------------------------- identités
14+
15+def test_identites_revenus_depenses(D):
16+ b = D["base"]
17+ assert b["rbp_contrat"] == pytest.approx(D["etat"]["loyer_mensuel_contrat"].sum() * 12)
18+ assert b["rbe"] == pytest.approx(b["rbp_contrat"] * (1 - 0.039) + 9360)
19+ assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"])
20+ det = b["detail"]
21+ assert det.loc[det["poste"] == "Taxes municipales", "normalise"].iloc[0] == pytest.approx(2_200_000 * 0.004631)
22+ assert det.loc[det["poste"].str.startswith("Gestion"), "normalise"].iloc[0] == pytest.approx(0.04 * b["rbe"])
23+
24+def test_capitalisation_et_dcf(D):
25+ b, tx, d, h = D["base"], D["taux"], D["dcf"], D["hyp"]
26+ assert tx["n"] == 40
27+ assert D["valeur_capitalisation"] == pytest.approx(b["rne"] / tx["mediane"])
28+ p = D["projection"]
29+ flux = p.loc[p["annee"] <= 10, "flux_avant_dette"].tolist()
30+ va = sum(cf / (1 + h["taux_actualisation"]) ** (t + 1) for t, cf in enumerate(flux)) + d["vt_nette"] / (1 + h["taux_actualisation"]) ** 10
31+ assert d["valeur"] == pytest.approx(va)
32+ assert d["vt_brute"] == pytest.approx(d["rne_T1"] / h["taux_terminal"]) and d["vt_nette"] == pytest.approx(d["vt_brute"] * 0.975)
33+ # taux implicite de première année de la valeur DCF et TRI non levier = taux d'actualisation
34+ assert D["financement_dcf"]["tri_sans_levier"] == pytest.approx(h["taux_actualisation"], abs=1e-8)
35+
36+def test_financement_identites(D):
37+ f = D["financement"]; h = f["hypotheque"]
38+ assert f["contrainte"] == "RCD" and f["pret"] == pytest.approx(f["pret_rcd"])
39+ assert f["rcd"] == pytest.approx(1.20) and f["rpv"] <= 0.75
40+ assert h.taux_periodique == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1)
41+ assert h.service_dette_annuel == pytest.approx(12 * h.paiement)
42+ assert h.solde(480) == pytest.approx(0.0, abs=1e-6)
43+ ff = D["financement_final"]
44+ assert fin.van(ff["flux_fonds_propres"], ff["tri_fonds_propres"]) == pytest.approx(0.0, abs=1e-3) # VAN nulle au TRI
45+
46+def test_projection_vectorisee_reproduit_cas3(D):
47+ """La projection vectorisée (Monte-Carlo) doit coïncider exactement avec cas3.projection_dcf aux paramètres de base."""
48+ h, b, e = D["hyp"], D["base"], D["etat"]
49+ one = lambda x: np.array([x])
50+ proj = ch.projeter_vectorise(e, h, b, one(h["taux_inoccupation_pertes"]), one(h["croissance_loyers_marche"]), one(h["rotation_annuelle"]), one(h["croissance_depenses"]), np.ones((1, 3)))
51+ p = D["projection"]
52+ assert np.allclose(proj["rne"][0], p["rne"].values, rtol=1e-12)
53+ assert np.allclose(proj["flux"][0], p.loc[p["annee"] <= 10, "flux_avant_dette"].values, rtol=1e-12)
54+ v = ch.valeur_vectorisee(proj, one(h["taux_actualisation"]), one(h["taux_terminal"]), h["couts_vente"])
55+ assert v["valeur"][0] == pytest.approx(D["dcf"]["valeur"], rel=1e-12)
56+ assert D["tornade"]["V_base"].iloc[0] == pytest.approx(D["dcf"]["valeur"], rel=1e-9)
57+
58+def test_droits_mutation_montreal():
59+ assert ch.droits_mutation_montreal(2_060_000) == pytest.approx(41_325.50)
60+ assert ch.droits_mutation_montreal(62_900) == pytest.approx(314.50)
61+ assert ch.droits_mutation_montreal(0) == 0.0
62+
63+# ----------------------------------------------------------------------------- valeurs citées dans ch47.tex (tolérance ±1 $ sauf indication)
64+
65+def test_valeurs_citees(D):
66+ b, tx, d, f, rec, cu = D["base"], D["taux"], D["dcf"], D["financement"], D["reconciliation"], D["comparaison_unite"]
67+ assert b["rbp_contrat"] == pytest.approx(165_540, abs=1) and b["rbp_marche"] == pytest.approx(186_036, abs=1)
68+ assert b["rbe"] == pytest.approx(168_443.94, abs=0.01)
69+ assert b["depenses_hors_reserve"] == pytest.approx(70_385.19, abs=0.01) and b["reserve"] == pytest.approx(8_103, abs=0.01)
70+ assert b["rne"] == pytest.approx(89_955.75, abs=0.01)
71+ assert tx["mediane"] == pytest.approx(0.04415, abs=1e-6) and tx["moyenne"] == pytest.approx(0.0440375, abs=1e-6) and tx["ponderee_rne"] == pytest.approx(0.043319, abs=1e-6)
72+ assert tx["q1"] == pytest.approx(0.041525, abs=1e-6) and tx["q3"] == pytest.approx(0.046375, abs=1e-6)
73+ assert D["valeur_capitalisation"] == pytest.approx(2_037_502.89, abs=0.01)
74+ assert d["valeur"] == pytest.approx(2_208_565.91, abs=0.01) and d["va_flux"] == pytest.approx(662_409.91, abs=0.01) and d["va_terminale"] == pytest.approx(1_546_156.00, abs=0.01)
75+ assert d["vt_brute"] == pytest.approx(3_193_167.52, abs=0.01) and d["rne_T1"] == pytest.approx(159_658.38, abs=0.01) and d["part_terminale"] == pytest.approx(0.7001, abs=1e-4)
76+ assert f["pret_rcd"] == pytest.approx(1_475_959.45, abs=0.01) and f["pret_rpv"] == pytest.approx(1_656_424.43, abs=0.01)
77+ assert f["service_dette"] == pytest.approx(74_963.13, abs=0.01) and f["constante"] == pytest.approx(0.050789, abs=1e-6) and f["hypotheque"].paiement == pytest.approx(6_246.93, abs=0.01)
78+ assert f["hypotheque"].solde(60) == pytest.approx(1_394_739.19, abs=0.01)
79+ assert D["financement_dcf"]["tri_fonds_propres"] == pytest.approx(0.1122, abs=1e-4) and D["financement_dcf"]["point_mort"] == pytest.approx(0.927, abs=1e-3)
80+ assert cu["indication"] == pytest.approx(1_870_902.67, abs=0.01) and cu["prix_par_logement_retenu"] == pytest.approx(155_908.56, abs=0.01)
81+ assert cu["mrb_mediane"] == pytest.approx(11.94, abs=1e-9) and cu["indication_mrb"] == pytest.approx(1_976_547.60, abs=0.01)
82+ assert cu["ols"]["coef"]["ln_loyer"] == pytest.approx(1.1707, abs=1e-4) and cu["ols"]["coef"]["taille"] == pytest.approx(0.0079, abs=1e-4)
83+ assert rec["valeur_ponderee"] == pytest.approx(2_064_054.90, abs=0.01) and rec["valeur_finale"] == 2_060_000
84+ assert rec["ratio_valeur_role"] == pytest.approx(0.9364, abs=1e-4) and rec["mrb_contrat"] == pytest.approx(12.44, abs=0.01) and rec["taux_implicite"] == pytest.approx(0.04367, abs=1e-5)
85+ ff = D["financement_final"]
86+ assert ff["mise_de_fonds"] == pytest.approx(584_040.55, abs=0.01) and ff["rpv"] == pytest.approx(0.7165, abs=1e-4) and ff["tri_fonds_propres"] == pytest.approx(0.1383, abs=1e-4)
87+ assert D["libre_de_baux"]["rne"] == pytest.approx(108_864.54, abs=0.01) and D["libre_de_baux"]["valeur"] == pytest.approx(2_465_788.04, abs=0.01)
88+ sc = D["scenarios"]
89+ assert sc.loc[sc["hausse_pb"] == 200, "service_dette"].iloc[0] == pytest.approx(95_920.20, abs=0.01) and sc.loc[sc["hausse_pb"] == 300, "rcd_6"].iloc[0] == pytest.approx(1.222, abs=1e-3)
90+ aph = D["aph"]
91+ assert aph.loc[aph["amortissement_ans"] == 40, "pret"].iloc[0] == pytest.approx(1_610_137.58, abs=0.01)
92+
93+def test_monte_carlo_citations_et_reproductibilite(D):
94+ mc = D["mc"]; st = mc["stats"]; pr = mc["probabilites"]
95+ assert mc["n"] == 10_000 and len(mc["valeur"]) == 10_000
96+ assert st["q05"] < st["q25"] < st["mediane"] < st["q75"] < st["q95"]
97+ assert st["mediane"] == pytest.approx(2_191_607.87, abs=1) and st["q05"] == pytest.approx(1_735_194.21, abs=1) and st["q95"] == pytest.approx(2_876_902.35, abs=1)
98+ assert st["moyenne"] == pytest.approx(2_233_505.68, abs=1) and st["ecart_type"] == pytest.approx(350_442.57, abs=1)
99+ assert pr["rcd1_sous_1.20"] == pytest.approx(0.4936, abs=1e-4) and pr["rcd1_sous_1.00"] == 0.0 and pr["rcd6_choc200_sous_1.20"] == pytest.approx(0.0140, abs=1e-4)
100+ assert pr["V_sous_cap"] == pytest.approx(0.3094, abs=1e-4) and pr["V_sous_role"] == pytest.approx(0.5118, abs=1e-4)
101+ # même graine → résultats identiques ; graine différente → distribution voisine (±1,5 % sur la médiane)
102+ mc2 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, ch.GRAINE)
103+ assert np.array_equal(mc2["valeur"], mc["valeur"])
104+ mc3 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, 7)
105+ assert mc3["stats"]["mediane"] == pytest.approx(st["mediane"], rel=0.015)
106+ # la médiane du RCD de première année est au seuil 1,20 (prêt calibré au seuil)
107+ assert float(np.median(mc["rcd"][:, 0])) == pytest.approx(1.20, abs=0.02)
108+
109+def test_exercices(D):
110+ ex = ch.exercices(D, D["comparaison_unite"], D["reconciliation"])
111+ assert ex["ex1"]["rne"] == pytest.approx(88_207.65, abs=0.01) and ex["ex1"]["valeur"] == pytest.approx(1_997_908.27, abs=0.01)
112+ assert ex["ex2"]["rne"] == pytest.approx(88_949.51, abs=0.01)
113+ assert ex["ex3"]["V_q1"] == pytest.approx(2_166_303.49, abs=0.01) and ex["ex3"]["V_q3"] == pytest.approx(1_939_746.68, abs=0.01)
114+ assert ex["ex4_rot10"]["valeur"] == pytest.approx(2_140_916.43, abs=0.01) and ex["ex4_rot25"]["valeur"] == pytest.approx(2_231_654.72, abs=0.01)
115+ assert ex["ex5"]["pret"] == pytest.approx(1_283_814.77, abs=0.01) and ex["ex5"]["contrainte"] == "RCD"
116+ assert ex["ex6"]["pret"] == pytest.approx(1_610_137.58, abs=0.01) and ex["ex6"]["prime_pct"] == pytest.approx(0.051, abs=1e-6)
117+ assert ex["ex7"]["p_rcd1_sous_1.20"] == pytest.approx(0.9262, abs=1e-4) and ex["ex7"]["mediane"] == pytest.approx(2_108_706.66, abs=1)
added assets/figures/ch21/ch21-fig-decomposition-corps.pdf +0 −0

Binary file not shown.

added assets/figures/ch21/ch21-fig-ipcb-montreal.pdf +0 −0

Binary file not shown.

added assets/figures/ch21/ch21-fig-sensibilite.pdf +0 −0

Binary file not shown.

added assets/figures/ch29/ch29-fig-bande-investissement.pdf +0 −0

Binary file not shown.

added assets/figures/ch29/ch29-fig-surface-v.pdf +0 −0

Binary file not shown.

added assets/figures/ch29/ch29-fig-taux-etat-taille.pdf +0 −0

Binary file not shown.

added assets/figures/ch29/ch29-fig-taux-extraits.pdf +0 −0

Binary file not shown.

added assets/figures/ch29/ch29-fig-taux-gdc.pdf +0 −0

Binary file not shown.

added assets/figures/ch30/ch30-fig-flux.pdf +0 −0

Binary file not shown.

added assets/figures/ch30/ch30-fig-gdc.pdf +0 −0

Binary file not shown.

added assets/figures/ch30/ch30-fig-loyers.pdf +0 −0

Binary file not shown.

added assets/figures/ch30/ch30-fig-surface.pdf +0 −0

Binary file not shown.

added assets/figures/ch30/ch30-fig-tornade.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-depenses.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-etat-locatif.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-flux-dcf.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-montecarlo.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-rcd-scenarios.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-taux-extraits.pdf +0 −0

Binary file not shown.

added assets/figures/ch47/ch47-fig-tornade.pdf +0 −0

Binary file not shown.

modified manuscript/chapters/ch16.tex +447 −1
@@ -1,2 +1,448 @@
1 \chapter{Substitution et choix des comparables}\label{ch:16} 1 \chapter{Substitution et choix des comparables}\label{ch:16}
2−\stubchapter 2+
3+La méthode de comparaison est la plus ancienne et la plus intuitive des méthodes d'évaluation : pour savoir ce que vaut une maison, on regarde ce que des maisons semblables se sont vendues. Cette simplicité apparente cache deux difficultés redoutables. La première est de décider ce que « semblable » veut dire, c'est-à-dire de choisir, parmi des centaines de transactions, la poignée de ventes qui informent réellement sur le sujet. La seconde est de corriger les différences résiduelles entre ces ventes et le sujet, ce qui fait l'objet du \cref{ch:17}. Le présent chapitre traite de la première difficulté. Il établit d'abord le fondement économique de la méthode, le principe de substitution, puis il le formalise : la valeur recherchée est une espérance conditionnelle et les comparables en sont un échantillon. Il en déduit des règles de sélection qui ne reposent plus seulement sur l'habitude, mais sur un arbitrage explicite entre le biais et la variance de l'estimation. L'exemple intégral conduit la sélection des comparables pour la maison du dossier de Gatineau (cas 1), et l'analyse de sensibilité montre que le choix des comparables pèse sur la conclusion autant que les ajustements qui viendront ensuite.
4+
5+\begin{objectifs}
6+\begin{itemize}
7+\item Énoncer le principe de substitution et le relier à la définition de la valeur marchande en vigueur au Québec.
8+\item Formaliser la méthode de comparaison comme l'estimation d'une espérance conditionnelle à partir d'un échantillon de ventes.
9+\item Choisir une unité de comparaison appropriée et en connaître les limites.
10+\item Appliquer les cinq critères de comparabilité et vérifier qu'une vente est représentative du marché.
11+\item Construire une procédure de sélection reproductible : filtres qualitatifs, distance d'attributs, voisins les plus proches, taille de l'échantillon.
12+\item Mesurer la sensibilité d'une indication de valeur au nombre, au rayon et à la fenêtre temporelle des comparables.
13+\end{itemize}
14+\end{objectifs}
15+
16+\section{Le principe de substitution et la logique de la méthode}\label{sec:16:substitution}
17+
18+\subsection{Un principe économique, pas une règle de calcul}
19+
20+Le \motcle{principe de substitution} affirme qu'un acheteur rationnel et informé ne paiera pas, pour un bien, davantage que le coût d'acquisition d'un bien de substitution offrant la même utilité et disponible dans un délai raisonnable. Appliqué aux immeubles résidentiels, le principe signifie que le prix d'une propriété est borné par les prix auxquels des propriétés concurrentes s'échangent au même moment sur le même marché. C'est ce principe qui fonde les trois méthodes classiques : la méthode de comparaison substitue des propriétés déjà vendues, la méthode du coût substitue la construction d'un bien équivalent (\cref{ch:21}), et la méthode du revenu substitue un placement de rendement équivalent (\cref{ch:29}). Dans le cas de la comparaison, la substitution est directe, ce qui explique à la fois sa force probante aux yeux des tribunaux et des prêteurs et sa fragilité lorsque le marché est mince.
21+
22+Le principe suppose trois conditions dont aucune n'est parfaitement remplie en pratique. Il faut que des biens substituables existent et se soient échangés récemment ; il faut que les acheteurs soient informés de ces échanges ; il faut enfin que les différences entre le sujet et ses substituts puissent être mesurées et traduites en dollars. Le marché résidentiel québécois satisfait raisonnablement la première condition dans les grandes agglomérations, où les ventes se comptent par milliers chaque trimestre\footnote{Au troisième trimestre de 2026, l'APCIQ recensait \num{9265} ventes résidentielles dans la RMR de Montréal, \num{2161} dans celle de Québec et \num{1044} dans celle de Gatineau \parencite{apciqt32026,apciqgatineaut32026}.}, mais beaucoup moins bien dans les petites municipalités ou pour les biens atypiques. La deuxième condition est mieux remplie depuis que les prix de vente sont publics au registre foncier et diffusés par les plateformes de courtage ; la troisième est l'objet même du \cref{ch:17}.
23+
24+\begin{definition}{Comparable}{comparable}
25+Une vente \motcle{comparable} est la transaction, conclue sur le même marché que le sujet et à une date proche de la date d'évaluation, d'un immeuble qui aurait pu constituer, pour l'acheteur type du sujet, un substitut acceptable. Un comparable n'est pas un bien identique : c'est un bien dont les différences avec le sujet sont suffisamment peu nombreuses et suffisamment bien comprises pour être quantifiées.
26+\end{definition}
27+
28+\begin{quebec}
29+La définition de la valeur marchande retenue par les Normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec repose explicitement sur la substitution : il s'agit du « prix sincère le plus probable » d'une vente « sur un marché libre et ouvert à la concurrence », entre des parties bien informées de l'état de l'immeuble et des conditions du marché, après une mise en vente d'une durée suffisante, pour un paiement en argent comptant ou l'équivalent, et une fois le prix épuré des incitatifs et des conditions de financement avantageuses \parencite[Intro-11]{oeaqNormes}. Chaque élément de cette définition devient, à la \cref{sec:16:criteres}, un critère de sélection ou de vérification des ventes. La définition de la valeur réelle aux fins du rôle d'évaluation foncière (art.~43 à 45 de la Loi sur la fiscalité municipale) est construite sur la même idée du prix le plus probable d'une vente de gré à gré entre parties informées \parencite[art.~43--45]{lfm} ; le \cref{ch:9} en détaille les conséquences.
30+\end{quebec}
31+
32+\subsection{La valeur comme espérance conditionnelle}
33+
34+Pour raisonner rigoureusement sur la sélection des comparables, il faut d'abord dire ce que la méthode cherche à estimer. Soit $P$ le prix auquel un immeuble se vend et $\mathbf{x}$ le vecteur de ses caractéristiques observables (superficie, âge, qualité, localisation, date de la vente, droits transmis, etc.). Deux immeubles de caractéristiques identiques ne se vendent pas au même prix : la négociation, l'urgence des parties, l'information imparfaite et la rareté des substituts au moment précis de la vente engendrent une dispersion que l'on représente par un terme aléatoire. La valeur marchande du sujet de caractéristiques $\mathbf{x}_s$ à la date $t$ est alors définie comme le prix espéré conditionnellement à ces caractéristiques :
35+\begin{equation}\label{eq:16:esperance}
36+V_s = \E\left[P \mid \mathbf{x} = \mathbf{x}_s,\ t\right].
37+\end{equation}
38+Cette écriture dit exactement ce que « prix le plus probable » veut dire dans les définitions professionnelles et légales : non pas le prix que le sujet obtiendra à coup sûr, mais le centre de la distribution des prix auxquels il pourrait s'échanger. Elle a une conséquence immédiate pour le choix des comparables. Si l'on disposait d'un très grand nombre de ventes d'immeubles identiques au sujet, conclues à la date $t$, la moyenne de leurs prix estimerait $V_s$ sans biais et avec une variance décroissant comme l'inverse de leur nombre. Comme ces ventes n'existent pas, on se rabat sur des immeubles \emph{voisins} du sujet dans l'espace des caractéristiques, et il faut alors corriger le prix de chacun de la différence entre ses caractéristiques et celles du sujet.
39+
40+Supposons, en première approximation, que le prix dépende linéairement des caractéristiques autour du sujet, avec des prix implicites $\beta_k$ pour chaque caractéristique $k$ :
41+\begin{equation}\label{eq:16:local}
42+P_j = V_s + \sum_{k=1}^{K} \beta_k \left(x_{jk} - x_{sk}\right) + \varepsilon_j, \qquad \E[\varepsilon_j] = 0,\ \Var[\varepsilon_j] = \sigma_j^2 .
43+\end{equation}
44+Le prix du comparable $j$ est le prix du sujet, plus la contribution de ses différences, plus un bruit propre à la transaction. En soustrayant la contribution des différences, on obtient le \motcle{prix ajusté}, qui est une mesure bruitée de la valeur du sujet :
45+\begin{equation}\label{eq:16:ajuste}
46+P^{\text{aj}}_j = P_j - \sum_{k=1}^{K} \beta_k \left(x_{jk} - x_{sk}\right) = P_j + \sum_{k=1}^{K} A_{jk} = V_s + \varepsilon_j ,
47+\end{equation}
48+où $A_{jk} = \beta_k (x_{sk} - x_{jk})$ est l'ajustement monétaire pour la caractéristique $k$, compté du point de vue du sujet : positif si le sujet est supérieur au comparable. La valeur indiquée par la méthode de comparaison est une moyenne pondérée des prix ajustés :
49+\begin{equation}\label{eq:16:moyenne}
50+\hat V_s = \sum_{j=1}^{n} w_j\, P^{\text{aj}}_j, \qquad \sum_{j=1}^{n} w_j = 1,\ w_j \geq 0 .
51+\end{equation}
52+
53+\begin{proposition}{Propriétés de l'indication de valeur}{indication}
54+Sous le modèle local \eqref{eq:16:local}, si les ajustements sont exacts et les bruits de transaction indépendants, l'estimateur \eqref{eq:16:moyenne} est sans biais, $\E[\hat V_s] = V_s$, et sa variance vaut
55+\begin{equation}\label{eq:16:variance}
56+\Var[\hat V_s] = \sum_{j=1}^{n} w_j^2\, \sigma_j^2 .
57+\end{equation}
58+Les pondérations qui minimisent cette variance sous la contrainte $\sum_j w_j = 1$ sont inversement proportionnelles à la variance de chaque comparable :
59+\begin{equation}\label{eq:16:poids}
60+w_j^{*} = \frac{1/\sigma_j^2}{\sum_{i=1}^{n} 1/\sigma_i^2}, \qquad \Var[\hat V_s^{*}] = \left(\sum_{i=1}^{n} \frac{1}{\sigma_i^2}\right)^{-1}.
61+\end{equation}
62+\end{proposition}
63+\begin{proof}
64+L'absence de biais découle de $\E[P^{\text{aj}}_j] = V_s$ et de $\sum_j w_j = 1$. La variance résulte de l'indépendance des $\varepsilon_j$. Pour les pondérations optimales, on minimise le lagrangien $\sum_j w_j^2 \sigma_j^2 - \lambda(\sum_j w_j - 1)$ ; la condition du premier ordre donne $w_j = \lambda / (2\sigma_j^2)$, et la contrainte fixe $\lambda$. La substitution dans \eqref{eq:16:variance} donne la variance minimale.
65+\end{proof}
66+
67+Ce résultat, dû dans sa forme générale à \textcite{vandell1991} et prolongé par \textcite{gauLaiWang1992}, est le fondement théorique de deux pratiques courantes. La première consiste à accorder plus de poids aux comparables qui exigent le moins d'ajustements : dans le modèle, un comparable éloigné du sujet n'est pas plus bruité en soi, mais l'incertitude sur les prix implicites $\beta_k$ s'ajoute au bruit de transaction, de sorte que la variance effective $\sigma_j^2$ croît avec l'ampleur des ajustements. La seconde consiste à ne retenir qu'un petit nombre de comparables : ajouter un comparable de grande variance réduit peu la variance totale, tandis qu'il augmente le risque d'un biais si ses ajustements sont mal estimés. \textcite{green1994} a souligné que les pondérations optimales exigent de connaître les $\sigma_j^2$, ce qui n'est jamais le cas ; en pratique, l'évaluateur les approxime par l'ampleur relative des ajustements, et le \cref{ch:20} revient sur cette approximation.
68+
69+\subsection{Biais et variance : pourquoi le nombre de comparables est un arbitrage}
70+
71+L'\cref{eq:16:variance} suggère d'augmenter indéfiniment le nombre de comparables. Ce serait oublier que l'\cref{eq:16:local} n'est qu'une approximation locale : plus un comparable est éloigné du sujet dans l'espace des caractéristiques, plus l'hypothèse de linéarité des prix implicites est douteuse et plus l'ajustement risque d'être biaisé. Si l'on ordonne les candidats par distance croissante au sujet et que l'on retient les $k$ premiers, le biais de l'estimateur croît avec $k$ tandis que sa variance décroît. Dans le cas simple où le biais croît linéairement, $b(k) = b_1 k$, et où la variance décroît comme $\sigma^2/k$, l'erreur quadratique moyenne $\mathrm{EQM}(k) = b_1^2 k^2 + \sigma^2/k$ est minimale pour
72+\begin{equation}\label{eq:16:kopt}
73+k^{*} = \left(\frac{\sigma^2}{2\, b_1^2}\right)^{1/3},
74+\end{equation}
75+une quantité qui croît avec la dispersion des prix et décroît avec la vitesse à laquelle les comparables lointains deviennent trompeurs. \textcite{kummerowGalfalvy2002} ont étudié cet arbitrage empiriquement et montré que la pratique professionnelle de trois à six comparables correspond à un marché où les prix implicites sont connus de façon imprécise ; lorsque les ajustements proviennent d'un modèle hédonique estimé sur des centaines de ventes (\cref{ch:18}), le $k$ optimal augmente, et la frontière entre « comparaison » et « régression » s'estompe. \textcite{isakson1986} avait déjà proposé de formaliser la sélection comme une recherche des plus proches voisins ; \textcite{laiVandellWangWelke2008} ont montré qu'une combinaison linéaire de comparables reproduisant exactement les caractéristiques du sujet élimine le besoin d'ajustements explicites, au prix d'une variance plus élevée. Les \cref{ex:16:5,ex:16:7} explorent ces résultats.
76+
77+\section{Les unités de comparaison}\label{sec:16:unites}
78+
79+Comparer des immeubles de tailles différentes exige de ramener les prix à une \motcle{unité de comparaison} commune. Le choix de cette unité est la première décision méthodologique de la grille, et il conditionne les ajustements qui suivent : un ajustement de superficie est inutile si l'on compare des prix au mètre carré, mais il devient indispensable si l'on compare des prix totaux. Le \cref{tab:16:unites} recense les unités usuelles pour le résidentiel et le multirésidentiel québécois.
80+
81+\begin{table}[htbp]
82+\caption{Unités de comparaison usuelles et conditions d'emploi}\label{tab:16:unites}
83+\centering
84+\begin{tabularx}{\textwidth}{lYY}
85+\toprule
86+Unité & Emploi typique & Limite principale \\
87+\midrule
88+Prix total & Unifamiliales homogènes d'un même secteur ; copropriétés d'un même immeuble & Exige un ajustement explicite de la taille \\
89+Prix par mètre carré habitable (\(\$/\mathrm{m}^2\)) & Unifamiliales et copropriétés de tailles variées ; rôle d'évaluation & Suppose une relation proportionnelle entre prix et superficie, alors que l'élasticité observée est inférieure à un \\
90+Prix par mètre carré de terrain & Terrains vacants ; terrains intercalaires (\cref{ch:25}) & Ignore la forme, la façade et la constructibilité \\
91+Prix par logement & Immeubles de 2 à 5 logements, et au-delà & Ignore la taille et la qualité des logements \\
92+Multiplicateur du revenu brut (MRB) & Immeubles locatifs ; premier filtre de comparabilité & Ignore les dépenses et la vacance (\cref{ch:29}) \\
93+Prix par chambre, par pièce & Petits immeubles locatifs ; résidences & Grossier ; dépend des conventions de dénombrement \\
94+\bottomrule
95+\end{tabularx}
96+\tabsource{synthèse de l'auteur d'après \textcite{appraisal2026} et la pratique québécoise.}
97+\end{table}
98+
99+Le prix par mètre carré habitable est l'unité la plus répandue pour les maisons, et il mérite qu'on en explicite l'hypothèse implicite. Écrire $P_j = p\, S_j$, où $p$ est un prix unitaire commun, revient à supposer que le prix est proportionnel à la superficie $S$. Or les études hédoniques trouvent presque toujours une élasticité du prix par rapport à la superficie inférieure à l'unité : une maison deux fois plus grande ne vaut pas deux fois plus, parce qu'une part du prix rémunère le terrain, les raccordements et les équipements, dont le coût ne croît pas avec la surface. Dans le jeu de ventes synthétique du cas 1, construit avec une élasticité de 0,55, le prix unitaire des petites maisons est systématiquement supérieur à celui des grandes. Il en résulte une règle simple : l'unité de comparaison ne dispense pas de choisir des comparables de taille voisine du sujet. Le prix unitaire corrige les différences de taille au premier ordre seulement, et plus l'écart de superficie est grand, plus le résidu non corrigé est important. C'est pourquoi la procédure de la \cref{sec:16:selection} impose une fourchette de superficie \emph{avant} de calculer les prix unitaires.
100+
101+Pour les immeubles locatifs, le multiplicateur du revenu brut, $\mathit{MRB} = P / RBP$, joue un rôle analogue : il ramène des immeubles de tailles différentes à un ratio comparable. Son usage comme indicateur de valeur est discuté au \cref{ch:29} ; comme critère de sélection, il permet d'écarter d'emblée les ventes dont le ratio s'éloigne manifestement de la fourchette du marché, ce qui signale soit une erreur de données, soit une transaction non représentative.
102+
103+\section{Les critères de comparabilité et la vérification des ventes}\label{sec:16:criteres}
104+
105+\subsection{Cinq dimensions de la comparabilité}
106+
107+La définition de la valeur marchande rappelée plus haut fournit la liste des conditions qu'une vente doit remplir pour informer sur la valeur du sujet. La pratique nord-américaine les regroupe en cinq dimensions, dans l'ordre où elles sont examinées, et cet ordre n'est pas indifférent : les trois premières portent sur la transaction et déterminent si la vente est utilisable, les deux dernières portent sur le bien et déterminent l'ampleur des ajustements.
108+
109+\begin{enumerate}
110+\item \textbf{Droits transmis.} La pleine propriété libre de charges est la référence ; une vente de droits démembrés (usufruit, emphytéose), d'une fraction indivise, ou d'un immeuble grevé de baux à long terme en deçà du marché transmet un faisceau de droits différent (\cref{ch:2,ch:7}). Au Québec, l'évaluateur s'assure aussi que la vente n'est pas celle d'un immeuble en copropriété divise lorsque le sujet est détenu en pleine propriété, et réciproquement.
111+\item \textbf{Conditions de financement.} Un prix conclu avec une balance de vente à taux réduit, une reprise d'hypothèque avantageuse ou un financement du vendeur n'est pas un prix comptant ; la définition professionnelle exige un prix « en argent comptant ou l'équivalent », ce qui impose soit l'exclusion, soit un ajustement de financement (\cref{ch:17}).
112+\item \textbf{Conditions de vente.} La vente doit être \motcle{sincère} (en anglais \emph{arm's length}), c'est-à-dire conclue entre parties indépendantes, informées et non contraintes. Sont suspectes les ventes entre personnes liées, les ventes successorales ou pour cause de divorce lorsque la liquidation est pressée, les reprises de finance et ventes sous contrôle de justice, les ventes à un locataire en place, les ventes d'un promoteur avec incitatifs, et les ventes conclues avant la mise en marché. L'exclusion n'est pas automatique : une vente successorale menée par un courtier avec une exposition normale peut être parfaitement représentative. La vérification tranche.
113+\item \textbf{Conditions du marché.} Une vente informe sur le marché à la date où le prix a été convenu, qui est la date de l'acceptation de la promesse d'achat, non celle de la signature de l'acte chez le notaire, souvent postérieure de un à trois mois. Plus la vente est ancienne, plus l'ajustement de conditions du marché est grand et incertain. La fenêtre temporelle admissible dépend de la vitesse d'évolution des prix et de l'abondance des ventes récentes.
114+\item \textbf{Caractéristiques du bien.} Localisation (secteur, rue, voisinage immédiat, nuisances), caractéristiques physiques (superficie, âge et état, qualité de construction, garage, sous-sol, terrain), caractéristiques économiques pour les immeubles de revenu (loyers, dépenses, état locatif), usage et zonage, et éléments non immobiliers inclus dans le prix (électroménagers, meubles).
115+\end{enumerate}
116+
117+\begin{quebec}
118+En évaluation foncière municipale, le Règlement sur la proportion médiane et le facteur comparatif du rôle d'évaluation foncière (RLRQ, c. F-2.1, r. 10) codifie des règles de sélection analogues pour les études de ratios : seules les ventes d'un montant d'au moins \dollars{5000} sont considérées, la taille de la liste de base dépend du nombre d'unités de la municipalité, et les ventes dont le ratio s'écarte de plus de deux écarts-types de la moyenne sont épurées \parencite{reglpropmed}. Ces règles, conçues pour des milliers de ventes, ne remplacent pas le jugement de l'évaluateur sur une sélection de cinq comparables, mais elles rappellent que l'épuration statistique des ventes non représentatives est une étape normale, documentée et reproductible de toute étude de marché (\cref{ch:9,ch:14}).
119+\end{quebec}
120+
121+\subsection{La vérification des ventes}
122+
123+Une vente ne devient un comparable qu'une fois vérifiée. Le \cref{tab:16:verification} résume les sources disponibles au Québec et ce que chacune permet d'établir. La vérification auprès d'une partie à la transaction ou du courtier reste la meilleure façon d'apprendre les conditions de vente qui ne figurent dans aucun registre ; elle est aussi la plus coûteuse en temps, et elle est réservée en pratique aux comparables retenus plutôt qu'à l'ensemble des candidats.
124+
125+\begin{table}[htbp]
126+\caption{Sources de vérification d'une vente au Québec}\label{tab:16:verification}
127+\centering
128+\begin{tabularx}{\textwidth}{lYY}
129+\toprule
130+Source & Ce qu'elle établit & Limites \\
131+\midrule
132+Acte de vente publié au registre foncier & Parties, prix déclaré, date de signature, droits transmis, charges, déclarations des parties aux fins des droits de mutation & Ne révèle ni la date de la promesse d'achat ni les conditions ; consultation payante à l'unité \\
133+Index aux immeubles & Chaîne de titres, hypothèques, servitudes, avis (contamination, préavis d'exercice) & Lecture technique ; renvois à des actes antérieurs \\
134+Fiche descriptive et historique de courtage & Date d'inscription, prix demandé et ses révisions, date d'acceptation, inclusions, description physique, photos & Données déclaratives ; accès réservé aux courtiers et à leurs partenaires \\
135+Rôle d'évaluation foncière (données ouvertes) & Superficies, année de construction, nombre de logements, valeur au rôle, usage & Date de référence ancienne ; mesures selon les conventions du MEFQ \\
136+Déclarations du vendeur (formulaire OACIQ) & État connu de l'immeuble, sinistres, travaux, vices & Document de la transaction, non public \\
137+Entretien avec une partie ou le courtier & Conditions de vente, motivation, négociation, inclusions non immobilières & Subjectif ; à documenter au dossier \\
138+\bottomrule
139+\end{tabularx}
140+\tabsource{synthèse de l'auteur d'après \textcite{registreFoncier,oaciqFormulaireDV} et les notes de vérification du dépôt (sources/notes-A.md).}
141+\end{table}
142+
143+Deux points méritent attention. D'abord, la date pertinente est celle à laquelle le prix a été convenu ; lorsque seule la date de l'acte est connue, l'évaluateur la corrige d'un délai typique, qu'il documente, ou il reconstitue la date d'acceptation à partir de l'historique de courtage. Ensuite, les prix déclarés dans les actes incluent parfois des biens meubles ; la contrepartie pour les meubles doit être retranchée avant toute comparaison, et la valeur attribuée aux meubles dans l'acte, souvent fixée pour des raisons fiscales, ne reflète pas nécessairement leur valeur.
144+
145+\section{La procédure de sélection}\label{sec:16:selection}
146+
147+\subsection{Des filtres qualitatifs à la distance d'attributs}
148+
149+La sélection des comparables gagne à être conduite en deux temps, que la \cref{fig:16:processus} schématise. Le premier temps applique des \motcle{filtres qualitatifs} éliminatoires : même marché et même segment (type de propriété, mode de détention), fenêtre temporelle, rayon géographique ou secteur, vente sincère et pleine propriété, et fourchette de taille. Chaque filtre réduit l'ensemble des candidats et chaque seuil est une décision à documenter, parce que l'analyse de sensibilité de la \cref{sec:16:sensibilite} montrera qu'elle n'est pas neutre. Le second temps ordonne les candidats restants par ressemblance décroissante avec le sujet et retient les $k$ premiers.
150+
151+\begin{figure}[htbp]
152+\centering
153+\begin{tikzpicture}[node distance=4mm, every node/.style={font=\sffamily\small}, boite/.style={draw=brandPrimary, fill=brandLight, rounded corners=1pt, minimum width=118mm, minimum height=8mm, align=center}, fleche/.style={-{Stealth[length=2mm]}, brandGray, thick}]
154+\node[boite] (a) {Ventes du marché (toutes propriétés, toutes dates) \hfill $n$ = toutes};
155+\node[boite, below=of a, minimum width=104mm] (b) {Filtre 1 — fenêtre temporelle (date de la promesse d'achat)};
156+\node[boite, below=of b, minimum width=90mm] (c) {Filtre 2 — marché et secteur (rayon ou sous-marché)};
157+\node[boite, below=of c, minimum width=76mm] (d) {Filtre 3 — segment (type, mode de détention, droits)};
158+\node[boite, below=of d, minimum width=62mm] (e) {Filtre 4 — fourchette de taille et d'âge};
159+\node[boite, below=of e, minimum width=48mm, fill=brandSecondary!25] (f) {Classement par distance d'attributs};
160+\node[boite, below=of f, minimum width=34mm, fill=brandSecondary!45] (g) {$k$ comparables vérifiés};
161+\foreach \i/\j in {a/b, b/c, c/d, d/e, e/f, f/g} \draw[fleche] (\i) -- (\j);
162+\node[right=2mm of b, text=brandGray, font=\sffamily\footnotesize, align=left] {éliminatoire};
163+\node[right=2mm of f, text=brandGray, font=\sffamily\footnotesize, align=left] {ordinal};
164+\node[right=2mm of g, text=brandGray, font=\sffamily\footnotesize, align=left] {vérification des conditions\\de chaque vente retenue};
165+\end{tikzpicture}
166+\caption{L'entonnoir de sélection des comparables : des filtres éliminatoires documentés, puis un classement par ressemblance. Chaque seuil fait l'objet d'une analyse de sensibilité.}\label{fig:16:processus}
167+\figsource{schéma de l'auteur.}
168+\end{figure}
169+
170+La ressemblance se mesure par une \motcle{distance d'attributs}. Pour un ensemble de caractéristiques $k = 1, \dots, K$, chacune normalisée par une échelle $e_k$ qui exprime l'écart jugé « significatif » pour cette caractéristique, et pondérée par un poids $\omega_k$ qui traduit son importance pour le prix, la distance entre le candidat $j$ et le sujet $s$ est
171+\begin{equation}\label{eq:16:distance}
172+d_j = \sqrt{\sum_{k=1}^{K} \omega_k \left(\frac{x_{jk} - x_{sk}}{e_k}\right)^{2}} .
173+\end{equation}
174+Le choix des échelles et des poids n'est pas arbitraire : l'échelle $e_k$ est l'écart de la caractéristique $k$ qui produit un ajustement d'ampleur comparable d'une caractéristique à l'autre, et le poids $\omega_k$ est d'autant plus élevé que le prix implicite de la caractéristique est grand ou mal connu. Idéalement, on tire $e_k$ et $\omega_k$ d'un modèle hédonique estimé sur le marché (\cref{ch:18}) : la distance devient alors une distance de prix, c'est-à-dire l'ampleur totale des ajustements qu'il faudra appliquer. À défaut, l'évaluateur les fixe par jugement et vérifie que le classement est robuste à des variations raisonnables de ces paramètres (\cref{ex:16:6}). La date de vente et la distance géographique entrent dans la distance au même titre que les caractéristiques physiques, puisqu'elles exigeront elles aussi des ajustements.
175+
176+\subsection{Combien de comparables, sur quelle période, dans quel rayon ?}
177+
178+La pratique professionnelle nord-américaine retient entre trois et six comparables ; les prêteurs hypothécaires en exigent souvent au moins trois, et les formulaires normalisés en prévoient trois à cinq. La \cref{sec:16:substitution} a montré que ce petit nombre est rationnel lorsque les prix implicites sont connus de façon imprécise : au-delà de quelques ventes très proches, les candidats suivants apportent plus de biais potentiel que de réduction de variance. Lorsque l'évaluateur dispose d'un modèle hédonique fiable pour estimer les ajustements, il peut élargir l'échantillon, et la comparaison tend alors vers la régression.
179+
180+La fenêtre temporelle et le rayon géographique obéissent au même arbitrage. Une fenêtre courte réduit l'ajustement de conditions du marché, mais diminue le nombre de candidats et oblige à accepter des biens plus différents ; une fenêtre longue fait l'inverse. Dans un marché qui évolue de quelques points de pourcentage par année, une fenêtre de douze à dix-huit mois est courante ; dans un marché qui a progressé de plus de dix pour cent en un an, on la raccourcit. Le rayon dépend de l'homogénéité du secteur : quelques centaines de mètres dans un quartier central dense où la valeur varie d'une rue à l'autre, plusieurs kilomètres dans un secteur de banlieue homogène ou en milieu rural. La règle est de préférer un comparable plus ancien du même micro-marché à un comparable récent d'un secteur différent lorsque l'ajustement de localisation est mal connu, et l'inverse lorsque l'indice de prix du secteur est bien mesuré.
181+
182+\subsection{Marchés minces et biens atypiques}
183+
184+La procédure décrite suppose un bassin de candidats suffisant pour que les filtres laissent une dizaine de ventes et que le classement ait un sens. Dans une municipalité de quelques milliers d'habitants, pour une propriété riveraine, une maison patrimoniale ou un immeuble mixte, les filtres habituels ne laissent parfois aucune vente. Trois stratégies s'offrent alors, et elles doivent être explicitées au rapport. La première consiste à relâcher les filtres dans l'ordre inverse de leur coût en ajustements : élargir d'abord la fenêtre temporelle si l'indice de prix régional est fiable, puis le rayon vers des municipalités de même profil socio-économique, puis la fourchette de taille et d'âge. La deuxième consiste à décomposer le bien : évaluer séparément le terrain par comparaison avec des terrains vacants, et le bâtiment par la méthode du coût (\cref{ch:21,ch:25}), ce qui revient à reconnaître que la comparaison directe n'est plus la méthode principale. La troisième consiste à recourir à un modèle hédonique estimé sur un territoire plus large (\cref{ch:18}), qui fournit les prix implicites nécessaires pour ajuster des comparables éloignés ; l'évaluateur troque alors la proximité des comparables contre la précision des ajustements, exactement dans l'esprit de l'\cref{eq:16:kopt}. Dans tous les cas, la rareté des substituts est une information sur le marché lui-même : un bien pour lequel il n'existe pas de substitut récent est un bien dont la liquidité est faible, et la fourchette de valeur conclue doit être plus large.
185+
186+Les biens atypiques posent un problème distinct : ce n'est pas le nombre de ventes qui manque, mais la comparabilité. Une maison de \msq{400} dans un quartier de maisons de \msq{120} a des substituts dans d'autres quartiers cossus, pas dans son voisinage ; l'acheteur type n'est pas le même. La sélection doit alors viser le marché de l'acheteur probable plutôt que le voisinage géographique, et le rapport doit le dire.
187+
188+\subsection{La sélection pour les immeubles de revenu}
189+
190+Pour les immeubles de deux logements et plus, les critères de comparabilité s'enrichissent de dimensions économiques. Le nombre de logements et leur typologie (studios, 3½, 4½, 5½ selon l'usage québécois) remplacent la superficie habitable comme premier critère de taille ; l'état locatif compte autant que l'état physique, parce que des loyers contractuels nettement inférieurs au marché, protégés par l'encadrement des loyers (\cref{ch:34}), réduisent la valeur pour un acheteur qui ne pourra les relever que lentement ; et la présence de revenus accessoires (stationnement, buanderie) ou de locaux commerciaux au rez-de-chaussée change la nature du bien. L'unité de comparaison est le prix par logement ou le multiplicateur du revenu brut (\cref{sec:16:unites}), et la distance d'attributs inclut le niveau des loyers par rapport au marché local, le ratio de dépenses lorsqu'il est connu et l'année de construction, qui conditionne la plupart des pathologies du bâti (\cref{ch:11}). Le \cref{ch:32} traite des duplex à quintuplex, pour lesquels la comparaison reste la méthode dominante parce que l'acheteur est souvent un propriétaire occupant, et le \cref{ch:33} des immeubles de six logements et plus, pour lesquels la méthode du revenu prend le pas et où les ventes comparables servent surtout à extraire des taux de capitalisation (\cref{ch:29}). Le dossier du cas 3 (\cref{ch:47}) applique cette logique à un immeuble de douze logements : les quarante ventes du jeu synthétique y sont filtrées par taille (six à trente logements), par date et par arrondissement avant que les taux de capitalisation ne soient extraits.
191+
192+\subsection{Combien de ventes faut-il vérifier ?}
193+
194+La vérification complète de chaque candidat n'est ni possible ni nécessaire. La pratique raisonnable consiste à vérifier sommairement, à partir des registres et de l'historique de courtage, tous les candidats qui franchissent les filtres, afin d'écarter les ventes manifestement non sincères, puis à vérifier en profondeur, y compris par entretien avec une partie ou le courtier, les comparables retenus et les deux ou trois suivants dans le classement. Ces « remplaçants » servent si la vérification approfondie disqualifie un comparable retenu ; leur présence au dossier évite d'avoir à recommencer la sélection, et elle documente le fait que le choix final n'a pas été dicté par les prix. Lorsque la vérification révèle qu'un comparable retenu a été vendu à des conditions particulières, deux solutions existent : l'ajuster si l'effet des conditions est mesurable, ou le remplacer. La seconde est préférable dès que l'ajustement de transaction dépasserait quelques points de pourcentage, parce que l'incertitude d'un tel ajustement est généralement plus grande que celle des ajustements de propriété.
195+
196+\begin{pratique}
197+Une procédure de sélection documentée comporte, au dossier : la liste complète des candidats après chaque filtre, avec le nombre de ventes éliminées à chaque étape ; les seuils retenus et la raison de chacun ; les échelles et les poids de la distance d'attributs ; le classement des candidats ; et la fiche de vérification de chaque comparable retenu. Cette documentation est ce qui distingue une sélection défendable devant un tribunal ou un réviseur d'une sélection « à l'œil ». Elle est aussi ce qui permet l'analyse de sensibilité de la \cref{sec:16:sensibilite}.
198+\end{pratique}
199+
200+\section{La grille d'analyse}\label{sec:16:grille}
201+
202+Les comparables retenus sont présentés dans une \motcle{grille d'analyse} qui aligne, en colonnes, le sujet et chaque comparable, et en lignes, les éléments de comparaison dans l'ordre des cinq dimensions de la \cref{sec:16:criteres}. La grille est à la fois un outil de calcul et un outil de communication : elle rend visibles les différences qui devront être ajustées et l'ampleur relative des ajustements. Le \cref{tab:16:grille} en donne la structure ; les cellules d'ajustement seront remplies au \cref{ch:17}.
203+
204+\begin{table}[htbp]
205+\caption{Structure de la grille d'analyse (les ajustements sont traités au chapitre 17)}\label{tab:16:grille}
206+\centering
207+\small
208+\begin{tabular}{lcccc}
209+\toprule
210+Élément de comparaison & Sujet & Comparable 1 & Comparable 2 & \dots \\
211+\midrule
212+Prix de vente & --- & $P_1$ & $P_2$ & \\
213+Prix par m\textsuperscript{2} habitable & --- & $P_1/S_1$ & $P_2/S_2$ & \\
214+\addlinespace
215+\multicolumn{5}{l}{\textit{Ajustements de transaction (en pourcentage, séquentiels)}} \\
216+Droits transmis & pleine propriété & & & \\
217+Financement & comptant & & & \\
218+Conditions de vente & sincère & & & \\
219+Dépenses après l'achat & aucune & & & \\
220+Conditions du marché (date) & date d'évaluation & & & \\
221+\addlinespace
222+\multicolumn{5}{l}{\textit{Ajustements de propriété (en dollars, additifs)}} \\
223+Localisation & & & & \\
224+Superficie habitable & & & & \\
225+Terrain & & & & \\
226+Âge et état & & & & \\
227+Qualité & & & & \\
228+Garage, sous-sol, piscine & & & & \\
229+Éléments non immobiliers & & & & \\
230+\addlinespace
231+Prix ajusté & --- & $P^{\text{aj}}_1$ & $P^{\text{aj}}_2$ & \\
232+Ajustement brut (\%) et net (\%) & --- & & & \\
233+\bottomrule
234+\end{tabular}
235+\tabsource{structure conventionnelle, d'après \textcite{appraisal2026} et les conventions de calcul de l'ouvrage (annexe~\ref{app:conventions-calcul}).}
236+\end{table}
237+
238+Deux indicateurs de qualité accompagnent chaque comparable dans la grille. L'\motcle{ajustement brut} est la somme des valeurs absolues des ajustements, rapportée au prix de vente ; il mesure l'éloignement total du comparable par rapport au sujet et sert de substitut à la variance $\sigma_j^2$ de la \cref{prop:indication}. L'\motcle{ajustement net} est la somme algébrique des ajustements ; un ajustement net important signale que le comparable est systématiquement inférieur ou supérieur au sujet, ce qui expose l'estimation à un biais si les prix implicites sont mal estimés. Des seuils indicatifs de 25 \% pour le brut et de 15 \% pour le net sont souvent cités dans la pratique nord-américaine comme signaux d'alerte, non comme règles ; un comparable qui les dépasse n'est pas nécessairement rejeté, mais il reçoit un poids moindre et son maintien doit être justifié.
239+
240+\section{Exemple intégral : la maison du cas 1 à Gatineau}\label{sec:16:exemple}
241+
242+\begin{synthetique}
243+Le dossier du cas 1 est synthétique. Le fichier \texttt{data/synthetic/cas1\_gatineau\_ventes.csv} contient 600 ventes d'unifamiliales fictives réparties dans les cinq secteurs de Gatineau entre janvier 2023 et août 2026 ; le sujet est décrit dans \texttt{cas1\_gatineau\_sujet.json}. Les ventes ont été générées par le script \texttt{analysis/python/traite/synth.py} (graine 20261031) selon des lois déclarées dans \texttt{data/synthetic/README.md}, et calibrées sur les prix médians par secteur publiés par l'APCIQ pour le troisième trimestre de 2026 et sur la variation de la médiane unifamiliale au rôle 2027-2029 de la Ville de Gatineau \parencite{apciqgatineaut32026,gatineaurole2027}. Les coordonnées sont fictives et aucune adresse n'existe. Tous les chiffres de cette section sont reproduits par \texttt{analysis/python/cases/ch16\_selection.py}.
244+\end{synthetique}
245+
246+\subsection{Le sujet et le marché}
247+
248+Le sujet est un cottage à étages construit en 1998 dans le secteur de Hull, d'une superficie habitable de \msq{165} sur un terrain de \msq{620}, de qualité moyenne (3 sur une échelle de 1 à 5), avec un garage simple et un sous-sol fini, quatre chambres et deux salles de bain. La date d'évaluation est le 1\textsuperscript{er} septembre 2026. La valeur inscrite au rôle fictif du dossier est de \dollars{468000}, pour une date de référence au marché du 1\textsuperscript{er} juillet 2024.
249+
250+\begin{quebec}
251+Le marché réel de référence est celui de la RMR de Gatineau au troisième trimestre de 2026 : prix médian des unifamiliales de \dollars{480000} (en baisse de 3 \% sur un an), \num{1044} ventes résidentielles, \num{2256} inscriptions en vigueur et un délai de vente médian de 34 jours pour les unifamiliales ; par secteur, les médianes publiées étaient de \dollars{475000} à Hull, \dollars{530000} à Aylmer, \dollars{464900} dans le secteur Gatineau et \dollars{395000} pour Buckingham et Masson-Angers réunis \parencite{apciqgatineaut32026}. Le rôle triennal 2027-2029 de la Ville de Gatineau, déposé le 27 octobre 2026 avec une date de référence au marché du 1\textsuperscript{er} juillet 2025, fait passer la valeur médiane des unifamiliales de \dollars{454600} à \dollars{495800}, soit une hausse de 9,06 \% en trois ans \parencite{gatineaurole2027}. Ces chiffres ont servi à calibrer le jeu synthétique ; ils ne décrivent pas les ventes fictives qui suivent.
252+\end{quebec}
253+
254+\subsection{L'entonnoir de sélection}
255+
256+Le \cref{tab:16:entonnoir} applique les filtres de la \cref{sec:16:selection} aux 600 ventes. La fenêtre temporelle de dix-huit mois avant la date d'évaluation conserve 236 ventes ; le rayon de trois kilomètres autour du sujet en conserve 40 ; l'exigence d'un bâtiment à étages comme le sujet en conserve 27 ; la fourchette de superficie habitable de plus ou moins 25 \% autour de \msq{165}, soit de 124 à \msq{206}, en conserve 16. Ces seize candidats sont ensuite classés par la distance d'attributs \eqref{eq:16:distance} avec les échelles et les poids du \cref{tab:16:parametres}, et les cinq premiers sont retenus.
257+
258+\begin{table}[htbp]
259+\caption{Entonnoir de sélection des comparables, cas 1 (données synthétiques)}\label{tab:16:entonnoir}
260+\centering
261+\begin{tabular}{lS[table-format=3.0]S[table-format=3.0]}
262+\toprule
263+Étape & {Ventes conservées} & {Ventes éliminées} \\
264+\midrule
265+Ventes disponibles (janv. 2023 -- août 2026) & 600 & {---} \\
266+Fenêtre de 18 mois avant le 1\textsuperscript{er} septembre 2026 & 236 & 364 \\
267+Rayon de 3 km autour du sujet & 40 & 196 \\
268+Bâtiment à étages (comme le sujet) & 27 & 13 \\
269+Superficie habitable de 124 à 206 m\textsuperscript{2} & 16 & 11 \\
270+Classement par distance d'attributs, $k = 5$ & 5 & 11 \\
271+\bottomrule
272+\end{tabular}
273+\tabsource{\synth{analysis/python/cases/ch16\_selection.py}}
274+\end{table}
275+
276+\begin{table}[htbp]
277+\caption{Échelles et poids de la distance d'attributs retenus pour le cas 1}\label{tab:16:parametres}
278+\centering
279+\begin{tabular}{lS[table-format=3.1]S[table-format=1.1]p{52mm}}
280+\toprule
281+Caractéristique $k$ & {Échelle $e_k$} & {Poids $\omega_k$} & Justification \\
282+\midrule
283+Superficie habitable (m\textsuperscript{2}) & 25 & 2.0 & premier déterminant du prix ; ajustement le plus coûteux \\
284+Superficie du terrain (m\textsuperscript{2}) & 150 & 0.7 & prix implicite faible en secteur bâti \\
285+Âge (années) & 10 & 1.0 & dépréciation et état \\
286+Qualité (échelle 1--5) & 1 & 1.0 & un échelon vaut un ajustement notable \\
287+Garage (places) & 1 & 0.5 & ajustement bien connu, modeste \\
288+Sous-sol fini (0/1) & 1 & 0.3 & ajustement modeste \\
289+Distance au sujet (km) & 1.5 & 1.5 & localisation : ajustement mal connu \\
290+Ancienneté de la vente (mois) & 6 & 1.0 & conditions du marché \\
291+\bottomrule
292+\end{tabular}
293+\tabsource{choix de l'auteur, discutés à la \cref{sec:16:selection} ; sensibilité à l'\cref{ex:16:6}.}
294+\end{table}
295+
296+\subsection{Les comparables retenus}
297+
298+Le \cref{tab:16:comparables} présente les cinq comparables retenus, tous situés dans le secteur de Hull, vendus entre mai 2025 et juin 2026, à moins de 2,4 kilomètres du sujet. La \cref{fig:16:nuage} les situe dans le nuage des 600 ventes, et la \cref{fig:16:carte} les localise.
299+
300+\begin{table}[htbp]
301+\caption{Les cinq comparables retenus pour le sujet du cas 1 (données synthétiques)}\label{tab:16:comparables}
302+\centering
303+\footnotesize\setlength{\tabcolsep}{3.5pt}
304+\begin{tabular}{lcccccc}
305+\toprule
306+ & Sujet & G0574 & G0404 & G0468 & G0477 & G0575 \\
307+\midrule
308+Date de vente & --- & 2026-06-22 & 2025-05-27 & 2025-09-27 & 2025-11-02 & 2026-06-23 \\
309+Ancienneté (mois) & 0 & 2,3 & 15,2 & 11,1 & 10,0 & 2,3 \\
310+Distance au sujet (km) & 0 & 0,79 & 0,76 & 1,78 & 2,38 & 2,33 \\
311+Superficie habitable (m\textsuperscript{2}) & 165 & 149 & 160 & 187 & 146 & 156 \\
312+Terrain (m\textsuperscript{2}) & 620 & 592 & 538 & 586 & 452 & 333 \\
313+Année de construction & 1998 & 1996 & 2003 & 1999 & 2002 & 2013 \\
314+Qualité (1--5) & 3 & 4 & 3 & 4 & 4 & 2 \\
315+Garage (places) & 1 & 0 & 1 & 1 & 1 & 0 \\
316+Sous-sol fini & oui & non & oui & oui & oui & oui \\
317+Distance d'attributs $d_j$ & --- & 1,80 & 2,71 & 2,86 & 3,12 & 3,22 \\
318+\midrule
319+Prix de vente (\$) & --- & 598 000 & 506 500 & 624 500 & 538 000 & 513 500 \\
320+Prix par m\textsuperscript{2} habitable (\$/m\textsuperscript{2}) & --- & 4 013 & 3 166 & 3 340 & 3 685 & 3 292 \\
321+\bottomrule
322+\end{tabular}
323+\tabsource{\synth{analysis/python/cases/ch16\_selection.py}}
324+\end{table}
325+
326+\begin{figure}[htbp]
327+\centering
328+\includegraphics[width=\textwidth]{ch16/ch16-fig-nuage-selection.pdf}
329+\caption{Prix de vente et superficie habitable des 600 ventes du cas 1 ; les 16 candidats conservés après les filtres et les 5 comparables retenus sont mis en évidence, la ligne pointillée marque la superficie du sujet. La dispersion verticale à superficie donnée illustre le terme aléatoire de l'\cref{eq:16:local}.}\label{fig:16:nuage}
330+\figsource{\synth{analysis/python/cases/ch16\_selection.py, graine 20261031}}
331+\end{figure}
332+
333+\begin{figure}[htbp]
334+\centering
335+\includegraphics[width=\textwidth]{ch16/ch16-fig-carte-comparables.pdf}
336+\caption{Localisation fictive des 600 ventes par secteur, du sujet (étoile) et des cinq comparables retenus (cercles), avec le rayon de recherche de 3 km. Les coordonnées sont synthétiques.}\label{fig:16:carte}
337+\figsource{\synth{analysis/python/cases/ch16\_selection.py}}
338+\end{figure}
339+
340+La lecture du \cref{tab:16:comparables} illustre ce que la distance d'attributs fait et ne fait pas. Le comparable le mieux classé, G0574, est le plus proche du sujet par la date, la localisation et l'âge, mais il est plus petit de \msq{16}, de qualité supérieure, sans garage et sans sous-sol fini : son prix unitaire élevé (\parmsq{4013}) tient en partie à sa petite taille et à sa qualité. Le deuxième, G0404, est presque identique au sujet par la taille, le garage et le sous-sol, mais sa vente remonte à quinze mois et exigera un ajustement de conditions du marché. Le troisième, G0468, est nettement plus grand. Aucun comparable n'est « le bon » ; chacun informe sur une dimension du sujet et exige des ajustements sur les autres. C'est précisément pour cela que l'on en retient plusieurs et qu'on les pondère.
341+
342+\subsection{Une première indication par l'unité de comparaison}
343+
344+Avant tout ajustement, l'unité de comparaison fournit une indication grossière. Le prix médian par mètre carré habitable des seize candidats est de \parmsq{3286}, dans une fourchette de \num{2361} à \parmsq{4140} ; celui des cinq comparables retenus est de \parmsq{3340}. Appliqué à la superficie du sujet, ce dernier donne
345+\begin{equation}\label{eq:16:indication}
346+\hat V^{\,\text{brut}}_s = \num{3339.57}\ \$/\mathrm{m}^2 \times \qty{165}{\metre\squared} = \dollars{551029},
347+\end{equation}
348+à rapprocher de la fourchette des prix bruts des comparables, de \dollars{506500} à \dollars{624500}, de leur moyenne de \dollars{556100} et de leur médiane de \dollars{538000}. La \cref{fig:16:boites} compare les distributions de prix unitaires du secteur, des candidats et des comparables. Cette indication n'est pas une conclusion de valeur : elle ne corrige ni la date, ni la qualité, ni le garage, ni le sous-sol, ni le terrain, et le prix unitaire des petites maisons surestime celui du sujet. Elle sert de point de repère, et tout écart important entre elle et la valeur ajustée du \cref{ch:17} devra être expliqué.
349+
350+\begin{figure}[htbp]
351+\centering
352+\includegraphics[width=\textwidth]{ch16/ch16-fig-boites-prix-m2.pdf}
353+\caption{Distribution du prix par mètre carré habitable : ventes du secteur de Hull, candidats conservés après les filtres, comparables retenus. Le resserrement progressif de la distribution est l'effet recherché de la sélection ; la médiane des comparables sert d'indication brute.}\label{fig:16:boites}
354+\figsource{\synth{analysis/python/cases/ch16\_selection.py}}
355+\end{figure}
356+
357+\section{Sensibilité au choix des comparables}\label{sec:16:sensibilite}
358+
359+La sélection repose sur quatre décisions : la fenêtre temporelle, le rayon, le nombre $k$ de comparables et les paramètres de la distance. Pour mesurer leur effet, la procédure a été répétée pour $k \in \{3, 4, 5, 6, 8, 10\}$, des rayons de 1,5, 3 et 5 kilomètres et des fenêtres de 12, 18 et 24 mois, en conservant à chaque fois l'indication brute par le prix unitaire médian. Le \cref{tab:16:sensibilite} et la \cref{fig:16:sensibilite} résument les résultats.
360+
361+\begin{table}[htbp]
362+\caption{Sensibilité de l'indication brute (\$) au nombre de comparables, toutes fenêtres et tous rayons confondus}\label{tab:16:sensibilite}
363+\centering
364+\begin{tabular}{S[table-format=2.0]S[table-format=6.0]S[table-format=6.0]S[table-format=6.0]}
365+\toprule
366+{$k$} & {Minimum} & {Médiane} & {Maximum} \\
367+\midrule
368+3 & 529207 & 551029 & 608014 \\
369+4 & 576593 & 579522 & 600845 \\
370+5 & 529207 & 551029 & 593676 \\
371+6 & 525768 & 572353 & 600845 \\
372+8 & 525768 & 568698 & 596921 \\
373+10 & 525768 & 553970 & 596921 \\
374+\bottomrule
375+\end{tabular}
376+\tabsource{\synth{analysis/python/cases/ch16\_selection.py ; 48 combinaisons de $k$, rayon et fenêtre laissant au moins trois comparables}}
377+\end{table}
378+
379+\begin{figure}[htbp]
380+\centering
381+\includegraphics[width=\textwidth]{ch16/ch16-fig-sensibilite-selection.pdf}
382+\caption{Indication brute par le prix unitaire médian selon le nombre de comparables retenus, pour trois rayons de recherche (fenêtre de 18 mois). Un rayon élargi admet des ventes de secteurs voisins plus chers et déplace l'indication vers le haut.}\label{fig:16:sensibilite}
383+\figsource{\synth{analysis/python/cases/ch16\_selection.py}}
384+\end{figure}
385+
386+Sur les 48 combinaisons retenues (celles qui laissent au moins trois comparables), l'indication brute varie de \dollars{525768} à \dollars{608014}, avec un écart-type de \dollars{24069}, soit environ 4,4 \% de la médiane des indications. Cette dispersion, obtenue avant tout ajustement et sur un marché synthétique homogène, est du même ordre que l'incertitude habituellement attribuée aux ajustements eux-mêmes ; elle montre que le choix des comparables n'est pas une étape préliminaire neutre, mais une source d'incertitude à part entière, que le \cref{ch:20} intégrera dans la fourchette de valeur. Deux effets se dégagent. Le rayon est le paramètre le plus influent : un rayon de cinq kilomètres admet des ventes du secteur d'Aylmer, plus cher, et relève l'indication d'environ \dollars{50000}, ce qui n'est pas un gain d'information mais un biais de localisation qu'un ajustement devrait corriger. Le nombre $k$ a un effet non monotone : passer de trois à quatre comparables modifie beaucoup la médiane parce qu'un seul comparable change la composition, puis l'indication se stabilise au-delà de six, ce qui illustre la réduction de variance de l'\cref{eq:16:variance} sur un marché où les candidats restent proches du sujet.
387+
388+\begin{erreurs}
389+\begin{itemize}
390+\item Choisir les comparables après avoir formé une opinion de valeur, puis retenir ceux qui la confirment : c'est le biais d'ancrage documenté par \textcite{northcraftNeale1987} et, chez les évaluateurs, par \textcite{diaz1997}. La parade est de fixer les filtres et la distance d'attributs avant de regarder les prix.
391+\item Utiliser la date de l'acte notarié comme date de la vente ; le prix a été convenu à la promesse d'achat, souvent un à trois mois plus tôt.
392+\item Comparer des prix au mètre carré sans borner la superficie : l'élasticité du prix à la superficie étant inférieure à un, les petites maisons ont un prix unitaire plus élevé et surestiment la valeur d'un sujet plus grand.
393+\item Retenir une vente non vérifiée parce qu'elle est « parfaite » sur le papier ; les ventes les plus proches du sujet sont celles qu'il faut vérifier le plus soigneusement, car elles pèseront le plus.
394+\item Élargir le rayon pour obtenir plus de ventes sans ajuster la localisation ; le gain de variance est annulé par le biais de secteur, comme le montre la \cref{fig:16:sensibilite}.
395+\item Mélanger des copropriétés divises et des maisons en pleine propriété, ou des ventes de promoteur avec incitatifs et des reventes, dans la même grille.
396+\end{itemize}
397+\end{erreurs}
398+
399+\begin{limites}
400+Le cadre de ce chapitre suppose un marché actif où des substituts existent et où les ventes sont observables. Pour un bien atypique, un marché rural mince ou un segment en rupture (par exemple au lendemain d'une variation brusque des taux d'intérêt), la méthode de comparaison perd de sa force probante et doit être complétée par la méthode du coût ou du revenu. La distance d'attributs dépend d'échelles et de poids fixés par l'évaluateur ; elle rend la sélection reproductible, pas objective. Les résultats numériques de la \cref{sec:16:exemple} proviennent d'un jeu synthétique calibré sur des médianes publiées, non d'un marché réel ; la dispersion observée dépend des lois de génération déclarées dans le dépôt et ne doit pas être lue comme une mesure de l'incertitude du marché de Gatineau. Enfin, la procédure de sélection ne corrige aucune différence : la valeur indiquée ne peut être conclue qu'après les ajustements du \cref{ch:17} et la réconciliation du \cref{ch:20}.
401+\end{limites}
402+
403+\begin{synthese}
404+\begin{itemize}
405+\item Le principe de substitution fonde la méthode de comparaison et se retrouve dans la définition professionnelle de la valeur marchande et dans celle de la valeur réelle au rôle.
406+\item La valeur recherchée est une espérance conditionnelle ; les comparables en sont un échantillon bruité, dont la moyenne pondérée des prix ajustés est l'estimateur. Les poids optimaux sont inversement proportionnels à la variance de chaque comparable, et le nombre optimal de comparables résulte d'un arbitrage biais--variance.
407+\item L'unité de comparaison corrige les différences de taille au premier ordre seulement ; elle ne dispense pas de borner la superficie des candidats.
408+\item Une vente est utilisable si elle transmet des droits comparables, à des conditions de financement et de vente sincères, à une date proche ; elle est comparable si ses caractéristiques physiques et économiques sont voisines de celles du sujet. La vérification au registre foncier, à l'historique de courtage et auprès des parties est indispensable pour les comparables retenus.
409+\item La sélection procède par filtres éliminatoires documentés, puis par classement selon une distance d'attributs ; trois à six comparables suffisent lorsque les ajustements sont incertains.
410+\item Le choix des comparables est une source d'incertitude du même ordre que les ajustements : l'analyse de sensibilité au rayon, à la fenêtre et au nombre de comparables fait partie du dossier.
411+\end{itemize}
412+\end{synthese}
413+
414+\section*{Exercices}
415+\addcontentsline{toc}{section}{Exercices}
416+
417+\begin{exercice}[1]{Unités de comparaison}{ex:16:1}
418+Trois ventes récentes de plain-pieds d'un même secteur sont retenues comme comparables d'un sujet de \msq{165} : \dollars{525000} pour \msq{152}, \dollars{489000} pour \msq{141} et \dollars{561000} pour \msq{171}. Calculez le prix par mètre carré de chaque comparable, puis l'indication brute obtenue en appliquant la médiane des prix unitaires à la superficie du sujet. Expliquez pourquoi cette indication tend à surestimer la valeur du sujet si l'élasticité du prix à la superficie est inférieure à un.
419+\end{exercice}
420+
421+\begin{exercice}[1]{Ventes utilisables ou non}{ex:16:2}
422+Pour chacune des ventes suivantes, dites si elle est utilisable comme comparable sans ajustement de transaction, utilisable avec ajustement, ou à écarter, et justifiez en vous référant aux cinq dimensions de la comparabilité : (a) vente d'une succession par courtier après 41 jours de mise en marché ; (b) vente à un locataire en place sans inscription ; (c) vente avec balance de prix de vente de \dollars{150000} à 2 \% sur cinq ans ; (d) reprise de finance vendue par l'institution prêteuse ; (e) vente d'une fraction de copropriété divise alors que le sujet est une maison en pleine propriété ; (f) vente conclue il y a vingt mois dans le même quartier.
423+\end{exercice}
424+
425+\begin{exercice}[2]{Distance d'attributs}{ex:16:3}
426+Le sujet a une superficie habitable de \msq{165}, un terrain de \msq{620}, 28 ans et une qualité de 3. Trois candidats : A (\msq{158}, \msq{600}, 25 ans, qualité 3), B (\msq{190}, \msq{650}, 10 ans, qualité 4), C (\msq{150}, \msq{400}, 30 ans, qualité 2). Avec les échelles 25 m\textsuperscript{2}, 150 m\textsuperscript{2}, 10 ans et 1 échelon, et les poids 2, 0,7, 1 et 1, calculez la distance d'attributs \eqref{eq:16:distance} de chaque candidat et classez-les. Quel candidat le classement changerait-il si le poids de l'âge passait à 3 ?
427+\end{exercice}
428+
429+\begin{exercice}[2]{Fenêtre temporelle et ajustement de conditions du marché}{ex:16:4}
430+Dans un marché dont les prix progressent de 4,43 \% par année (taux annuel composé), un comparable vendu \dollars{506500} il y a quinze mois est retenu. Calculez l'ajustement de conditions du marché à appliquer à son prix. Élargir la fenêtre de 12 à 24 mois ferait passer le nombre de candidats de 9 à 21 : discutez l'arbitrage entre la réduction de variance et l'incertitude accrue sur l'ajustement temporel, en vous appuyant sur l'\cref{eq:16:variance}.
431+\end{exercice}
432+
433+\begin{exercice}[2]{Pondérations optimales}{ex:16:5}
434+Trois comparables ont des prix ajustés dont les écarts-types sont estimés à \dollars{20000}, \dollars{30000} et \dollars{40000}. Calculez les pondérations optimales de l'\cref{eq:16:poids} et l'écart-type de l'indication qui en résulte. Comparez à l'écart-type obtenu avec des pondérations égales. Que devient la pondération du premier comparable si son écart-type est réestimé à \dollars{10000} ?
435+\end{exercice}
436+
437+\begin{exercice}[3]{Robustesse de la sélection aux paramètres de la distance}{ex:16:6}
438+À l'aide du script \texttt{analysis/python/cases/ch16\_selection.py} et du jeu \texttt{cas1\_gatineau\_ventes.csv}, modifiez les poids de la distance d'attributs : (a) doublez le poids de la distance géographique ; (b) annulez le poids de l'ancienneté de la vente ; (c) donnez un poids de 3 à la qualité. Pour chaque variante, indiquez quels comparables entrent ou sortent des cinq retenus et comment l'indication brute change. Rédigez une courte note (150 mots) sur ce que cette robustesse, ou son absence, implique pour la documentation du dossier.
439+\end{exercice}
440+
441+\begin{exercice}[3]{Nombre optimal de comparables}{ex:16:7}
442+On modélise l'estimateur des $k$ plus proches voisins par un biais $b(k) = b_1 k$ et une variance $\sigma^2/k$. Dérivez l'\cref{eq:16:kopt}. Avec $\sigma = \dollars{25000}$ et $b_1 = \dollars{2000}$ par comparable supplémentaire, calculez $k^*$ et l'erreur quadratique moyenne correspondante. Que devient $k^*$ si les ajustements proviennent d'un modèle hédonique qui divise $b_1$ par quatre ? Commentez le lien avec les résultats de \textcite{kummerowGalfalvy2002}.
443+\end{exercice}
444+
445+\section*{Pour aller plus loin}
446+\addcontentsline{toc}{section}{Pour aller plus loin}
447+
448+Le traitement de référence de la méthode de comparaison dans la pratique nord-américaine demeure celui de l'Appraisal Institute \parencite{appraisal2026}, dont l'édition canadienne \parencite{appraisalCdn} adapte le vocabulaire et les exemples. La théorie de la sélection et de la pondération optimales des comparables est développée par \textcite{vandell1991}, complétée par \textcite{gauLaiWang1992} et critiquée par \textcite{green1994} ; \textcite{isakson1986} formalise la recherche des plus proches voisins, \textcite{isakson2002} donne l'algèbre linéaire de la grille et \textcite{laiVandellWangWelke2008} proposent l'estimation par réplication du sujet. L'arbitrage entre peu de comparables ajustés et beaucoup de comparables régressés est étudié par \textcite{kummerowGalfalvy2002}. Sur les biais de jugement des évaluateurs dans le choix et la pondération des comparables, on lira \textcite{northcraftNeale1987}, \textcite{diaz1990} et \textcite{diaz1997}. \textcite{lentzWang1998} passent en revue les enjeux de la méthode en contexte hypothécaire. Pour une perspective québécoise sur la mesure de la ressemblance et de la localisation, les travaux de \textcite{desrosiersTheriaultVilleneuve2000} et de \textcite{dubeLegros2014} sur la dépendance spatiale des prix préparent le \cref{ch:19}.
modified manuscript/chapters/ch21.tex +176 −1
@@ -1,2 +1,177 @@
1 \chapter{Reproduction et remplacement}\label{ch:21} 1 \chapter{Reproduction et remplacement}\label{ch:21}
2−\stubchapter 2+
3+La méthode du coût répond à une question que tout acheteur se pose au moins une fois devant une maison : « combien coûterait-il de la construire aujourd'hui ? ». Cette question paraît concrète, presque comptable, et c'est précisément ce qui la rend dangereuse : un coût n'est pas une valeur, et le passage de l'un à l'autre exige une chaîne de raisonnements dont chaque maillon peut céder. Le présent chapitre ouvre la partie consacrée au coût en posant la logique de la méthode, son champ d'application légitime et la première de ses trois composantes, le coût neuf, c'est-à-dire ce que coûterait la construction d'un bâtiment identique ou équivalent à la date d'évaluation. Les deux chapitres suivants détaillent la mesure des quantités (\cref{ch:22}) et la structure des coûts directs et indirects (\cref{ch:23}) ; le \cref{ch:24} traite des dépréciations, et le \cref{ch:25} de la valeur du terrain, sans laquelle l'indication de valeur par le coût reste incomplète. Tout au long du chapitre, le sujet du premier dossier de cas, un cottage de \msq{165} construit en 1998 dans le secteur Hull de Gatineau, sert de fil conducteur ; ses chiffres sont synthétiques et identifiés comme tels.
4+
5+\begin{objectifs}
6+À la fin de ce chapitre, le lecteur sait :
7+\begin{itemize}
8+\item énoncer la logique économique de la méthode du coût, l'identité $V = V_L + C_N - D$ et les situations où la méthode est pertinente, acceptable ou trompeuse ;
9+\item distinguer le coût de reproduction du coût de remplacement, choisir la base appropriée et comprendre ce que ce choix implique pour la mesure de la désuétude fonctionnelle ;
10+\item décomposer le coût neuf en coûts directs, coûts indirects et incitatif de l'entrepreneur, déclarer la convention d'assiette retenue et repérer les doubles comptages ;
11+\item estimer un coût neuf par comparaison unitaire, par composantes, par métré ou par indexation, avec les ajustements de taille, de date et de lieu qui s'imposent ;
12+\item mobiliser les sources québécoises et canadiennes de coûts, publiques et sous licence, en respectant leurs limites et leurs conditions d'utilisation ;
13+\item conduire un calcul intégral du coût neuf d'une maison unifamiliale et en mesurer la sensibilité aux hypothèses.
14+\end{itemize}
15+\end{objectifs}
16+
17+\section{La logique de la méthode et son champ d'application}\label{sec:21:logique}
18+
19+\subsection{Substitution, production et prix de réserve}
20+
21+La méthode du coût repose sur une application particulière du principe de substitution : un acheteur raisonnablement informé ne paiera pas, pour un bien existant, plus que ce qu'il lui en coûterait d'acquérir un terrain comparable et d'y faire construire, sans délai indu, un bâtiment d'une utilité équivalente. Le coût de production d'un substitut fixe donc un plafond au prix que le marché est disposé à payer. \textcite{appraisal2026} présentent la méthode comme la traduction, en termes d'évaluation, du raisonnement d'un promoteur qui compare le prix d'un bien existant au coût de sa reconstitution ; \textcite{achour1992} y voit, dans le vocabulaire québécois, l'« approche par le coût de remplacement déprécié », et la tradition britannique la nomme méthode de l'entrepreneur (\emph{contractor's method}), ce qui dit bien qu'elle raisonne du point de vue de celui qui construit plutôt que de celui qui achète \parencite{wyatt2022,scarrettOsborn2014}.
22+
23+Le plafond ainsi posé n'est toutefois atteint que si trois conditions sont réunies. Il faut d'abord que la construction soit matériellement et juridiquement possible : un terrain disponible, un zonage qui autorise l'usage, des entrepreneurs et des matériaux accessibles. Il faut ensuite que l'acheteur accepte d'attendre la fin des travaux et d'en supporter le risque, ce qui suppose que le délai et l'incertitude soient compensés, sans quoi le bien existant, disponible immédiatement, mérite une prime. Il faut enfin que le bâtiment existant offre la même utilité que le substitut neuf ; dès qu'il est plus vieux, moins bien conçu ou mal situé, sa valeur s'éloigne du coût neuf par un montant qu'il faut mesurer : c'est la dépréciation. La méthode du coût se résume alors à une identité comptable dont chacun des termes est une estimation distincte.
24+
25+\begin{definition}{Méthode du coût}{methode-cout}
26+La méthode du coût estime la valeur d'un bien immobilier bâti comme la somme de la valeur du terrain, réputé vacant et disponible pour sa plus haute et meilleure utilisation, et du coût neuf des améliorations, diminué de l'ensemble des dépréciations qui affectent ces améliorations à la date d'évaluation :
27+\begin{equation}\label{eq:21:identite}
28+\hat V = V_L + C_N - D_{\text{phys}} - D_{\text{fonc}} - D_{\text{ext}},
29+\end{equation}
30+où $V_L$ est la valeur du terrain, $C_N$ le coût neuf (de reproduction ou de remplacement) et $D_{\text{phys}}$, $D_{\text{fonc}}$, $D_{\text{ext}}$ les dépréciations physique, fonctionnelle et externe. Le coût neuf comprend les coûts directs, les coûts indirects et l'incitatif de l'entrepreneur.
31+\end{definition}
32+
33+L'\cref{eq:21:identite} appelle trois remarques qui structurent toute la partie V. Premièrement, le terrain et le bâtiment sont estimés séparément, ce qui est à la fois la force de la méthode, puisqu'elle rend explicite une décomposition que la comparaison laisse implicite, et sa faiblesse, puisqu'aucune transaction n'observe jamais ces deux composantes isolément ; \textcite{davisHeathcote2007} ont montré, à l'échelle agrégée, combien la part du terrain dans la valeur des logements varie dans le temps et l'espace, et \textcite{clappSalavei2010} ont établi que, lorsque la valeur du terrain domine, le coût déprécié du bâtiment cesse d'être une lecture fiable de la contribution de celui-ci. Deuxièmement, les dépréciations sont des soustractions mesurées sur une base qui diminue à chaque étape ; l'ordre et l'assiette de ces soustractions sont fixés dans la bibliothèque de calcul de l'ouvrage et discutés au \cref{ch:24}. Troisièmement, le coût neuf lui-même est une estimation, non une observation : c'est l'objet du présent chapitre.
34+
35+\begin{figure}[tbp]
36+\centering
37+\begin{tikzpicture}[
38+ bloc/.style={draw, rounded corners=2pt, minimum height=9mm, text width=27mm, align=center, font=\sffamily\footnotesize, line width=0.6pt},
39+ terrain/.style={bloc, fill=brandLight, draw=brandPrimary},
40+ cout/.style={bloc, fill=white, draw=brandDark},
41+ dep/.style={bloc, fill=orange!8, draw=brandAmber},
42+ res/.style={bloc, fill=brandPrimary, text=white, draw=brandPrimary, text width=32mm},
43+ op/.style={font=\large\sffamily\bfseries, color=brandGray},
44+ fl/.style={-{Stealth[length=2mm]}, line width=0.6pt, color=brandGray}]
45+ \node[cout] (cd) at (0,0) {Coûts directs\\ $D$};
46+ \node[cout, right=4mm of cd] (ci) {Coûts indirects\\ $I = \tau_I D$};
47+ \node[cout, right=4mm of ci] (ce) {Incitatif de\\ l'entrepreneur\\ $E = \tau_E (D+I)$};
48+ \node[op] at ($(cd.east)!0.5!(ci.west)$) {+};
49+ \node[op] at ($(ci.east)!0.5!(ce.west)$) {+};
50+ \node[draw=brandDark, dashed, rounded corners=3pt, fit=(cd)(ci)(ce), inner sep=3mm, label={[font=\sffamily\footnotesize\bfseries, color=brandDark]above:{Coût neuf $C_N$ (ch.~21, 22, 23)}}] (cn) {};
51+ \node[dep, below=10mm of cd] (dp) {Dépréciation\\ physique $D_{\text{phys}}$};
52+ \node[dep, below=10mm of ci] (df) {Désuétude\\ fonctionnelle $D_{\text{fonc}}$};
53+ \node[dep, below=10mm of ce] (de) {Désuétude\\ externe $D_{\text{ext}}$};
54+ \node[draw=brandAmber, dashed, rounded corners=3pt, fit=(dp)(df)(de), inner sep=3mm, label={[font=\sffamily\footnotesize\bfseries, color=brandAmber]below:{Dépréciations $D$ (ch.~24), déduites dans cet ordre}}] (dd) {};
55+ \node[terrain, below=16mm of dp, text width=38mm] (vl) {Valeur du terrain $V_L$\\ (vacant, PHMU ; ch.~25)};
56+ \node[res, below=16mm of de] (v) {Indication de valeur\\ $\hat V = V_L + C_N - D$};
57+ \draw[fl] (cn.south) -- node[op, right=1mm] {$-$} (dd.north);
58+ \draw[fl] (dd.south) |- ($(vl.east)!0.5!(v.west)$) -- (v.west);
59+ \draw[fl] (vl.east) -- node[op, above] {+} (v.west);
60+\end{tikzpicture}
61+\caption{Architecture de la méthode du coût : le coût neuf, somme des coûts directs, des coûts indirects et de l'incitatif de l'entrepreneur, est diminué des trois dépréciations puis ajouté à la valeur du terrain. Le présent chapitre traite du bloc supérieur ; chaque bloc renvoie au chapitre qui le détaille.}\label{fig:21:schema}
62+\figsource{schéma de l'auteur (TikZ, \texttt{manuscript/chapters/ch21.tex}).}
63+\end{figure}
64+
65+\subsection{Quand la méthode du coût est-elle pertinente ?}
66+
67+La pertinence de la méthode dépend de la part de l'incertitude totale qui loge dans le coût neuf plutôt que dans la dépréciation. Pour un bâtiment neuf ou récent, la dépréciation est faible et son erreur de mesure l'est aussi ; l'indication de valeur dépend surtout de la qualité de l'estimation du coût et de celle du terrain, deux grandeurs pour lesquelles il existe des sources. C'est le domaine naturel de la méthode : constructions neuves, immeubles de moins d'une dizaine d'années, projets en cours ou à l'état de plans, pour lesquels la comparaison est souvent impossible parce que le bien n'existe pas encore. À l'autre extrême, pour une maison de soixante ans dans un quartier établi, la dépréciation représente la moitié ou davantage du coût neuf et son estimation, qu'elle procède d'une règle âge-vie ou d'une extraction du marché, est entachée d'une erreur qui rend l'indication finale peu fiable ; \textcite{hultenWykoff1981} et \textcite{malpezziOzanneThibodeau1987} ont montré combien les profils de dépréciation estimés sur données réelles s'écartent des schémas linéaires que la pratique applique par commodité. Entre ces deux pôles, la méthode garde une utilité comme contrôle de vraisemblance : si une indication par comparaison dépasse nettement le coût neuf plus le terrain, c'est que le marché paie une prime de rareté ou d'immédiateté qu'il faut expliquer.
68+
69+Quatre situations justifient par ailleurs le recours à la méthode indépendamment de l'âge du bâtiment. La première est le bien atypique : une maison d'architecte, un bâtiment à usage spécialisé, une résidence hors norme dans un marché mince, pour lesquels les comparables font défaut et où le coût reste la seule base objective. La deuxième est l'assurance : la valeur assurable d'un bâtiment est par construction un coût de reconstruction, car l'assureur indemnise la remise en état et non la valeur d'échange ; le terrain en est exclu, de même que, selon les contrats, les fondations et les ouvrages souterrains, ce qui en fait une grandeur distincte de la valeur marchande que les normes de pratique traitent dans des dispositions propres \parencite{oeaqNormes}. La troisième est l'évaluation foncière municipale : le Manuel d'évaluation foncière du Québec consacre la partie 3E de son processus d'évaluation à la méthode du coût et l'évaluateur municipal doit s'y conformer lorsque c'est la méthode la plus pertinente \parencite[art.~8]{reglrole}, ce qui fait du coût la méthode dominante pour une grande partie du parc résidentiel évalué en masse. La quatrième est la plus haute et meilleure utilisation (PHMU, \emph{highest and best use}) : la méthode du coût suppose que les améliorations existantes correspondent à l'utilisation optimale du terrain ; lorsque ce n'est pas le cas, l'écart entre le bâtiment idéal et le bâtiment réel est précisément ce que mesure la désuétude fonctionnelle ou externe, et l'analyse de PHMU du \cref{ch:41} devient le préalable à tout calcul.
70+
71+Sur le plan normatif, les trois corpus de référence placent la méthode du coût au même rang que la comparaison et le revenu. Les IVS la décrivent, à la section 70 de la norme IVS 103, comme l'une des trois approches dont le choix et la pondération doivent être justifiés \parencite{ivs2025} ; les normes de pratique de l'OEAQ exigent que le rapport expose le raisonnement et explique le rejet de toute méthode traditionnelle non retenue \parencite{oeaqNormes}, et les CUSPAP imposent la même obligation d'explication \parencite{cuspap2026}. Il s'ensuit que l'évaluateur qui écarte la méthode du coût pour une maison ancienne doit le dire et le motiver, et que celui qui la retient pour un bien neuf doit documenter chaque composante du coût neuf avec la même rigueur qu'il documenterait un comparable. \textcite{bowes2011} a défendu, contre une pratique qui tend à négliger la méthode, l'idée qu'un coût neuf bien établi et une dépréciation extraite du marché livrent une information que les deux autres méthodes ne contiennent pas.
72+
73+\section{Coût de reproduction et coût de remplacement}\label{sec:21:repro}
74+
75+\subsection{Deux définitions pour une même question}
76+
77+Le coût neuf peut être défini de deux manières qui conduisent à des montants différents et, surtout, à des traitements différents de la désuétude. Le coût de reproduction est le coût de construire, à la date d'évaluation et aux prix courants, une réplique du bâtiment existant : mêmes matériaux, mêmes dimensions, même conception, mêmes normes de construction que celles de l'époque, y compris les éléments aujourd'hui dépassés, superflus ou interdits. Le coût de remplacement est le coût de construire, à la même date et aux mêmes prix, un bâtiment d'une utilité équivalente, avec les matériaux, la conception et les normes actuels, sans reproduire les éléments qui n'ajoutent rien à l'utilité. La distinction est classique dans la littérature nord-américaine \parencite{appraisal2026,aiDictionary2022} et la terminologie québécoise la reprend telle quelle \parencite{achour1992}.
78+
79+\begin{definition}{Coût de reproduction et coût de remplacement}{repro-rempl}
80+Le \emph{coût de reproduction} $C_N^{\text{repro}}$ est le coût estimé, à la date d'évaluation, de la construction d'une réplique exacte du bâtiment évalué, avec les mêmes matériaux, la même conception et la même qualité d'exécution, y compris ses carences et ses surabondances. Le \emph{coût de remplacement} $C_N^{\text{rempl}}$ est le coût estimé, à la même date, de la construction d'un bâtiment d'utilité équivalente selon les matériaux, les techniques, les normes et la conception courants. En règle générale, $C_N^{\text{rempl}} \le C_N^{\text{repro}}$ ; l'inégalité est stricte dès que le bâtiment existant comporte des éléments surabondants ou des méthodes de construction devenues coûteuses.
81+\end{definition}
82+
83+L'exemple le plus parlant au Québec est celui des maisons d'avant 1950 : murs de maçonnerie pleine, plâtre sur lattes, planchers de bois massif sur solives de pleine dimension, hauteur de plafond de dix pieds. Reproduire ces éléments coûterait aujourd'hui beaucoup plus que construire une maison moderne de même surface et de même utilité en ossature de bois, gypse et isolant ; la différence n'ajoute rien à ce qu'un acheteur est disposé à payer. Inversement, le remplacement intègre des exigences que le bâtiment existant ne satisfait pas : isolation thermique du chapitre I.1 du Code de construction, ventilation mécanique, protection parasismique, sécurité électrique, autant d'éléments qui renchérissent le substitut neuf sans que le bâtiment ancien en bénéficie \parencite{codeConstruction}. Le coût de remplacement est donc à la fois plus bas, parce qu'il élimine les surabondances, et plus haut, parce qu'il incorpore les normes nouvelles ; c'est le solde de ces deux effets qui le sépare du coût de reproduction.
84+
85+\subsection{Quelle base retenir ?}
86+
87+Le coût de reproduction s'impose dans trois cas. Le premier est l'assurance lorsque le contrat garantit la reconstruction « à l'identique », ce qui est fréquent pour les bâtiments patrimoniaux ou les propriétés dont le cachet fait la valeur ; il faut alors chiffrer la réplique, pierre de taille et boiseries comprises. Le deuxième est le litige ou l'expropriation lorsque la question posée porte sur ce qu'il en coûterait de remettre le propriétaire dans la situation exacte où il se trouvait. Le troisième est l'analyse de la désuétude fonctionnelle elle-même : pour mesurer ce que coûte une surabondance, il faut connaître le coût de reproduction de l'élément surabondant, car c'est de ce coût que la désuétude est déduite. Dans toutes les autres situations, et en particulier pour l'estimation de la valeur marchande d'une habitation courante, le coût de remplacement est la base de travail usuelle, et c'est celle que retiennent les conventions de calcul de l'ouvrage (annexe \emph{Conventions de calcul}) et la bibliothèque \texttt{traite.cout}. Il est d'usage, dans l'évaluation de masse comme dans la pratique privée, de travailler au coût de remplacement parce que les barèmes unitaires et les guides de coûts décrivent des bâtiments construits selon les techniques courantes ; appliquer un barème moderne à une maison ancienne revient de fait à calculer un coût de remplacement, même si le rapport ne le dit pas.
88+
89+\subsection{Le lien avec la désuétude fonctionnelle}
90+
91+Le choix de la base n'est pas neutre pour la suite du calcul : il détermine ce qui reste à déduire au titre de la désuétude fonctionnelle. Si le coût neuf est un coût de reproduction, il inclut le coût des éléments surabondants ; la désuétude fonctionnelle doit alors retrancher le coût de reproduction de ces éléments, net de la dépréciation physique déjà imputée, puis ajouter la valeur capitalisée de l'excès de coûts d'exploitation qu'ils engendrent, s'il y en a. Si le coût neuf est un coût de remplacement, les surabondances en sont déjà exclues ; ne subsiste à déduire que l'excès de coûts d'exploitation, et il serait fautif de retrancher une seconde fois le coût des éléments superflus. La proposition suivante formalise cette équivalence, que le \cref{ch:24} exploite en détail.
92+
93+\begin{proposition}{Équivalence des deux bases sous une mesure cohérente de la désuétude}{equivalence}
94+Soit un bâtiment dont le coût de reproduction se décompose en $C_N^{\text{repro}} = C^{\text{util}} + S$, où $S$ est le coût de reproduction des éléments surabondants et $C^{\text{util}}$ celui des éléments utiles, et soit $C_N^{\text{rempl}} = C^{\text{util}}$ le coût de remplacement d'un bâtiment d'utilité équivalente, les éléments utiles étant supposés construits au même coût sous les deux bases. Notons $\delta \in [0,1]$ le taux de dépréciation physique appliqué uniformément et $X$ la valeur capitalisée de l'excès de coûts d'exploitation imputable aux éléments surabondants. Alors l'indication de valeur est la même sous les deux bases si et seulement si la désuétude fonctionnelle est mesurée par
95+\begin{equation}\label{eq:21:dfonc}
96+D_{\text{fonc}}^{\text{repro}} = (1-\delta)\,S + X
97+\qquad\text{et}\qquad
98+D_{\text{fonc}}^{\text{rempl}} = X .
99+\end{equation}
100+\end{proposition}
101+\begin{proof}
102+Sous la base de reproduction, la contribution du bâtiment après dépréciation physique est $(1-\delta)(C^{\text{util}} + S)$, et la désuétude fonctionnelle doit éliminer la part non encore dépréciée des éléments surabondants, soit $(1-\delta)S$, ainsi que la pénalité d'exploitation $X$ : la contribution nette vaut $(1-\delta)(C^{\text{util}}+S) - (1-\delta)S - X = (1-\delta)C^{\text{util}} - X$. Sous la base de remplacement, la contribution après dépréciation physique est $(1-\delta)C^{\text{util}}$ et la désuétude fonctionnelle se réduit à $X$, d'où la même contribution nette $(1-\delta)C^{\text{util}} - X$. Si l'on déduisait en outre $(1-\delta)S$ sous la base de remplacement, la contribution serait minorée de ce montant : c'est le double comptage que la proposition écarte. Réciproquement, toute autre mesure de $D_{\text{fonc}}$ sous l'une des bases rompt l'égalité.
103+\end{proof}
104+
105+La proposition suppose que les éléments utiles coûtent le même prix sous les deux bases ; dans la réalité, le remplacement substitue des techniques modernes aux techniques anciennes même pour les éléments utiles, de sorte que $C_N^{\text{rempl}}$ et $C^{\text{util}}$ diffèrent et que l'équivalence n'est qu'approximative. Elle a néanmoins une portée pratique immédiate : le rapport doit dire quelle base a été retenue, et la mesure de la désuétude fonctionnelle doit être cohérente avec ce choix. Le \cref{tab:21:repro-rempl} résume les différences entre les deux bases.
106+
107+\begin{table}[tbp]
108+\caption{Coût de reproduction et coût de remplacement : définitions, usages et conséquences pour la mesure de la dépréciation.}\label{tab:21:repro-rempl}
109+\centering\small
110+\begin{tabularx}{\textwidth}{@{}L{30mm}YY@{}}
111+\toprule
112+Critère & Coût de reproduction & Coût de remplacement \\
113+\midrule
114+Objet chiffré & Réplique exacte du bâtiment (matériaux, conception, normes d'origine) & Bâtiment d'utilité équivalente (matériaux, conception et normes actuels) \\
115+Surabondances & Incluses dans le coût neuf ; déduites ensuite en désuétude fonctionnelle & Exclues du coût neuf ; seul l'excès de coûts d'exploitation est déduit \\
116+Carences & Reproduites ; le coût de les corriger est déduit en désuétude curable & Comblées par conception ; la carence du bâtiment réel est déduite en désuétude \\
117+Normes nouvelles (Code de construction) & Non intégrées & Intégrées, ce qui hausse le coût neuf \\
118+Usages typiques & Assurance « à l'identique », patrimoine, litiges, mesure des surabondances & Valeur marchande, évaluation de masse (barèmes), études de faisabilité \\
119+Sources de coûts & Devis spécialisés, métré détaillé, restauration & Barèmes unitaires, guides de coûts, soumissions courantes \\
120+Convention de l'ouvrage & Sur demande explicite & Par défaut (\texttt{traite.cout}, annexe des conventions) \\
121+\bottomrule
122+\end{tabularx}
123+\tabsource{synthèse de l'auteur d'après \textcite{appraisal2026}, \textcite{aiDictionary2022} et \textcite{achour1992}.}
124+\end{table}
125+
126+\section{Les composantes du coût neuf}\label{sec:21:composantes}
127+
128+\subsection{Coûts directs, coûts indirects, incitatif}
129+
130+Quelle que soit la base retenue, le coût neuf se compose de trois blocs. Les \emph{coûts directs} (\emph{hard costs}) sont les dépenses incorporées physiquement au bâtiment ou nécessaires à son édification sur le chantier : matériaux, main-d'œuvre des corps de métier, équipement de chantier, sous-traitance, frais généraux de chantier tels que la roulotte, les clôtures, les conteneurs et le nettoyage, ainsi que la marge et les frais généraux des sous-traitants, qui sont inclus dans leurs prix. Les \emph{coûts indirects} (\emph{soft costs}) sont les dépenses nécessaires au projet mais qui ne se matérialisent pas dans l'ouvrage : honoraires d'architecte, d'ingénieur et d'arpenteur-géomètre, études de sol, permis et frais municipaux, assurances du chantier et cautionnements, financement intérimaire, frais juridiques et comptables, taxes de vente non récupérables, frais de mise en marché et de vente pour un bien destiné à la revente, frais d'administration du promoteur. L'\emph{incitatif de l'entrepreneur} (\emph{entrepreneurial incentive}), enfin, est la rémunération que le marché doit offrir à celui qui coordonne et porte le risque du projet pour qu'il accepte de l'entreprendre ; sans elle, personne ne construirait, et un acheteur qui veut un bâtiment neuf doit la payer.
131+
132+La frontière entre direct et indirect n'est pas tracée de la même façon par toutes les sources, et c'est la première source de confusion. La supervision du chantier, par exemple, est tantôt classée dans les frais généraux de l'entrepreneur général, donc dans les coûts directs, tantôt dans les coûts indirects du promoteur ; les guides de coûts commerciaux l'incluent généralement dans leurs prix unitaires, tandis qu'un devis détaillé la fait apparaître comme un poste séparé. Les raccordements aux services municipaux relèvent du chantier, mais les contributions pour fins de parcs et les frais d'infrastructure imposés par la municipalité relèvent du projet. Il n'y a pas de bon classement dans l'absolu ; il y a une exigence de cohérence : le taux d'indirects appliqué doit avoir été mesuré avec la même définition des directs que celle utilisée pour le bâtiment évalué, et l'incitatif ne doit pas s'ajouter à une marge déjà incluse dans les prix unitaires.
133+
134+L'incitatif appelle une précision conceptuelle que \textcite{appraisal2026} et \textcite{geltnerMillerClaytonEichholtz2014} soulignent chacun à leur manière. Il faut distinguer l'incitatif, qui est la rémunération \emph{anticipée} nécessaire pour qu'un projet se fasse, du profit, qui est la rémunération \emph{réalisée} une fois le bien vendu. Le premier est une composante du coût, parce qu'il est une condition de l'offre ; le second est un résultat, qui peut être nul ou négatif si le marché a baissé entre le lancement et la livraison. C'est l'incitatif, et non le profit constaté sur un projet particulier, qui entre dans le coût neuf, et il doit être tiré du marché : des entretiens avec des constructeurs, de l'analyse de projets comparables où prix de vente, terrain et coûts sont connus, ou de la différence entre le prix des maisons neuves et la somme de leurs coûts et du terrain. Il varie avec le risque, donc avec la taille du projet, la durée, la préventes et le cycle ; une marge jugée normale pour une maison individuelle construite sur commande ne l'est pas pour une tour de condominiums vendue sur plans.
135+
136+\subsection{Dérivation du coût neuf et conventions d'assiette}
137+
138+Notons $D$ les coûts directs, $I$ les coûts indirects et $E$ l'incitatif. Le coût neuf est la somme $C_N = D + I + E$. Dans la pratique, les coûts indirects sont souvent exprimés en proportion des coûts directs, $I = \tau_I D$, et l'incitatif en proportion d'une assiette qu'il faut déclarer. Deux conventions coexistent. Si l'assiette de l'incitatif est la somme des directs et des indirects,
139+\begin{equation}\label{eq:21:cn-multiplicatif}
140+C_N = D + \tau_I D + \tau_E (D + \tau_I D) = D\,(1+\tau_I)(1+\tau_E) ;
141+\end{equation}
142+si l'assiette se limite aux coûts directs,
143+\begin{equation}\label{eq:21:cn-additif}
144+C_N = D + \tau_I D + \tau_E D = D\,(1+\tau_I+\tau_E) .
145+\end{equation}
146+La différence entre les deux conventions vaut $D\,\tau_I\,\tau_E$, c'est-à-dire l'incitatif calculé sur les seuls indirects ; pour des taux de \pct{20} et \pct{10}, elle représente \pct{2} des coûts directs, un écart modeste mais qui n'a aucune raison d'être laissé au hasard. Les conventions de l'ouvrage retiennent l'\cref{eq:21:cn-multiplicatif}, parce que le promoteur qui porte le risque le porte sur l'ensemble de ses déboursés, honoraires et financement compris, et non sur les seuls travaux ; la fonction \texttt{cout\_neuf} de la bibliothèque accepte les deux conventions par son argument \texttt{base\_incitatif} et oblige ainsi l'analyste à choisir.
147+
148+\begin{formule}[Coût neuf : composantes et parts]
149+Avec $I = \tau_I D$ et $E = \tau_E(D+I)$, les parts de chaque composante dans le coût neuf sont
150+\begin{equation}\label{eq:21:parts}
151+\frac{D}{C_N} = \frac{1}{(1+\tau_I)(1+\tau_E)},\qquad
152+\frac{I}{C_N} = \frac{\tau_I}{(1+\tau_I)(1+\tau_E)},\qquad
153+\frac{E}{C_N} = \frac{\tau_E}{1+\tau_E} .
154+\end{equation}
155+La part de l'incitatif ne dépend que de $\tau_E$ ; celle des indirects dépend des deux taux. Un coût neuf de \dollars{400000} avec $\tau_I = \pct{20}$ et $\tau_E = \pct{10}$ se décompose en \pct{75.8} de directs, \pct{15.2} d'indirects et \pct{9.1} d'incitatif.
156+\end{formule}
157+
158+\subsection{Les doubles comptages à éviter}
159+
160+La structure additive du coût neuf invite à compter deux fois certains éléments, et le \cref{tab:21:composantes} recense les cas les plus fréquents. Le premier concerne la marge des sous-traitants : un devis par composantes bâti sur des prix de sous-traitants installés inclut déjà leurs frais généraux et leur profit ; ajouter un incitatif revient à rémunérer le coordonnateur du projet, non les métiers, et le taux doit être calibré en conséquence. Le deuxième concerne les frais généraux de l'entrepreneur général : s'ils sont inclus dans les prix unitaires d'un guide de coûts, le taux d'indirects ne doit pas les contenir de nouveau. Le troisième concerne le financement intérimaire : les intérêts sur le prêt de construction sont un coût indirect du bâtiment, mais les intérêts sur le coût d'acquisition du terrain pendant la construction relèvent du terrain ou de l'incitatif selon la convention ; les compter dans les deux est une erreur. Le quatrième concerne les taxes de vente : pour un acheteur occupant, la TPS et la TVQ payées sur une habitation neuve, nettes des remboursements, sont un déboursé réel et font partie du coût qu'il compare au prix d'une maison existante, tandis que pour un investisseur inscrit aux taxes elles sont récupérables et ne sont pas un coût ; le traitement doit suivre le profil de l'acheteur typique et être déclaré. Le cinquième concerne les améliorations du site : entrée, aménagement paysager, clôtures et piscine sont tantôt inclus dans le coût du bâtiment, tantôt évalués séparément et dépréciés à leur propre rythme ; ils ne doivent pas figurer aux deux endroits. Le sixième, enfin, est le plus subtil : la valeur du terrain estimée par comparaison avec des terrains viabilisés inclut déjà les raccordements et les infrastructures ; les inclure aussi dans le coût neuf du bâtiment les compte deux fois.
161+
162+\begin{table}[tbp]
163+\caption{Composantes du coût neuf, exemples québécois et risque de double comptage.}\label{tab:21:composantes}
164+\centering\small
165+\begin{tabularx}{\textwidth}{@{}L{26mm}YL{44mm}@{}}
166+\toprule
167+Composante & Exemples & Point de vigilance \\
168+\midrule
169+Coûts directs & Excavation, fondations, charpente, enveloppe, finition, plomberie, électricité, chauffage, frais de chantier (conteneurs, nettoyage) & Les prix de sous-traitants incluent leur marge ; les barèmes incluent souvent les frais généraux de l'entrepreneur général \\
170+Coûts indirects & Plans et honoraires professionnels, permis et études (arpentage, sol), assurances et cautionnement, financement intérimaire, frais juridiques, taxes non récupérables, mise en marché & Le taux d'indirects doit être mesuré avec la même définition des directs ; les intérêts sur le terrain ne sont comptés qu'une fois \\
171+Incitatif de l'entrepreneur & Rémunération du risque et de la coordination, tirée du marché (projets comparables, constructeurs) & Ne pas confondre avec le profit réalisé ; déclarer l'assiette (directs seuls ou directs + indirects) \\
172+Améliorations du site & Entrée asphaltée, aménagement paysager, balcon, clôture, piscine & Inclure dans le bâtiment \emph{ou} évaluer à part, jamais les deux ; durée de vie propre \\
173+Infrastructures & Raccordements aqueduc/égout, contributions municipales & Déjà dans la valeur d'un terrain viabilisé estimé par comparaison \\
174+\bottomrule
175+\end{tabularx}
176+\tabsource{synthèse de l'auteur ; structure d'après \textcite{appraisal2026} et les postes du devis synthétique du cas 1 (\texttt{data/synthetic/cas1\_gatineau\_couts.csv}).}
177+\end{table}
modified solutions/ch16.tex +35 −0
@@ -1 +1,36 @@
1 % Solutions du chapitre 16 — Substitution et choix des comparables 1 % Solutions du chapitre 16 — Substitution et choix des comparables
2+% Les chiffres sont vérifiés par analysis/python/tests/test_ch16.py
3+
4+\begin{solution}{ex:16:1}{Unités de comparaison}
5+Les prix unitaires sont $525\,000/152 = \parmsq{3453.95}$, $489\,000/141 = \parmsq{3468.09}$ et $561\,000/171 = \parmsq{3280.70}$. La médiane est \parmsq{3453.95} ; appliquée aux \msq{165} du sujet, elle donne une indication brute de $3\,453{,}95 \times 165 = \dollars{569902}$, arrondie à \dollars{570000}. Si l'élasticité du prix à la superficie est inférieure à un, le prix unitaire décroît avec la taille : les deux comparables plus petits que le sujet (152 et \msq{141}) ont des prix unitaires supérieurs à celui qu'aurait le sujet, et la médiane, tirée de l'un d'eux, surestime la valeur. Le comparable le plus grand (\msq{171}) donne la borne basse (\parmsq{3280.70}, soit \dollars{541316} pour le sujet) ; l'indication « juste » se situe entre les deux et exige un ajustement de superficie plutôt qu'une simple proportionnalité.
6+\end{solution}
7+
8+\begin{solution}{ex:16:2}{Ventes utilisables ou non}
9+(a) Utilisable sans ajustement : la vente successorale a bénéficié d'une exposition normale (41 jours, courtier) ; la motivation du vendeur n'a pas comprimé le prix de façon démontrable. (b) À vérifier, probablement à écarter : la vente à un locataire en place sans inscription n'a pas été exposée au marché ; le prix peut refléter une relation préexistante. Elle ne serait utilisable qu'avec la preuve que le prix a été fixé par référence au marché. (c) Utilisable avec ajustement de financement : la balance de prix de vente à 2 \% sur cinq ans, alors que les taux de marché sont nettement supérieurs, a une valeur pour l'acheteur qui gonfle le prix nominal ; on retranche la valeur actualisée de l'avantage de taux (\cref{ch:17}). (d) À écarter en général : la reprise de finance est vendue par un vendeur contraint dont l'objectif est de recouvrer la créance ; la vente n'est pas sincère au sens de la définition. Elle peut servir d'indice de borne basse. (e) À écarter : les droits transmis diffèrent (fraction de copropriété divise avec charges communes et déclaration de copropriété, contre pleine propriété) ; aucun ajustement de droits n'est fiable entre ces deux segments. (f) Utilisable avec ajustement de conditions du marché : vingt mois dépassent la fenêtre usuelle, mais si le marché est mince et que l'indice de prix du secteur est bien mesuré, l'ajustement temporel est préférable à un comparable d'un autre secteur. Le dossier doit justifier la fenêtre.
10+\end{solution}
11+
12+\begin{solution}{ex:16:3}{Distance d'attributs}
13+Avec les écarts normalisés et pondérés de l'\cref{eq:16:distance} :
14+\begin{align*}
15+d_A &= \sqrt{2\left(\tfrac{7}{25}\right)^2 + 0{,}7\left(\tfrac{20}{150}\right)^2 + \left(\tfrac{3}{10}\right)^2 + 0} = \sqrt{0{,}1568 + 0{,}0124 + 0{,}09} = 0{,}509, \\
16+d_B &= \sqrt{2\left(\tfrac{25}{25}\right)^2 + 0{,}7\left(\tfrac{30}{150}\right)^2 + \left(\tfrac{18}{10}\right)^2 + 1} = \sqrt{2 + 0{,}028 + 3{,}24 + 1} = 2{,}504, \\
17+d_C &= \sqrt{2\left(\tfrac{15}{25}\right)^2 + 0{,}7\left(\tfrac{220}{150}\right)^2 + \left(\tfrac{2}{10}\right)^2 + 1} = \sqrt{0{,}72 + 1{,}506 + 0{,}04 + 1} = 1{,}807 .
18+\end{align*}
19+Classement : A, puis C, puis B. Avec un poids de 3 sur l'âge, le terme d'âge de B passe de 3,24 à 9,72 ($d_B = 3{,}57$) et celui de C de 0,04 à 0,12 ($d_C = 1{,}83$) : le classement ne change pas, A restant de loin le plus proche. Le candidat B, récent et de qualité supérieure, est pénalisé surtout par son âge et sa qualité ; il exigerait des ajustements importants sur ces deux éléments.
20+\end{solution}
21+
22+\begin{solution}{ex:16:4}{Fenêtre temporelle et ajustement de conditions du marché}
23+Le facteur d'ajustement pour quinze mois à 4,43 \% par année composé est $(1{,}0443)^{15/12} - 1 = 0{,}0557$, soit un ajustement de $506\,500 \times 0{,}0557 = \dollars{28200}$ environ, et un prix ajusté pour les conditions du marché de \dollars{534700}. Élargir la fenêtre fait passer $n$ de 9 à 21 : si les comparables étaient de variance égale et pondérés également, la variance de l'indication passerait de $\sigma^2/9$ à $\sigma^2/21$, soit une réduction de l'écart-type d'environ 35 \%. Mais les douze candidats supplémentaires exigent des ajustements temporels de 12 à 24 mois dont l'incertitude s'ajoute à $\sigma_j^2$ : si le taux de croissance est connu à plus ou moins un point de pourcentage par année, l'incertitude de l'ajustement à 24 mois atteint à elle seule environ 2 \% du prix. L'élargissement n'est avantageux que si cette incertitude reste petite devant la dispersion des prix ; dans un marché dont l'indice est bien mesuré, il l'est ; dans un marché en retournement, il ne l'est pas.
24+\end{solution}
25+
26+\begin{solution}{ex:16:5}{Pondérations optimales}
27+Les inverses des variances, en milliers de dollars au carré, sont $1/400 = 0{,}0025$, $1/900 = 0{,}001111$ et $1/1600 = 0{,}000625$, de somme $0{,}004236$. Les pondérations optimales sont donc $w_1 = 0{,}590$, $w_2 = 0{,}262$ et $w_3 = 0{,}148$. La variance minimale est $1/0{,}004236 = 236{,}1$ (milliers\textsuperscript{2}), soit un écart-type de \dollars{15400}. Avec des pondérations égales, la variance est $(400 + 900 + 1600)/9 = 322{,}2$, soit un écart-type de \dollars{17950} : la pondération optimale réduit l'écart-type d'environ 14 \%. Si $\sigma_1$ est réestimé à \dollars{10000}, $1/\sigma_1^2 = 0{,}01$, la somme devient $0{,}011736$ et $w_1 = 0{,}852$ : l'indication repose presque entièrement sur le premier comparable, ce qui illustre la sensibilité des pondérations optimales à l'estimation des variances soulignée par Green (1994).
28+\end{solution}
29+
30+\begin{solution}{ex:16:6}{Robustesse de la sélection aux paramètres de la distance}
31+Les résultats dépendent de la version du jeu de données ; la solution décrit la démarche et les résultats obtenus avec la graine 20261031. (a) Doubler le poids de la distance géographique favorise les ventes à moins d'un kilomètre : les comparables G0574 et G0404, déjà les plus proches, restent en tête, et les candidats à plus de deux kilomètres (G0477, G0575) tendent à être remplacés par des ventes plus proches mais plus anciennes ou de taille plus éloignée. (b) Annuler le poids de l'ancienneté fait remonter des ventes de 2025 ; l'indication brute change peu, parce que le filtre de dix-huit mois a déjà borné l'ancienneté. (c) Un poids de 3 sur la qualité pénalise G0574, G0468 et G0477 (qualité 4) et G0575 (qualité 2) au profit de candidats de qualité 3 ; c'est la variante qui modifie le plus la composition. La note doit souligner que la robustesse de la composition aux poids est une information en soi : lorsque la sélection change beaucoup pour de petites variations des paramètres, le marché offre plusieurs ensembles de comparables également défendables, et la fourchette de valeur du \cref{ch:20} doit en tenir compte ; le dossier doit consigner les paramètres retenus et les variantes essayées.
32+\end{solution}
33+
34+\begin{solution}{ex:16:7}{Nombre optimal de comparables}
35+L'erreur quadratique moyenne est $\mathrm{EQM}(k) = b_1^2 k^2 + \sigma^2 / k$. Sa dérivée, $2 b_1^2 k - \sigma^2 / k^2$, s'annule pour $k^3 = \sigma^2 / (2 b_1^2)$, d'où l'\cref{eq:16:kopt}. Avec $\sigma = 25\,000$ et $b_1 = 2\,000$ : $\sigma^2 / (2 b_1^2) = 6{,}25 \times 10^8 / (8 \times 10^6) = 78{,}1$, et $k^* = 78{,}1^{1/3} = 4{,}27$, soit quatre comparables ; l'EQM vaut alors $4 \times 10^6 \times 16 + 6{,}25 \times 10^8 / 4 = 2{,}20 \times 10^8$, soit une erreur type d'environ \dollars{14800}. Si un modèle hédonique divise $b_1$ par quatre ($b_1 = 500$), le ratio devient $1\,250$ et $k^* = 10{,}8$ : on retient une dizaine de comparables. C'est le résultat de Kummerow et Galfalvy (2002) : plus les ajustements sont précis, plus il est avantageux d'élargir l'échantillon, jusqu'au point où la méthode de comparaison rejoint la régression hédonique.
36+\end{solution}
modified styles/traite-qc.sty +32 −32
@@ -111,32 +111,32 @@
111 \RequirePackage[hidelinks, pdfusetitle, bookmarksnumbered]{hyperref}% 111 \RequirePackage[hidelinks, pdfusetitle, bookmarksnumbered]{hyperref}%
112 } 112 }
113 \RequirePackage[french, capitalise, noabbrev, nameinlink]{cleveref} 113 \RequirePackage[french, capitalise, noabbrev, nameinlink]{cleveref}
114−\crefname{equation}{l'équation}{les équations} 114+\crefname{equation}{équation}{équations}
115−\Crefname{equation}{L'équation}{Les équations} 115+\Crefname{equation}{Équation}{Équations}
116−\crefname{figure}{la figure}{les figures} 116+\crefname{figure}{figure}{figures}
117−\Crefname{figure}{La figure}{Les figures} 117+\Crefname{figure}{Figure}{Figures}
118−\crefname{table}{le tableau}{les tableaux} 118+\crefname{table}{tableau}{tableaux}
119−\Crefname{table}{Le tableau}{Les tableaux} 119+\Crefname{table}{Tableau}{Tableaux}
120−\crefname{chapter}{le chapitre}{les chapitres} 120+\crefname{chapter}{chapitre}{chapitres}
121−\Crefname{chapter}{Le chapitre}{Les chapitres} 121+\Crefname{chapter}{Chapitre}{Chapitres}
122−\crefname{section}{la section}{les sections} 122+\crefname{section}{section}{sections}
123−\Crefname{section}{La section}{Les sections} 123+\Crefname{section}{Section}{Sections}
124−\crefname{part}{la partie}{les parties} 124+\crefname{part}{partie}{parties}
125−\Crefname{part}{La partie}{Les parties} 125+\Crefname{part}{Partie}{Parties}
126−\crefname{appendix}{l'annexe}{les annexes} 126+\crefname{appendix}{annexe}{annexes}
127−\Crefname{appendix}{L'annexe}{Les annexes} 127+\Crefname{appendix}{Annexe}{Annexes}
128−\crefname{exercice}{l'exercice}{les exercices} 128+\crefname{exercice}{exercice}{exercices}
129−\Crefname{exercice}{L'exercice}{Les exercices} 129+\Crefname{exercice}{Exercice}{Exercices}
130−\crefname{exemple}{l'exemple}{les exemples} 130+\crefname{exemple}{exemple}{exemples}
131−\Crefname{exemple}{L'exemple}{Les exemples} 131+\Crefname{exemple}{Exemple}{Exemples}
132−\crefname{definition}{la définition}{les définitions} 132+\crefname{definition}{définition}{définitions}
133−\Crefname{definition}{La définition}{Les définitions} 133+\Crefname{definition}{Définition}{Définitions}
134−\crefname{theoreme}{le théorème}{les théorèmes} 134+\crefname{theoreme}{théorème}{théorèmes}
135−\Crefname{theoreme}{Le théorème}{Les théorèmes} 135+\Crefname{theoreme}{Théorème}{Théorèmes}
136−\crefname{proposition}{la proposition}{les propositions} 136+\crefname{proposition}{proposition}{propositions}
137−\Crefname{proposition}{La proposition}{Les propositions} 137+\Crefname{proposition}{Proposition}{Propositions}
138−\crefname{encadre}{l'encadré}{les encadrés} 138+\crefname{encadre}{encadré}{encadrés}
139−\Crefname{encadre}{L'encadré}{Les encadrés} 139+\Crefname{encadre}{Encadré}{Encadrés}
140 140
141 % --- Numérotation ------------------------------------------------------------- 141 % --- Numérotation -------------------------------------------------------------
142 \numberwithin{equation}{chapter} 142 \numberwithin{equation}{chapter}
@@ -186,17 +186,17 @@
186 \tcbset{traitebase/.style={enhanced, breakable, boxrule=0pt, arc=0pt, outer arc=0pt, 186 \tcbset{traitebase/.style={enhanced, breakable, boxrule=0pt, arc=0pt, outer arc=0pt,
187 left=8pt, right=8pt, top=6pt, bottom=6pt, before skip=10pt plus 2pt, after skip=10pt plus 2pt, 187 left=8pt, right=8pt, top=6pt, bottom=6pt, before skip=10pt plus 2pt, after skip=10pt plus 2pt,
188 fonttitle=\sffamily\bfseries, coltitle=white, titlerule=0pt, toptitle=3pt, bottomtitle=3pt}} 188 fonttitle=\sffamily\bfseries, coltitle=white, titlerule=0pt, toptitle=3pt, bottomtitle=3pt}}
189−\newtcbtheorem[number within=chapter, crefname={la définition}{les définitions}, Crefname={La définition}{Les définitions}]{definition}{Définition} 189+\newtcbtheorem[number within=chapter, crefname={définition}{définitions}, Crefname={Définition}{Définitions}]{definition}{Définition}
190 {traitebase, colback=brandLight, colbacktitle=brandPrimary, borderline west={2.5pt}{0pt}{brandPrimary}}{def} 190 {traitebase, colback=brandLight, colbacktitle=brandPrimary, borderline west={2.5pt}{0pt}{brandPrimary}}{def}
191−\newtcbtheorem[number within=chapter, crefname={le théorème}{les théorèmes}, Crefname={Le théorème}{Les théorèmes}]{theoreme}{Théorème} 191+\newtcbtheorem[number within=chapter, crefname={théorème}{théorèmes}, Crefname={Théorème}{Théorèmes}]{theoreme}{Théorème}
192 {traitebase, colback=white, colbacktitle=brandDark, borderline west={2.5pt}{0pt}{brandDark}}{thm} 192 {traitebase, colback=white, colbacktitle=brandDark, borderline west={2.5pt}{0pt}{brandDark}}{thm}
193−\newtcbtheorem[use counter from=theoreme, crefname={la proposition}{les propositions}, Crefname={La proposition}{Les propositions}]{proposition}{Proposition} 193+\newtcbtheorem[use counter from=theoreme, crefname={proposition}{propositions}, Crefname={Proposition}{Propositions}]{proposition}{Proposition}
194 {traitebase, colback=white, colbacktitle=brandDark, borderline west={2.5pt}{0pt}{brandDark}}{prop} 194 {traitebase, colback=white, colbacktitle=brandDark, borderline west={2.5pt}{0pt}{brandDark}}{prop}
195−\newtcbtheorem[use counter from=theoreme, crefname={le lemme}{les lemmes}, Crefname={Le lemme}{Les lemmes}]{lemme}{Lemme} 195+\newtcbtheorem[use counter from=theoreme, crefname={lemme}{lemmes}, Crefname={Lemme}{Lemmes}]{lemme}{Lemme}
196 {traitebase, colback=white, colbacktitle=brandGray, borderline west={2.5pt}{0pt}{brandGray}}{lem} 196 {traitebase, colback=white, colbacktitle=brandGray, borderline west={2.5pt}{0pt}{brandGray}}{lem}
197−\newtcbtheorem[number within=chapter, crefname={l'exemple}{les exemples}, Crefname={L'exemple}{Les exemples}]{exemple}{Exemple} 197+\newtcbtheorem[number within=chapter, crefname={exemple}{exemples}, Crefname={Exemple}{Exemples}]{exemple}{Exemple}
198 {traitebase, colback=white, colframe=brandGrayLight, boxrule=0.6pt, colbacktitle=brandGrayLight, coltitle=brandDark}{ex} 198 {traitebase, colback=white, colframe=brandGrayLight, boxrule=0.6pt, colbacktitle=brandGrayLight, coltitle=brandDark}{ex}
199−\newtcbtheorem[number within=chapter, crefname={l'encadré}{les encadrés}, Crefname={L'encadré}{Les encadrés}]{encadre}{Encadré} 199+\newtcbtheorem[number within=chapter, crefname={encadré}{encadrés}, Crefname={Encadré}{Encadrés}]{encadre}{Encadré}
200 {traitebase, colback=brandLight, colbacktitle=brandSecondary, coltitle=brandDark, borderline west={2.5pt}{0pt}{brandSecondary}}{enc} 200 {traitebase, colback=brandLight, colbacktitle=brandSecondary, coltitle=brandDark, borderline west={2.5pt}{0pt}{brandSecondary}}{enc}
201 \renewcommand{\proofname}{Démonstration} 201 \renewcommand{\proofname}{Démonstration}
202 \renewenvironment{proof}[1][\proofname]{\par\noindent\textit{#1.}\ \ignorespaces}{\hfill$\square$\par\medskip} 202 \renewenvironment{proof}[1][\proofname]{\par\noindent\textit{#1.}\ \ignorespaces}{\hfill$\square$\par\medskip}
modified tools/compile_chapter.sh +2 −0
@@ -18,6 +18,7 @@ WRAP="$ROOT/manuscript/_wrap_$JOB.tex"
18 printf '%s\n' '\newif\ifLogoAutorise\LogoAutorisefalse\def\PALETTE{uqo}' 18 printf '%s\n' '\newif\ifLogoAutorise\LogoAutorisefalse\def\PALETTE{uqo}'
19 printf '%s\n' '\usepackage{traite-qc}' 19 printf '%s\n' '\usepackage{traite-qc}'
20 printf '%s\n' '\input{../manuscript/frontmatter/metadata.tex}' 20 printf '%s\n' '\input{../manuscript/frontmatter/metadata.tex}'
21+ printf '\\input{%s}\n' "$OUT/_labels.tex"
21 printf '%s\n' '\begin{document}' 22 printf '%s\n' '\begin{document}'
22 printf '\\setcounter{chapter}{%d}\n' "$PREV" 23 printf '\\setcounter{chapter}{%d}\n' "$PREV"
23 printf '\\input{%s}\n' "$SRC" 24 printf '\\input{%s}\n' "$SRC"
@@ -25,6 +26,7 @@ WRAP="$ROOT/manuscript/_wrap_$JOB.tex"
25 printf '%s\n' '\printbibliography' 26 printf '%s\n' '\printbibliography'
26 printf '%s\n' '\end{document}' 27 printf '%s\n' '\end{document}'
27 } > "$WRAP" 28 } > "$WRAP"
29+python3 "$ROOT/tools/make_labels.py" "$OUT/_labels.tex"
28 cd "$ROOT/manuscript"; export TEXINPUTS="../styles:${TEXINPUTS:-}" 30 cd "$ROOT/manuscript"; export TEXINPUTS="../styles:${TEXINPUTS:-}"
29 latexmk -g -lualatex -interaction=nonstopmode -halt-on-error -file-line-error -bibtex -e "\$biber=q{$ROOT/tools/biber %O %S};" -output-directory="$OUT" "$WRAP" >"$OUT/latexmk.out" 2>&1 31 latexmk -g -lualatex -interaction=nonstopmode -halt-on-error -file-line-error -bibtex -e "\$biber=q{$ROOT/tools/biber %O %S};" -output-directory="$OUT" "$WRAP" >"$OUT/latexmk.out" 2>&1
30 RC=$? 32 RC=$?
added tools/make_labels.py +17 −0
@@ -0,0 +1,17 @@
1+#!/usr/bin/env python3
2+"""Écrit un fichier d'étiquettes (\\newlabel) pour tous les chapitres et annexes, afin que \\cref{ch:NN} / \\cref{app:x}
3+se résolvent lors de la compilation isolée d'un chapitre (tools/compile_chapter.sh). Usage : make_labels.py <sortie.tex>"""
4+import json, sys, pathlib
5+ROOT = pathlib.Path(__file__).resolve().parents[1]
6+t = json.load(open(ROOT / "manuscript/chapters/TITRES.json", encoding="utf-8"))
7+out = ["\\makeatletter"]
8+def lab(name, ref, title, anchor, kind):
9+ out.append("\\@newl@bel{r}{%s}{{%s}{1}{%s}{%s}{}}" % (name, ref, title, anchor))
10+ out.append("\\@newl@bel{r}{%s@cref}{{[%s][%s][]%s}{[1][1][]1}{}{}{}}" % (name, kind, ref, ref))
11+for n, title in t.items():
12+ n = int(n); lab(f"ch:{n:02d}", str(n), title, f"chapter.{n}", "chapter")
13+ if n < 10: lab(f"ch:{n}", str(n), title, f"chapter.{n}", "chapter")
14+for L, k in zip("ABCDEF", ["glossaire", "formulaires-inspection", "modeles-rapport", "conventions-calcul", "scripts-classeurs", "solutions"]):
15+ lab(f"app:{k}", L, "", f"appendix.{L}", "chapter")
16+out.append("\\makeatother")
17+pathlib.Path(sys.argv[1]).write_text("\n".join(out) + "\n", encoding="utf-8")
modified tools/merge_sources.py +42 −0
@@ -50,3 +50,45 @@ for f in sorted((ROOT / "references").glob("lib-*.bib")):
50 lib.write_text(base + marker + "\n".join(added), encoding="utf-8") 50 lib.write_text(base + marker + "\n".join(added), encoding="utf-8")
51 print(f"library.bib : {len(have)} clés ({len(added)} ajoutées ; doublons ignorés : {len(dup)})") 51 print(f"library.bib : {len(have)} clés ({len(added)} ajoutées ; doublons ignorés : {len(dup)})")
52 if dup: print(" doublons :", ", ".join(dup[:20])) 52 if dup: print(" doublons :", ", ".join(dup[:20]))
53+
54+# --- fichiers par chapitre (verification/chapters/chNN-*.csv, references/lib-chNN.bib) → fichiers globaux
55+def merge_csv(pattern, target, keycol):
56+ tgt = ROOT / target
57+ with tgt.open(encoding="utf-8") as fh:
58+ r = csv.DictReader(fh); cols = r.fieldnames; rows = list(r)
59+ keys = {row[keycol] for row in rows}
60+ n = 0
61+ for f in sorted((ROOT / "verification/chapters").glob(pattern)):
62+ with f.open(encoding="utf-8") as fh:
63+ for row in csv.DictReader(fh):
64+ row = {c: (row.get(c) or "").strip() for c in cols}
65+ if row[keycol] and row[keycol] not in keys:
66+ rows.append(row); keys.add(row[keycol]); n += 1
67+ with tgt.open("w", encoding="utf-8", newline="") as fh:
68+ w = csv.DictWriter(fh, fieldnames=cols); w.writeheader(); w.writerows(rows)
69+ print(f"{target} : {len(rows)} lignes (+{n})")
70+merge_csv("ch*-figures.csv", "verification/figures.csv", "fichier")
71+merge_csv("ch*-calculations.csv", "verification/calculations.csv", "id")
72+merge_csv("ch*-claims.csv", "verification/claims.csv", "id")
73+# registre par chapitre → registry.csv (append sans doublon d'id)
74+reg = ROOT / "sources/registry.csv"
75+with reg.open(encoding="utf-8") as fh:
76+ r = csv.DictReader(fh); rrows = list(r)
77+ids = {x["id"] for x in rrows}; n = 0
78+for f in sorted((ROOT / "verification/chapters").glob("ch*-registry.csv")):
79+ with f.open(encoding="utf-8") as fh:
80+ for row in csv.DictReader(fh):
81+ row = {c: (row.get(c) or "").strip() for c in hdr}
82+ if row["id"] and row["id"] not in ids: rrows.append(row); ids.add(row["id"]); n += 1
83+with reg.open("w", encoding="utf-8", newline="") as fh:
84+ w = csv.DictWriter(fh, fieldnames=hdr); w.writeheader(); w.writerows(rrows)
85+print(f"registry.csv (+chapitres) : {len(rrows)} (+{n})")
86+# bib par chapitre
87+txt = lib.read_text(encoding="utf-8"); have = set(keys(txt)); added = []
88+for f in sorted((ROOT / "references").glob("lib-ch*.bib")):
89+ for p in re.split(r"(?m)^(?=@\w+\s*\{)", f.read_text(encoding="utf-8")):
90+ m = re.match(r"@\w+\s*\{\s*([^,\s]+)\s*,", p)
91+ if m and m.group(1) not in have: have.add(m.group(1)); added.append(p.strip() + "\n")
92+if added:
93+ lib.write_text(txt.rstrip() + "\n\n% ===== ENTRÉES AJOUTÉES PAR LES CHAPITRES =====\n" + "\n".join(added), encoding="utf-8")
94+print(f"library.bib (+chapitres) : {len(have)} clés (+{len(added)})")
modified verification/calculations.csv +1 −1
@@ -1 +1 @@
1−id,chapitre,description,script,test,resultat_attendu,resultat_obtenu,statut,date 1+id,chapitre,description,script,test,resultat_attendu,resultat_obtenu,statut,date
added verification/chapters/ch16-calculations.csv +7 −0
@@ -0,0 +1,7 @@
1+id,chapitre,description,script,test,resultat_attendu,resultat_obtenu,statut,date
2+CALC-16-01,16,Entonnoir de sélection (600→236→40→27→16→5),analysis/python/cases/ch16_selection.py,analysis/python/tests/test_ch16.py::test_entonnoir_et_comparables,"600;236;40;27;16;5","600;236;40;27;16;5",verifie,2026-10-31
3+CALC-16-02,16,Comparables retenus et prix,analysis/python/cases/ch16_selection.py,analysis/python/tests/test_ch16.py::test_entonnoir_et_comparables,"G0574 598000;G0404 506500;G0468 624500;G0477 538000;G0575 513500",idem,verifie,2026-10-31
4+CALC-16-03,16,Indication brute $/m² médian × 165 m²,analysis/python/cases/ch16_selection.py,analysis/python/tests/test_ch16.py::test_indication_brute,551029,551029,verifie,2026-10-31
5+CALC-16-04,16,Statistiques $/m² candidats (méd/min/max) et prix bruts (min/max/moy/méd),analysis/python/cases/ch16_selection.py,analysis/python/tests/test_ch16.py::test_indication_brute,"3286/2361/4140 ; 506500/624500/556100/538000",idem,verifie,2026-10-31
6+CALC-16-05,16,Sensibilité (48 combinaisons) min/max/écart-type,analysis/python/cases/ch16_selection.py,analysis/python/tests/test_ch16.py::test_sensibilite,"525768/608014/24069",idem,verifie,2026-10-31
7+CALC-16-06,16,Solutions des exercices 16.1 16.3 16.4 16.5 16.7,solutions/ch16.tex,analysis/python/tests/test_ch16.py::test_solutions_exercices,"569902 ; 0.509/2.504/1.807 ; 28200 ; 0.590/0.262/0.148 ; k*=4.27",idem,verifie,2026-10-31
added verification/chapters/ch16-claims.csv +10 −0
@@ -0,0 +1,10 @@
1+id,chapitre,affirmation,source_id,verification,date,statut,notes
2+CLM-16-01,16,Définition OEAQ de la valeur marchande (prix sincère le plus probable ; marché libre et ouvert ; parties informées ; exposition suffisante ; comptant ; épuré des incitatifs),SRC-C-001,NPP OEAQ p. Intro-11 (notes-C.md §1.1),2026-10-31,verifie,citation ≤ 25 mots
3+CLM-16-02,16,LFM art. 43-45 : valeur réelle = prix le plus probable d'une vente de gré à gré entre parties informées,SRC-B-001,LégisQuébec F-2.1 à jour au 2026-10-15 (notes-B.md),2026-10-31,verifie,
4+CLM-16-03,16,F-2.1 r. 10 : ventes ≥ 5 000 $ ; taille de la liste de base ; épuration ± 2 écarts-types,SRC-B-010,LégisQuébec F-2.1 r. 10 (notes-B.md),2026-10-31,verifie,id source approximatif : voir registry.csv (lot B)
5+CLM-16-04,16,APCIQ T3 2026 RMR Gatineau : médiane unifamiliale 480 000 $ (−3 %) ; 1 044 ventes ; 2 256 inscriptions ; délai 34 jours ; médianes par secteur Hull 475 000 / Aylmer 530 000 / Gatineau 464 900 / Buckingham-Masson-Angers 395 000,SRC-E-002,Baromètre APCIQ Gatineau T3 2026 (calibration.json),2026-10-31,verifie,source d'industrie citée avec attribution
6+CLM-16-05,16,APCIQ T3 2026 : 9 265 ventes RMR Montréal ; 2 161 RMR Québec,SRC-E-004;SRC-E-003,calibration.json,2026-10-31,verifie,
7+CLM-16-06,16,Rôle 2027-2029 Gatineau déposé le 2026-10-27 ; référence 2025-07-01 ; médiane unifamiliale 454 600 → 495 800 $ (+9,06 %),SRC-E-054;SRC-E-057,calibration.json (communiqué Ville de Gatineau),2026-10-31,verifie,
8+CLM-16-07,16,Consultation du registre foncier payante à l'unité (1 $/document),SRC-A-030,notes-A.md (grille tarifaire 1er avril 2025),2026-10-31,verifie,id source approximatif : voir registry.csv (lot A)
9+CLM-16-08,16,Seuils indicatifs d'ajustement brut 25 % et net 15 % dans la pratique nord-américaine,,pratique courante (formulaires des prêteurs américains) — non attribuée à une norme,2026-10-31,non_verifie,présenté comme signal d'alerte et non comme règle
10+CLM-16-09,16,Pratique de 3 à 6 comparables ; exigence fréquente d'au moins 3 par les prêteurs,,pratique courante,2026-10-31,partiellement_verifie,appuyé sur Kummerow & Galfalvy 2002 et Appraisal Institute 2026 (usage)
added verification/chapters/ch16-figures.csv +6 −0
@@ -0,0 +1,6 @@
1+fichier,chapitre,script,donnees,type_donnees,legende_courte,statut
2+ch16/ch16-fig-nuage-selection.pdf,16,analysis/python/cases/ch16_selection.py,data/synthetic/cas1_gatineau_ventes.csv,synthetique,"Prix vs superficie, candidats et comparables retenus [données synthétiques]",verifie
3+ch16/ch16-fig-carte-comparables.pdf,16,analysis/python/cases/ch16_selection.py,data/synthetic/cas1_gatineau_ventes.csv,synthetique,"Carte fictive des ventes par secteur, sujet et comparables [données synthétiques]",verifie
4+ch16/ch16-fig-boites-prix-m2.pdf,16,analysis/python/cases/ch16_selection.py,data/synthetic/cas1_gatineau_ventes.csv,synthetique,"Boîtes à moustaches du prix par m² : secteur, candidats, retenus [données synthétiques]",verifie
5+ch16/ch16-fig-sensibilite-selection.pdf,16,analysis/python/cases/ch16_selection.py,data/synthetic/cas1_gatineau_ventes.csv,synthetique,"Indication brute selon k et rayon [données synthétiques]",verifie
6+ch16/tikz-processus-selection,16,manuscript/chapters/ch16.tex,,schema,"Entonnoir de sélection des comparables (TikZ)",verifie
added verification/chapters/ch16-registry.csv +1 −0
@@ -0,0 +1 @@
1+id,type,titre,organisme,version_ou_date,url,date_consultation,statut,utilise_dans,notes
added verification/chapters/ch16.md +7 −0
@@ -0,0 +1,7 @@
1+# Chapitre 16 — Substitution et choix des comparables (bilan, 2026-10-31)
2+
3+- Rédigé par l'orchestrateur (chapitre modèle). `tools/compile_chapter.sh 16` : OK, 28 pages, 0 référence/citation non définie, 1 overfull mineur. `pytest test_ch16.py` : 4 tests verts.
4+- Compteurs : ≈ 8 500 mots ; 5 figures (4 PDF Matplotlib + 1 schéma TikZ) ; 7 tableaux ; 8 équations numérotées ; 1 définition, 1 proposition démontrée ; 7 exercices (2 × niveau 1, 3 × niveau 2, 2 × niveau 3) avec solutions ; encadrés quebec × 3, pratique, erreurs, limites, synthese, synthetique.
5+- Données : cas 1 (synthétique, graine 20261031) ; chiffres reproduits par `analysis/python/cases/ch16_selection.py` ; registres `verification/chapters/ch16-{figures,calculations,claims}.csv`.
6+- Points non vérifiés : seuils d'ajustement brut 25 % / net 15 % (pratique courante, non normée) ; « 3 à 6 comparables » (usage) ; les identifiants de source SRC-A/B des claims sont à faire correspondre aux lignes exactes du registre lors de l'harmonisation.
7+- Suggestions pour l'harmonisation : le chapitre 17 doit reprendre les cinq mêmes comparables (G0574, G0404, G0468, G0477, G0575) et le chapitre 20 la fourchette de sensibilité (525 768 – 608 014 $) ; mots : viser 9 000+ si la relecture ajoute des exemples (biens atypiques, multilogement).
modified verification/claims.csv +1 −1
@@ -1 +1 @@
1−id,chapitre,affirmation,source_id,verification,date,statut,notes 1+id,chapitre,affirmation,source_id,verification,date,statut,notes
modified verification/figures.csv +1 −1
@@ -1 +1 @@
1−fichier,chapitre,script,donnees,type_donnees,legende_courte,statut 1+fichier,chapitre,script,donnees,type_donnees,legende_courte,statut
2 2
Menu

Theme