SPB Git
30.1 KB · 355 lines python
1"""Chapitre 46 — Cas 2 : triplex à Québec (Limoilou), dossier complet (DONNÉES SYNTHÉTIQUES, graine 20261031).23Tout chiffre cité dans manuscript/chapters/ch46.tex et solutions/ch46.tex provient de ce script. Il s'appuie sur :4- traite.cas2 (revenus, RNE, 127 ventes de plex, indicateurs) — partagé avec les ch. 26, 27, 28 et 32 ;5- analysis/python/cases/ch32_plex.py (sélection des cinq comparables triplex de Limoilou, régression taux/loyer relatif,6  financement résidentiel assuré, coût d'occupation) — importé, jamais recalculé autrement ;7- traite.finance (Hypotheque, capitalisation_directe, rne), traite.comparables (ComparableAjuste, poids_inverse_brut,8  reconciliation, ajustement_temps_taux), traite.cout (cout_par_superficie_ajuste, depreciation_age_vie, valeur_par_cout).910Ce que le chapitre 46 AJOUTE au chapitre 32 : normalisation des taxes aux tarifs 2026 de la Ville de Québec (RNE normalisé),11grille d'ajustements explicites en $/logement sur les cinq comparables (régression auxiliaire sur les 127 ventes), indication12par le coût (contrôle), droits de mutation de la Ville de Québec, valeur de la faculté de reprise, réconciliation et rapport.1314Exécution : python3 analysis/python/cases/ch46_dossier.py   (depuis la racine du dépôt)15"""16from __future__ import annotations17import sys, pathlib18import numpy as np, pandas as pd1920ROOT = pathlib.Path(__file__).resolve().parents[3]21sys.path.insert(0, str(ROOT / "analysis/python")); sys.path.insert(0, str(ROOT / "analysis/python/cases"))22from traite import cas2, finance as fin, comparables as cmp, cout  # noqa: E40223import ch32_plex as C  # noqa: E4022425GRAINE = 2026103126DATE_EVAL = "2026-09-01"2728# ----------------------------------------------------------------------------- paramètres déclarés (sources : notes-E, notes-B, notes-C, ch. 21/24)29TAUX_TAXE_1A5 = 0.7464 / 100          # Ville de Québec 2026, résidentiel 1 à 5 logements, $/100 $ (R.V.Q. 3492 ; SRC-E-027)30TARIF_AQUEDUC_EGOUT = 386.0           # $/logement (idem)31TARIF_MATIERES_RESIDUELLES = 195.0    # $/logement (idem)32TAUX_TAXE_SCOLAIRE = 0.07899 / 100    # 2026-2027, $/100 $ (SRC-E-026) ; exemption des premiers 25 000 $33EXEMPTION_SCOLAIRE = 25_000.034FACTEUR_COMPARATIF_2026 = 1.08        # Ville de Québec, facteur comparatif 2026 (source secondaire, SRC-A-023)35# Droits sur les mutations immobilières, Ville de Québec, 2026 (LDMI art. 2 + taux municipaux ; SRC-A-023, partiellement vérifié)36TRANCHES_MUTATION_QUEBEC = [(62_900.0, 0.005), (315_000.0, 0.01), (500_000.0, 0.015), (750_000.0, 0.025), (float("inf"), 0.03)]37# Coût (contrôle seulement) : coût unitaire du cas 1 au T3 2026 (ch. 21, 2 409 $/m² habitable pour 165 m²), élasticité de taille −0,10,38# ratio IPCB « appartements < 5 étages » / « maison individuelle », Montréal, T3 2026 (118,5 / 116,5 ; tableau 18-10-0289-01)39COUT_UNITAIRE_CAS1, SUPERFICIE_CAS1, ELASTICITE = 2409.0, 165.0, -0.1040IPCB_APPART, IPCB_MAISON = 118.5, 116.541VIE_ECONOMIQUE = 90.0                  # hypothèse du dossier : plex de brique de 1948, vie économique totale42PHI_ETAT3 = 0.5                        # fraction de vie consommée associée à la note d'état 3 (convention ch. 11/24)43TERRAIN_PAR_M2 = 900.0                 # HYPOTHÈSE DU DOSSIER (non vérifiée) : valeur unitaire du terrain, Limoilou44POIDS_46 = {"par_logement_ajuste": 0.40, "mrb": 0.35, "capitalisation": 0.25}   # pondération de l'ouvrage pour un triplex de quartier central (ch. 32, sect. 32.4)45T_MIN = 2.0                            # seuil de significativité (|t|) pour appliquer un coefficient de la régression auxiliaire dans la grille46PAS_ARRONDI = 5_000.047ROTATION_QUEBEC = 0.13                 # taux de rotation des locataires, RMR de Québec, SCHL oct. 2025 (SRC-E-007)48G_MARCHE, G_TAL, R_ACT = 0.035, 0.031, 0.06   # croissance des loyers de marché, pourcentage de base TAL 2026, taux d'actualisation (hypothèses, ch. 26/32)495051# ----------------------------------------------------------------------------- 46.5 normalisation des taxes et RNE52def taxes_2026(valeur_role: float, nb_logements: int) -> dict:53    """Taxes municipales (taux 1-5 logements + tarifs par logement) et taxe scolaire (exemption des premiers 25 000 $) sur la valeur au rôle."""54    fonciere = valeur_role * TAUX_TAXE_1A555    tarifs = nb_logements * (TARIF_AQUEDUC_EGOUT + TARIF_MATIERES_RESIDUELLES)56    scolaire = max(valeur_role - EXEMPTION_SCOLAIRE, 0.0) * TAUX_TAXE_SCOLAIRE57    return {"fonciere": fonciere, "tarifs": tarifs, "municipale": fonciere + tarifs, "scolaire": scolaire, "total": fonciere + tarifs + scolaire}585960def depenses_normalisees_2026(dep: pd.DataFrame, sujet: dict) -> pd.DataFrame:61    """Dépenses du dossier avec les taxes recalculées aux taux 2026 (les autres postes sont conservés : polices, contrats, factures)."""62    t = taxes_2026(sujet["valeur_role_2025"], sujet["nb_logements"])63    d = dep.copy(); d["declare"] = d["montant_annuel"].astype(float); d["montant_annuel"] = d["montant_annuel"].astype(float)64    d.loc[d["poste"] == "Taxes municipales", "montant_annuel"] = t["municipale"]65    d.loc[d["poste"] == "Taxe scolaire", "montant_annuel"] = t["scolaire"]66    return d676869def base_normalisee(d: dict) -> dict:70    """RNE du dossier (taxes 2026) par traite.cas2.rne, et RNE « déclaré » (ch. 32) pour mémoire."""71    dep_n = depenses_normalisees_2026(d["depenses"], d["sujet"])72    b = cas2.rne(d["loyers"], dep_n)73    b["detail"]["declare"] = dep_n["declare"].values74    b_decl = cas2.rne(d["loyers"], d["depenses"])75    b["rne_declare"] = b_decl["rne"]; b["depenses_declarees"] = b_decl["depenses_totales"]76    b["ecart_taxes"] = b_decl["depenses_totales"] - b["depenses_totales"]77    b["taxes"] = taxes_2026(d["sujet"]["valeur_role_2025"], d["sujet"]["nb_logements"])78    # RNE aux loyers de marché (même structure de dépenses, gestion imputée sur le RBE de marché)79    det = b["detail"]; dep_m = float(det.loc[~det["type"].isin(["imputée", "réserve"]), "normalise"].sum()) + cas2.GESTION_IMPUTEE * b["rbe_marche"]80    b["rne_marche"] = fin.rne(b["rbe_marche"], dep_m, b["reserve"])81    b["loyer_marche_moyen"] = float(d["loyers"]["loyer_marche_estime"].mean()); b["loyer_contrat_moyen"] = float(d["loyers"]["loyer_mensuel"].mean())82    b["rho"] = b["rbp_contrat"] / b["rbp_marche"]83    return b848586# ----------------------------------------------------------------------------- 46.6 régression auxiliaire et grille d'ajustements87VARS = ["mois", "ln_sup_log", "age", "etat_1a5", "stationnement", "ln_loyer_marche", "loyer_relatif", "triplex", "q_Saint-Sauveur", "q_Saint-Roch", "q_Maizerets"]888990def _matrice(c: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:91    g = c.copy()92    g["ln_sup_log"] = np.log(g["superficie_habitable_m2"] / g["nb_logements"])93    g["ln_loyer_marche"] = np.log(g["loyer_mensuel_moyen_marche"])94    g["loyer_relatif"] = g["loyer_mensuel_moyen_contrat"] / g["loyer_mensuel_moyen_marche"]95    g["triplex"] = (g["nb_logements"] == 3).astype(float)96    for q in ("Saint-Sauveur", "Saint-Roch", "Maizerets"): g[f"q_{q}"] = (g["quartier"] == q).astype(float)97    X = np.column_stack([np.ones(len(g))] + [g[v].to_numpy(dtype=float) for v in VARS])98    y = np.log(g["prix_par_logement"].to_numpy(dtype=float))99    return X, y100101102def regression_auxiliaire(c: pd.DataFrame) -> dict:103    """OLS : ln(prix par logement) sur l'ancienneté de la vente (mois), ln(m² par logement), l'âge, l'état, le stationnement,104    ln(loyer de marché moyen), le loyer relatif, un indicateur triplex et les quartiers (référence Limoilou). Erreurs-types classiques."""105    X, y = _matrice(c); n, k = X.shape106    beta, *_ = np.linalg.lstsq(X, y, rcond=None); res = y - X @ beta107    s2 = float(res @ res) / (n - k); cov = s2 * np.linalg.inv(X.T @ X)108    se = np.sqrt(np.diag(cov)); r2 = 1 - float(res @ res) / float(((y - y.mean()) ** 2).sum())109    noms = ["const"] + VARS110    return {"beta": dict(zip(noms, beta)), "se": dict(zip(noms, se)), "t": dict(zip(noms, beta / se)), "r2": r2, "sigma": float(np.sqrt(s2)), "n": n, "k": k,111            "taux_annuel_marche": float(np.exp(-12 * beta[1]) - 1)}   # croissance annuelle implicite des prix (mois = ancienneté de la vente)112113114def grille_ajustements(comp5: pd.DataFrame, sujet: dict, base: dict, reg: dict) -> dict:115    """Grille d'ajustements en $ par logement, ordre de l'ouvrage : conditions du marché (temps, %) puis attributs (additifs, $) :116    superficie par logement, âge, état, stationnement, niveau de loyer de marché, loyer relatif. Signe du point de vue du sujet."""117    b = reg["beta"]; n_s = sujet["nb_logements"]118    s = {"ln_sup_log": np.log(sujet["superficie_habitable_m2"] / n_s), "age": sujet["age"], "etat_1a5": sujet["etat_1a5"], "stationnement": sujet["stationnement"],119         "ln_loyer_marche": np.log(base["loyer_marche_moyen"]), "loyer_relatif": base["rho"]}120    g_annuel = reg["taux_annuel_marche"]121    comps, lignes = [], []122    for _, r in comp5.iterrows():123        p = float(r["prix_par_logement"])124        a_t = cmp.ajustement_temps_taux(p, g_annuel, float(r["mois"]))   # conditions du marché125        p_t = p + a_t126        x = {"ln_sup_log": np.log(r["superficie_habitable_m2"] / r["nb_logements"]), "age": float(r["age"]), "etat_1a5": float(r["etat_1a5"]),127             "stationnement": float(r["stationnement"]), "ln_loyer_marche": np.log(r["loyer_mensuel_moyen_marche"]),128             "loyer_relatif": r["loyer_mensuel_moyen_contrat"] / r["loyer_mensuel_moyen_marche"]}129        aj = {"temps": a_t}130        for v, lib in (("ln_sup_log", "superficie"), ("age", "age"), ("etat_1a5", "etat"), ("stationnement", "stationnement"), ("ln_loyer_marche", "loyer_marche"), ("loyer_relatif", "loyer_relatif")):131            # seuls les coefficients significatifs (|t| >= T_MIN) sont appliqués ; le loyer relatif (non significatif) est traité par le taux et le multiplicateur132            aj[lib] = p_t * (np.exp(b[v] * (s[v] - x[v])) - 1.0) if abs(reg["t"][v]) >= T_MIN else 0.0133        ca = cmp.ComparableAjuste(r["id_vente"], p, aj); comps.append(ca)134        lignes.append({"id_vente": r["id_vente"], "date_vente": r["date_vente"].date().isoformat(), "mois": float(r["mois"]), "prix_vente": float(r["prix_vente"]),135                       "prix_logement": p, **aj, "prix_ajuste": ca.prix_ajuste, "brut_pct": ca.brut_pct, "net_pct": ca.net_pct})136    poids = cmp.poids_inverse_brut(comps); rec = cmp.reconciliation(comps, poids)137    tab = pd.DataFrame(lignes); tab["poids"] = poids138    se_log = rec["ecart_type_pondere"] * float(np.sqrt(sum(w * w for w in poids)))   # erreur-type de la moyenne pondérée des prix ajustés139    return {"comparables": comps, "tableau": tab, "reconciliation": rec, "poids": poids, "g_annuel": g_annuel,140            "valeur_par_logement": rec["valeur_ponderee"], "valeur": rec["valeur_ponderee"] * n_s,141            "moyenne_simple": rec["moyenne"] * n_s, "ecart_type_valeur": rec["ecart_type_pondere"] * n_s, "se_valeur": se_log * n_s}142143144# ----------------------------------------------------------------------------- 46.7 revenu145def indications_revenu(base: dict, ind: dict, comp5: pd.DataFrame, reg_taux: dict, valeur_comparaison: float, mrb_comp: float) -> dict:146    rne = base["rne"]; rbp = base["rbp_contrat"]147    taux_ajuste = reg_taux["a"] + reg_taux["b_rho"] * (base["rho"] - 1.0)148    taux_comp = float(comp5["taux_global"].median())149    out = {"taux_median_127": ind["taux_median"], "taux_q1": ind["taux_q1"], "taux_q3": ind["taux_q3"], "taux_ajuste": taux_ajuste, "taux_comp": taux_comp,150           "v_cap_median": fin.capitalisation_directe(rne, ind["taux_median"]), "v_cap_ajuste": fin.capitalisation_directe(rne, taux_ajuste),151           "v_cap_comp": fin.capitalisation_directe(rne, taux_comp), "v_cap_q1": fin.capitalisation_directe(rne, ind["taux_q1"]), "v_cap_q3": fin.capitalisation_directe(rne, ind["taux_q3"]),152           "v_cap_ajuste_rne_declare": fin.capitalisation_directe(base["rne_declare"], taux_ajuste),153           "mrb_comp": mrb_comp, "v_mrb_comp": mrb_comp * rbp, "mrb_127": ind["mrb_median_triplex"], "v_mrb_127": ind["mrb_median_triplex"] * rbp,154           "taux_implicite_comparaison": rne / valeur_comparaison, "mrb_implicite_comparaison": valeur_comparaison / rbp,155           "taux_implicite_marche": base["rne_marche"] / valeur_comparaison}156    # sensibilité : multiplicateur des comparables ramené au loyer relatif du sujet par la pente de la régression ln MRB (ch. 32)157    rho_j = comp5["loyer_mensuel_moyen_contrat"] / comp5["loyer_mensuel_moyen_marche"]158    mrb_aj = comp5["mrb"] * np.exp(reg_taux["mrb_b_rho"] * (base["rho"] - rho_j))159    out["mrb_comp_ajuste_rho"] = float(mrb_aj.median()); out["v_mrb_comp_ajuste_rho"] = out["mrb_comp_ajuste_rho"] * rbp160    # lecture « taux implicite » : écart entre le taux ajusté et le taux implicite du prix de comparaison, en points de base161    out["ecart_pb"] = (out["taux_ajuste"] - out["taux_implicite_comparaison"]) * 1e4162    # ratio de dépenses du sujet vs médiane des ventes : part de l'écart capitalisation/comparaison expliquée par la structure de dépenses163    return out164165166# ----------------------------------------------------------------------------- 46.8 coût (contrôle)167def indication_cout(sujet: dict, terrain_par_m2: float = TERRAIN_PAR_M2, vie: float = VIE_ECONOMIQUE, phi: float = PHI_ETAT3) -> dict:168    cu_taille = cout.cout_par_superficie_ajuste(COUT_UNITAIRE_CAS1, SUPERFICIE_CAS1, sujet["superficie_habitable_m2"], ELASTICITE)169    cu = cout.indexer_cout(cu_taille, IPCB_MAISON, IPCB_APPART)170    cn = cu * sujet["superficie_habitable_m2"]171    a_eff = 0.5 * sujet["age"] + 0.5 * phi * vie            # convention ch. 24 (eq. âge effectif) avec la note d'état 3172    a_eff = min(a_eff, vie)173    dep = cout.depreciation_age_vie(cn, a_eff, vie)174    terrain = terrain_par_m2 * sujet["superficie_terrain_m2"]175    v = cout.valeur_par_cout(terrain, cn, dep)176    return {"cu_taille": cu_taille, "cu": cu, "cout_neuf": cn, "age_effectif": a_eff, "vie": vie, "taux_depreciation": dep / cn, "depreciation": dep,177            "terrain": terrain, "contribution_batiment": v["contribution_batiment"], "valeur": v["valeur"]}178179180def sensibilite_cout(sujet: dict) -> pd.DataFrame:181    rows = []182    for t in (700.0, 900.0, 1100.0):183        for vie, phi in ((80.0, 0.5), (90.0, 0.5), (100.0, 0.5), (90.0, 0.35)):184            r = indication_cout(sujet, t, vie, phi); rows.append({"terrain_m2": t, "vie": vie, "phi": phi, "age_effectif": r["age_effectif"], "taux_dep": r["taux_depreciation"], "valeur": r["valeur"]})185    return pd.DataFrame(rows)186187188def depreciation_extraite(prix_marche: float, sujet: dict) -> dict:189    """Lecture inverse : dépréciation que le marché implique si l'on retient le coût neuf et le terrain du dossier."""190    r = indication_cout(sujet)191    return cout.extraction_depreciation_marche(prix_marche, r["terrain"], r["cout_neuf"], sujet["age"])192193194# ----------------------------------------------------------------------------- 46.9 financement de l'acheteur-occupant195def droits_mutation_quebec(base_imposition: float) -> dict:196    d, bas, detail = 0.0, 0.0, []197    for haut, taux in TRANCHES_MUTATION_QUEBEC:198        tranche = max(min(base_imposition, haut) - bas, 0.0); detail.append({"de": bas, "a": haut, "taux": taux, "montant": tranche * taux}); d += tranche * taux199        bas = haut200        if base_imposition <= haut: break201    return {"base": base_imposition, "droits": d, "detail": detail}202203204def financement(prix: float, base: dict, loyers: pd.DataFrame, sujet: dict) -> dict:205    mf = C.mise_de_fonds_minimale_assuree(prix)206    fi = C.financement_occupant(prix, mf, C.TAUX_ASSURE)207    occ = C.cout_occupation(fi, base, loyers, "RC")208    h = fin.Hypotheque(fi["pret"], C.TAUX_ASSURE, C.AMORT_MOIS)209    base_mut = max(prix, sujet["valeur_role_2025"] * FACTEUR_COMPARATIF_2026)210    mut = droits_mutation_quebec(base_mut)211    autres = loyers[loyers["logement"] != "RC"]; loyers_bruts = float(autres["loyer_mensuel"].sum())212    variantes = []213    for lib, mfv, taux, amort in (("assuré, mise minimale, 25 ans", mf, C.TAUX_ASSURE, 300), ("assuré, mise minimale, 30 ans (premier acheteur)", mf, C.TAUX_ASSURE, 360),214                                  ("assuré, mise de 10 %", 0.10 * prix, C.TAUX_ASSURE, 300), ("non assuré, mise de 20 %", 0.20 * prix, C.TAUX_NON_ASSURE, 300),215                                  ("assuré, mise minimale, taux +100 pb", mf, C.TAUX_ASSURE + 0.01, 300), ("assuré, mise minimale, taux +200 pb", mf, C.TAUX_ASSURE + 0.02, 300)):216        f = C.financement_occupant(prix, mfv, taux, amort); o = C.cout_occupation(f, base, loyers, "RC")217        variantes.append({"scenario": lib, "mise_de_fonds": f["mise_de_fonds"], "rpv": f["rpv"], "pret": f["pret"], "taux": taux, "amort_mois": amort, "paiement_mensuel": f["paiement_mensuel"],218                          "paiement_mensuel_tam": f["paiement_mensuel_tam"], "cout_net_mensuel": o["cout_net_mensuel"], "cout_net_mensuel_hors_capital": o["cout_net_mensuel_hors_capital"]})219    return {**fi, "interets_an1": h.interets_annee(1), "capital_an1": h.capital_annee(1), "occupation": occ, "mutation": mut, "liquidites": mf + mut["droits"],220            "loyers_bruts_autres": loyers_bruts, "part_paiement_couverte_brute": loyers_bruts / fi["paiement_mensuel"],221            "part_paiement_couverte_nette": occ["loyers_percus_nets"] / 12 / fi["paiement_mensuel"], "variantes": pd.DataFrame(variantes),222            "ratio_couverture_investisseur": base["rne"] / fi["service_dette"]}223224225def grille_cout_occupation(prix: float, base: dict, loyers: pd.DataFrame) -> pd.DataFrame:226    """Coût économique mensuel d'occupation (hors capital) selon la mise de fonds (part du prix) et le taux hypothécaire."""227    rows = []228    for part in (None, 0.10, 0.15, 0.20):229        for taux in np.arange(0.0312, 0.0713, 0.0025):230            mf = C.mise_de_fonds_minimale_assuree(prix) if part is None else part * prix231            t = taux if (part is None or part < 0.20) else taux + (C.TAUX_NON_ASSURE - C.TAUX_ASSURE)232            f = C.financement_occupant(prix, mf, t, 300); o = C.cout_occupation(f, base, loyers, "RC")233            rows.append({"mise": "minimale" if part is None else f"{int(part*100)} %", "taux": taux, "taux_effectif": t, "cout_net_mensuel": o["cout_net_mensuel"], "hors_capital": o["cout_net_mensuel_hors_capital"]})234    return pd.DataFrame(rows)235236237# ----------------------------------------------------------------------------- 46.3 valeur de la faculté de reprise (rez-de-chaussée)238def va_ecart_rc(loyers: pd.DataFrame, rotation: float = ROTATION_QUEBEC, g: float = G_MARCHE, g_tal: float = G_TAL, r: float = R_ACT, v: float = cas2.TAUX_VACANCE_PERTES) -> dict:239    rc = loyers[loyers["logement"] == "RC"].iloc[0]; lc, lm = float(rc["loyer_mensuel"]), float(rc["loyer_marche_estime"])240    delta1 = (lm - lc) * 12 * (1 - v)241    # écart croissant : marché à g, contrat à g_tal ; valeur actuelle sur 10 ans sans rotation (reprise impossible, locataire reste)242    va_10 = sum(((lm * (1 + g) ** (t - 1) - lc * (1 + g_tal) ** (t - 1)) * 12 * (1 - v)) / (1 + r) ** t for t in range(1, 11))243    # espérance sous rotation géométrique (ch. 26, éq. va-ecart) avec g = croissance de l'écart ≈ croissance moyenne pondérée244    g_ecart = (lm * (1 + g) - lc * (1 + g_tal)) / (lm - lc) - 1245    va_rot = delta1 / (r - g_ecart + rotation * (1 + g_ecart))246    return {"loyer_contrat": lc, "loyer_marche": lm, "ecart_mensuel": lm - lc, "delta1": delta1, "g_ecart": g_ecart, "va_10ans_sans_rotation": va_10, "va_rotation": va_rot,247            "valeur_faculte_reprise": va_10 - va_rot, "date_limite_avis": "2026-12-31", "echeance_bail": "2027-06-30"}248249250# ----------------------------------------------------------------------------- 46.10 réconciliation251def reconciliation(v_log: float, v_mrb: float, v_cap: float, base: dict, sujet: dict, sigma_log: float, poids: dict = POIDS_46) -> dict:252    ret = {"par_logement_ajuste": v_log, "mrb": v_mrb, "capitalisation": v_cap}253    vp = sum(poids[k] * ret[k] for k in ret)254    arr = round(vp / PAS_ARRONDI) * PAS_ARRONDI255    alt = {"retenue 40/35/25": vp, "45/30/25": 0.45 * v_log + 0.30 * v_mrb + 0.25 * v_cap, "égale": sum(ret.values()) / 3,256           "comparaison seule 60/40/0": 0.60 * v_log + 0.40 * v_mrb, "revenu dominant 20/20/60": 0.2 * v_log + 0.2 * v_mrb + 0.6 * v_cap}257    fourchette = (min(ret.values()), max(ret.values()))258    # fourchette conclue : ± une erreur-type de la moyenne pondérée des prix ajustés (comparaison) autour de la valeur pondérée, arrondie vers l'extérieur259    bas = np.floor((vp - sigma_log) / PAS_ARRONDI) * PAS_ARRONDI; haut = np.ceil((vp + sigma_log) / PAS_ARRONDI) * PAS_ARRONDI260    contributions = {k: poids[k] * ret[k] for k in ret}261    return {"retenues": ret, "poids": poids, "contributions": contributions, "valeur_ponderee": vp, "valeur_finale": arr, "alternatives": alt, "fourchette_indications": fourchette, "se_comparaison": sigma_log,262            "fourchette_conclue": (bas, haut), "prix_par_logement": arr / sujet["nb_logements"], "prix_m2": arr / sujet["superficie_habitable_m2"],263            "taux_implicite": base["rne"] / arr, "taux_implicite_marche": base["rne_marche"] / arr, "mrb_implicite": arr / base["rbp_contrat"],264            "ratio_role": arr / sujet["valeur_role_2025"], "ratio_role_indexe": arr / (sujet["valeur_role_2025"] * FACTEUR_COMPARATIF_2026),265            "ecart_ch32": arr - 870_000.0, "ecart_ch32_pct": arr / 870_000.0 - 1}266267268# ----------------------------------------------------------------------------- tout269def dossier() -> dict:270    r = cas2.tout(); d = {k: r[k] for k in ("ventes", "sujet", "loyers", "depenses")}271    base = base_normalisee(d); c = r["comparables"]; ind = r["indicateurs"]272    cand = C.selection_comparables(c, d["sujet"], base); comp5 = cand[cand["retenu"]]273    reg = regression_auxiliaire(c); gr = grille_ajustements(comp5, d["sujet"], base, reg)274    reg_taux = C.regression_taux_loyer_relatif(C.decomposition_taux(c))275    rev = indications_revenu(base, ind, comp5, reg_taux, gr["valeur"], float(comp5["mrb"].median()))276    rec = reconciliation(gr["valeur"], rev["v_mrb_comp"], rev["v_cap_ajuste"], base, d["sujet"], gr["se_valeur"])277    co = indication_cout(d["sujet"]); co_sens = sensibilite_cout(d["sujet"]); dep_ext = depreciation_extraite(rec["valeur_finale"], d["sujet"])278    fi = financement(rec["valeur_finale"], base, d["loyers"], d["sujet"]); grille_occ = grille_cout_occupation(rec["valeur_finale"], base, d["loyers"])279    rep = va_ecart_rc(d["loyers"])280    ch32 = C.indications(C.cas2.rne(d["loyers"], d["depenses"]), d["sujet"], ind, comp5, reg_taux, c)281    return {**d, "base": base, "comparables127": c, "indicateurs": ind, "candidats": cand, "comparables5": comp5, "regression": reg, "grille": gr,282            "regression_taux": reg_taux, "revenu": rev, "cout": co, "cout_sensibilite": co_sens, "depreciation_extraite": dep_ext, "financement": fi,283            "grille_occupation": grille_occ, "reprise": rep, "reconciliation": rec, "ch32": ch32}284285286# ----------------------------------------------------------------------------- exercices287def exercices(D: dict | None = None) -> dict:288    D = D or dossier(); b = D["base"]; s = D["sujet"]; rec = D["reconciliation"]; ex = {}289    # 1. taxes : compte déclaré vs recalcul 2026, effet sur le RNE et sur la capitalisation au taux ajusté290    t = b["taxes"]; ex["1"] = {"municipale_2026": t["municipale"], "scolaire_2026": t["scolaire"], "declare": 5900 + 470, "ecart": b["ecart_taxes"], "rne_declare": b["rne_declare"], "rne_2026": b["rne"],291                               "v_cap_declare": fin.capitalisation_directe(b["rne_declare"], D["revenu"]["taux_ajuste"]), "v_cap_2026": D["revenu"]["v_cap_ajuste"]}292    # 2. avis de reprise : calendrier et VA de l'écart selon la rotation293    ex["2"] = {**D["reprise"], "va_rotation_9_4": va_ecart_rc(D["loyers"], rotation=0.094)["va_rotation"], "va_rotation_20": va_ecart_rc(D["loyers"], rotation=0.20)["va_rotation"],294               "prise_possession_si_acte_2027_01_15": "2028-07-01"}295    # 3. grille : refaire la cascade de Q0243 et vérifier brut/net296    tab = D["grille"]["tableau"]; q = tab[tab["id_vente"] == "Q0243"].iloc[0].to_dict(); ex["3"] = q297    # 4. capitalisation : taux médian, Q1, Q3, ajusté, comparables298    ex["4"] = {k: D["revenu"][k] for k in ("taux_median_127", "taux_q1", "taux_q3", "taux_ajuste", "taux_comp", "v_cap_median", "v_cap_q1", "v_cap_q3", "v_cap_ajuste", "v_cap_comp")}299    d_med = float(D["comparables127"]["ratio_depenses"].median()); rne_ratio = b["rbe"] * (1 - d_med)300    ex["4"].update({"ratio_median_ventes": d_med, "rne_ratio_ventes": rne_ratio, "v_cap_ratio_ventes": fin.capitalisation_directe(rne_ratio, D["revenu"]["taux_ajuste"]),301                    "taux_implicite_ratio_ventes": rne_ratio / D["grille"]["valeur"], "ecart_pb_restant": (D["revenu"]["taux_ajuste"] - rne_ratio / D["grille"]["valeur"]) * 1e4})302    # 5. financement : prix final, mise de fonds, paiement, TAM, coût net ; variante 2e étage303    f = D["financement"]; occ2 = C.cout_occupation({k: f[k] for k in ("mise_de_fonds", "service_dette", "capital_rembourse_5ans")}, b, D["loyers"], "2e")304    ex["5"] = {"prix": f["prix"], "mise": f["mise_de_fonds"], "pret": f["pret"], "paiement": f["paiement_mensuel"], "tam": f["taux_admissible"], "paiement_tam": f["paiement_mensuel_tam"],305               "cout_net_rc": f["occupation"]["cout_net_mensuel"], "hors_capital_rc": f["occupation"]["cout_net_mensuel_hors_capital"], "cout_net_2e": occ2["cout_net_mensuel"], "hors_capital_2e": occ2["cout_net_mensuel_hors_capital"],306               "droits_mutation": f["mutation"]["droits"], "liquidites": f["liquidites"]}307    # 5 bis : au prix de la promesse d'achat (895 000 $)308    fp = financement(895_000.0, b, D["loyers"], s); occ2p = C.cout_occupation({k: fp[k] for k in ("mise_de_fonds", "service_dette", "capital_rembourse_5ans")}, b, D["loyers"], "2e")309    ex["5"]["promesse"] = {"prix": fp["prix"], "mise": fp["mise_de_fonds"], "pret": fp["pret"], "rpv": fp["rpv"], "paiement": fp["paiement_mensuel"], "service_dette": fp["service_dette"], "paiement_tam": fp["paiement_mensuel_tam"],310                           "droits_mutation": fp["mutation"]["droits"], "liquidites": fp["liquidites"], "capital_moyen_5ans": fp["capital_rembourse_5ans"] / 5, "cout_opportunite": fp["occupation"]["cout_opportunite"],311                           "cout_net_rc": fp["occupation"]["cout_net_mensuel"], "hors_capital_rc": fp["occupation"]["cout_net_mensuel_hors_capital"], "cout_net_2e": occ2p["cout_net_mensuel"], "hors_capital_2e": occ2p["cout_net_mensuel_hors_capital"]}312    # 6. pondérations alternatives + pondération à l'inverse de la variance313    ret = rec["retenues"]; sig = {"par_logement_ajuste": rec["se_comparaison"], "mrb": 0.10 * ret["mrb"], "capitalisation": 0.15 * ret["capitalisation"]}314    w = {k: 1 / sig[k] ** 2 for k in ret}; tot = sum(w.values()); w = {k: v / tot for k, v in w.items()}315    ex["6"] = {**rec["alternatives"], "poids_inverse_variance": w, "valeur_inverse_variance": sum(w[k] * ret[k] for k in ret), "sigmas": sig}316    # 7. coût : sensibilité et dépréciation extraite317    co = D["cout"]; v_final = rec["valeur_finale"]318    terrain_egal = v_final - co["contribution_batiment"]; dep_egal = 1 - (v_final - co["terrain"]) / co["cout_neuf"]319    ex["7"] = {"cout": co, "extraite_900": D["depreciation_extraite"], "terrain_pour_egaler": terrain_egal, "terrain_m2_pour_egaler": terrain_egal / s["superficie_terrain_m2"],320               "taux_dep_pour_egaler": dep_egal, "extraite_1100": cout.extraction_depreciation_marche(v_final, 1100.0 * s["superficie_terrain_m2"], co["cout_neuf"], s["age"])}321    return ex322323324def _d(x): return f"{x:,.2f} $".replace(",", " ")325def _p(x, k=2): return f"{100 * x:.{k}f} %"326327328if __name__ == "__main__":329    D = dossier(); b = D["base"]; s = D["sujet"]330    print("== 46.2/46.5 Sujet, taxes 2026, RNE normalisé (traite.cas2.rne sur dépenses normalisées)")331    print(b["taxes"]); print(f"écart taxes déclarées − 2026 : {_d(b['ecart_taxes'])}")332    print(b["detail"][["poste", "declare", "normalise", "type"]].to_string(index=False))333    print(f"RBP {_d(b['rbp_contrat'])} ; marché {_d(b['rbp_marche'])} ; ρ {b['rho']:.4f} ; RBE {_d(b['rbe'])} ; dépenses {_d(b['depenses_totales'])} (ratio {_p(b['ratio_depenses'],1)}) ; "334          f"RNE 2026 {_d(b['rne'])} ; RNE déclaré (ch. 32) {_d(b['rne_declare'])} ; RNE marché {_d(b['rne_marche'])}")335    print("\n== 46.6 Régression auxiliaire (127 ventes, ln prix/logement)")336    reg = D["regression"]337    for k in reg["beta"]: print(f"  {k:18s} β = {reg['beta'][k]:+.5f}  (e.-t. {reg['se'][k]:.5f}, t = {reg['t'][k]:+.2f})")338    print(f"  R² {reg['r2']:.3f} ; σ {reg['sigma']:.4f} ; croissance annuelle implicite des prix {_p(reg['taux_annuel_marche'])}")339    print("\n== Comparables (ch. 32) et grille d'ajustements ($/logement)")340    cols = ["id_vente", "date_vente", "superficie_habitable_m2", "annee_construction", "etat_1a5", "stationnement", "loyer_mensuel_moyen_contrat", "loyer_mensuel_moyen_marche", "loyer_relatif", "prix_vente", "mrb", "taux_global", "prix_par_logement", "mois"]341    print(D["comparables5"][cols].assign(date_vente=lambda x: x["date_vente"].dt.date).round(3).to_string(index=False))342    print(D["grille"]["tableau"].round(2).to_string(index=False))343    g = D["grille"]; print(f"prix/logement pondéré {_d(g['valeur_par_logement'])} → valeur {_d(g['valeur'])} ; moyenne simple {_d(g['moyenne_simple'])} ; é.-t. {_d(g['ecart_type_valeur'])} ; "344                           f"médiane {g['reconciliation']['mediane']:.0f} ; min {g['reconciliation']['min']:.0f} ; max {g['reconciliation']['max']:.0f} ; étendue {_p(g['reconciliation']['etendue_pct'],1)}")345    print("\n== 46.7 Revenu"); print({k: (round(v, 6) if isinstance(v, float) else v) for k, v in D["revenu"].items()})346    print("\n== 46.8 Coût (contrôle)"); print({k: round(v, 2) for k, v in D["cout"].items()}); print(D["cout_sensibilite"].round(3).to_string(index=False)); print(D["depreciation_extraite"])347    print("\n== 46.10 Réconciliation"); rec = D["reconciliation"]348    for k, v in rec.items(): print(f"  {k}: {v}")349    print("\n== 46.9 Financement au prix final"); f = D["financement"]350    print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in f.items() if k not in ("occupation", "mutation", "variantes")})351    print({k: round(v, 2) for k, v in f["occupation"].items() if isinstance(v, float)}); print(f["mutation"]); print(f["variantes"].round(2).to_string(index=False))352    print("\n== 46.3 Reprise du rez-de-chaussée"); print({k: (round(v, 2) if isinstance(v, float) else v) for k, v in D["reprise"].items()})353    print("\n== Exercices"); E = exercices(D)354    for k, v in E.items(): print(k, v)355
Files · 960e587174484ddf8cb3579ab544c4d47de3ba3c
Menu

Theme