SPB Git
22.9 KB · 352 lines python
1#!/usr/bin/env python32"""Chapitre 8 — Zonage et urbanisme : DONNÉES SYNTHÉTIQUES et calculs de l'exemple intégral.34Tout est fictif : la « municipalité de Val-Fictif » est une grille de 20 × 20 lots répartie en cinq zones5(H1 unifamiliale, H2 2 à 4 logements, H3 multifamiliale, C mixte, P parc/institutionnel). Un îlot de 36 lots6de la zone H1 est rezoné H2 le 1er janvier 2025 (T0). Les ventes (une ou deux par lot résidentiel) suivent7une loi déclarée ci-dessous ; l'effet du rezonage y est injecté sous la forme d'une prime croissante avec8l'âge de la maison (option de redéveloppement : plus la maison est vieille, plus le terrain domine la valeur).910Le script imprime tous les chiffres cités dans manuscript/chapters/ch08.tex et écrit, s'il y a lieu,11data/synthetic/ch08_zonage_lots.csv et data/synthetic/ch08_zonage_ventes.csv (graine 20261031).1213Usage : python3 analysis/python/cases/ch08_zonage.py14"""15from __future__ import annotations16import sys, pathlib, math, json17from datetime import date, timedelta18import numpy as np19import pandas as pd2021ROOT = pathlib.Path(__file__).resolve().parents[3]22sys.path.insert(0, str(ROOT / "analysis/python"))23from traite import comparables as cp  # noqa: E4022425SEED = 2026103126T0 = date(2025, 1, 1)                 # entrée en vigueur du rezonage H1 → H227DATE_DEBUT, DATE_FIN = date(2023, 1, 1), date(2026, 8, 31)28DATE_REF = date(2024, 7, 1)           # référence de l'indice temporel29TAUX_MARCHE = 0.03                    # croissance annuelle composée des prix (hypothèse)30ELAST_HAB, ELAST_TERR = 0.50, 0.15    # élasticités prix/superficie (hypothèses pédagogiques, cf. cas 1 : 0,55 / 0,10)31BETA_AGE = -0.004                     # effet de l'âge sur ln P (par année)32SIGMA = 0.07                          # bruit transactionnel (écart-type de ln P)33PRIX_BASE = {"H1": 430000.0, "H2": 640000.0, "H3": 1150000.0}  # prix de référence par zone (bâtiment type)34NCOL = NLIG = 203536# ----------------------------------------------------------------------------- lots et zones3738def zone_du_lot(x: int, y: int) -> str:39    if y < 3:40        return "C"41    if 3 <= y < 8 and x < 4:42        return "P"43    if y >= 8 and 10 <= x < 15:44        return "H2"45    if y >= 8 and x >= 15:46        return "H3"47    return "H1"484950def est_rezone(x: int, y: int) -> bool:51    return y >= 8 and 7 <= x < 10525354def generer_lots(seed: int = SEED) -> pd.DataFrame:55    rng = np.random.default_rng(seed)56    rows = []57    for y in range(NLIG):58        for x in range(NCOL):59            z = zone_du_lot(x, y)60            front = 20.0 if z in ("H1", "P") else (24.0 if z == "H2" else (30.0 if z == "H3" else 25.0))61            prof = 30.0 if z != "H3" else 36.062            s_terr = round(front * prof * rng.uniform(0.92, 1.12), 0)63            if z == "H1":64                age = int(rng.integers(10, 76))           # maisons de 1951 à 201665                s_hab = round(float(rng.normal(135, 22)), 0)66                s_hab = float(min(max(s_hab, 85), 210))67                nlog = 168            elif z == "H2":69                age = int(rng.integers(5, 71)); s_hab = round(float(rng.normal(230, 35)), 0); nlog = int(rng.integers(2, 5))70            elif z == "H3":71                age = int(rng.integers(5, 61)); s_hab = round(float(rng.normal(520, 80)), 0); nlog = int(rng.integers(4, 9))72            else:73                age = int(rng.integers(5, 61)); s_hab = float("nan"); nlog = 074            rows.append({"id_lot": f"L{y:02d}{x:02d}", "x": x, "y": y, "zone_initiale": z,75                         "zone_apres_T0": "H2" if (z == "H1" and est_rezone(x, y)) else z,76                         "rezone": bool(z == "H1" and est_rezone(x, y)), "superficie_terrain_m2": s_terr,77                         "superficie_habitable_m2": s_hab, "age_2026": age, "logements": nlog})78    return pd.DataFrame(rows)798081# ----------------------------------------------------------------------------- ventes8283def prime_rezonage(age: float) -> float:84    """Prime injectée (en ln P) pour une maison de l'îlot rezoné vendue après T0 : 0,03 + 0,0015 × max(âge − 20, 0)."""85    return 0.03 + 0.0015 * max(age - 20.0, 0.0)868788def _indice(d: date) -> float:89    return (1.0 + TAUX_MARCHE) ** ((d - DATE_REF).days / 365.25)909192def generer_ventes(lots: pd.DataFrame, seed: int = SEED) -> pd.DataFrame:93    rng = np.random.default_rng(seed + 1)94    res = lots[lots["zone_initiale"].isin(["H1", "H2", "H3"])].reset_index(drop=True)95    n_jours = (DATE_FIN - DATE_DEBUT).days96    rows = []97    for _, L in res.iterrows():98        n_ventes = 1 + int(rng.random() < 0.5)99        jours = sorted(rng.integers(0, n_jours + 1, size=n_ventes).tolist())100        for k, j in enumerate(jours):101            d = DATE_DEBUT + timedelta(days=int(j))102            age_vente = L["age_2026"] - (2026 - d.year)103            z = L["zone_initiale"]104            lnP = (math.log(PRIX_BASE[z]) + ELAST_HAB * math.log(L["superficie_habitable_m2"] / (135.0 if z == "H1" else 230.0 if z == "H2" else 520.0))105                   + ELAST_TERR * math.log(L["superficie_terrain_m2"] / 600.0) + BETA_AGE * age_vente + math.log(_indice(d)))106            traite_post = bool(L["rezone"] and d >= T0)107            if traite_post:108                lnP += prime_rezonage(age_vente)109            lnP += rng.normal(0.0, SIGMA)110            prix = round(math.exp(lnP) / 500.0) * 500.0111            rows.append({"id_vente": f"V{len(rows)+1:04d}", "id_lot": L["id_lot"], "zone_initiale": z, "rezone": bool(L["rezone"]),112                         "date_vente": d.isoformat(), "post_T0": d >= T0, "traite_post": traite_post,113                         "superficie_habitable_m2": L["superficie_habitable_m2"], "superficie_terrain_m2": L["superficie_terrain_m2"],114                         "age_vente": int(age_vente), "prix_vente": prix})115    return pd.DataFrame(rows)116117118# ----------------------------------------------------------------------------- analyse : paires et DiD119120def _mois(d1: date, d2: date) -> float:121    return (d1 - d2).days / 30.4375122123124def apparier(ventes: pd.DataFrame, date_eval: date = date(2026, 9, 1)) -> pd.DataFrame:125    """Chaque vente traitée (îlot rezoné, après T0) est appariée à la vente témoin H1 non rezonée, postérieure à T0,126    la plus proche (superficies, âge, date), sans remise. Le prix du témoin est ajusté au temps, à la superficie127    habitable, au terrain et à l'âge avec les coefficients déclarés ; la différence est exprimée en % du témoin ajusté."""128    tr = ventes[ventes["traite_post"]].copy()129    ctl = ventes[(ventes["zone_initiale"] == "H1") & (~ventes["rezone"]) & (ventes["post_T0"])].copy()130    utilises: set[str] = set(); paires = []131    for _, t in tr.sort_values("date_vente").iterrows():132        dt = date.fromisoformat(t["date_vente"])133        cand = ctl[~ctl["id_vente"].isin(utilises)]134        d = np.sqrt(((cand["superficie_habitable_m2"] - t["superficie_habitable_m2"]) / 15.0) ** 2135                    + ((cand["superficie_terrain_m2"] - t["superficie_terrain_m2"]) / 80.0) ** 2136                    + ((cand["age_vente"] - t["age_vente"]) / 8.0) ** 2137                    + (np.array([_mois(dt, date.fromisoformat(s)) for s in cand["date_vente"]]) / 4.0) ** 2)138        c = cand.loc[d.idxmin()]; utilises.add(c["id_vente"])139        dc = date.fromisoformat(c["date_vente"])140        # ajustements du témoin vers le traité (point de vue du traité) : temps, superficies, âge141        a_temps = cp.ajustement_temps_taux(c["prix_vente"], TAUX_MARCHE, _mois(dt, dc))142        p1 = c["prix_vente"] + a_temps143        fac = ((t["superficie_habitable_m2"] / c["superficie_habitable_m2"]) ** ELAST_HAB144               * (t["superficie_terrain_m2"] / c["superficie_terrain_m2"]) ** ELAST_TERR145               * math.exp(BETA_AGE * (t["age_vente"] - c["age_vente"])))146        p_adj = p1 * fac147        paires.append({"id_traite": t["id_vente"], "id_temoin": c["id_vente"], "date_traite": t["date_vente"], "date_temoin": c["date_vente"],148                       "age_traite": int(t["age_vente"]), "age_temoin": int(c["age_vente"]), "distance": float(d.min()),149                       "prix_traite": t["prix_vente"], "prix_temoin": c["prix_vente"], "ajust_temps": a_temps,150                       "facteur_physique": fac, "prix_temoin_ajuste": p_adj, "difference": t["prix_vente"] - p_adj,151                       "difference_pct": (t["prix_vente"] - p_adj) / p_adj})152    return pd.DataFrame(paires)153154155def resume_paires(paires: pd.DataFrame) -> dict:156    r = cp.ventes_appariees(paires["prix_traite"].tolist(), paires["prix_temoin_ajuste"].tolist())157    pct = paires["difference_pct"].to_numpy()158    jeunes = paires[paires["age_traite"] < 40]; vieux = paires[paires["age_traite"] >= 40]159    return {"n": r["n"], "moyenne": r["moyenne"], "mediane": r["mediane"], "ecart_type": r["ecart_type"],160            "erreur_type_moyenne": r["ecart_type"] / math.sqrt(r["n"]), "moyenne_pct": float(pct.mean()), "mediane_pct": float(np.median(pct)),161            "ecart_type_pct": float(pct.std(ddof=1)), "ic95_pct": (float(pct.mean() - 1.96 * pct.std(ddof=1) / math.sqrt(len(pct))),162                                                                   float(pct.mean() + 1.96 * pct.std(ddof=1) / math.sqrt(len(pct)))),163            "n_jeunes": len(jeunes), "moyenne_pct_jeunes": float(jeunes["difference_pct"].mean()),164            "n_vieux": len(vieux), "moyenne_pct_vieux": float(vieux["difference_pct"].mean()),165            "pente_age": float(np.polyfit(paires["age_traite"], pct, 1)[0])}166167168def did(ventes: pd.DataFrame) -> dict:169    """Doubles différences sur les ventes H1 (îlot rezoné = traité ; reste de H1 = témoin). (a) 2 × 2 sur ln P ;170    (b) régression avec superficies, âge et tendance : ln P = a + b·traité + c·post + δ·traité×post + x'γ + ε."""171    h1 = ventes[ventes["zone_initiale"] == "H1"].copy()172    h1["lnP"] = np.log(h1["prix_vente"])173    g = h1.groupby(["rezone", "post_T0"])["lnP"].agg(["mean", "count"])174    m = {(bool(i), bool(j)): float(g.loc[(i, j), "mean"]) for i, j in g.index}175    n = {(bool(i), bool(j)): int(g.loc[(i, j), "count"]) for i, j in g.index}176    delta_2x2 = (m[(True, True)] - m[(True, False)]) - (m[(False, True)] - m[(False, False)])177    t_annees = np.array([(date.fromisoformat(s) - DATE_REF).days / 365.25 for s in h1["date_vente"]])178    X = np.column_stack([np.ones(len(h1)), h1["rezone"].astype(float), h1["post_T0"].astype(float),179                         (h1["rezone"] & h1["post_T0"]).astype(float), np.log(h1["superficie_habitable_m2"] / 135.0),180                         np.log(h1["superficie_terrain_m2"] / 600.0), h1["age_vente"].astype(float), t_annees])181    y = h1["lnP"].to_numpy()182    beta, *_ = np.linalg.lstsq(X, y, rcond=None)183    resid = y - X @ beta; nobs, k = X.shape184    s2 = resid @ resid / (nobs - k); cov = s2 * np.linalg.inv(X.T @ X); se = np.sqrt(np.diag(cov))185    noms = ["const", "traite", "post", "traite_x_post", "ln_hab", "ln_terr", "age", "tendance"]186    return {"moyennes_lnP": m, "effectifs": n, "delta_2x2": delta_2x2, "delta_2x2_pct": math.exp(delta_2x2) - 1.0,187            "coef": dict(zip(noms, beta.tolist())), "se": dict(zip(noms, se.tolist())),188            "delta_reg": float(beta[3]), "se_delta": float(se[3]), "delta_reg_pct": math.exp(float(beta[3])) - 1.0,189            "n_obs": int(nobs), "sigma_resid": float(math.sqrt(s2))}190191192def prime_theorique_moyenne(ventes: pd.DataFrame) -> float:193    """Moyenne de la prime injectée sur les ventes traitées (vérité terrain du générateur)."""194    tr = ventes[ventes["traite_post"]]195    return float(np.mean([math.exp(prime_rezonage(a)) - 1.0 for a in tr["age_vente"]]))196197198# ----------------------------------------------------------------------------- outils du chapitre (règles, options, droits acquis)199200def densite_admissible(s_terrain: float, s_min_par_logement: float, logements_max_grille: int, cos_max: float, s_min_lot: float) -> dict:201    """Nombre de logements et aire de plancher admissibles d'après une grille des usages (lecture de la grille)."""202    conforme_lot = s_terrain >= s_min_lot203    n_densite = math.floor(s_terrain / s_min_par_logement)204    n = max(0, min(n_densite, logements_max_grille))205    return {"lot_conforme": conforme_lot, "logements_par_densite": n_densite, "logements_admissibles": n, "aire_plancher_max_m2": cos_max * s_terrain}206207208def contribution_parcs(valeur_site: float, superficie_site: float, taux: float = 0.10) -> dict:209    """Contribution pour fins de parcs (LAU 117.4 al. 1) : au plus 10 % de la superficie et de la valeur du site."""210    return {"terrain_max_m2": taux * superficie_site, "somme_max": taux * valeur_site, "taux": taux}211212213def valeur_residuelle_redeveloppement(n_logements: int, s_logement_m2: float, prix_neuf_m2: float, cout_constr_m2: float,214                                      taux_indirects: float, demolition: float, marge_promoteur: float, financement: float,215                                      taux_parcs: float = 0.10, charge_inclusion: float = 0.0) -> dict:216    """Valeur résiduelle du terrain redéveloppé : V_L = [GDV − C_dir − C_ind − démolition − marge − financement − inclusion] / (1 + taux_parcs).217    La contribution pour fins de parcs est proportionnelle à la valeur du site : V_L·taux_parcs, d'où la division."""218    gdv = n_logements * s_logement_m2 * prix_neuf_m2219    c_dir = n_logements * s_logement_m2 * cout_constr_m2220    c_ind = taux_indirects * c_dir221    marge = marge_promoteur * gdv222    fin = financement * gdv223    reste = gdv - c_dir - c_ind - demolition - marge - fin - charge_inclusion224    v_l = reste / (1.0 + taux_parcs)225    return {"gdv": gdv, "couts_directs": c_dir, "couts_indirects": c_ind, "demolition": demolition, "marge": marge, "financement": fin,226            "inclusion": charge_inclusion, "reste_avant_parcs": reste, "contribution_parcs": v_l * taux_parcs, "valeur_residuelle": v_l}227228229def prime_option(v_actuel: float, v_redev: float) -> float:230    """Prime d'option de redéveloppement = max(0, V_redev − V_actuel)."""231    return max(0.0, v_redev - v_actuel)232233234def valeur_attendue_rezonage(v_actuel: float, v_redev: float, p: float, delai_annees: float, r: float, cout_demande: float = 0.0) -> float:235    """Valeur attendue d'un immeuble dont le rezonage (ou le PPCMOI) est probable : V = V_act + p·max(0, V_redev − V_act)·(1+r)^−T − c."""236    return v_actuel + p * prime_option(v_actuel, v_redev) * (1.0 + r) ** (-delai_annees) - cout_demande237238239def grille_attendue(v_actuel: float, v_redev: float, probs, delais, r: float) -> np.ndarray:240    return np.array([[valeur_attendue_rezonage(v_actuel, v_redev, p, T, r) - v_actuel for T in delais] for p in probs])241242243def valeur_surplus_derogatoire(delta_noi: float, R: float, lam: float) -> float:244    """Surplus de revenu protégé par droits acquis, extinction au taux de risque λ : V_Δ = ΔRNE / (R + λ)."""245    return delta_noi / (R + lam)246247248def valeur_surplus_horizon(delta_noi: float, R: float, lam: float, T: float) -> float:249    """Même surplus mais sur un horizon fini T (annuités continues) : V_Δ(T) = ΔRNE·[1 − e^{−(R+λ)T}]/(R+λ)."""250    return delta_noi * (1.0 - math.exp(-(R + lam) * T)) / (R + lam)251252253def valeur_attendue_zone_agricole(v_agricole: float, v_si_autorise: float, p: float, delai_annees: float, r: float, cout_demande: float) -> float:254    """Zone agricole : V = p·V_autorisé·(1+r)^−T + (1−p)·V_agricole − coût de la demande."""255    return p * v_si_autorise * (1.0 + r) ** (-delai_annees) + (1.0 - p) * v_agricole - cout_demande256257258# ----------------------------------------------------------------------------- exemple intégral (paramètres déclarés)259260EXEMPLE = {261    # lecture de la grille (zone H2 fictive de Val-Fictif)262    "grille": {"s_min_lot": 450.0, "s_min_par_logement": 150.0, "logements_max": 4, "cos_max": 0.60, "marge_avant": 6.0, "marge_lat": 2.0, "marge_arr": 9.0, "hauteur_max_etages": 3},263    # lot témoin de l'exemple : maison 1958 de 95 m² sur 600 m², îlot rezoné264    "lot": {"superficie_terrain_m2": 600.0, "superficie_habitable_m2": 95.0, "annee": 1958, "v_actuel": 380000.0},265    # redéveloppement en quadruplex : 4 × 90 m² (hypothèses, cf. encadré synthétique)266    "redev": {"n_logements": 4, "s_logement_m2": 90.0, "prix_neuf_m2": 5800.0, "cout_constr_m2": 2900.0, "taux_indirects": 0.12,267              "demolition": 30000.0, "marge_promoteur": 0.15, "financement": 0.03, "taux_parcs": 0.10},268    "option": {"r": 0.08, "probs": [0.25, 0.50, 0.75, 1.00], "delais": [0.5, 1.0, 2.0, 3.0], "cout_demande": 8000.0},269    # duplex dérogatoire protégé par droits acquis dans une zone H1270    "droits_acquis": {"loyer_mensuel_logement": 1450.0, "ratio_depenses_duplex": 0.35, "loyer_mensuel_maison": 2300.0, "ratio_depenses_maison": 0.30,271                      "R": 0.055, "lambdas": [0.0, 0.01, 0.02, 0.05], "v_conforme": 430000.0, "horizon": 25.0},272    "parcs": {"valeur_site": 960000.0, "superficie_site": 2400.0},273    "agricole": {"superficie_ha": 5.0, "v_ha_agricole": 15000.0, "v_lot_residentiel": 90000.0, "superficie_lot_ha": 0.5, "p": 0.30, "delai": 1.5, "r": 0.06, "cout_demande": 5000.0},274}275276277def exemple_complet(ventes: pd.DataFrame | None = None) -> dict:278    E = EXEMPLE279    g = densite_admissible(E["lot"]["superficie_terrain_m2"], E["grille"]["s_min_par_logement"], E["grille"]["logements_max"], E["grille"]["cos_max"], E["grille"]["s_min_lot"])280    rd = valeur_residuelle_redeveloppement(**E["redev"])281    v_act = E["lot"]["v_actuel"]282    prime = prime_option(v_act, rd["valeur_residuelle"])283    G = grille_attendue(v_act, rd["valeur_residuelle"], E["option"]["probs"], E["option"]["delais"], E["option"]["r"])284    da = E["droits_acquis"]285    noi_dup = 2 * 12 * da["loyer_mensuel_logement"] * (1 - da["ratio_depenses_duplex"])286    noi_mai = 12 * da["loyer_mensuel_maison"] * (1 - da["ratio_depenses_maison"])287    d_noi = noi_dup - noi_mai288    v_delta = {lam: valeur_surplus_derogatoire(d_noi, da["R"], lam) for lam in da["lambdas"]}289    v_delta_T = {lam: valeur_surplus_horizon(d_noi, da["R"], lam, da["horizon"]) for lam in da["lambdas"]}290    pc = contribution_parcs(E["parcs"]["valeur_site"], E["parcs"]["superficie_site"])291    ag = E["agricole"]292    v_agr = ag["superficie_ha"] * ag["v_ha_agricole"]293    v_aut = ag["v_lot_residentiel"] + (ag["superficie_ha"] - ag["superficie_lot_ha"]) * ag["v_ha_agricole"]294    v_att_agr = valeur_attendue_zone_agricole(v_agr, v_aut, ag["p"], ag["delai"], ag["r"], ag["cout_demande"])295    out = {"grille": g, "redev": rd, "prime_option": prime, "prime_option_pct": prime / v_act, "grille_attendue": G,296           "exemple_p75_T2": valeur_attendue_rezonage(v_act, rd["valeur_residuelle"], 0.75, 2.0, E["option"]["r"], E["option"]["cout_demande"]),297           "noi_duplex": noi_dup, "noi_maison": noi_mai, "delta_noi": d_noi, "v_delta": v_delta, "v_delta_T": v_delta_T,298           "v_droits_acquis_lam02": da["v_conforme"] + v_delta[0.02], "parcs": pc, "v_agricole": v_agr, "v_si_autorise": v_aut, "v_attendue_agricole": v_att_agr}299    if ventes is not None:300        paires = apparier(ventes); out["paires"] = paires; out["resume_paires"] = resume_paires(paires); out["did"] = did(ventes)301        out["prime_theorique_moyenne"] = prime_theorique_moyenne(ventes)302    return out303304305def charger_ou_generer(ecrire: bool = True) -> tuple[pd.DataFrame, pd.DataFrame]:306    lots = generer_lots(); ventes = generer_ventes(lots)307    if ecrire:308        d = ROOT / "data/synthetic"; d.mkdir(parents=True, exist_ok=True)309        lots.to_csv(d / "ch08_zonage_lots.csv", index=False); ventes.to_csv(d / "ch08_zonage_ventes.csv", index=False)310    return lots, ventes311312313def main(ecrire: bool = True) -> dict:314    lots, ventes = charger_ou_generer(ecrire)315    res = exemple_complet(ventes)316    f = lambda x: f"{x:,.0f}".replace(",", " ")317    print("=== Ch. 8 — DONNÉES SYNTHÉTIQUES (graine 20261031) ===")318    print(f"Lots : {len(lots)} ; par zone : {lots['zone_initiale'].value_counts().to_dict()} ; rezonés H1→H2 : {int(lots['rezone'].sum())}")319    print(f"Ventes : {len(ventes)} ; H1 : {int((ventes['zone_initiale']=='H1').sum())} ; traitées (îlot rezoné, après T0) : {int(ventes['traite_post'].sum())}")320    rp = res["resume_paires"]321    print(f"\n--- Paires (traité vs témoin ajusté) : n = {rp['n']} ---")322    print(f"Différence moyenne {f(rp['moyenne'])} $ (médiane {f(rp['mediane'])} $, écart-type {f(rp['ecart_type'])} $, erreur-type {f(rp['erreur_type_moyenne'])} $)")323    print(f"En % du témoin ajusté : moyenne {rp['moyenne_pct']*100:.2f} %, médiane {rp['mediane_pct']*100:.2f} %, IC95 [{rp['ic95_pct'][0]*100:.2f} ; {rp['ic95_pct'][1]*100:.2f}]")324    print(f"Maisons < 40 ans : n = {rp['n_jeunes']}, +{rp['moyenne_pct_jeunes']*100:.2f} % ; ≥ 40 ans : n = {rp['n_vieux']}, +{rp['moyenne_pct_vieux']*100:.2f} % ; pente {rp['pente_age']*100:.3f} pt/an")325    print(f"Prime théorique moyenne injectée : {res['prime_theorique_moyenne']*100:.2f} %")326    d = res["did"]327    print(f"\n--- Doubles différences (H1, n = {d['n_obs']}) ---")328    for k, v in d["moyennes_lnP"].items():329        print(f"  ln P moyen traité={k[0]} post={k[1]} : {v:.4f} (n = {d['effectifs'][k]})")330    print(f"δ (2×2) = {d['delta_2x2']:.4f} → {d['delta_2x2_pct']*100:.2f} % ; δ (régression) = {d['delta_reg']:.4f} (e.-t. {d['se_delta']:.4f}) → {d['delta_reg_pct']*100:.2f} % ; σ résiduel {d['sigma_resid']:.4f}")331    print(f"  coefficients : " + ", ".join(f"{k} {v:.4f}" for k, v in d["coef"].items()))332    g = res["grille"]333    print(f"\n--- Lecture de la grille H2 (lot 600 m²) : lot conforme {g['lot_conforme']}, logements par densité {g['logements_par_densite']}, admissibles {g['logements_admissibles']}, plancher max {g['aire_plancher_max_m2']:.0f} m²")334    rd = res["redev"]335    print(f"--- Redéveloppement en quadruplex : GDV {f(rd['gdv'])} ; directs {f(rd['couts_directs'])} ; indirects {f(rd['couts_indirects'])} ; démolition {f(rd['demolition'])} ; marge {f(rd['marge'])} ; financement {f(rd['financement'])}")336    print(f"    reste avant parcs {f(rd['reste_avant_parcs'])} ; contribution parcs {f(rd['contribution_parcs'])} ; valeur résiduelle {f(rd['valeur_residuelle'])} ; V_actuel {f(EXEMPLE['lot']['v_actuel'])} ; prime d'option {f(res['prime_option'])} ({res['prime_option_pct']*100:.1f} %)")337    print("    grille attendue (prime espérée, $) lignes p = " + str(EXEMPLE["option"]["probs"]) + " ; colonnes T = " + str(EXEMPLE["option"]["delais"]))338    for p, row in zip(EXEMPLE["option"]["probs"], res["grille_attendue"]):339        print("     p = %.2f : " % p + "  ".join(f(x) for x in row))340    print(f"    exemple p = 0,75, T = 2, coût 8 000 $ : V = {f(res['exemple_p75_T2'])}")341    print(f"--- Droits acquis : RNE duplex {f(res['noi_duplex'])} ; RNE maison {f(res['noi_maison'])} ; ΔRNE {f(res['delta_noi'])}")342    for lam in EXEMPLE["droits_acquis"]["lambdas"]:343        print(f"    λ = {lam:.2f} : V_Δ = {f(res['v_delta'][lam])} (horizon 25 ans : {f(res['v_delta_T'][lam])})")344    print(f"    V avec droits acquis (λ = 2 %) = {f(res['v_droits_acquis_lam02'])}")345    print(f"--- Parcs : terrain max {res['parcs']['terrain_max_m2']:.0f} m² ou somme max {f(res['parcs']['somme_max'])}")346    print(f"--- Zone agricole : V agricole {f(res['v_agricole'])} ; V si autorisé {f(res['v_si_autorise'])} ; V attendue {f(res['v_attendue_agricole'])}")347    return {"lots": lots, "ventes": ventes, **res}348349350if __name__ == "__main__":351    main()352
Files · 4958fcec3502ce9b6da151998f7eaebf95b47bcc
Menu

Theme