SPB Git
27.0 KB · 365 lines python
1#!/usr/bin/env python32"""Chapitre 25 — Évaluation du terrain (DONNÉES SYNTHÉTIQUES, graine 20261031).34Imprime tous les chiffres cités dans le chapitre 25 :5  * jeu synthétique de ventes de terrains vacants « Gatineau » (data/synthetic/ch25_terrains_ventes.csv) : lois déclarées ;6  * modèle hédonique des terrains (traite.hedonique.mco) : élasticité de superficie, façade, services, topographie, forme, densité ;7  * comparaison directe : sélection de cinq terrains comparables pour le sujet du cas 1 et grille d'ajustements ;8  * allocation : part du terrain dans les ventes du cas 1 (secteur Hull) ; prix implicite marginal du modèle hédonique (traite.cas1) ;9  * extraction : prix − coût déprécié du bâtiment sur les cinq comparables du chapitre 16 (coût neuf du chapitre 21) ;10  * technique résiduelle du terrain sur le cas 3 (RNE de traite.cas3 ; taux déclarés) et sensibilité ;11  * lotissement (développement actualisé) d'un site de 10 000 m² en 12 terrains (traite.finance.van) ;12  * capitalisation d'une rente foncière (emphytéose) ;13  * synthèse des indications et valeur de terrain retenue pour le sujet du cas 1 (reprise par les ch. 20, 24, 45).1415Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch25_terrain.py16Toutes les fonctions financières proviennent de analysis/python/traite (aucune réécriture)."""17from __future__ import annotations18import sys, json, pathlib19import numpy as np, pandas as pd2021ROOT = pathlib.Path(__file__).resolve().parents[3]22sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))23from traite import hedonique as hd, comparables as cp, finance as fin, cout as ct, cas1, cas32425D = ROOT / "data/synthetic"26CSV_TERRAINS = D / "ch25_terrains_ventes.csv"27GRAINE = 2026103128REF_DATE = pd.Timestamp("2024-07-01")29DATE_EVAL = pd.Timestamp("2026-09-01")3031# ----------------------------------------------------------------------------- 1. jeu synthétique de ventes de terrains32# Lois déclarées (hypothèses pédagogiques ; voir data/synthetic/README.md, section « Chapitre 25 »)33P_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-0134ELAST_T = 0.50           # élasticité du prix du terrain à la superficie (< 1 : valeur unitaire décroissante)35ELAST_F = 0.08           # élasticité à la façade (à superficie donnée)36B_NON_DESSERVI = -0.30   # ni aqueduc ni égout (puits + installation septique à la charge de l'acheteur)37B_PENTE = -0.06          # pente marquée38B_ROC = -0.12            # roc affleurant ou remblai important39B_IRREG = -0.05          # forme irrégulière (triangle, drapeau, trapèze prononcé)40B_LOGEMENT = 0.12        # par logement constructible supplémentaire (zonage H2/H3)41TENDANCE_MOIS = 0.0036   # même tendance que le coefficient « mois » du modèle hédonique du cas 1 (≈ 4,4 %/an)42SIGMA = 0.1043MEDIANES_APCIQ = {"Hull": 475_000, "Gatineau": 464_900, "Aylmer": 530_000, "Buckingham": 395_000, "Masson-Angers": 395_000 * 0.98}44PRIME_SECTEUR = {k: float(np.log(v / MEDIANES_APCIQ["Hull"])) for k, v in MEDIANES_APCIQ.items()}45CENTROIDES = {"Hull": (45.4296, -75.7450), "Gatineau": (45.4769, -75.6650), "Aylmer": (45.3936, -75.8450),46              "Buckingham": (45.5899, -75.4200), "Masson-Angers": (45.5468, -75.4150)}47N_PAR_SECTEUR = {"Hull": 70, "Gatineau": 60, "Aylmer": 40, "Buckingham": 15, "Masson-Angers": 15}4849def generer_terrains(graine: int = GRAINE) -> pd.DataFrame:50    """Ventes synthétiques de terrains vacants résidentiels (2023-01 → 2026-08). Aucun terrain réel."""51    rng = np.random.default_rng(graine)52    rows = []53    k = 054    for sct, n in N_PAR_SECTEUR.items():55        periurbain = sct in ("Buckingham", "Masson-Angers")56        for _ in range(n):57            k += 158            desservi = int(rng.random() < (0.30 if periurbain else 0.85))59            if desservi:60                T = float(np.clip(np.exp(rng.normal(np.log(600), 0.28)), 320, 2500))61            else:62                T = float(np.clip(np.exp(rng.normal(np.log(2200), 0.45)), 900, 9000))63            F = float(np.clip(0.60 * np.sqrt(T) * np.exp(rng.normal(0, 0.14)), 10, 90))64            topo = rng.choice(["plat", "pente", "roc"], p=[0.70, 0.20, 0.10])65            forme = rng.choice(["régulier", "irrégulier"], p=[0.80, 0.20])66            logements = int(rng.choice([1, 2, 3], p=[0.82, 0.12, 0.06])) if desservi and not periurbain else 167            jours = int(rng.integers(0, (pd.Timestamp("2026-08-31") - pd.Timestamp("2023-01-01")).days + 1))68            date = pd.Timestamp("2023-01-01") + pd.Timedelta(days=jours)69            mois = (date - REF_DATE).days / 30.437570            lat0, lon0 = CENTROIDES[sct]71            lat = lat0 + rng.normal(0, 0.012); lon = lon0 + rng.normal(0, 0.017)72            lnp = (np.log(P_REF) + ELAST_T * np.log(T / 560) + ELAST_F * np.log(F / 18) + B_NON_DESSERVI * (1 - desservi)73                   + B_PENTE * (topo == "pente") + B_ROC * (topo == "roc") + B_IRREG * (forme == "irrégulier")74                   + B_LOGEMENT * (logements - 1) + PRIME_SECTEUR[sct] + TENDANCE_MOIS * mois + rng.normal(0, SIGMA))75            prix = float(np.round(np.exp(lnp) / 500) * 500)76            rows.append({"id_vente": f"T{k:03d}", "date_vente": date.strftime("%Y-%m-%d"), "secteur": sct, "superficie_m2": round(T, 1),77                         "facade_m": round(F, 2), "desservi": desservi, "topographie": topo, "forme": forme, "logements_constructibles": logements,78                         "lat": round(lat, 5), "lon": round(lon, 5), "prix_vente": prix})79    df = pd.DataFrame(rows)80    df["prix_m2"] = df["prix_vente"] / df["superficie_m2"]81    return df8283def charger_terrains(regenerer: bool = False) -> pd.DataFrame:84    if regenerer or not CSV_TERRAINS.exists():85        df = generer_terrains(); df.to_csv(CSV_TERRAINS, index=False)86    df = pd.read_csv(CSV_TERRAINS, parse_dates=["date_vente"])87    return df8889# ----------------------------------------------------------------------------- 2. modèle hédonique des terrains90SECTEURS = ["Hull", "Gatineau", "Aylmer", "Buckingham", "Masson-Angers"]9192def matrice_terrains(df: pd.DataFrame) -> pd.DataFrame:93    X = pd.DataFrame(index=df.index)94    X["const"] = 1.095    X["ln_T"] = np.log(df["superficie_m2"] / 560.0)96    X["ln_F"] = np.log(df["facade_m"] / 18.0)97    X["non_desservi"] = 1.0 - df["desservi"].astype(float)98    X["pente"] = (df["topographie"] == "pente").astype(float)99    X["roc"] = (df["topographie"] == "roc").astype(float)100    X["irregulier"] = (df["forme"] == "irrégulier").astype(float)101    X["logements_sup"] = df["logements_constructibles"].astype(float) - 1.0102    for s in SECTEURS[1:]:103        X[f"sect_{s}"] = (df["secteur"] == s).astype(float)104    X["mois"] = ((pd.to_datetime(df["date_vente"]) - REF_DATE).dt.days / 30.4375).astype(float)105    return X106107def hedonique_terrains(df: pd.DataFrame) -> dict:108    X = matrice_terrains(df); y = np.log(df["prix_vente"].astype(float))109    r = hd.mco(X.values, y.values)110    coef = pd.DataFrame({"coef": r["beta"], "se_hc1": r["se_hc1"]}, index=X.columns)111    coef["t"] = coef["coef"] / coef["se_hc1"]; coef["effet_pct"] = np.exp(coef["coef"]) - 1112    return {"coef": coef, "beta": pd.Series(r["beta"], index=X.columns), "r2": r["r2"], "sigma": float(np.sqrt(r["sigma2"])), "n": r["n"],113            "residus": r["residus"], "X": X, "duan": float(np.mean(np.exp(r["residus"])))}114115def sujet_terrain() -> dict:116    """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)."""117    s = cas1.charger()["sujet"]118    return {"id_vente": "SUJET", "secteur": s["secteur"], "superficie_m2": float(s["superficie_terrain_m2"]), "facade_m": 20.0, "desservi": 1,119            "topographie": "plat", "forme": "régulier", "logements_constructibles": 1, "lat": s["lat"], "lon": s["lon"], "date_vente": DATE_EVAL}120121def predire_terrain(modele: dict, terrain: dict | pd.DataFrame, duan: bool = True) -> float:122    df = pd.DataFrame([terrain]) if isinstance(terrain, dict) else terrain123    X = matrice_terrains(df)[modele["X"].columns]124    return float(np.exp(X.values @ modele["beta"].values)[0] * (modele["duan"] if duan else 1.0))125126# ----------------------------------------------------------------------------- 3. comparaison directe127ECHELLES = {"superficie_m2": 100.0, "facade_m": 4.0, "dist_km": 1.5, "mois_anc": 6.0}128POIDS = {"superficie_m2": 2.0, "facade_m": 0.5, "dist_km": 1.5, "mois_anc": 1.0}129130def selection_terrains(df: pd.DataFrame, suj: dict, k: int = 5, mois_max: float = 24, dist_max: float = 4.0) -> dict:131    v = df.copy()132    v["mois_anc"] = (DATE_EVAL - v["date_vente"]).dt.days / 30.4375133    v["dist_km"] = np.sqrt(((v["lat"] - suj["lat"]) * 111) ** 2 + ((v["lon"] - suj["lon"]) * 78) ** 2)134    etapes = {"total": len(v)}135    f = v[v["mois_anc"].between(0, mois_max)]; etapes["fenetre"] = len(f)136    f = f[f["dist_km"] <= dist_max]; etapes["rayon"] = len(f)137    f = f[(f["desservi"] == 1) & (f["logements_constructibles"] == 1)]; etapes["desservi_unifamilial"] = len(f)138    f = f[f["superficie_m2"].between(0.6 * suj["superficie_m2"], 1.4 * suj["superficie_m2"])]; etapes["superficie"] = len(f)139    s0 = dict(suj); s0["mois_anc"] = 0.0; s0["dist_km"] = 0.0140    f = f.assign(distance=[cp.distance_attributs(s0, r, ECHELLES, POIDS) for r in f.to_dict("records")]).sort_values("distance")141    return {"etapes": etapes, "candidats": f, "comparables": f.head(k), "ventes": v}142143GROUPES = {"conditions_marche": ["mois"], "localisation": [f"sect_{s}" for s in SECTEURS[1:]], "superficie": ["ln_T"], "facade": ["ln_F"],144           "services": ["non_desservi"], "topographie": ["pente", "roc"], "forme": ["irregulier"], "densite": ["logements_sup"]}145146def grille_terrains(modele: dict, comps: pd.DataFrame, suj: dict) -> pd.DataFrame:147    """Ajustements en cascade dérivés du modèle hédonique des terrains (même logique que traite.cas1.ajustements_hedoniques) :148    A_k = base × [exp(β_k (x_s − x_c)) − 1], conditions du marché d'abord, puis localisation, puis caractéristiques physiques."""149    Xs = matrice_terrains(pd.DataFrame([suj])).iloc[0]; Xc = matrice_terrains(comps); beta = modele["beta"]150    rows = []151    for i, c in comps.iterrows():152        base = float(c["prix_vente"]); row = {"id_vente": c["id_vente"], "prix_vente": base}153        for g, cols in GROUPES.items():154            dlog = float(sum(beta[col] * (Xs[col] - Xc.loc[i, col]) for col in cols))155            adj = base * (np.exp(dlog) - 1.0); row[g] = adj; base += adj156        row["prix_ajuste"] = base157        row["ajust_brut_pct"] = sum(abs(row[g]) for g in GROUPES) / row["prix_vente"]158        row["ajust_net_pct"] = (base - row["prix_vente"]) / row["prix_vente"]159        rows.append(row)160    return pd.DataFrame(rows)161162def reconcilier(grille: pd.DataFrame) -> dict:163    objs = [cp.ComparableAjuste(r["id_vente"], r["prix_vente"], {g: r[g] for g in GROUPES}) for _, r in grille.iterrows()]164    w = cp.poids_inverse_brut(objs)165    return {**cp.reconciliation(objs, w), "objets": objs}166167def comparaison(df: pd.DataFrame | None = None) -> dict:168    df = charger_terrains() if df is None else df169    m = hedonique_terrains(df); suj = sujet_terrain(); sel = selection_terrains(df, suj)170    g = grille_terrains(m, sel["comparables"], suj); rec = reconcilier(g)171    pred = predire_terrain(m, suj)172    return {"terrains": df, "modele": m, "sujet": suj, "selection": sel, "grille": g, "reconciliation": rec, "prediction_hedonique": pred,173            "valeur_m2": rec["valeur_ponderee"] / suj["superficie_m2"]}174175# ----------------------------------------------------------------------------- 4. allocation et prix implicite marginal (cas 1)176C_N_SUJET = 397_567.33   # coût neuf du sujet du cas 1 (ch. 21, analysis/python/cases/ch21_cout_neuf.py)177ELAST_COUT_TOTAL = 0.90  # élasticité du coût total à la superficie (ch. 21 : ε = −0,10 sur le coût unitaire)178QUALITE_COUT = 0.10      # ±10 % de coût neuf par classe de qualité (hypothèse pédagogique)179VIE_ECONOMIQUE = 60.0    # variante « âge-vie » de l'extraction (convention déclarée ; le ch. 24 discute la vie économique)180181def cout_neuf_comparable(S: float, qualite: float) -> float:182    """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))."""183    return C_N_SUJET * (S / 165.0) ** ELAST_COUT_TOTAL * (1 + QUALITE_COUT * (qualite - 3))184185def facteur_age(modele_cas1: dict, age) -> np.ndarray:186    """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)."""187    b = modele_cas1["coef"]["coef"]188    a = np.asarray(age, float)189    return np.exp(b["age"] * a + b["age2_100"] * a ** 2 / 100.0)190191def extraction_cas1(cas: dict | None = None) -> dict:192    """Extraction sur les cinq comparables du chapitre 16 : V_L = P − C_N(1 − δ), sous deux conventions de dépréciation :193    (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."""194    cas = cas or cas1.tout()195    comps = cas["selection"]["comparables"].copy(); m = cas["modele"]196    comps["C_N"] = cout_neuf_comparable(comps["superficie_habitable_m2"], comps["qualite_1a5"])197    comps["delta_agevie"] = comps["age"] / VIE_ECONOMIQUE198    comps["V_L_agevie"] = comps["prix_vente"] - comps["C_N"] * (1 - comps["delta_agevie"])199    f = facteur_age(m, comps["age"]); comps["P_neuf"] = comps["prix_vente"] / f200    comps["delta_marche"] = (comps["P_neuf"] - comps["prix_vente"]) / comps["C_N"]201    comps["V_L_marche"] = comps["P_neuf"] - comps["C_N"]202    for k in ("agevie", "marche"):203        comps[f"V_L_{k}_m2"] = comps[f"V_L_{k}"] / comps["superficie_terrain_m2"]204        # transposition au terrain du sujet (620 m²) avec l'élasticité de superficie du marché des terrains205        comps[f"V_L_{k}_sujet"] = comps[f"V_L_{k}"] * (620.0 / comps["superficie_terrain_m2"]) ** ELAST_T206    # Hull, toutes ventes : part du terrain par extraction (dépréciation de marché)207    v = cas["ventes"]; h = v[v["secteur"] == "Hull"].copy()208    h["C_N"] = cout_neuf_comparable(h["superficie_habitable_m2"], h["qualite_1a5"])209    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"]210    return {"comparables": comps, "hull": h, "part_hull_mediane": float(h["part"].median()), "part_hull_q1": float(h["part"].quantile(0.25)),211            "part_hull_q3": float(h["part"].quantile(0.75)), "V_L_hull_mediane": float(h["V_L"].median()),212            "indication_agevie": float(comps["V_L_agevie_sujet"].median()), "indication_marche": float(comps["V_L_marche_sujet"].median())}213214def allocation_cas1(cas: dict | None = None, extr: dict | None = None) -> dict:215    cas = cas or cas1.tout(); extr = extr or extraction_cas1(cas)216    a = extr["part_hull_mediane"]217    P_grille = float(cas["ajustements"]["prix_ajuste"].mean())      # moyenne des prix ajustés des 5 comparables (ch. 17)218    P_hed = float(cas["prediction_hedonique"])                        # prédiction hédonique du sujet (ch. 18)219    b = cas["modele"]["coef"]["coef"]; T = 620.0220    return {"ratio": a, "sur_grille": a * P_grille, "sur_hedonique": a * P_hed, "P_grille": P_grille, "P_hed": P_hed,221            "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),222            "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)}223224# ----------------------------------------------------------------------------- 5. technique résiduelle du terrain (cas 3)225RES = {"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}226227def residuelle_cas3(params: dict | None = None, t3: dict | None = None) -> dict:228    p = {**RES, **(params or {})}; t3 = t3 or cas3.tout()229    noi = float(t3["base"]["rne"]); R = float(t3["taux"]["mediane"]); V_cap = float(t3["valeur_capitalisation"])230    C_N = p["cout_unitaire_neuf"] * p["superficie_brute_m2"]; C_B = C_N * (1 - p["taux_depreciation"])231    R_B = p["R_L"] + 1.0 / p["n_restante"]232    V_L = (noi - R_B * C_B) / p["R_L"]233    R_L_implicite = (noi - R_B * C_B) / (V_cap - C_B) if V_cap > C_B else float("nan")234    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,235            "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,236            "R_L_implicite": R_L_implicite, "part_terrain": V_L / (V_L + C_B), "params": p}237238def 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:239    t3 = t3 or cas3.tout(); rows = []240    for d in deltas:241        for n in ns:242            for rl in R_Ls:243                r = residuelle_cas3({"taux_depreciation": d, "n_restante": n, "R_L": rl}, t3)244                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"]})245    return pd.DataFrame(rows)246247# ----------------------------------------------------------------------------- 6. lotissement (développement actualisé)248LOT = {"site_m2": 10_000.0, "n_lots": 12, "lot_m2": 625.0, "rue_m2": 1500.0, "parc_m2": 1000.0, "facade_m": 20.0,249       "infrastructure_par_lot": 65_000.0, "honoraires_permis": 90_000.0, "taxes_annuelles": 12_000.0, "frais_vente_pct": 0.04,250       "profit_pct": 0.15, "lots_par_an": 4, "taux": 0.10, "croissance_prix": 0.0}251252def lotissement(modele_terrains: dict, params: dict | None = None) -> dict:253    """Valeur d'un site brut par la méthode du lotissement actualisé (subdivision development method).254    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ûts255    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é."""256    p = {**LOT, **(params or {})}257    assert abs(p["n_lots"] * p["lot_m2"] + p["rue_m2"] + p["parc_m2"] - p["site_m2"]) < 1e-6258    lot = {"secteur": "Hull", "superficie_m2": p["lot_m2"], "facade_m": p["facade_m"], "desservi": 1, "topographie": "plat", "forme": "régulier",259           "logements_constructibles": 1, "date_vente": DATE_EVAL}260    prix_lot = predire_terrain(modele_terrains, lot)261    n_ans = int(np.ceil(p["n_lots"] / p["lots_par_an"]))262    flux, detail = [], []263    # t = 0 : honoraires, permis, études264    f0 = -p["honoraires_permis"]; flux.append(f0); detail.append({"annee": 0, "lots_vendus": 0, "revenus": 0.0, "infrastructure": 0.0, "honoraires": p["honoraires_permis"],265                                                                 "taxes": 0.0, "frais_vente": 0.0, "profit": 0.0, "flux": f0})266    restants = p["n_lots"]267    for t in range(1, n_ans + 1):268        vendus = min(p["lots_par_an"], restants); restants -= vendus269        rev = vendus * prix_lot * (1 + p["croissance_prix"]) ** (t - 1)270        infra = p["infrastructure_par_lot"] * p["n_lots"] if t == 1 else 0.0271        taxes = p["taxes_annuelles"] * (restants + vendus) / p["n_lots"]272        fv = p["frais_vente_pct"] * rev; prof = p["profit_pct"] * rev273        f = rev - infra - taxes - fv - prof274        flux.append(f); detail.append({"annee": t, "lots_vendus": vendus, "revenus": rev, "infrastructure": infra, "honoraires": 0.0, "taxes": taxes,275                                       "frais_vente": fv, "profit": prof, "flux": f})276    V = fin.van(flux, p["taux"])277    statique = sum(d["flux"] for d in detail)278    return {"prix_lot": prix_lot, "produit_brut": prix_lot * p["n_lots"], "flux": flux, "detail": pd.DataFrame(detail), "valeur_actualisee": V,279            "valeur_statique": statique, "valeur_m2": V / p["site_m2"], "valeur_statique_m2": statique / p["site_m2"], "n_ans": n_ans,280            "ratio_terrain_brut_sur_produit": V / (prix_lot * p["n_lots"]), "params": p}281282def sensibilite_lotissement(modele_terrains: dict) -> dict:283    base = lotissement(modele_terrains)["valeur_actualisee"]284    out = {"base": base}285    out["absorption"] = {k: lotissement(modele_terrains, {"lots_par_an": k})["valeur_actualisee"] for k in (2, 3, 4, 6, 12)}286    out["taux"] = {r: lotissement(modele_terrains, {"taux": r})["valeur_actualisee"] for r in (0.08, 0.10, 0.12, 0.15)}287    out["prix"] = {}288    for d in (-0.10, 0.0, 0.10):289        m2 = dict(modele_terrains); m2["duan"] = modele_terrains["duan"] * (1 + d)290        out["prix"][d] = lotissement(m2)["valeur_actualisee"]291    out["infrastructure"] = {c: lotissement(modele_terrains, {"infrastructure_par_lot": c})["valeur_actualisee"] for c in (50_000, 65_000, 80_000)}292    out["profit"] = {c: lotissement(modele_terrains, {"profit_pct": c})["valeur_actualisee"] for c in (0.10, 0.15, 0.20)}293    return out294295# ----------------------------------------------------------------------------- 7. capitalisation de la rente foncière296def 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:297    """Terrain loué (emphytéose ou bail foncier) : V_L = rente/R_L (rente constante perpétuelle) ; V_L = rente_1/(R_L − g) (Gordon) ;298    bail de n ans à rente croissante + réversion actualisée."""299    perp = rente / R_L300    gordon = rente / fin.taux_gordon(R_L, g)301    va_rentes = sum(rente * (1 + g) ** (t - 1) / (1 + R_L) ** t for t in range(1, n + 1))302    va_rev = valeur_reversion / (1 + R_L) ** n303    return {"perpetuelle": perp, "gordon": gordon, "bail_n": va_rentes + va_rev, "va_rentes": va_rentes, "va_reversion": va_rev, "n": n}304305# ----------------------------------------------------------------------------- 8. synthèse et valeur retenue306def synthese() -> dict:307    df = charger_terrains(); comp = comparaison(df); cas = cas1.tout(); extr = extraction_cas1(cas); allo = allocation_cas1(cas, extr)308    t3 = cas3.tout(); res = residuelle_cas3(t3=t3); lot = lotissement(comp["modele"]); rf = rente_fonciere()309    retenue = round(comp["reconciliation"]["valeur_ponderee"] / 5000) * 5000310    return {"comparaison": comp, "cas1": cas, "extraction": extr, "allocation": allo, "cas3": t3, "residuelle": res, "lotissement": lot,311            "rente": rf, "valeur_retenue": retenue, "part_retenue_grille": retenue / allo["P_grille"], "part_retenue_hed": retenue / allo["P_hed"]}312313def _d(x): return f"{x:,.0f} $".replace(",", " ")314315def main() -> dict:316    S = synthese(); c = S["comparaison"]; m = c["modele"]; df = c["terrains"]317    print("=== 1. Jeu synthétique de terrains (ch25_terrains_ventes.csv, graine 20261031) ===")318    print(f"n = {len(df)} ; desservis = {int(df.desservi.sum())} ; période {df.date_vente.min().date()} → {df.date_vente.max().date()}")319    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))320    print(df.groupby("desservi")[["superficie_m2", "prix_vente", "prix_m2"]].median().round(0))321    print("\n=== 2. Modèle hédonique des terrains ===")322    print(m["coef"].round(4)); print(f"R² = {m['r2']:.3f} ; σ = {m['sigma']:.3f} ; Duan = {m['duan']:.4f}")323    print("\n=== 3. Comparaison directe — sujet 620 m², Hull ===")324    print("Entonnoir :", c["selection"]["etapes"])325    comps = c["selection"]["comparables"]326    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())327    print(c["grille"].round(0).to_string())328    r = c["reconciliation"]329    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'])} ; "330          f"min {_d(r['min'])} ; max {_d(r['max'])} ; étendue {r['etendue_pct']*100:.1f} % ; {_d(c['valeur_m2'])}/m²")331    print(f"Prédiction hédonique directe du terrain du sujet : {_d(c['prediction_hedonique'])}")332    print("\n=== 4. Allocation et prix implicite marginal (cas 1) ===")333    a = S["allocation"]; e = S["extraction"]334    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'])}")335    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'])}")336    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'])}")337    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")338    print("\n=== 5. Extraction sur les 5 comparables du ch. 16 ===")339    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",340            "P_neuf", "delta_marche", "V_L_marche", "V_L_marche_m2", "V_L_agevie_sujet", "V_L_marche_sujet"]341    print(e["comparables"][cols].round(3).to_string())342    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'])}")343    print("\n=== 6. Technique résiduelle du terrain (cas 3) ===")344    rs = S["residuelle"]345    print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in rs.items() if k != "params"})346    sens = sensibilite_residuelle(S["cas3"]); print(sens.pivot_table(index=["taux_depreciation", "n_restante"], columns="R_L", values="V_L").round(0))347    print("\n=== 7. Lotissement actualisé ===")348    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'])}")349    print(L["detail"].round(0).to_string())350    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²) ; "351          f"ratio terrain brut / produit {L['ratio_terrain_brut_sur_produit']*100:.1f} %")352    sl = sensibilite_lotissement(m)353    for k, v in sl.items():354        if k != "base": print(k, {kk: round(vv) for kk, vv in v.items()})355    print("\n=== 8. Rente foncière ===")356    print({k: round(v, 0) if isinstance(v, float) else v for k, v in S["rente"].items()})357    print("\n=== 9. Synthèse — sujet du cas 1 ===")358    print(f"Comparaison (terrains) {_d(r['valeur_ponderee'])} ; extraction (marché) {_d(e['indication_marche'])} ; extraction (âge-vie 60) {_d(e['indication_agevie'])} ; "359          f"allocation (grille) {_d(a['sur_grille'])} ; allocation (hédonique) {_d(a['sur_hedonique'])}")360    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} %")361    return S362363if __name__ == "__main__":364    main()365
Files · d49e547b9b0535e034d80fed31849741adb255e2
Menu

Theme