#!/usr/bin/env python3 """Figure ch23 — sensibilité (DONNÉES SYNTHÉTIQUES, cas 1 ; taux documenté Banque du Canada V80691335) : (a) intérêts intérimaires selon la durée du chantier et le taux (tirage uniforme, L·i·T/2) ; (b) tornade du coût neuf : durée, taux intérimaire, indirects hors financement, hausse salariale (λ = 40 %), incitatif. Sortie : assets/figures/ch23/ch23-fig-sensibilite.pdf""" import sys, pathlib ROOT = pathlib.Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases")) import numpy as np, matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter from traite.style import setup, palette, savefig, LARGEUR, fmt_milliers import ch21_cout_neuf as c21, ch23_structure_couts as cas def main(): setup() devis, sujet = c21.charger(); cn = c21.cout_neuf_sujet(devis); i_aff = cas.taux_affiche_5ans() fc = cas.financement_cas1(devis, cn, i_aff); base = fc["base_hors_financement"] mois = np.arange(3, 25); taux = [0.04, 0.05, i_aff, 0.07, 0.09] fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.4), gridspec_kw={"width_ratios": [1, 1.25]}) for k, t in enumerate(taux): ax1.plot(mois, [cas.fin.interets_intermediaires(base, t, m / 12)["interets"] / 1000 for m in mois], color=palette.cat[k], lw=2.0 if abs(t - i_aff) < 1e-6 else 1.2, label=f"{t*100:.2f} %".replace(".", ",")) ax1.axhline(fc["forfait"] / 1000, color=palette.ink2, lw=0.8, ls="--"); ax1.text(3.2, fc["forfait"] / 1000 + 0.3, f"forfait du devis {fc['forfait']/1000:.1f} k$".replace(".", ","), fontsize=6.8, color=palette.ink2, ha="left", va="bottom") ax1.set_xlabel("Durée du chantier (mois)"); ax1.set_ylabel("Intérêts intérimaires (k$)"); ax1.set_title("(a) Intérêts selon durée et taux", loc="left") ax1.legend(title="Taux annuel", fontsize=7, title_fontsize=7, loc="upper left") torn = cas.tornade(devis, cn, i_aff); base_cn = torn[0]["base"] torn = sorted(torn, key=lambda o: abs(o["haut"] - o["bas"])) y = np.arange(len(torn)) for yi, o in zip(y, torn): lo, hi = min(o["bas"], o["haut"]), max(o["bas"], o["haut"]) ax2.barh(yi, (hi - lo) / 1000, left=lo / 1000, color=palette.cat[0], height=0.55) ax2.text(lo / 1000 - 1, yi, f"{lo/1000:.0f}", ha="right", va="center", fontsize=7); ax2.text(hi / 1000 + 1, yi, f"{hi/1000:.0f}", ha="left", va="center", fontsize=7) ax2.axvline(base_cn / 1000, color=palette.ink2, lw=0.8); ax2.text(base_cn / 1000, -0.95, f"base {base_cn/1000:.1f} k$".replace(".", ","), ha="center", va="bottom", fontsize=7, color=palette.ink2); ax2.set_ylim(-1.1, len(torn) - 0.5) ax2.set_yticks(y); ax2.set_yticklabels([f"{o['param']}\n{o['etiquette']}" for o in torn], fontsize=7) ax2.set_xlabel("Coût neuf (k$)"); ax2.set_title("(b) Tornade du coût neuf", loc="left"); ax2.set_xlim(365, 430) ax2.xaxis.set_major_formatter(FuncFormatter(fmt_milliers)); ax2.grid(axis="y", visible=False) fig.tight_layout(w_pad=2.0) out = savefig(fig, "ch23/ch23-fig-sensibilite.pdf"); print("OK", out) if __name__ == "__main__": main()