SPB Git
23.4 KB · 319 lines python
1#!/usr/bin/env python32"""Chapitre 26 — Baux et loyers : calculs de l'exemple intégral (cas 3, 12 logements à Montréal ; cas 2, triplex à Québec ;3DONNÉES SYNTHÉTIQUES) et des exercices.45Tous les chiffres cités dans manuscript/chapters/ch26.tex et solutions/ch26.tex sortent d'ici. Les revenus du cas 3 et du6cas 2 viennent des modules partagés traite.cas3 / traite.cas2 (fonctions `charger`, `revenus`, `rne`, `taux_extraits`,7`indicateurs_marche`) afin de coïncider avec les chapitres 2, 27-31 et 46-47 ; les facteurs d'actualisation viennent de8traite.finance. La formule fermée de la valeur actuelle de l'écart contractuel/marché sous rotation géométrique est celle9du chapitre 2 (éq. « rotation-va ») ; elle est appliquée ici logement par logement.1011Hypothèses de travail propres au chapitre (déclarées comme telles dans le texte, pas des statistiques) :12  - profil mensuel de consommation d'électricité d'un 4½ chauffé à l'électricité, 12 000 kWh/an (PROFIL_KWH) ;13  - loyer « chauffé-éclairé » d'un comparable fictif : 1 450 $/mois (COMPARABLE_CHAUFFE) ;14  - structure du tarif D d'Hydro-Québec en vigueur le 1er avril 2026 (source : hydrotarifs2026, section 2.5).1516Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch26_etat_locatif.py17"""18from __future__ import annotations19import sys, pathlib, datetime as dt20import numpy as np, pandas as pd21sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))22from traite import cas3, cas2, finance as fin2324ROOT = pathlib.Path(__file__).resolve().parents[3]25DATE_EVAL = dt.date(2026, 9, 1)2627# ----------------------------------------------------------------------------- tarif D (Hydro-Québec, 1er avril 2026)28TARIF_D = {"redevance_par_jour": 0.46154, "prix_tranche1": 0.07065, "seuil_kwh_par_jour": 40.0, "prix_tranche2": 0.11142}29# Hypothèse de travail (pas une statistique) : consommation mensuelle d'un 4½ de 70 m² chauffé à l'électricité, 12 000 kWh/an.30MOIS = ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."]31JOURS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]32PROFIL_KWH = [1900, 1700, 1400, 1000, 700, 550, 500, 500, 600, 850, 1150, 1150]33COMPARABLE_CHAUFFE = 1450.0   # $/mois, loyer « chauffé-éclairé » d'un comparable fictif (hypothèse pédagogique)34GESTION = 0.04                # convention du cas 3 : gestion imputée à 4 % du RBE353637# ----------------------------------------------------------------------------- état locatif et indicateurs38def tableau_etat(etat: pd.DataFrame, date_eval: dt.date = DATE_EVAL) -> pd.DataFrame:39    """État locatif enrichi : ancienneté (années), écarts mensuel/annuel, ratio contrat/marché, mois avant l'échéance."""40    t = etat.copy()41    t["anciennete"] = date_eval.year - t["locataire_depuis"].astype(int)42    t["ecart_mensuel"] = t["loyer_mensuel_marche"] - t["loyer_mensuel_contrat"]43    t["ecart_annuel"] = 12 * t["ecart_mensuel"]44    t["ecart_pct"] = t["ecart_mensuel"] / t["loyer_mensuel_marche"]45    t["ratio"] = t["loyer_mensuel_contrat"] / t["loyer_mensuel_marche"]46    ech = pd.to_datetime(t["echeance_bail"]).dt.date47    t["mois_echeance"] = [round((e - date_eval).days / 30.4375, 1) for e in ech]48    t["loyer_m2_contrat"] = t["loyer_mensuel_contrat"] / t["superficie_m2"]49    t["loyer_m2_marche"] = t["loyer_mensuel_marche"] / t["superficie_m2"]50    return t515253def indicateurs(etat: pd.DataFrame, hyp: dict, seuils=(0.05, 0.10, 0.20)) -> dict:54    """Indicateurs synthétiques de l'état locatif (éq. 26.1 à 26.4 du chapitre)."""55    t = tableau_etat(etat)56    r = cas3.revenus(etat, hyp)                       # RBP contrat / marché / écart (module partagé)57    lc, lm = t["loyer_mensuel_contrat"].astype(float), t["loyer_mensuel_marche"].astype(float)58    out = {**r, "n": len(t),59           "ecart_pondere": float((lm - lc).sum() / lm.sum()),           # δ̄ = Σ(Lm − Lc) / Σ Lm60           "ratio_pondere": float(lc.sum() / lm.sum()),61           "ratio_moyen_simple": float(t["ratio"].mean()),62           "ecart_mensuel_moyen": float((lm - lc).mean()),63           "ecart_mensuel_median": float((lm - lc).median()),64           "anciennete_moyenne": float(t["anciennete"].mean()), "anciennete_mediane": float(t["anciennete"].median()),65           "anciennete_max": int(t["anciennete"].max()),66           "loyer_moyen_contrat": float(lc.mean()), "loyer_moyen_marche": float(lm.mean()),67           "loyer_m2_contrat_moyen": float(lc.sum() / t["superficie_m2"].sum()), "loyer_m2_marche_moyen": float(lm.sum() / t["superficie_m2"].sum()),68           "part_ecart_top3": float((lm - lc).sort_values(ascending=False).head(3).sum() / (lm - lc).sum()),69           "nb_stationnements": int(t["stationnement"].sum()), "tableau": t}70    for s in seuils:71        out[f"part_sous_marche_{int(s*100)}"] = float((t["ratio"] < 1 - s).mean())72        out[f"nb_sous_marche_{int(s*100)}"] = int((t["ratio"] < 1 - s).sum())73    return out747576# ----------------------------------------------------------------------------- rotation, survie et valeur de l'écart77def survie(rho: float, t: int | np.ndarray) -> float | np.ndarray:78    """Probabilité qu'un locataire en place le soit encore après t années (départs géométriques, taux ρ)."""79    return (1 - rho) ** t808182def esperance_duree(rho: float) -> float:83    """Durée espérée de l'occupation restante, E[n] = Σ t ρ(1−ρ)^(t−1) = 1/ρ (années ; départ compté en fin d'année)."""84    if not 0 < rho <= 1:85        raise ValueError("ρ doit être dans ]0, 1]")86    return 1.0 / rho878889def va_ecart(delta1: float, r: float, g: float, rho: float) -> float:90    """VA espérée de l'écart contractuel/marché d'un logement : Δ₁ reçu avec certitude l'année 1, puis l'écart croît à g et91    survit avec probabilité (1−ρ) chaque année → Δ₁ / (r − g + ρ(1+g)) (formule fermée du chapitre 2, éq. rotation-va).92    Condition de convergence : (1−ρ)(1+g) < 1+r."""93    if (1 - rho) * (1 + g) >= 1 + r:94        raise ValueError("série divergente : (1−ρ)(1+g) ≥ 1+r")95    return delta1 / (r - g + rho * (1 + g))969798def va_ecart_serie(delta1: float, r: float, g: float, rho: float, T: int = 400) -> float:99    """Même valeur par sommation explicite (vérification numérique de la formule fermée)."""100    t = np.arange(1, T + 1)101    return float(np.sum(delta1 * ((1 - rho) * (1 + g)) ** (t - 1) / (1 + r) ** t))102103104def va_annuite(delta: float, r: float, n: float) -> float:105    """Annuité d'un écart constant pendant n années (éq. « annuite » du chapitre 2) ; n peut être non entier."""106    return delta * (1 - (1 + r) ** (-n)) / r107108109def va_ecart_logements(etat: pd.DataFrame, hyp: dict, rhos=(0.085, 0.094, 0.15), r: float | None = None, g: float | None = None) -> pd.DataFrame:110    """VA de l'écart par logement, nette de l'inoccupation (convention du chapitre 2 : Δ₁ = 12 × écart mensuel × (1 − v))."""111    t = tableau_etat(etat); r = hyp["taux_actualisation"] if r is None else r; g = hyp["croissance_loyers_marche"] if g is None else g112    v = hyp["taux_inoccupation_pertes"]113    out = t[["logement", "pieces", "anciennete", "ecart_mensuel", "ecart_annuel"]].copy()114    out["delta1_net"] = out["ecart_annuel"] * (1 - v)115    for rho in rhos:116        out[f"va_rho_{rho:.3f}"] = [va_ecart(d, r, g, rho) for d in out["delta1_net"]]117    out["va_perpetuel"] = out["delta1_net"] / (r - g)118    return out119120121def sensibilite_va(etat: pd.DataFrame, hyp: dict, rhos=(0.05, 0.085, 0.094, 0.15, 0.25), rs=(0.0625, 0.0725, 0.0825), gs=(0.025, 0.035, 0.045)) -> dict:122    """VA agrégée de l'écart (12 logements) selon ρ × r (g du cas) et ρ × g (r du cas)."""123    t = tableau_etat(etat); v = hyp["taux_inoccupation_pertes"]; d1 = float(t["ecart_annuel"].sum() * (1 - v))124    g0, r0 = hyp["croissance_loyers_marche"], hyp["taux_actualisation"]125    tab_r = pd.DataFrame({f"r={r:.4f}": [va_ecart(d1, r, g0, rho) for rho in rhos] for r in rs}, index=[f"rho={rho:.3f}" for rho in rhos])126    tab_g = pd.DataFrame({f"g={g:.3f}": [va_ecart(d1, r0, g, rho) for rho in rhos] for g in gs}, index=[f"rho={rho:.3f}" for rho in rhos])127    return {"delta1_net": d1, "par_taux": tab_r, "par_croissance": tab_g, "sans_rotation": d1 / (r0 - g0)}128129130# ----------------------------------------------------------------------------- loyer économique131def loyer_economique(base: dict, R: float, V: float, n_logements: int, m: float = GESTION) -> dict:132    """Loyer économique (loyer de faisabilité) : RBP requis pour que le RNE après réserve rémunère la valeur V au taux R,133    compte tenu des dépenses hors gestion DE_hg, de la réserve RR, des autres revenus A, de l'inoccupation v et d'une134    gestion à m × RBE :  RNE* = R·V ;  RBE* = (RNE* + DE_hg + RR)/(1 − m) ;  RBP* = (RBE* − A)/(1 − v)."""135    det = base["detail"]; v = base["taux_vacance_pertes"]; A = base["autres_revenus"]136    gestion = float(det.loc[det["poste"].str.startswith("Gestion"), "normalise"].sum())137    de_hg = base["depenses_hors_reserve"] - gestion; RR = base["reserve"]138    rne_req = R * V; rbe_req = (rne_req + de_hg + RR) / (1 - m); rbp_req = (rbe_req - A) / (1 - v)139    return {"R": R, "V": V, "rne_requis": rne_req, "rbe_requis": rbe_req, "rbp_requis": rbp_req, "de_hors_gestion": de_hg, "reserve": RR,140            "loyer_economique_mensuel": rbp_req / (12 * n_logements), "loyer_contrat_mensuel": base["rbp_contrat"] / (12 * n_logements),141            "loyer_marche_mensuel": base["rbp_marche"] / (12 * n_logements)}142143144# ----------------------------------------------------------------------------- normalisation : électricité (tarif D)145def cout_electricite_mensuel(kwh: float, jours: int, tarif: dict = TARIF_D) -> dict:146    """Coût mensuel hors taxes de vente selon la structure du tarif D : redevance × jours + 1re tranche (≤ 40 kWh/jour) + 2e tranche."""147    seuil = tarif["seuil_kwh_par_jour"] * jours148    t1 = min(kwh, seuil); t2 = max(kwh - seuil, 0.0)149    red = tarif["redevance_par_jour"] * jours150    return {"kwh": kwh, "jours": jours, "redevance": red, "tranche1_kwh": t1, "tranche2_kwh": t2,151            "cout_tranche1": t1 * tarif["prix_tranche1"], "cout_tranche2": t2 * tarif["prix_tranche2"],152            "total": red + t1 * tarif["prix_tranche1"] + t2 * tarif["prix_tranche2"]}153154155def normalisation_electricite(profil=PROFIL_KWH, jours=JOURS, loyer_chauffe: float = COMPARABLE_CHAUFFE, tarif: dict = TARIF_D) -> dict:156    rows = [{"mois": m, **cout_electricite_mensuel(k, j, tarif)} for m, k, j in zip(MOIS, profil, jours)]157    tab = pd.DataFrame(rows)158    annuel = float(tab["total"].sum()); mensuel = annuel / 12159    return {"tableau": tab, "kwh_annuel": float(sum(profil)), "cout_annuel": annuel, "cout_mensuel_moyen": mensuel,160            "cout_moyen_kwh": annuel / sum(profil), "part_redevance": float(tab["redevance"].sum() / annuel),161            "kwh_tranche2": float(tab["tranche2_kwh"].sum()), "mois_depassement": int((tab["tranche2_kwh"] > 0).sum()),162            "loyer_chauffe": loyer_chauffe, "loyer_nu": loyer_chauffe - mensuel,163            "cout_mensuel_indexe_2027": mensuel * 1.03}   # hausse domestique 3,0 % (Régie, D-2026-033) appliquée à toute la facture, approximation164165166# ----------------------------------------------------------------------------- calendrier des avis167def _moins_mois(d: dt.date, k: int) -> dt.date:168    """Date d, moins k mois (même quantième, borné à la fin du mois)."""169    y, m = d.year, d.month - k170    while m <= 0:171        m += 12; y -= 1172    while m > 12:173        m -= 12; y += 1174    import calendar175    return dt.date(y, m, min(d.day, calendar.monthrange(y, m)[1]))176177178def calendrier(echeance: dt.date = dt.date(2027, 6, 30), duree_mois: int = 12) -> dict:179    """Fenêtres légales (C.c.Q.) pour un bail à durée fixe se terminant à `echeance` :180    avis de modification 3 à 6 mois avant le terme (art. 1942 ; 1 à 2 mois si bail < 12 mois) ; réponse du locataire dans le181    mois (1945) ; demande de fixation dans le mois du refus (1947) ; avis de reprise/éviction 6 mois avant (1960 ; 1 mois si182    bail ≤ 6 mois) ; nouveau locataire : 10 jours (1950) après la conclusion du bail suivant."""183    if duree_mois >= 12:184        a_min, a_max = 3, 6185    else:186        a_min, a_max = 1, 2187    debut_fenetre, fin_fenetre = _moins_mois(echeance, a_max), _moins_mois(echeance, a_min)188    reprise = _moins_mois(echeance, 6 if duree_mois > 6 else 1)189    reponse_si_fin = _moins_mois(echeance, a_min - 1)          # avis reçu au dernier jour permis → réponse dans le mois190    demande_tal = _moins_mois(echeance, a_min - 2)              # refus au dernier jour → demande dans le mois191    debut_suivant = echeance + dt.timedelta(days=1)192    return {"echeance": echeance, "avis_modification_debut": debut_fenetre, "avis_modification_fin": fin_fenetre,193            "reponse_locataire_fin_si_avis_tardif": reponse_si_fin, "demande_fixation_fin_si_refus_tardif": demande_tal,194            "avis_reprise_eviction_fin": reprise, "debut_bail_suivant": debut_suivant,195            "nouveau_locataire_fixation_10j": debut_suivant + dt.timedelta(days=10), "nouveau_locataire_sans_avis_2mois": _moins_mois(debut_suivant, -2)}196197198# ----------------------------------------------------------------------------- loyer maximal des cinq ans (art. 1955)199def croissance_max_1955(loyer_initial: float, loyer_maximal: float, annees: int = 5) -> float:200    """Taux annuel composé maximal compatible avec le loyer maximal indiqué au bail (art. 1955 C.c.Q., 2024, c. 2)."""201    return (loyer_maximal / loyer_initial) ** (1 / annees) - 1202203204# ----------------------------------------------------------------------------- bail commercial : loyer net équivalent205def loyer_net_equivalent(loyer_brut_annuel: float, depenses_recuperables: float, superficie_m2: float) -> dict:206    net = loyer_brut_annuel - depenses_recuperables207    return {"loyer_brut": loyer_brut_annuel, "recuperables": depenses_recuperables, "loyer_net": net,208            "brut_m2": loyer_brut_annuel / superficie_m2, "net_m2": net / superficie_m2}209210211# ----------------------------------------------------------------------------- cas 2 (triplex)212def cas2_etat() -> dict:213    d = cas2.charger(); l = d["loyers"].copy(); r = cas2.revenus(l)214    l["anciennete"] = DATE_EVAL.year - l["locataire_depuis"].astype(int)215    l["ecart_mensuel"] = l["loyer_marche_estime"] - l["loyer_mensuel"]; l["ratio"] = l["loyer_mensuel"] / l["loyer_marche_estime"]216    return {**r, "tableau": l, "ecart_pondere": float(l["ecart_mensuel"].sum() / l["loyer_marche_estime"].sum()),217            "anciennete_moyenne": float(l["anciennete"].mean()), "nb_sous_marche_10": int((l["ratio"] < 0.9).sum()), "sujet": d["sujet"], "data": d}218219220# ----------------------------------------------------------------------------- tout221def calculer() -> dict:222    d = cas3.charger(); etat, dep, hyp, sujet = d["etat"], d["dep"], d["hyp"], d["sujet"]223    ind = indicateurs(etat, hyp); base = cas3.rne(etat, dep, hyp); tx = cas3.taux_extraits(d["comps"])224    R = tx["mediane"]; V_cap = fin.capitalisation_directe(base["rne"], R)225    econ_role = loyer_economique(base, R, hyp["valeur_role_2026"], sujet["nb_logements"])226    econ_cap = loyer_economique(base, R, V_cap, sujet["nb_logements"])227    va = va_ecart_logements(etat, hyp); sens = sensibilite_va(etat, hyp)228    r, g, rho = hyp["taux_actualisation"], hyp["croissance_loyers_marche"], hyp["rotation_annuelle"]229    va_agregee_cas = va_ecart(sens["delta1_net"], r, g, rho)       # = 102 188 $ (chapitre 2, ρ = 15 %)230    norm = normalisation_electricite(); cal = calendrier()231    return {"etat": etat, "hyp": hyp, "sujet": sujet, "ind": ind, "base": base, "R": R, "V_cap": V_cap,232            "econ_role": econ_role, "econ_cap": econ_cap, "va": va, "sens": sens, "va_agregee_cas": va_agregee_cas,233            "esperance_duree": {rho_: esperance_duree(rho_) for rho_ in (0.085, 0.094, 0.15, 0.13)},234            "norm": norm, "cal": cal, "cas2": cas2_etat()}235236237# ----------------------------------------------------------------------------- exercices238def exercices(res: dict | None = None) -> dict:239    res = res or calculer(); hyp = res["hyp"]; ex = {}240    # 26.1 — délais : bail 12 mois échéant le 30 juin 2027 et bail de 6 mois échéant le 31 décembre 2026241    ex["1"] = {"bail12": calendrier(dt.date(2027, 6, 30), 12), "bail6": calendrier(dt.date(2026, 12, 31), 6)}242    # 26.2 — indicateurs d'un état locatif fictif de six logements (énoncé)243    e2 = pd.DataFrame({"logement": ["1", "2", "3", "4", "5", "6"], "loyer_mensuel_contrat": [850, 925, 1180, 1240, 1410, 1495],244                       "loyer_mensuel_marche": [1100, 1100, 1300, 1300, 1550, 1550], "locataire_depuis": [2012, 2016, 2021, 2025, 2019, 2024],245                       "superficie_m2": [52, 52, 70, 70, 92, 92], "echeance_bail": ["2027-06-30"] * 6, "chauffage_inclus": [0] * 6, "stationnement": [1, 0, 1, 0, 1, 0]})246    hyp2 = {**hyp, "autres_revenus_annuels": 0.0, "taux_inoccupation_pertes": 0.03}247    ex["2"] = indicateurs(e2, hyp2)248    # 26.3 — normalisation : comparable chauffé-éclairé 1 450 $, profil de consommation donné ; variante 1 500 $ et 14 000 kWh (profil × 14/12)249    ex["3"] = {"base": normalisation_electricite(), "variante": normalisation_electricite(profil=[k * 14000 / 12000 for k in PROFIL_KWH], loyer_chauffe=1500.0)}250    # 26.4 — VA de l'écart du logement 103 (319 $/mois) : ρ = 9,4 % vs 15 % ; annuité sur E[n] = 1/ρ années ; perpétuel251    d103 = 12 * 319 * (1 - hyp["taux_inoccupation_pertes"]); r, g = hyp["taux_actualisation"], hyp["croissance_loyers_marche"]252    ex["4"] = {"delta1_net": d103, "va_094": va_ecart(d103, r, g, 0.094), "va_150": va_ecart(d103, r, g, 0.15),253               "E_n_094": esperance_duree(0.094), "E_n_150": esperance_duree(0.15),254               "annuite_094": va_annuite(d103, r, esperance_duree(0.094)), "annuite_150": va_annuite(d103, r, esperance_duree(0.15)),255               "perpetuel": d103 / (r - g), "serie_094": va_ecart_serie(d103, r, g, 0.094)}256    # 26.5 — loyer économique du triplex (cas 2) sur la valeur au rôle 495 000 $ et sur la valeur par capitalisation257    c2 = res["cas2"]["data"]; base2 = cas2.rne(c2["loyers"], c2["depenses"]); comp2 = cas2.comparables_plex(c2["ventes"], c2["sujet"]); ind2 = cas2.indicateurs_marche(comp2)258    R2 = ind2["taux_median"]; V2_role = c2["sujet"]["valeur_role_2025"]; V2_cap = fin.capitalisation_directe(base2["rne"], R2)259    det2 = base2["detail"]; gestion2 = float(det2.loc[det2["type"] == "imputée", "normalise"].sum()); de_hg2 = base2["depenses_hors_reserve"] - gestion2260    def econ2(V):261        rne_req = R2 * V; rbe_req = (rne_req + de_hg2 + base2["reserve"]) / (1 - cas2.GESTION_IMPUTEE); rbp_req = rbe_req / (1 - cas2.TAUX_VACANCE_PERTES)262        return {"V": V, "rne_requis": rne_req, "rbe_requis": rbe_req, "rbp_requis": rbp_req, "loyer_mensuel_moyen": rbp_req / 36}263    ex["5"] = {"R": R2, "base": base2, "de_hors_gestion": de_hg2, "role": econ2(V2_role), "cap": econ2(V2_cap), "V_cap": V2_cap,264               "rbp_contrat": base2["rbp_contrat"], "rbp_marche": base2["rbp_marche"]}265    # 26.6 — immeuble neuf (art. 1955) : loyer 1 600 $, loyer maximal 1 850 $ indiqué au bail ; trois trajectoires sur cinq ans :266    #        (a) indexation au taux de base 3,1 % (sans clause : fixation possible), (b) saut au loyer maximal dès la 2e année (borne haute permise267    #        par la clause), (c) croissance régulière au taux g_max ; VA des loyers des années 1 à 5 au taux r du cas (7,25 %).268    L0, Lmax, gtal = 1600.0, 1850.0, 0.031269    gmax = croissance_max_1955(L0, Lmax)270    traj_tal = [L0 * (1 + gtal) ** t for t in range(0, 5)]271    traj_saut = [L0] + [Lmax] * 4272    traj_reg = [L0 * (1 + gmax) ** t for t in range(0, 5)]273    va = lambda traj: sum(12 * traj[t - 1] / (1 + r) ** t for t in range(1, 6))274    ex["6"] = {"g_max": gmax, "traj_tal": traj_tal, "traj_saut": traj_saut, "traj_reg": traj_reg, "va_tal": va(traj_tal), "va_saut": va(traj_saut), "va_reg": va(traj_reg),275               "loyer_an5_tal": traj_tal[4], "depasse_tal": traj_tal[4] > Lmax, "annee_depassement": next((t + 1 for t, L in enumerate(traj_tal) if L > Lmax), None),276               "loyer_an6_tal": L0 * (1 + gtal) ** 5}277    # 26.7 — bail commercial au rez-de-chaussée : brut 36 000 $/an, récupérables 9 500 $, 110 m², 5 ans fixes, option 5 ans ; VA à 7,25 %278    lne = loyer_net_equivalent(36000.0, 9500.0, 110.0)279    va_com = fin.van([0.0] + [lne["loyer_net"]] * 5, r)280    loyer_res = 1292.0 * 12 * (1 - hyp["taux_inoccupation_pertes"])        # logement de marché moyen du cas 3 (1 292 $) indexé à g281    va_res = sum(loyer_res * (1 + g) ** (t - 1) / (1 + r) ** t for t in range(1, 6))282    ex["7"] = {**lne, "va_net_5ans": va_com, "va_residentiel_5ans": va_res, "ratio_va": va_com / va_res, "loyer_res_an1_net": loyer_res}283    return ex284285286def _f(x): return f"{x:,.0f}".replace(",", " ")287288289if __name__ == "__main__":290    res = calculer(); ind = res["ind"]; t = ind["tableau"]291    print("=== Cas 3 — état locatif (données synthétiques)")292    print(t[["logement", "pieces", "superficie_m2", "loyer_mensuel_contrat", "loyer_mensuel_marche", "anciennete", "ecart_mensuel", "ecart_pct", "ratio", "echeance_bail", "mois_echeance"]].to_string(index=False))293    for k in ["rbp_contrat", "rbp_marche", "ecart_marche", "ecart_pondere", "ratio_pondere", "ratio_moyen_simple", "ecart_mensuel_moyen", "ecart_mensuel_median",294              "anciennete_moyenne", "anciennete_mediane", "anciennete_max", "part_sous_marche_5", "part_sous_marche_10", "part_sous_marche_20",295              "nb_sous_marche_5", "nb_sous_marche_10", "nb_sous_marche_20", "loyer_moyen_contrat", "loyer_moyen_marche", "loyer_m2_contrat_moyen", "loyer_m2_marche_moyen", "part_ecart_top3"]:296        print(f"{k:28s} {ind[k]:.4f}" if isinstance(ind[k], float) else f"{k:28s} {ind[k]}")297    print("\n=== Loyer économique (R médian = %.4f %%)" % (100 * res["R"]))298    for lab, e in (("sur valeur au rôle 2 200 000 $", res["econ_role"]), ("sur valeur par capitalisation", res["econ_cap"])):299        print(lab, {k: round(v, 2) for k, v in e.items()})300    print("\n=== VA de l'écart par logement (r = %.4f, g = %.3f ; nette de l'inoccupation)" % (res["hyp"]["taux_actualisation"], res["hyp"]["croissance_loyers_marche"]))301    print(res["va"].round(0).to_string(index=False)); print("totaux :", res["va"].drop(columns=["logement", "pieces"]).sum().round(0).to_dict())302    print("VA agrégée ρ = 15 % (chapitre 2) :", round(res["va_agregee_cas"], 0), "; sans rotation :", round(res["sens"]["sans_rotation"], 0))303    print("Durées espérées 1/ρ :", {k: round(v, 2) for k, v in res["esperance_duree"].items()})304    print("\n=== Sensibilité (VA agrégée)"); print(res["sens"]["par_taux"].round(0)); print(res["sens"]["par_croissance"].round(0))305    n = res["norm"]; print("\n=== Normalisation électricité (tarif D, hypothèse 12 000 kWh/an)")306    print(n["tableau"].round(2).to_string(index=False))307    print({k: round(v, 2) for k, v in n.items() if k != "tableau"})308    print("\n=== Calendrier (bail 2026-07-01 → 2027-06-30)"); print(res["cal"])309    c2 = res["cas2"]; print("\n=== Cas 2 — triplex"); print(c2["tableau"].to_string(index=False))310    print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in c2.items() if k not in ("tableau", "sujet", "data")})311    print("\n=== Exercices"); ex = exercices(res)312    for k, v in ex.items():313        print(f"--- {k}")314        if isinstance(v, dict):315            for kk, vv in v.items():316                if isinstance(vv, (int, float)): print(f"  {kk}: {vv:.4f}")317                elif isinstance(vv, dict): print(f"  {kk}: {{" + ", ".join(f"{a}: {round(b, 4) if isinstance(b, float) else b}" for a, b in vv.items() if not isinstance(b, (pd.DataFrame, dict))) + "}")318                elif isinstance(vv, list): print(f"  {kk}: {[round(x, 2) for x in vv]}")319
Files · 5d3405eba4a492b45545bd8aa847669ebad92a1d
Menu

Theme