#!/usr/bin/env python3 """Figures du chapitre 25 — Évaluation du terrain (DONNÉES SYNTHÉTIQUES, graine 20261031). Sorties (assets/figures/ch25/) : ch25-fig-valeur-unitaire-superficie.pdf — valeur unitaire ($/m²) des terrains vacants selon la superficie, desservis ou non, et courbes du modèle ; ch25-fig-carte-terrains.pdf — carte synthétique des 200 ventes de terrains, du sujet et des cinq comparables retenus ; ch25-fig-lotissement.pdf — flux annuels du lotissement et valeur actualisée cumulée ; tornade de sensibilité ; ch25-fig-residuelle-sensibilite.pdf — technique résiduelle du terrain (cas 3) : V_L selon le taux du terrain pour trois coûts dépréciés. Usage (racine) : python3 analysis/python/figures/ch25_figures.py""" from __future__ import annotations import sys, pathlib import numpy as np 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 matplotlib.pyplot as plt from matplotlib.lines import Line2D from traite.style import setup, palette, savefig, LARGEUR, fmt_dollars, fmt_milliers import ch25_terrain as cas def fig_valeur_unitaire(S): c = S["comparaison"]; df = c["terrains"]; m = c["modele"]; b = m["beta"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.4)) for val, lab, col, mk in [(1, "Terrain desservi (aqueduc et égout)", palette.cat[0], "o"), (0, "Terrain non desservi", palette.cat[1], "^")]: d = df[df["desservi"] == val] ax.scatter(d["superficie_m2"], d["prix_m2"], s=14, alpha=0.65, color=col, marker=mk, label=lab, linewidths=0) T = np.linspace(320, 9000, 300) base = np.exp(b["const"] + b["ln_T"] * np.log(T / 560.0)) * m["duan"] # Hull, plat, régulier, façade 18 m, 1 logement, 2024-07-01 ax.plot(T, base / T, color=palette.cat[0], lw=1.6, label=f"Modèle, desservi (élasticité {b['ln_T']:.2f})") ax.plot(T, base * np.exp(b["non_desservi"]) / T, color=palette.cat[1], lw=1.6, ls="--", label="Modèle, non desservi") vr = S["valeur_retenue"] / 620.0 ax.scatter([620], [vr], marker="*", s=120, color=palette.cat[3], zorder=5, edgecolor=palette.ink, linewidths=0.5) ax.annotate(f"Sujet du cas 1 : 620 m², {fmt_dollars(vr)}/m² (valeur retenue)", (620, vr), xytext=(1000, vr + 90), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6), color=palette.ink) ax.set_xscale("log"); ax.set_xlim(300, 10000); ax.set_ylim(0, 800) ax.set_xticks([300, 500, 1000, 2000, 5000, 10000]); ax.set_xticklabels(["300", "500", "1 000", "2 000", "5 000", "10 000"]) ax.set_xlabel("Superficie du terrain (m², échelle logarithmique)"); ax.set_ylabel("Prix de vente par m² ($)") ax.legend(loc="upper right", fontsize=7.5) savefig(fig, "ch25/ch25-fig-valeur-unitaire-superficie.pdf") def fig_carte(S): c = S["comparaison"]; df = c["terrains"]; comps = c["selection"]["comparables"]; suj = c["sujet"] fig, ax = plt.subplots(figsize=(LARGEUR, 4.2)) d1 = df[df["desservi"] == 1]; d0 = df[df["desservi"] == 0] ax.scatter(d1["lon"], d1["lat"], s=12, color=palette.ink2, alpha=0.45, linewidths=0, label="Vente de terrain desservi") ax.scatter(d0["lon"], d0["lat"], s=16, facecolors="none", edgecolors=palette.ink2, alpha=0.7, linewidths=0.7, label="Vente de terrain non desservi") ax.scatter(comps["lon"], comps["lat"], s=42, color=palette.cat[1], edgecolor=palette.ink, linewidths=0.5, zorder=4, label="Comparables retenus (5)") ax.scatter([suj["lon"]], [suj["lat"]], marker="*", s=170, color=palette.cat[3], edgecolor=palette.ink, linewidths=0.6, zorder=5, label="Sujet (cas 1)") for _, r in comps.iterrows(): ax.annotate(r["id_vente"], (r["lon"], r["lat"]), xytext=(4, 3), textcoords="offset points", fontsize=7, color=palette.ink) # rayon de recherche de 4 km (approximation locale : 1° lat ≈ 111 km, 1° lon ≈ 78 km) th = np.linspace(0, 2 * np.pi, 200) ax.plot(suj["lon"] + 4 / 78 * np.cos(th), suj["lat"] + 4 / 111 * np.sin(th), color=palette.cat[3], lw=0.8, ls=":") for sct, (la, lo) in cas.CENTROIDES.items(): dy = -0.036 if sct == "Masson-Angers" else 0.030 ax.text(lo, la + dy, sct, ha="center", fontsize=8, color=palette.ink, fontweight="semibold") ax.set_xlabel("Longitude (coordonnées fictives)"); ax.set_ylabel("Latitude (coordonnées fictives)") ax.set_aspect(111 / 78); ax.legend(loc="lower right", fontsize=7.5) ax.set_xlim(-75.92, -75.33); ax.set_ylim(45.34, 45.64) savefig(fig, "ch25/ch25-fig-carte-terrains.pdf") def fig_lotissement(S): L = S["lotissement"]; det = L["detail"]; p = L["params"]; m = S["comparaison"]["modele"] fig, (a1, a2) = plt.subplots(1, 2, figsize=(LARGEUR, 3.6), gridspec_kw={"width_ratios": [1.0, 1.0], "wspace": 0.95}) yrs = det["annee"].values couts = -(det["infrastructure"] + det["honoraires"] + det["taxes"] + det["frais_vente"] + det["profit"]).values a1.bar(yrs, det["revenus"].values, color=palette.cat[0], width=0.6, label="Produit des ventes de lots") a1.bar(yrs, couts, color=palette.cat[1], width=0.6, label="Coûts, frais et profit du promoteur") r = p["taux"]; cum = np.cumsum([f / (1 + r) ** t for t, f in zip(yrs, L["flux"])]) a1.plot(yrs, cum, color=palette.cat[3], marker="o", lw=1.6, label="Valeur actualisée cumulée (10 %)") for t, v in zip(yrs, cum): a1.annotate(fmt_milliers(v / 1000), (t, v), xytext=(0, 6), textcoords="offset points", ha="center", fontsize=7, color=palette.ink) a1.axhline(0, color=palette.ink2, lw=0.6) a1.set_xticks(yrs); a1.set_xticklabels([str(t) for t in yrs]); a1.set_xlabel("Année (flux de fin d'année)"); a1.set_ylim(-1_250_000, 2_300_000); a1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p_: fmt_milliers(x / 1000))) a1.set_ylabel("Milliers de dollars"); a1.set_title("(a) Flux du lotissement de 12 lots", loc="left", fontsize=9) a1.legend(fontsize=6.5, loc="upper left", handlelength=1.2) sl = cas.sensibilite_lotissement(m); base = sl["base"] items = [("Absorption\n2 → 6 lots/an", sl["absorption"][2], sl["absorption"][6]), ("Prix des lots\n−10 % → +10 %", sl["prix"][-0.10], sl["prix"][0.10]), ("Infrastructures\n80 000 → 50 000 $/lot", sl["infrastructure"][80_000], sl["infrastructure"][50_000]), ("Profit du promoteur\n20 % → 10 %", sl["profit"][0.20], sl["profit"][0.10]), ("Taux d'actualisation\n15 % → 8 %", sl["taux"][0.15], sl["taux"][0.08])] items.sort(key=lambda x: abs(x[2] - x[1]), reverse=True) for k, (lab, lo, hi) in enumerate(items): a2.barh(k, lo - base, left=base, color=palette.cat[1], height=0.55) a2.barh(k, hi - base, left=base, color=palette.cat[0], height=0.55) a2.text(min(lo, hi) - 8000, k, fmt_milliers((min(lo, hi)) / 1000), va="center", ha="right", fontsize=7, color=palette.ink) a2.text(max(lo, hi) + 8000, k, fmt_milliers((max(lo, hi)) / 1000), va="center", ha="left", fontsize=7, color=palette.ink) a2.set_yticks(range(len(items))); a2.set_yticklabels([i[0] for i in items], fontsize=7) a2.axvline(base, color=palette.ink, lw=0.8) a2.set_xlim(base - 420_000, base + 420_000); a2.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, p_: fmt_milliers(x / 1000))) a2.set_xlabel("Valeur du site brut (milliers de dollars)"); a2.set_title(f"(b) Sensibilité autour de {fmt_dollars(base)}", loc="left", fontsize=9) a2.grid(axis="y", visible=False); a2.invert_yaxis() savefig(fig, "ch25/ch25-fig-lotissement.pdf") def fig_residuelle(S): t3 = S["cas3"]; rs = S["residuelle"] fig, ax = plt.subplots(figsize=(LARGEUR, 3.3)) RL = np.linspace(0.03, 0.05, 81) for d, col, ls in [(0.44, palette.cat[1], "--"), (0.52, palette.cat[0], "-"), (0.60, palette.cat[2], "-.")]: VL = [cas.residuelle_cas3({"taux_depreciation": d, "R_L": r}, t3)["V_L"] for r in RL] CB = cas.residuelle_cas3({"taux_depreciation": d}, t3)["C_B"] ax.plot(RL * 100, VL, color=col, ls=ls, lw=1.6, label=f"Dépréciation {d*100:.0f} % : bâtiment déprécié {fmt_dollars(CB)}") ax.axhline(0, color=palette.ink2, lw=0.6) ax.scatter([rs["R_L"] * 100], [rs["V_L"]], s=60, color=palette.cat[3], zorder=5, edgecolor=palette.ink, linewidths=0.5) ax.annotate(f"Cas de base : taux du terrain 3,75 %, 40 ans restants\nvaleur résiduelle {fmt_dollars(rs['V_L'])}", (rs["R_L"] * 100, rs["V_L"]), xytext=(4.05, 1_000_000), fontsize=8, arrowprops=dict(arrowstyle="-", color=palette.ink2, lw=0.6)) ext = rs["V_cap"] - rs["C_B"] ax.axhline(ext, color=palette.ink2, lw=0.8, ls=":") ax.text(3.02, ext + 25_000, f"Extraction sur la valeur capitalisée (V − bâtiment déprécié) : {fmt_dollars(ext)}", fontsize=7.5, color=palette.ink2) ax.set_xlabel("Taux de rendement du terrain $R_L$ (%)"); ax.set_ylabel("Valeur résiduelle du terrain (dollars)") ax.yaxis.set_major_formatter(plt.FuncFormatter(fmt_dollars)); ax.set_ylim(-300_000, 1_300_000) ax.legend(loc="lower left", fontsize=7.5) savefig(fig, "ch25/ch25-fig-residuelle-sensibilite.pdf") def main(): setup() S = cas.synthese() fig_valeur_unitaire(S); fig_carte(S); fig_lotissement(S); fig_residuelle(S) print("Figures ch25 écrites dans", ROOT / "assets/figures/ch25") if __name__ == "__main__": main()