#!/usr/bin/env python3 """Chapitre 25 — Évaluation du terrain (DONNÉES SYNTHÉTIQUES, graine 20261031). Imprime tous les chiffres cités dans le chapitre 25 : * jeu synthétique de ventes de terrains vacants « Gatineau » (data/synthetic/ch25_terrains_ventes.csv) : lois déclarées ; * modèle hédonique des terrains (traite.hedonique.mco) : élasticité de superficie, façade, services, topographie, forme, densité ; * comparaison directe : sélection de cinq terrains comparables pour le sujet du cas 1 et grille d'ajustements ; * allocation : part du terrain dans les ventes du cas 1 (secteur Hull) ; prix implicite marginal du modèle hédonique (traite.cas1) ; * extraction : prix − coût déprécié du bâtiment sur les cinq comparables du chapitre 16 (coût neuf du chapitre 21) ; * technique résiduelle du terrain sur le cas 3 (RNE de traite.cas3 ; taux déclarés) et sensibilité ; * lotissement (développement actualisé) d'un site de 10 000 m² en 12 terrains (traite.finance.van) ; * capitalisation d'une rente foncière (emphytéose) ; * synthèse des indications et valeur de terrain retenue pour le sujet du cas 1 (reprise par les ch. 20, 24, 45). Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch25_terrain.py Toutes les fonctions financières proviennent de analysis/python/traite (aucune réécriture).""" from __future__ import annotations import sys, json, pathlib import numpy as np, pandas as pd ROOT = pathlib.Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases")) from traite import hedonique as hd, comparables as cp, finance as fin, cout as ct, cas1, cas3 D = ROOT / "data/synthetic" CSV_TERRAINS = D / "ch25_terrains_ventes.csv" GRAINE = 20261031 REF_DATE = pd.Timestamp("2024-07-01") DATE_EVAL = pd.Timestamp("2026-09-01") # ----------------------------------------------------------------------------- 1. jeu synthétique de ventes de terrains # Lois déclarées (hypothèses pédagogiques ; voir data/synthetic/README.md, section « Chapitre 25 ») P_REF = 225_000.0 # prix du terrain de référence : 560 m², façade 18 m, desservi, plat, régulier, 1 logement, Hull, 2024-07-01 ELAST_T = 0.50 # élasticité du prix du terrain à la superficie (< 1 : valeur unitaire décroissante) ELAST_F = 0.08 # élasticité à la façade (à superficie donnée) B_NON_DESSERVI = -0.30 # ni aqueduc ni égout (puits + installation septique à la charge de l'acheteur) B_PENTE = -0.06 # pente marquée B_ROC = -0.12 # roc affleurant ou remblai important B_IRREG = -0.05 # forme irrégulière (triangle, drapeau, trapèze prononcé) B_LOGEMENT = 0.12 # par logement constructible supplémentaire (zonage H2/H3) TENDANCE_MOIS = 0.0036 # même tendance que le coefficient « mois » du modèle hédonique du cas 1 (≈ 4,4 %/an) SIGMA = 0.10 MEDIANES_APCIQ = {"Hull": 475_000, "Gatineau": 464_900, "Aylmer": 530_000, "Buckingham": 395_000, "Masson-Angers": 395_000 * 0.98} PRIME_SECTEUR = {k: float(np.log(v / MEDIANES_APCIQ["Hull"])) for k, v in MEDIANES_APCIQ.items()} CENTROIDES = {"Hull": (45.4296, -75.7450), "Gatineau": (45.4769, -75.6650), "Aylmer": (45.3936, -75.8450), "Buckingham": (45.5899, -75.4200), "Masson-Angers": (45.5468, -75.4150)} N_PAR_SECTEUR = {"Hull": 70, "Gatineau": 60, "Aylmer": 40, "Buckingham": 15, "Masson-Angers": 15} def generer_terrains(graine: int = GRAINE) -> pd.DataFrame: """Ventes synthétiques de terrains vacants résidentiels (2023-01 → 2026-08). Aucun terrain réel.""" rng = np.random.default_rng(graine) rows = [] k = 0 for sct, n in N_PAR_SECTEUR.items(): periurbain = sct in ("Buckingham", "Masson-Angers") for _ in range(n): k += 1 desservi = int(rng.random() < (0.30 if periurbain else 0.85)) if desservi: T = float(np.clip(np.exp(rng.normal(np.log(600), 0.28)), 320, 2500)) else: T = float(np.clip(np.exp(rng.normal(np.log(2200), 0.45)), 900, 9000)) F = float(np.clip(0.60 * np.sqrt(T) * np.exp(rng.normal(0, 0.14)), 10, 90)) topo = rng.choice(["plat", "pente", "roc"], p=[0.70, 0.20, 0.10]) forme = rng.choice(["régulier", "irrégulier"], p=[0.80, 0.20]) logements = int(rng.choice([1, 2, 3], p=[0.82, 0.12, 0.06])) if desservi and not periurbain else 1 jours = int(rng.integers(0, (pd.Timestamp("2026-08-31") - pd.Timestamp("2023-01-01")).days + 1)) date = pd.Timestamp("2023-01-01") + pd.Timedelta(days=jours) mois = (date - REF_DATE).days / 30.4375 lat0, lon0 = CENTROIDES[sct] lat = lat0 + rng.normal(0, 0.012); lon = lon0 + rng.normal(0, 0.017) lnp = (np.log(P_REF) + ELAST_T * np.log(T / 560) + ELAST_F * np.log(F / 18) + B_NON_DESSERVI * (1 - desservi) + B_PENTE * (topo == "pente") + B_ROC * (topo == "roc") + B_IRREG * (forme == "irrégulier") + B_LOGEMENT * (logements - 1) + PRIME_SECTEUR[sct] + TENDANCE_MOIS * mois + rng.normal(0, SIGMA)) prix = float(np.round(np.exp(lnp) / 500) * 500) rows.append({"id_vente": f"T{k:03d}", "date_vente": date.strftime("%Y-%m-%d"), "secteur": sct, "superficie_m2": round(T, 1), "facade_m": round(F, 2), "desservi": desservi, "topographie": topo, "forme": forme, "logements_constructibles": logements, "lat": round(lat, 5), "lon": round(lon, 5), "prix_vente": prix}) df = pd.DataFrame(rows) df["prix_m2"] = df["prix_vente"] / df["superficie_m2"] return df def charger_terrains(regenerer: bool = False) -> pd.DataFrame: if regenerer or not CSV_TERRAINS.exists(): df = generer_terrains(); df.to_csv(CSV_TERRAINS, index=False) df = pd.read_csv(CSV_TERRAINS, parse_dates=["date_vente"]) return df # ----------------------------------------------------------------------------- 2. modèle hédonique des terrains SECTEURS = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"] def matrice_terrains(df: pd.DataFrame) -> pd.DataFrame: X = pd.DataFrame(index=df.index) X["const"] = 1.0 X["ln_T"] = np.log(df["superficie_m2"] / 560.0) X["ln_F"] = np.log(df["facade_m"] / 18.0) X["non_desservi"] = 1.0 - df["desservi"].astype(float) X["pente"] = (df["topographie"] == "pente").astype(float) X["roc"] = (df["topographie"] == "roc").astype(float) X["irregulier"] = (df["forme"] == "irrégulier").astype(float) X["logements_sup"] = df["logements_constructibles"].astype(float) - 1.0 for s in SECTEURS[1:]: X[f"sect_{s}"] = (df["secteur"] == s).astype(float) X["mois"] = ((pd.to_datetime(df["date_vente"]) - REF_DATE).dt.days / 30.4375).astype(float) return X def hedonique_terrains(df: pd.DataFrame) -> dict: X = matrice_terrains(df); y = np.log(df["prix_vente"].astype(float)) r = hd.mco(X.values, y.values) coef = pd.DataFrame({"coef": r["beta"], "se_hc1": r["se_hc1"]}, index=X.columns) coef["t"] = coef["coef"] / coef["se_hc1"]; coef["effet_pct"] = np.exp(coef["coef"]) - 1 return {"coef": coef, "beta": pd.Series(r["beta"], index=X.columns), "r2": r["r2"], "sigma": float(np.sqrt(r["sigma2"])), "n": r["n"], "residus": r["residus"], "X": X, "duan": float(np.mean(np.exp(r["residus"])))} def sujet_terrain() -> dict: """Le terrain du sujet du cas 1, réputé vacant : 620 m², Hull, desservi, plat, régulier, zonage unifamilial ; façade posée à 20 m (hypothèse).""" s = cas1.charger()["sujet"] return {"id_vente": "SUJET", "secteur": s["secteur"], "superficie_m2": float(s["superficie_terrain_m2"]), "facade_m": 20.0, "desservi": 1, "topographie": "plat", "forme": "régulier", "logements_constructibles": 1, "lat": s["lat"], "lon": s["lon"], "date_vente": DATE_EVAL} def predire_terrain(modele: dict, terrain: dict | pd.DataFrame, duan: bool = True) -> float: df = pd.DataFrame([terrain]) if isinstance(terrain, dict) else terrain X = matrice_terrains(df)[modele["X"].columns] return float(np.exp(X.values @ modele["beta"].values)[0] * (modele["duan"] if duan else 1.0)) # ----------------------------------------------------------------------------- 3. comparaison directe ECHELLES = {"superficie_m2": 100.0, "facade_m": 4.0, "dist_km": 1.5, "mois_anc": 6.0} POIDS = {"superficie_m2": 2.0, "facade_m": 0.5, "dist_km": 1.5, "mois_anc": 1.0} def selection_terrains(df: pd.DataFrame, suj: dict, k: int = 5, mois_max: float = 24, dist_max: float = 4.0) -> dict: v = df.copy() v["mois_anc"] = (DATE_EVAL - v["date_vente"]).dt.days / 30.4375 v["dist_km"] = np.sqrt(((v["lat"] - suj["lat"]) * 111) ** 2 + ((v["lon"] - suj["lon"]) * 78) ** 2) etapes = {"total": len(v)} f = v[v["mois_anc"].between(0, mois_max)]; etapes["fenetre"] = len(f) f = f[f["dist_km"] <= dist_max]; etapes["rayon"] = len(f) f = f[(f["desservi"] == 1) & (f["logements_constructibles"] == 1)]; etapes["desservi_unifamilial"] = len(f) f = f[f["superficie_m2"].between(0.6 * suj["superficie_m2"], 1.4 * suj["superficie_m2"])]; etapes["superficie"] = len(f) s0 = dict(suj); s0["mois_anc"] = 0.0; s0["dist_km"] = 0.0 f = f.assign(distance=[cp.distance_attributs(s0, r, ECHELLES, POIDS) for r in f.to_dict("records")]).sort_values("distance") return {"etapes": etapes, "candidats": f, "comparables": f.head(k), "ventes": v} GROUPES = {"conditions_marche": ["mois"], "localisation": [f"sect_{s}" for s in SECTEURS[1:]], "superficie": ["ln_T"], "facade": ["ln_F"], "services": ["non_desservi"], "topographie": ["pente", "roc"], "forme": ["irregulier"], "densite": ["logements_sup"]} def grille_terrains(modele: dict, comps: pd.DataFrame, suj: dict) -> pd.DataFrame: """Ajustements en cascade dérivés du modèle hédonique des terrains (même logique que traite.cas1.ajustements_hedoniques) : A_k = base × [exp(β_k (x_s − x_c)) − 1], conditions du marché d'abord, puis localisation, puis caractéristiques physiques.""" Xs = matrice_terrains(pd.DataFrame([suj])).iloc[0]; Xc = matrice_terrains(comps); beta = modele["beta"] rows = [] for i, c in comps.iterrows(): base = float(c["prix_vente"]); row = {"id_vente": c["id_vente"], "prix_vente": base} for g, cols in GROUPES.items(): dlog = float(sum(beta[col] * (Xs[col] - Xc.loc[i, col]) for col in cols)) adj = base * (np.exp(dlog) - 1.0); row[g] = adj; base += adj row["prix_ajuste"] = base row["ajust_brut_pct"] = sum(abs(row[g]) for g in GROUPES) / row["prix_vente"] row["ajust_net_pct"] = (base - row["prix_vente"]) / row["prix_vente"] rows.append(row) return pd.DataFrame(rows) def reconcilier(grille: pd.DataFrame) -> dict: objs = [cp.ComparableAjuste(r["id_vente"], r["prix_vente"], {g: r[g] for g in GROUPES}) for _, r in grille.iterrows()] w = cp.poids_inverse_brut(objs) return {**cp.reconciliation(objs, w), "objets": objs} def comparaison(df: pd.DataFrame | None = None) -> dict: df = charger_terrains() if df is None else df m = hedonique_terrains(df); suj = sujet_terrain(); sel = selection_terrains(df, suj) g = grille_terrains(m, sel["comparables"], suj); rec = reconcilier(g) pred = predire_terrain(m, suj) return {"terrains": df, "modele": m, "sujet": suj, "selection": sel, "grille": g, "reconciliation": rec, "prediction_hedonique": pred, "valeur_m2": rec["valeur_ponderee"] / suj["superficie_m2"]} # ----------------------------------------------------------------------------- 4. allocation et prix implicite marginal (cas 1) C_N_SUJET = 397_567.33 # coût neuf du sujet du cas 1 (ch. 21, analysis/python/cases/ch21_cout_neuf.py) ELAST_COUT_TOTAL = 0.90 # élasticité du coût total à la superficie (ch. 21 : ε = −0,10 sur le coût unitaire) QUALITE_COUT = 0.10 # ±10 % de coût neuf par classe de qualité (hypothèse pédagogique) VIE_ECONOMIQUE = 60.0 # variante « âge-vie » de l'extraction (convention déclarée ; le ch. 24 discute la vie économique) def cout_neuf_comparable(S: float, qualite: float) -> float: """Coût neuf d'un comparable transposé depuis le sujet : C_N(S, q) = C_N,sujet × (S/165)^0,90 × (1 + 0,10 (q − 3)).""" return C_N_SUJET * (S / 165.0) ** ELAST_COUT_TOTAL * (1 + QUALITE_COUT * (qualite - 3)) def facteur_age(modele_cas1: dict, age) -> np.ndarray: """Rapport prix à l'âge a / prix neuf selon le profil d'âge du modèle hédonique du cas 1 : exp(β_age a + β_age2 a²/100).""" b = modele_cas1["coef"]["coef"] a = np.asarray(age, float) return np.exp(b["age"] * a + b["age2_100"] * a ** 2 / 100.0) def extraction_cas1(cas: dict | None = None) -> dict: """Extraction sur les cinq comparables du chapitre 16 : V_L = P − C_N(1 − δ), sous deux conventions de dépréciation : (a) âge-vie linéaire (vie 60 ans) ; (b) dépréciation implicite du marché (profil d'âge hédonique) : V_L = P_neuf − C_N.""" cas = cas or cas1.tout() comps = cas["selection"]["comparables"].copy(); m = cas["modele"] comps["C_N"] = cout_neuf_comparable(comps["superficie_habitable_m2"], comps["qualite_1a5"]) comps["delta_agevie"] = comps["age"] / VIE_ECONOMIQUE comps["V_L_agevie"] = comps["prix_vente"] - comps["C_N"] * (1 - comps["delta_agevie"]) f = facteur_age(m, comps["age"]); comps["P_neuf"] = comps["prix_vente"] / f comps["delta_marche"] = (comps["P_neuf"] - comps["prix_vente"]) / comps["C_N"] comps["V_L_marche"] = comps["P_neuf"] - comps["C_N"] for k in ("agevie", "marche"): comps[f"V_L_{k}_m2"] = comps[f"V_L_{k}"] / comps["superficie_terrain_m2"] # transposition au terrain du sujet (620 m²) avec l'élasticité de superficie du marché des terrains comps[f"V_L_{k}_sujet"] = comps[f"V_L_{k}"] * (620.0 / comps["superficie_terrain_m2"]) ** ELAST_T # Hull, toutes ventes : part du terrain par extraction (dépréciation de marché) v = cas["ventes"]; h = v[v["secteur"] == "Hull"].copy() h["C_N"] = cout_neuf_comparable(h["superficie_habitable_m2"], h["qualite_1a5"]) h["V_L"] = h["prix_vente"] / facteur_age(m, h["age"]) - h["C_N"]; h["part"] = h["V_L"] / h["prix_vente"]; h["V_L_m2"] = h["V_L"] / h["superficie_terrain_m2"] return {"comparables": comps, "hull": h, "part_hull_mediane": float(h["part"].median()), "part_hull_q1": float(h["part"].quantile(0.25)), "part_hull_q3": float(h["part"].quantile(0.75)), "V_L_hull_mediane": float(h["V_L"].median()), "indication_agevie": float(comps["V_L_agevie_sujet"].median()), "indication_marche": float(comps["V_L_marche_sujet"].median())} def allocation_cas1(cas: dict | None = None, extr: dict | None = None) -> dict: cas = cas or cas1.tout(); extr = extr or extraction_cas1(cas) a = extr["part_hull_mediane"] P_grille = float(cas["ajustements"]["prix_ajuste"].mean()) # moyenne des prix ajustés des 5 comparables (ch. 17) P_hed = float(cas["prediction_hedonique"]) # prédiction hédonique du sujet (ch. 18) b = cas["modele"]["coef"]["coef"]; T = 620.0 return {"ratio": a, "sur_grille": a * P_grille, "sur_hedonique": a * P_hed, "P_grille": P_grille, "P_hed": P_hed, "beta_ln_T": float(b["ln_T"]), "prix_marginal_m2": float(b["ln_T"] * P_hed / T), "contribution_marginale": float(b["ln_T"] * P_hed), "sujet_age_factor": float(facteur_age(cas["modele"], 28)), "delta_marche_sujet": float((P_hed / facteur_age(cas["modele"], 28) - P_hed) / C_N_SUJET)} # ----------------------------------------------------------------------------- 5. technique résiduelle du terrain (cas 3) RES = {"cout_unitaire_neuf": 2500.0, "superficie_brute_m2": 950.0, "taux_depreciation": 0.52, "R_L": 0.0375, "n_restante": 40, "terrain_m2": 740.0} def residuelle_cas3(params: dict | None = None, t3: dict | None = None) -> dict: p = {**RES, **(params or {})}; t3 = t3 or cas3.tout() noi = float(t3["base"]["rne"]); R = float(t3["taux"]["mediane"]); V_cap = float(t3["valeur_capitalisation"]) C_N = p["cout_unitaire_neuf"] * p["superficie_brute_m2"]; C_B = C_N * (1 - p["taux_depreciation"]) R_B = p["R_L"] + 1.0 / p["n_restante"] V_L = (noi - R_B * C_B) / p["R_L"] R_L_implicite = (noi - R_B * C_B) / (V_cap - C_B) if V_cap > C_B else float("nan") return {"noi": noi, "R": R, "V_cap": V_cap, "C_N": C_N, "C_B": C_B, "R_L": p["R_L"], "R_B": R_B, "revenu_batiment": R_B * C_B, "revenu_terrain": noi - R_B * C_B, "V_L": V_L, "V_L_m2": V_L / p["terrain_m2"], "somme": V_L + C_B, "ecart_vs_cap": (V_L + C_B) / V_cap - 1, "R_L_implicite": R_L_implicite, "part_terrain": V_L / (V_L + C_B), "params": p} def sensibilite_residuelle(t3: dict | None = None, R_Ls=(0.0325, 0.035, 0.0375, 0.04, 0.0425), ns=(30, 40, 50), deltas=(0.44, 0.52, 0.60)) -> pd.DataFrame: t3 = t3 or cas3.tout(); rows = [] for d in deltas: for n in ns: for rl in R_Ls: r = residuelle_cas3({"taux_depreciation": d, "n_restante": n, "R_L": rl}, t3) rows.append({"taux_depreciation": d, "C_B": r["C_B"], "n_restante": n, "R_L": rl, "R_B": r["R_B"], "V_L": r["V_L"], "V_L_m2": r["V_L_m2"]}) return pd.DataFrame(rows) # ----------------------------------------------------------------------------- 6. lotissement (développement actualisé) LOT = {"site_m2": 10_000.0, "n_lots": 12, "lot_m2": 625.0, "rue_m2": 1500.0, "parc_m2": 1000.0, "facade_m": 20.0, "infrastructure_par_lot": 65_000.0, "honoraires_permis": 90_000.0, "taxes_annuelles": 12_000.0, "frais_vente_pct": 0.04, "profit_pct": 0.15, "lots_par_an": 4, "taux": 0.10, "croissance_prix": 0.0} def lotissement(modele_terrains: dict, params: dict | None = None) -> dict: """Valeur d'un site brut par la méthode du lotissement actualisé (subdivision development method). Produit des ventes de lots finis au prix prédit par le modèle hédonique des terrains à la date d'évaluation, moins les coûts de viabilisation, d'honoraires, de portage, de vente et le profit du promoteur ; flux de fin d'année actualisés au taux déclaré.""" p = {**LOT, **(params or {})} assert abs(p["n_lots"] * p["lot_m2"] + p["rue_m2"] + p["parc_m2"] - p["site_m2"]) < 1e-6 lot = {"secteur": "Hull", "superficie_m2": p["lot_m2"], "facade_m": p["facade_m"], "desservi": 1, "topographie": "plat", "forme": "régulier", "logements_constructibles": 1, "date_vente": DATE_EVAL} prix_lot = predire_terrain(modele_terrains, lot) n_ans = int(np.ceil(p["n_lots"] / p["lots_par_an"])) flux, detail = [], [] # t = 0 : honoraires, permis, études f0 = -p["honoraires_permis"]; flux.append(f0); detail.append({"annee": 0, "lots_vendus": 0, "revenus": 0.0, "infrastructure": 0.0, "honoraires": p["honoraires_permis"], "taxes": 0.0, "frais_vente": 0.0, "profit": 0.0, "flux": f0}) restants = p["n_lots"] for t in range(1, n_ans + 1): vendus = min(p["lots_par_an"], restants); restants -= vendus rev = vendus * prix_lot * (1 + p["croissance_prix"]) ** (t - 1) infra = p["infrastructure_par_lot"] * p["n_lots"] if t == 1 else 0.0 taxes = p["taxes_annuelles"] * (restants + vendus) / p["n_lots"] fv = p["frais_vente_pct"] * rev; prof = p["profit_pct"] * rev f = rev - infra - taxes - fv - prof flux.append(f); detail.append({"annee": t, "lots_vendus": vendus, "revenus": rev, "infrastructure": infra, "honoraires": 0.0, "taxes": taxes, "frais_vente": fv, "profit": prof, "flux": f}) V = fin.van(flux, p["taux"]) statique = sum(d["flux"] for d in detail) return {"prix_lot": prix_lot, "produit_brut": prix_lot * p["n_lots"], "flux": flux, "detail": pd.DataFrame(detail), "valeur_actualisee": V, "valeur_statique": statique, "valeur_m2": V / p["site_m2"], "valeur_statique_m2": statique / p["site_m2"], "n_ans": n_ans, "ratio_terrain_brut_sur_produit": V / (prix_lot * p["n_lots"]), "params": p} def sensibilite_lotissement(modele_terrains: dict) -> dict: base = lotissement(modele_terrains)["valeur_actualisee"] out = {"base": base} out["absorption"] = {k: lotissement(modele_terrains, {"lots_par_an": k})["valeur_actualisee"] for k in (2, 3, 4, 6, 12)} out["taux"] = {r: lotissement(modele_terrains, {"taux": r})["valeur_actualisee"] for r in (0.08, 0.10, 0.12, 0.15)} out["prix"] = {} for d in (-0.10, 0.0, 0.10): m2 = dict(modele_terrains); m2["duan"] = modele_terrains["duan"] * (1 + d) out["prix"][d] = lotissement(m2)["valeur_actualisee"] out["infrastructure"] = {c: lotissement(modele_terrains, {"infrastructure_par_lot": c})["valeur_actualisee"] for c in (50_000, 65_000, 80_000)} out["profit"] = {c: lotissement(modele_terrains, {"profit_pct": c})["valeur_actualisee"] for c in (0.10, 0.15, 0.20)} return out # ----------------------------------------------------------------------------- 7. capitalisation de la rente foncière def rente_fonciere(rente: float = 11_000.0, R_L: float = 0.05, g: float = 0.02, n: int = 40, valeur_reversion: float = 300_000.0) -> dict: """Terrain loué (emphytéose ou bail foncier) : V_L = rente/R_L (rente constante perpétuelle) ; V_L = rente_1/(R_L − g) (Gordon) ; bail de n ans à rente croissante + réversion actualisée.""" perp = rente / R_L gordon = rente / fin.taux_gordon(R_L, g) va_rentes = sum(rente * (1 + g) ** (t - 1) / (1 + R_L) ** t for t in range(1, n + 1)) va_rev = valeur_reversion / (1 + R_L) ** n return {"perpetuelle": perp, "gordon": gordon, "bail_n": va_rentes + va_rev, "va_rentes": va_rentes, "va_reversion": va_rev, "n": n} # ----------------------------------------------------------------------------- 8. synthèse et valeur retenue def synthese() -> dict: df = charger_terrains(); comp = comparaison(df); cas = cas1.tout(); extr = extraction_cas1(cas); allo = allocation_cas1(cas, extr) t3 = cas3.tout(); res = residuelle_cas3(t3=t3); lot = lotissement(comp["modele"]); rf = rente_fonciere() retenue = round(comp["reconciliation"]["valeur_ponderee"] / 5000) * 5000 return {"comparaison": comp, "cas1": cas, "extraction": extr, "allocation": allo, "cas3": t3, "residuelle": res, "lotissement": lot, "rente": rf, "valeur_retenue": retenue, "part_retenue_grille": retenue / allo["P_grille"], "part_retenue_hed": retenue / allo["P_hed"]} def _d(x): return f"{x:,.0f} $".replace(",", " ") def main() -> dict: S = synthese(); c = S["comparaison"]; m = c["modele"]; df = c["terrains"] print("=== 1. Jeu synthétique de terrains (ch25_terrains_ventes.csv, graine 20261031) ===") print(f"n = {len(df)} ; desservis = {int(df.desservi.sum())} ; période {df.date_vente.min().date()} → {df.date_vente.max().date()}") print(df.groupby("secteur").agg(n=("prix_vente", "size"), T_med=("superficie_m2", "median"), prix_med=("prix_vente", "median"), prix_m2_med=("prix_m2", "median")).round(0)) print(df.groupby("desservi")[["superficie_m2", "prix_vente", "prix_m2"]].median().round(0)) print("\n=== 2. Modèle hédonique des terrains ===") print(m["coef"].round(4)); print(f"R² = {m['r2']:.3f} ; σ = {m['sigma']:.3f} ; Duan = {m['duan']:.4f}") print("\n=== 3. Comparaison directe — sujet 620 m², Hull ===") print("Entonnoir :", c["selection"]["etapes"]) comps = c["selection"]["comparables"] print(comps[["id_vente", "date_vente", "secteur", "superficie_m2", "facade_m", "topographie", "forme", "dist_km", "mois_anc", "prix_vente", "prix_m2", "distance"]].round(2).to_string()) print(c["grille"].round(0).to_string()) r = c["reconciliation"] print(f"Poids (inverse du brut) : {np.round(r['poids'], 3)} ; valeur pondérée {_d(r['valeur_ponderee'])} ; moyenne {_d(r['moyenne'])} ; médiane {_d(r['mediane'])} ; " f"min {_d(r['min'])} ; max {_d(r['max'])} ; étendue {r['etendue_pct']*100:.1f} % ; {_d(c['valeur_m2'])}/m²") print(f"Prédiction hédonique directe du terrain du sujet : {_d(c['prediction_hedonique'])}") print("\n=== 4. Allocation et prix implicite marginal (cas 1) ===") a = S["allocation"]; e = S["extraction"] print(f"Part du terrain, ventes de Hull (extraction, dépréciation de marché) : médiane {a['ratio']*100:.1f} % (Q1 {e['part_hull_q1']*100:.1f} %, Q3 {e['part_hull_q3']*100:.1f} %) ; V_L médian Hull {_d(e['V_L_hull_mediane'])}") print(f"Allocation sur la grille du ch. 17 ({_d(a['P_grille'])}) : {_d(a['sur_grille'])} ; sur la prédiction hédonique ({_d(a['P_hed'])}) : {_d(a['sur_hedonique'])}") print(f"β(ln T) = {a['beta_ln_T']:.4f} → prix implicite marginal {a['prix_marginal_m2']:.1f} $/m² ; contribution marginale β × P = {_d(a['contribution_marginale'])}") print(f"Facteur d'âge du sujet (28 ans) : {a['sujet_age_factor']:.4f} → dépréciation implicite {a['delta_marche_sujet']*100:.1f} % de C_N") print("\n=== 5. Extraction sur les 5 comparables du ch. 16 ===") cols = ["id_vente", "prix_vente", "superficie_habitable_m2", "superficie_terrain_m2", "age", "qualite_1a5", "C_N", "delta_agevie", "V_L_agevie", "V_L_agevie_m2", "P_neuf", "delta_marche", "V_L_marche", "V_L_marche_m2", "V_L_agevie_sujet", "V_L_marche_sujet"] print(e["comparables"][cols].round(3).to_string()) print(f"Indication (médiane transposée à 620 m²) : âge-vie 60 ans {_d(e['indication_agevie'])} ; dépréciation de marché {_d(e['indication_marche'])}") print("\n=== 6. Technique résiduelle du terrain (cas 3) ===") rs = S["residuelle"] print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in rs.items() if k != "params"}) sens = sensibilite_residuelle(S["cas3"]); print(sens.pivot_table(index=["taux_depreciation", "n_restante"], columns="R_L", values="V_L").round(0)) print("\n=== 7. Lotissement actualisé ===") L = S["lotissement"]; print(f"Prix d'un lot fini (625 m², Hull, {DATE_EVAL.date()}) : {_d(L['prix_lot'])} ; produit brut {_d(L['produit_brut'])}") print(L["detail"].round(0).to_string()) print(f"Valeur actualisée du site brut : {_d(L['valeur_actualisee'])} ({L['valeur_m2']:.1f} $/m²) ; statique {_d(L['valeur_statique'])} ({L['valeur_statique_m2']:.1f} $/m²) ; " f"ratio terrain brut / produit {L['ratio_terrain_brut_sur_produit']*100:.1f} %") sl = sensibilite_lotissement(m) for k, v in sl.items(): if k != "base": print(k, {kk: round(vv) for kk, vv in v.items()}) print("\n=== 8. Rente foncière ===") print({k: round(v, 0) if isinstance(v, float) else v for k, v in S["rente"].items()}) print("\n=== 9. Synthèse — sujet du cas 1 ===") print(f"Comparaison (terrains) {_d(r['valeur_ponderee'])} ; extraction (marché) {_d(e['indication_marche'])} ; extraction (âge-vie 60) {_d(e['indication_agevie'])} ; " f"allocation (grille) {_d(a['sur_grille'])} ; allocation (hédonique) {_d(a['sur_hedonique'])}") print(f"VALEUR DU TERRAIN RETENUE : {_d(S['valeur_retenue'])} ({S['valeur_retenue']/620:.0f} $/m²) ; part de la grille {S['part_retenue_grille']*100:.1f} % ; part de la prédiction hédonique {S['part_retenue_hed']*100:.1f} %") return S if __name__ == "__main__": main()