SPB Git
16.9 KB · 228 lines python
1#!/usr/bin/env python32"""Chapitre 29 — Capitalisation directe : calculs du cas 3 (12 logements, Rosemont ; DONNÉES SYNTHÉTIQUES).34Tous les chiffres cités dans manuscript/chapters/ch29.tex et solutions/ch29.tex sortent d'ici. Les calculs partagés5(RNE normalisé, taux extraits, valeur par capitalisation) viennent de traite.cas3 afin de coïncider avec les6chapitres 30 et 47 ; les formules financières viennent de traite.finance (aucune n'est réécrite ici).78Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch29_capitalisation.py9"""10from __future__ import annotations11import sys, pathlib12import numpy as np, pandas as pd13sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))14from traite import cas3, finance as fin1516ROOT = pathlib.Path(__file__).resolve().parents[3]1718# Barème 2026 des droits sur les mutations immobilières, Ville de Montréal (sources/notes-B.md §, clé mtlmutations2026) :19# bornes supérieures des tranches ($) et taux marginaux. Facteur comparatif 2026 = 1,00 (LDMI art. 1.1 ; rôle 2026-2028).20MUTATIONS_MTL_2026 = [(62_900, 0.005), (315_000, 0.01), (552_300, 0.015), (1_104_700, 0.02), (2_136_500, 0.025), (3_113_000, 0.035), (float("inf"), 0.04)]21FACTEUR_COMPARATIF_MTL_2026 = 1.00222324def droits_mutation(base: float, bareme=MUTATIONS_MTL_2026) -> float:25    """Droits progressifs par tranches (LDMI art. 2 ; barème municipal)."""26    total, bas = 0.0, 0.027    for haut, taux in bareme:28        if base <= bas:29            break30        total += (min(base, haut) - bas) * taux31        bas = haut32    return total333435def comparables_proches(c: pd.DataFrame, sujet: dict, k: int = 10, date_eval="2026-09-01") -> pd.DataFrame:36    """Les k ventes les plus proches du sujet : taille, état, âge, quartier, ancienneté de la vente (distance pédagogique)."""37    c = c.copy()38    mois = (pd.Timestamp(date_eval) - c["date_vente"]).dt.days / 30.437539    c["mois"] = mois40    c["distance"] = (abs(c["nb_logements"] - sujet["nb_logements"]) / 6 + abs(c["etat_1a5"] - sujet["etat_1a5"])41                     + abs(c["annee_construction"] - sujet["annee_construction"]) / 20 + 0.5 * (c["quartier"] != "Rosemont") + mois / 24)42    return c.sort_values("distance").head(k)434445def calculer() -> dict:46    d = cas3.charger()47    etat, dep, hyp, sujet, comps = d["etat"], d["dep"], d["hyp"], d["sujet"], d["comps"]48    base = cas3.rne(etat, dep, hyp)49    tx = cas3.taux_extraits(comps)50    c = tx["comparables"]51    R = tx["mediane"]52    V = fin.capitalisation_directe(base["rne"], R)53    proches = comparables_proches(c, sujet)54    # régression : résidus55    X = np.column_stack([np.ones(len(c)), c["etat_1a5"] - 3, c["nb_logements"] - 12])56    beta = np.array([tx["regression"]["constante"], tx["regression"]["etat"], tx["regression"]["taille"]])57    resid = c["taux_global"].values - X @ beta58    reg = {**tx["regression"], "sigma_resid": float(resid.std(ddof=3)), "sigma_brut": float(c["taux_global"].std(ddof=1)),59           "r2": float(1 - resid.var(ddof=0) / c["taux_global"].var(ddof=0)), "prevision_sujet": float(beta[0])}60    V_reg = fin.capitalisation_directe(base["rne"], reg["prevision_sujet"])6162    # --- construction du taux -------------------------------------------------------------------------------------63    f_ = hyp["financement"]64    h1 = fin.Hypotheque(1.0, f_["taux"], f_["amortissement_mois"])        # prêt unitaire : constante hypothécaire65    f = h1.constante_hypothecaire66    M = f_["rpv"]67    re_implicite = (R - M * f) / (1 - M)                                   # rendement courant des fonds propres impliqué par le marché68    grille_re = {re: fin.taux_bande_investissement(M, f, re) for re in (0.04, 0.05, 0.06, 0.07, 0.08)}69    R_rcd = fin.taux_par_rcd(f_["rcd_min"], M, f)70    V_rcd = fin.capitalisation_directe(base["rne"], R_rcd)71    r = hyp["taux_actualisation"]; Rt = hyp["taux_terminal"]72    g_terminal = r - Rt                                                     # croissance impliquée par le taux terminal du cas 373    gordon = {"r": r, "g_terminal": g_terminal, "R_g_terminal": fin.taux_gordon(r, g_terminal),74              "g_tal": hyp["croissance_loyers_contrat"], "R_g_tal": fin.taux_gordon(r, hyp["croissance_loyers_contrat"]),75              "g_implicite_marche": r - R}76    n = hyp["horizon_ans"]77    P = 1 - h1.solde(12 * n)                                                # part du capital remboursée en n ans78    sff = fin.facteur_fonds_amortissement(r, n)79    delta = (1 + g_terminal) ** n - 1                                       # appréciation cohérente avec g_terminal80    ellwood = {"Y": r, "M": M, "f": f, "P": P, "n": n, "SFF": sff, "delta": delta,81               "R_delta0": fin.taux_ellwood(r, M, f, P, n, 0.0), "R_delta": fin.taux_ellwood(r, M, f, P, n, delta),82               "R_akerson": fin.taux_akerson(r, M, f, P, n, delta), "R_sans_dette": fin.taux_ellwood(r, 0.0, f, P, n, delta),83               "R_sans_dette_delta0": fin.taux_ellwood(r, 0.0, f, P, n, 0.0),84               "composantes": {"M f": M * f, "(1-M) Y": (1 - M) * r, "-M P SFF": -M * P * sff, "-delta SFF": -delta * sff}}85    ellwood["V_delta"] = fin.capitalisation_directe(base["rne"], ellwood["R_delta"])86    # lecture inverse : appréciation Δ cohérente avec le taux de marché (Y, M, f fixés) et rendement Y cohérent avec Δ fixé87    ellwood["delta_implicite"] = (ellwood["R_delta0"] - R) / sff88    lo, hi = 0.02, 0.3089    for _ in range(200):90        mid = 0.5 * (lo + hi); Pm = 1 - h1.solde(12 * n)91        if fin.taux_ellwood(mid, M, f, Pm, n, delta) < R: lo = mid92        else: hi = mid93    ellwood["Y_implicite"] = 0.5 * (lo + hi)94    ellwood["composantes_marche"] = {"M f": M * f, "(1-M) Y": (1 - M) * r, "-M P SFF": -M * P * sff, "-delta SFF": -ellwood["delta_implicite"] * sff}9596    # --- multiplicateurs -------------------------------------------------------------------------------------------97    mrb = {"mediane": float(c["mrb"].median()), "moyenne": float(c["mrb"].mean()), "min": float(c["mrb"].min()), "max": float(c["mrb"].max()),98           "cv": float(c["mrb"].std(ddof=1) / c["mrb"].mean()), "cv_taux": float(c["taux_global"].std(ddof=1) / c["taux_global"].mean())}99    mrb["V_contrat"] = mrb["mediane"] * base["rbp_contrat"]; mrb["V_marche"] = mrb["mediane"] * base["rbp_marche"]100    mrb["mrn"] = 1 / R; mrb["mrb_sujet_implicite"] = V / base["rbp_contrat"]101    mrb["ratio_dep_median_comps"] = float(c["ratio_depenses"].median()); mrb["ratio_dep_sujet_rbe"] = base["depenses_totales"] / base["rbe"]102103    # --- cohérence RNE–taux ----------------------------------------------------------------------------------------104    taux_taxes = hyp["taux_taxe_fonciere"] + hyp["taux_taxe_scolaire"]105    taxes_role = hyp["valeur_role_2026"] * taux_taxes106    taxes_valeur = V * taux_taxes107    coherence = {"taxes_role": taxes_role, "taxes_sur_valeur": taxes_valeur, "ecart_taxes": taxes_role - taxes_valeur,108                 "effet_valeur": (taxes_role - taxes_valeur) / R, "valeur_role": hyp["valeur_role_2026"], "ratio_valeur_role": V / hyp["valeur_role_2026"],109                 "reserve": base["reserve"], "reserve_par_logement": base["reserve"] / len(etat),110                 "R_avant_reserve_equiv": (base["rne"] + base["reserve"]) / V, "gestion": 0.04 * base["rbe"],111                 "V_si_taux_avant_reserve_sur_rne_apres": fin.capitalisation_directe(base["rne"], (base["rne"] + base["reserve"]) / V)}112    # loyers de marché capitalisés à tort (baux en place)113    rbe_m = fin.revenu_brut_effectif(base["rbp_marche"], base["taux_vacance_pertes"], base["autres_revenus"])114    de_m = base["depenses_hors_reserve"] - 0.04 * base["rbe"] + 0.04 * rbe_m115    rne_m = fin.rne(rbe_m, de_m, base["reserve"])116    coherence["rne_marche"] = rne_m; coherence["V_marche"] = fin.capitalisation_directe(rne_m, R); coherence["surevaluation_marche"] = coherence["V_marche"] / V - 1117    # droits de mutation (encadré Québec)118    base_droits = max(V, hyp["valeur_role_2026"] * FACTEUR_COMPARATIF_MTL_2026)119    mut = {"base": base_droits, "droits": droits_mutation(base_droits), "droits_sur_prix": droits_mutation(V)}120    mut["pct_prix"] = mut["droits"] / V121122    # --- sensibilité -----------------------------------------------------------------------------------------------123    grille_R = np.round(np.arange(0.0375, 0.0526, 0.0025), 4)124    sens = pd.DataFrame({"R": grille_R})125    for k, m in (("rne_m5", 0.95), ("rne", 1.0), ("rne_p5", 1.05)):126        sens[k] = [fin.capitalisation_directe(base["rne"] * m, x) for x in grille_R]127    sens["var_vs_base"] = sens["rne"] / V - 1128    derive = {"dV_dR": -V / R, "dV_25pb": -V / R * 0.0025, "V_plus25": fin.capitalisation_directe(base["rne"], R + 0.0025),129              "V_moins25": fin.capitalisation_directe(base["rne"], R - 0.0025), "elasticite_R": -1.0, "elasticite_rne": 1.0}130    derive["var_plus25_exacte"] = derive["V_plus25"] / V - 1; derive["var_plus25_lineaire"] = -0.0025 / R131132    return {"base": base, "taux": tx, "R": R, "V": V, "V_arrondie": round(V / 5000) * 5000, "proches": proches, "mediane_proches": float(proches["taux_global"].median()),133            "V_proches": fin.capitalisation_directe(base["rne"], float(proches["taux_global"].median())), "V_q1": fin.capitalisation_directe(base["rne"], tx["q3"]), "V_q3": fin.capitalisation_directe(base["rne"], tx["q1"]),134            "regression": reg, "V_reg": V_reg, "f": f, "M": M, "re_implicite": re_implicite, "grille_re": grille_re, "R_rcd": R_rcd, "V_rcd": V_rcd,135            "gordon": gordon, "ellwood": ellwood, "mrb": mrb, "coherence": coherence, "mutations": mut, "sensibilite": sens, "derive": derive,136            "hyp": hyp, "sujet": sujet, "comps": comps}137138139def exercices(res: dict | None = None) -> dict:140    """Chiffres des exercices 29.1 à 29.8 (énoncés dans ch29.tex, solutions dans solutions/ch29.tex)."""141    res = res or calculer(); c = res["comps"].set_index("id_vente"); b = res["base"]142    ex = {}143    # 29.1 — capitalisation simple : RNE de 61 200 $ (énoncé), R = 4,75 % ; +25 pb144    ex["1"] = {"rne": 61_200.0, "R": 0.0475, "V": fin.capitalisation_directe(61_200.0, 0.0475), "V_plus25": fin.capitalisation_directe(61_200.0, 0.05)}145    ex["1"]["var"] = ex["1"]["V_plus25"] / ex["1"]["V"] - 1; ex["1"]["lineaire"] = -0.0025 / 0.0475146    # 29.2 — extraction sur cinq ventes du fichier cas 3147    ids = ["M008", "M011", "M022", "M034", "M040"]; s = c.loc[ids]148    taux = s["rne"] / s["prix_vente"]149    ex["2"] = {"ids": ids, "taux": taux.to_dict(), "mediane": float(taux.median()), "moyenne": float(taux.mean()),150               "ponderee": float((s["rne"].sum()) / s["prix_vente"].sum()), "V_mediane": fin.capitalisation_directe(b["rne"], float(taux.median()))}151    # 29.3 — convention de la réserve : un comparable de 18 logements (énoncé) ; réserve 675 $/logement152    rne_apres, reserve, prix = 150_000.0, 18 * 675.0, 3_400_000.0153    R_apres = fin.taux_global_extrait(rne_apres, prix); R_avant = fin.taux_global_extrait(rne_apres + reserve, prix)154    ex["3"] = {"R_apres": R_apres, "R_avant": R_avant, "V_correct": fin.capitalisation_directe(b["rne"], R_apres),155               "V_errone": fin.capitalisation_directe(b["rne"], R_avant), "V_coherent_avant": fin.capitalisation_directe(b["rne"] + b["reserve"], R_avant)}156    ex["3"]["erreur"] = ex["3"]["V_errone"] / ex["3"]["V_correct"] - 1157    # 29.4 — bande d'investissement et RCD (R_E = 6 % et RCD = 1,25 : hypothèses de l'énoncé)158    f = res["f"]; M = res["M"]159    ex["4"] = {"f": f, "R_bande": fin.taux_bande_investissement(M, f, 0.06), "R_rcd": fin.taux_par_rcd(1.25, M, f),160               "V_bande": fin.capitalisation_directe(b["rne"], fin.taux_bande_investissement(M, f, 0.06)),161               "V_rcd": fin.capitalisation_directe(b["rne"], fin.taux_par_rcd(1.25, M, f)), "re_implicite": res["re_implicite"]}162    # 29.5 — Gordon163    g = res["gordon"]164    ex["5"] = {"R_225": g["R_g_terminal"], "R_310": g["R_g_tal"], "g_implicite": g["g_implicite_marche"],165               "V_225": fin.capitalisation_directe(b["rne"], g["R_g_terminal"]), "V_310": fin.capitalisation_directe(b["rne"], g["R_g_tal"]),166               "R_g4": fin.taux_gordon(g["r"], 0.04), "V_g4": fin.capitalisation_directe(b["rne"], fin.taux_gordon(g["r"], 0.04))}167    # 29.6 — Ellwood / Akerson (Δ = 25 % sur 10 ans, énoncé)168    e = res["ellwood"]169    ex["6"] = {"P": e["P"], "SFF": e["SFF"], "R_ellwood": fin.taux_ellwood(e["Y"], e["M"], e["f"], e["P"], e["n"], 0.25),170               "R_akerson": fin.taux_akerson(e["Y"], e["M"], e["f"], e["P"], e["n"], 0.25), "R_sans_dette": fin.taux_ellwood(e["Y"], 0.0, e["f"], e["P"], e["n"], 0.25),171               "R_delta0": e["R_delta0"], "g_equiv": 1.25 ** (1 / e["n"]) - 1}172    ex["6"]["R_gordon_equiv"] = fin.taux_gordon(e["Y"], ex["6"]["g_equiv"]); ex["6"]["V"] = fin.capitalisation_directe(b["rne"], ex["6"]["R_ellwood"])173    # 29.7 — loyers de marché vs baux en place ; MRB174    co = res["coherence"]; m = res["mrb"]175    ex["7"] = {"rne_marche": co["rne_marche"], "V_marche": co["V_marche"], "surevaluation": co["surevaluation_marche"], "V_contrat": res["V"],176               "V_mrb_contrat": m["V_contrat"], "V_mrb_marche": m["V_marche"], "ecart_rbp": b["ecart_marche"], "v": b["taux_vacance_pertes"],177               "ecart_rne": co["rne_marche"] - b["rne"], "V_mrb_vs_cap": m["V_contrat"] / res["V"] - 1}178    # 29.8 — régression et sensibilité croisée179    r = res["regression"]180    R24 = r["constante"] + r["etat"] * (2 - 3) + r["taille"] * (24 - 12)181    ex["8"] = {"R_24_etat2": R24, "R_6_etat4": r["constante"] + r["etat"] * (4 - 3) + r["taille"] * (6 - 12), "sigma_resid": r["sigma_resid"],182               "V_pire": fin.capitalisation_directe(b["rne"] * 0.95, res["R"] + 0.0025), "V_meilleur": fin.capitalisation_directe(b["rne"] * 1.05, res["R"] - 0.0025)}183    ex["8"]["var_pire"] = ex["8"]["V_pire"] / res["V"] - 1; ex["8"]["var_meilleur"] = ex["8"]["V_meilleur"] / res["V"] - 1184    ex["8"]["approx_pire"] = -0.05 - 0.0025 / res["R"]; ex["8"]["approx_meilleur"] = 0.05 + 0.0025 / res["R"]185    return ex186187188def _p(x, dec=0):189    return f"{x:,.{dec}f}".replace(",", " ")190191192def main():193    res = calculer(); b = res["base"]; tx = res["taux"]194    pd.set_option("display.width", 220); pd.set_option("display.max_columns", 30)195    print("=== Cas 3 (synthétique) — revenus et RNE normalisé (traite.cas3) ===")196    for k in ["rbp_contrat", "rbp_marche", "ecart_marche", "vacance_pertes", "autres_revenus", "rbe", "depenses_hors_reserve", "reserve", "depenses_totales", "rne", "rne_par_logement"]:197        print(f"  {k:24s} {_p(b[k], 2)}")198    print(f"  ratio dépenses/RBE        {b['ratio_depenses']:.4f}")199    print(b["detail"].round(2).to_string(index=False))200    print("\n=== Extraction du taux (40 ventes, 6-30 logements, 24 mois) ===")201    for k in ["n", "mediane", "moyenne", "ponderee_rne", "q1", "q3", "min", "max"]:202        print(f"  {k:14s} {tx[k]}")203    print("  régression :", {k: round(v, 6) for k, v in res["regression"].items()})204    print(f"  V (RNE / médiane) = {_p(b['rne'], 2)} / {res['R']:.5f} = {_p(res['V'], 2)}  (arrondie {_p(res['V_arrondie'])}) ; V au taux de régression = {_p(res['V_reg'])}")205    print(f"  10 comparables les plus proches (médiane {res['mediane_proches']:.5f}) :")206    cols = ["id_vente", "date_vente", "quartier", "nb_logements", "annee_construction", "etat_1a5", "rne", "prix_vente", "taux_global", "mrb", "distance"]207    pr = res["proches"][cols].copy(); pr["date_vente"] = pr["date_vente"].dt.date; print(pr.round(3).to_string(index=False))208    print("\n=== Construction du taux ===")209    print(f"  constante hypothécaire f (4,12 %, 480 mois, composition semestrielle) = {res['f']:.6f} ; M = {res['M']}")210    print(f"  bande d'investissement : R_E impliqué par R = {res['R']:.5f} → {res['re_implicite']:.5f} ; grille R(R_E) = {{ {', '.join(f'{k:.2f}: {v:.5f}' for k, v in res['grille_re'].items())} }}")211    print(f"  formule RCD : R = 1,20 × 0,75 × f = {res['R_rcd']:.5f} → V = {_p(res['V_rcd'])}")212    print("  Gordon :", {k: round(v, 5) for k, v in res["gordon"].items()})213    e = res["ellwood"]; print("  Ellwood/Akerson :", {k: (round(v, 6) if isinstance(v, float) else v) for k, v in e.items() if not k.startswith("composantes")})214    print("    composantes (Δ du cas) :", {k: round(v, 6) for k, v in e["composantes"].items()}); print("    composantes (Δ implicite) :", {k: round(v, 6) for k, v in e["composantes_marche"].items()})215    print(f"  V (médiane des 10 proches {res['mediane_proches']:.5f}) = {_p(res['V_proches'])} ; V(Q3 des taux) = {_p(res['V_q1'])} ; V(Q1 des taux) = {_p(res['V_q3'])}")216    print("\n=== Multiplicateurs ===", {k: round(v, 4) for k, v in res["mrb"].items()})217    print("\n=== Cohérence RNE–taux ===", {k: round(v, 4) for k, v in res["coherence"].items()})218    print("=== Droits de mutation Montréal 2026 ===", {k: round(v, 4) for k, v in res["mutations"].items()})219    print("\n=== Sensibilité ===")220    print(res["sensibilite"].round(4).to_string(index=False)); print("  dérivée :", {k: round(v, 6) for k, v in res["derive"].items()})221    print("\n=== Exercices ===")222    for k, v in exercices(res).items():223        print(f"  29.{k} :", {kk: (round(vv, 6) if isinstance(vv, float) else vv) for kk, vv in v.items()})224225226if __name__ == "__main__":227    main()228
Files · d49e547b9b0535e034d80fed31849741adb255e2
Menu

Theme