SPB Git
11.4 KB · 177 lines python
1#!/usr/bin/env python32"""Chapitre 2 — Droits immobiliers et bases de valeur : calculs de l'exemple intégral sur le cas 3 (12 logements, Montréal,3DONNÉES SYNTHÉTIQUES). Valeur du droit du locateur (baux en place) vs valeur de la pleine propriété libre de baux (loyers de4marché) ; valeur des baux pour les locataires ; sensibilité à la rotation et au taux d'actualisation ; chiffres des exercices.5Toutes les grandeurs du cas 3 proviennent de `traite.cas3` (module partagé) ; les formules financières de `traite.finance`.6Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch02_droits.py"""7from __future__ import annotations8import sys, copy, pathlib9import numpy as np, pandas as pd10sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))11from traite import cas3, finance as fin1213ROTATIONS = [0.0, 1 / 12, 2 / 12, 3 / 12, 6 / 12]   # 0, 1, 2, 3, 6 logements reloués par an (le module arrondit ρ × 12 à l'entier)14TAUX = [0.0625, 0.0725, 0.0825]151617# ----------------------------------------------------------------------------- écart contractuel / marché1819def etat_marche(etat: pd.DataFrame) -> pd.DataFrame:20    """État locatif hypothétique « libre de baux » : chaque logement est reloué au loyer de marché dès l'année 1."""21    e = etat.copy()22    e["loyer_mensuel_contrat"] = e["loyer_mensuel_marche"].astype(float)23    return e242526def ecart_par_logement(etat: pd.DataFrame, annee_eval: int = 2026) -> pd.DataFrame:27    e = etat.copy()28    e["ecart_mensuel"] = e["loyer_mensuel_marche"] - e["loyer_mensuel_contrat"]29    e["ecart_annuel"] = 12 * e["ecart_mensuel"]30    e["ratio_contrat_marche"] = e["loyer_mensuel_contrat"] / e["loyer_mensuel_marche"]31    e["anciennete"] = annee_eval - e["locataire_depuis"]32    return e[["logement", "pieces", "locataire_depuis", "anciennete", "loyer_mensuel_contrat", "loyer_mensuel_marche",33              "ecart_mensuel", "ecart_annuel", "ratio_contrat_marche"]]343536# ----------------------------------------------------------------------------- formules fermées (équations du chapitre)3738def valeur_bail_locataire(ecart_annuel: float, n: int, r: float) -> float:39    """Valeur du bail pour le locataire = Σ_{t=1}^{n} (L_marché − L_contrat)/(1+r)^t, écart constant (éq. annuité)."""40    return ecart_annuel * fin.facteur_va_annuite(r, n)414243def valeur_droit_temporaire_et_reversion(revenu_annuel: float, n: int, r: float, valeur_reversion: float) -> dict:44    """Décomposition d'un droit temporaire (usufruit, bail au locateur) et de la réversion (nue-propriété)."""45    va_droit = revenu_annuel * fin.facteur_va_annuite(r, n)46    va_rev = valeur_reversion * fin.facteur_va(r, n)47    return {"droit_temporaire": va_droit, "reversion": va_rev, "total": va_droit + va_rev}484950def usufruit_nue_propriete(revenu_annee1: float, g: float, r: float, n: int) -> dict:51    """Usufruit de n ans sur un bien dont le revenu net croît au taux g : VA de l'annuité croissante ; nue-propriété = réversion52    V_n = R_{n+1}/(r−g) actualisée ; la somme redonne la pleine propriété V_0 = R_1/(r−g) (identité de Gordon)."""53    flux = [revenu_annee1 * (1 + g) ** (t - 1) for t in range(1, n + 1)]54    va_usufruit = fin.van([0.0] + flux, r)55    v_pleine = revenu_annee1 / (r - g)56    v_n = revenu_annee1 * (1 + g) ** n / (r - g)57    va_nue = v_n * fin.facteur_va(r, n)58    return {"V_pleine": v_pleine, "usufruit": va_usufruit, "nue_propriete": va_nue, "somme": va_usufruit + va_nue, "V_n": v_n,59            "part_usufruit": va_usufruit / v_pleine}606162def va_ecart_rotation(ecart_net_annee1: float, r: float, g: float, rho: float, T: int | None = None) -> float:63    """Valeur actuelle de l'écart contractuel/marché sous rotation ρ : écart_t = Δ_1 [(1−ρ)(1+g)]^{t−1}.64    Perpétuité (T=None) : Δ_1 / (r − g + ρ(1+g)) ; sinon somme finie."""65    q = (1 - rho) * (1 + g) / (1 + r)66    if T is None:67        if q >= 1:68            raise ValueError("série divergente : (1−ρ)(1+g) ≥ 1+r")69        return ecart_net_annee1 / (1 + r) / (1 - q)70    return ecart_net_annee1 / (1 + r) * (1 - q ** T) / (1 - q) if abs(1 - q) > 1e-14 else ecart_net_annee1 * T / (1 + r)717273# ----------------------------------------------------------------------------- décomposition par le module partagé7475def decomposition(d: dict, rotation: float | None = None, taux: float | None = None) -> dict:76    """V_pleine (libre de baux, loyers de marché) − V_locateur (baux en place) = valeur agrégée des baux pour les locataires.77    Les deux valeurs sont des DCF de `traite.cas3` avec les mêmes hypothèses (dépenses, CAPEX, taux terminal, coûts de vente)."""78    hyp = copy.deepcopy(d["hyp"])79    if rotation is not None:80        hyp["rotation_annuelle"] = rotation81    if taux is not None:82        hyp["taux_actualisation"] = taux83    proj_c = cas3.projection_dcf(d["etat"], d["dep"], hyp)84    proj_m = cas3.projection_dcf(etat_marche(d["etat"]), d["dep"], hyp)85    v_c = cas3.valeur_dcf(proj_c, hyp); v_m = cas3.valeur_dcf(proj_m, hyp)86    rne_c = cas3.rne(d["etat"], d["dep"], hyp); rne_m = cas3.rne(etat_marche(d["etat"]), d["dep"], hyp)87    return {"rotation": hyp["rotation_annuelle"], "taux": hyp["taux_actualisation"], "proj_contrat": proj_c, "proj_marche": proj_m,88            "V_locateur": v_c["valeur"], "V_pleine": v_m["valeur"], "V_locataires": v_m["valeur"] - v_c["valeur"],89            "part_locataires": (v_m["valeur"] - v_c["valeur"]) / v_m["valeur"],90            "va_flux_contrat": v_c["va_flux"], "va_flux_marche": v_m["va_flux"], "va_vt_contrat": v_c["va_terminale"], "va_vt_marche": v_m["va_terminale"],91            "rne1_contrat": rne_c["rne"], "rne1_marche": rne_m["rne"], "rbe_contrat": rne_c["rbe"], "rbe_marche": rne_m["rbe"]}929394def capitalisation_naive(dec: dict, taux_cap: float) -> dict:95    """Erreur classique : capitaliser l'écart de RNE de l'année 1 à perpétuité (comme si les baux ne se renouvelaient jamais au marché)."""96    v_c = fin.capitalisation_directe(dec["rne1_contrat"], taux_cap); v_m = fin.capitalisation_directe(dec["rne1_marche"], taux_cap)97    return {"Vcap_contrat": v_c, "Vcap_marche": v_m, "ecart_capitalise": v_m - v_c}9899100def sensibilite(d: dict) -> pd.DataFrame:101    rows = []102    for rho in ROTATIONS:103        for r in TAUX:104            dec = decomposition(d, rotation=rho, taux=r)105            rows.append({"rotation": rho, "taux": r, "V_locateur": dec["V_locateur"], "V_pleine": dec["V_pleine"],106                         "V_locataires": dec["V_locataires"], "part": dec["part_locataires"]})107    return pd.DataFrame(rows)108109110# ----------------------------------------------------------------------------- exercices111112def exercices(d: dict, base: dict) -> dict:113    r = d["hyp"]["taux_actualisation"]114    e = ecart_par_logement(d["etat"])115    l103 = e.loc[e["logement"] == 103].iloc[0]116    ex3 = {"ecart_mensuel": float(l103["ecart_mensuel"]), "ecart_annuel": float(l103["ecart_annuel"]),117           "va_5ans": valeur_bail_locataire(float(l103["ecart_annuel"]), 5, r), "va_10ans": valeur_bail_locataire(float(l103["ecart_annuel"]), 10, r),118           "va_perpetuite": float(l103["ecart_annuel"]) / r}119    # ex 4 : usufruit 15 ans sur un immeuble de 600 000 $ ; revenu net 30 000 $ croissant à 1 %/an ; r = 6 % (V = R_1/(r−g) = 600 000 $)120    ex4 = usufruit_nue_propriete(30000.0, 0.01, 0.06, 15)121    # ex 5 : valeur au rôle fictive du cas 1, 468 000 $ au 2024-07-01 ; indice +4,43 %/an ; date d'évaluation 2026-09-01 (26 mois)122    ex5 = {"role": 468000.0, "mois": 26, "indice": 1.0443 ** (26 / 12), "valeur_indexee": 468000.0 * 1.0443 ** (26 / 12)}123    # ex 6 : part des locataires selon la rotation (taux de base) — repris de la sensibilité124    # ex 7 : hypothèse extraordinaire : décontamination 150 000 $ + stigmate 5 % sur la valeur DCF du locateur125    v = base["V_locateur"]126    ex7 = {"V_hyp": v, "couts": 150000.0, "stigmate": 0.05, "V_sans_hyp": (v - 150000.0) * 0.95, "incidence": v - (v - 150000.0) * 0.95}127    return {"ex3": ex3, "ex4": ex4, "ex5": ex5, "ex7": ex7}128129130# ----------------------------------------------------------------------------- principal131132def main(verbose: bool = True) -> dict:133    d = cas3.charger()134    rev = cas3.revenus(d["etat"], d["hyp"])135    e = ecart_par_logement(d["etat"])136    base = decomposition(d)137    cap = capitalisation_naive(base, cas3.taux_extraits(d["comps"])["mediane"])138    hyp = d["hyp"]; r = hyp["taux_actualisation"]; g = hyp["croissance_loyers_marche"]; rho = hyp["rotation_annuelle"]; v = hyp["taux_inoccupation_pertes"]139    ecart_net1 = rev["ecart_marche"] * (1 - v)140    ferme = {"ecart_net_annee1": ecart_net1,141             "perpetuite_rotation": va_ecart_rotation(ecart_net1, r, g, rho),142             "perpetuite_sans_rotation": va_ecart_rotation(ecart_net1, r, g, 0.0),143             "horizon10_rotation": va_ecart_rotation(ecart_net1, r, g, rho, T=10),144             "annuite_5ans": valeur_bail_locataire(ecart_net1, 5, r), "annuite_10ans": valeur_bail_locataire(ecart_net1, 10, r)}145    sens = sensibilite(d)146    ex = exercices(d, base)147    res = {"d": d, "revenus": rev, "ecarts": e, "base": base, "cap": cap, "ferme": ferme, "sensibilite": sens, "exercices": ex}148    if verbose:149        pd.set_option("display.width", 200); pd.set_option("display.max_columns", 20)150        print("=== Cas 3 — écart contractuel / marché (données synthétiques) ===")151        print(f"RBP contractuel {rev['rbp_contrat']:,.0f} $ ; RBP au marché {rev['rbp_marche']:,.0f} $ ; écart {rev['ecart_marche']:,.0f} $/an "152              f"({rev['ecart_marche']/rev['rbp_marche']*100:.1f} % du marché) ; écart net de vacance (3,9 %) {ecart_net1:,.0f} $")153        print(e.to_string(index=False))154        print(f"ratio contrat/marché : moyenne {e['ratio_contrat_marche'].mean():.3f}, min {e['ratio_contrat_marche'].min():.3f} (log. {int(e.loc[e['ratio_contrat_marche'].idxmin(),'logement'])}), max {e['ratio_contrat_marche'].max():.3f}")155        print("\n=== Décomposition DCF (hypothèses du cas 3 : r = 7,25 %, rotation 15 %, g_c 3,1 %, g_m 3,5 %, R_T 5 %, c_v 2,5 %) ===")156        for k in ["rne1_contrat", "rne1_marche", "rbe_contrat", "rbe_marche", "va_flux_contrat", "va_flux_marche", "va_vt_contrat", "va_vt_marche", "V_locateur", "V_pleine", "V_locataires"]:157            print(f"{k:18s} {base[k]:>14,.2f} $")158        print(f"part des locataires : {base['part_locataires']*100:.2f} %")159        print("\nProjection contractuelle vs marché (RBP) :")160        pc, pm = base["proj_contrat"], base["proj_marche"]161        print(pd.DataFrame({"annee": pc["annee"], "rbp_contrat": pc["rbp"], "rbp_marche_scen": pm["rbp"], "ecart_rbp": pm["rbp"] - pc["rbp"],162                            "rne_contrat": pc["rne"], "rne_marche": pm["rne"], "ecart_rne": pm["rne"] - pc["rne"]}).round(0).to_string(index=False))163        print("\n=== Capitalisation naïve de l'écart (taux médian extrait) ===")164        print({k: round(x, 0) for k, x in cap.items()})165        print("\n=== Formules fermées (écart net année 1, g = 3,5 %) ===")166        print({k: round(x, 0) for k, x in ferme.items()})167        print("\n=== Sensibilité rotation × taux (V_locataires et part) ===")168        print(sens.assign(logements_an=(sens["rotation"] * 12).round(0), V_locataires=sens["V_locataires"].round(0), part=(sens["part"] * 100).round(2), V_locateur=sens["V_locateur"].round(0), V_pleine=sens["V_pleine"].round(0)).to_string(index=False))169        print("\n=== Exercices ===")170        for k, x in ex.items():171            print(k, {kk: (round(vv, 4) if isinstance(vv, float) else vv) for kk, vv in x.items()})172    return res173174175if __name__ == "__main__":176    main()177
Files · fd7f44c8789c8b7d81811c1898b290bc997b5c13
Menu

Theme