#!/usr/bin/env python3 """Chapitre 6 — Code civil et droit immobilier : effets des modalités et démembrements du droit de propriété sur la valeur. Imprime tous les chiffres cités dans le chapitre 6 (DONNÉES SYNTHÉTIQUES et hypothèses pédagogiques déclarées) : * table de mortalité SYNTHÉTIQUE (loi de Gompertz–Makeham à paramètres déclarés ; ce n'est PAS une table de Statistique Canada) : probabilités de survie et espérances de vie ; * usufruit à terme fixe et usufruit viager (art. 1120, 1123, 1162 C.c.Q.) sur la maison du cas 1 (Gatineau) : valeur de l'usufruit U, de la nue-propriété NP, identité U + NP = V, rente viagère équivalente (art. 1171) ; * emphytéose (art. 1195, 1197, 1208-1210) sur un immeuble locatif dont le RNE est celui du cas 3 (Montréal) : valeur du droit de l'emphytéote et du droit du propriétaire selon la durée restante ; seuils de 40 ans (D-15.1, art. 1) et de 50 ans (art. 1040 C.c.Q.) ; * part indivise (art. 1010, 1015) avec décote déclarée ; servitude de non-construction (art. 1177) par l'élasticité prix-terrain du modèle hédonique du cas 1 ; rang hypothécaire (art. 2660, 2945) ; droits sur les mutations 2026 (D-15.1, art. 2) ; quantum d'un vice caché (art. 1726-1728) ; budget de recherche au registre foncier. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch06_demembrements.py Les fonctions d'actualisation proviennent de analysis/python/traite/finance.py (aucune réécriture) ; les valeurs des cas proviennent de traite.cas1 et traite.cas3.""" from __future__ import annotations import sys, math, pathlib import numpy as np, pandas as pd sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from traite import finance as fin ROOT = pathlib.Path(__file__).resolve().parents[3] # ----------------------------------------------------------------------------- hypothèses déclarées # Table de mortalité SYNTHÉTIQUE : force de mortalité de Gompertz–Makeham mu(x) = A + B c^x. # Paramètres pédagogiques choisis pour une espérance de vie à 65 ans de l'ordre de vingt ans (voir table_mortalite()) ; # ils ne reproduisent aucune table officielle. En pratique, utiliser les tables complètes de mortalité de Statistique # Canada (tableau 13-10-0114-01) ou les tables de l'Institut canadien des actuaires, selon la fin de l'évaluation. GM_A, GM_B, GM_C = 0.0004, 0.000025, 1.102 OMEGA = 111 # âge limite de la table synthétique (survie nulle au-delà) R_ACTU = 0.05 # taux d'actualisation des flux d'une résidence (hypothèse pédagogique) G_LOYER = 0.015 # croissance annuelle de la valeur locative nette (hypothèse pédagogique) V_ARRONDI = 638_000.0 # valeur de la maison du cas 1 (prédiction hédonique 637 953 $ arrondie au millier) # Emphytéose : immeuble dont le RNE est celui du cas 3 ; monde de Gordon r - g = taux global R_EMPH, G_EMPH = 0.07, 0.025 # r = taux d'actualisation du cas 3 (hypothèses DCF) ; g déclaré RENTE_EMPH = 30_000.0 # rente emphytéotique annuelle fixe (hypothèse pédagogique) N_EMPH = 35 # durée restante de référence (années) # Droits sur les mutations immobilières, tranches 2026 (RLRQ, c. D-15.1, art. 2 ; notes-A § 2.3) TRANCHES_2026 = [(62_900.0, 0.005), (315_000.0, 0.010), (float("inf"), 0.015)] # Registre foncier — grille tarifaire en vigueur à compter du 1er avril 2025 (notes-A § 3.1) TARIFS_REGISTRE = {"consultation_document": 1.0, "inscription": 142.0, "radiation": 162.0, "etat_certifie": 16.0, "copie_non_certifiee": 26.0, "copie_certifiee": 42.0} # ----------------------------------------------------------------------------- mortalité synthétique def mu(x: float) -> float: """Force de mortalité synthétique (Gompertz–Makeham).""" return GM_A + GM_B * GM_C ** x def survie(x: float, t: float) -> float: """Probabilité tp_x qu'une personne d'âge x soit vivante à l'âge x + t (intégrale exacte de mu).""" if t <= 0: return 1.0 if x + t >= OMEGA: return 0.0 integ = GM_A * t + GM_B * GM_C ** x * (GM_C ** t - 1.0) / math.log(GM_C) return math.exp(-integ) def esperance_vie(x: float) -> float: """Espérance de vie abrégée (somme des tp_x, correction d'un demi-an).""" return sum(survie(x, t) for t in range(1, OMEGA - int(x) + 1)) + 0.5 def table_mortalite(ages=(55, 60, 65, 70, 75, 80, 85)) -> pd.DataFrame: rows = [{"age": a, "esperance_vie": esperance_vie(a), "p10": survie(a, 10), "p20": survie(a, 20), "p30": survie(a, 30)} for a in ages] return pd.DataFrame(rows).set_index("age") # ----------------------------------------------------------------------------- valeur de la pleine propriété def valeur_cas1() -> dict: """Prédiction hédonique du sujet du cas 1 (traite.cas1) et valeur arrondie retenue dans le chapitre.""" from traite import cas1 t = cas1.tout() return {"prediction_hedonique": float(t["prediction_hedonique"]), "V": V_ARRONDI, "terrain_m2": t["sujet"]["superficie_terrain_m2"], "elasticite_terrain": float(t["modele"]["coef"].loc["ln_T", "coef"])} def loyer_net(V: float, r: float = R_ACTU, g: float = G_LOYER) -> float: """Valeur locative nette annuelle cohérente avec V dans un monde de Gordon : L = (r - g) V.""" return (r - g) * V # ----------------------------------------------------------------------------- usufruit def usufruit_terme(V: float, n: int, r: float = R_ACTU, g: float = G_LOYER) -> dict: """Usufruit à terme fixe de n ans : U = somme actualisée des fruits L(1+g)^(t-1), NP = V - U = V((1+g)/(1+r))^n.""" L = loyer_net(V, r, g) flux = [L * (1 + g) ** (t - 1) for t in range(1, n + 1)] U = fin.van([0.0] + flux, r) NP = V - U return {"n": n, "L": L, "U": U, "NP": NP, "part_NP": NP / V, "NP_formule": V * ((1 + g) / (1 + r)) ** n} def usufruit_viager(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> dict: """Usufruit viager d'un usufruitier d'âge x (convention : le revenu de l'année t revient à l'usufruitier s'il est vivant au début de l'année, probabilité (t-1)p_x ; la nue-propriété reçoit la pleine propriété à la fin de l'année du décès).""" L = loyer_net(V, r, g) U = 0.0; NP_direct = 0.0; esp = 0.0 for t in range(1, OMEGA - x + 1): p_debut, p_fin = survie(x, t - 1), survie(x, t) v = fin.facteur_va(r, t) U += v * p_debut * L * (1 + g) ** (t - 1) NP_direct += v * (p_debut - p_fin) * V * (1 + g) ** t esp += p_fin return {"age": x, "L": L, "U": U, "NP": V - U, "NP_direct": NP_direct, "part_NP": (V - U) / V, "esperance_vie": esp + 0.5, "annuite_viagere": U / L} def approximation_terme_espere(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> dict: """Approximation courante : remplacer la durée aléatoire par la durée espérée du droit, n_eff = E[nombre de fruits annuels] = somme des (t-1)p_x (= e_x + 1/2 sous la convention de décompte), puis appliquer la formule à terme fixe avec n_eff réel. Par concavité de l'annuité en n (Jensen), U_approx >= U_viager et NP_approx <= NP_viager.""" n_eff = sum(survie(x, t - 1) for t in range(1, OMEGA - x + 1)) NP = V * ((1 + g) / (1 + r)) ** n_eff return {"n_eff": n_eff, "U": V - NP, "NP": NP, "part_NP": NP / V} def rente_viagere_equivalente(V: float, x: int, r: float = R_ACTU, g: float = G_LOYER) -> float: """Rente annuelle constante, versée tant que l'usufruitier vit, de même valeur actuelle que l'usufruit (art. 1171).""" u = usufruit_viager(V, x, r, g) a_x = sum(fin.facteur_va(r, t) * survie(x, t - 1) for t in range(1, OMEGA - x + 1)) return u["U"] / a_x def grille_usufruit(V: float, ages=(55, 60, 65, 70, 75, 80, 85), taux=(0.04, 0.05, 0.06), g: float = G_LOYER) -> pd.DataFrame: """Part de la nue-propriété (NP/V) selon l'âge de l'usufruitier et le taux d'actualisation.""" return pd.DataFrame({f"r={r:.2f}": [usufruit_viager(V, a, r, g)["part_NP"] for a in ages] for r in taux}, index=list(ages)) # ----------------------------------------------------------------------------- emphytéose def emphyteose(noi_1: float, rente: float, n: int, r: float = R_EMPH, g: float = G_EMPH, r_prop: float | None = None) -> dict: """Droits de l'emphytéote et du propriétaire pour une durée restante de n ans. Monde de Gordon : V_pleine = NOI_1/(r-g) ; NOI_t = NOI_1 (1+g)^(t-1) ; reprise à la fin de l'année n à V_n = V_pleine (1+g)^n. Si r_prop est donné, les flux du propriétaire (rente + reprise) sont actualisés à ce taux (sinon à r : la somme vaut V_pleine).""" V = noi_1 / (r - g) rp = r if r_prop is None else r_prop flux_emph = [noi_1 * (1 + g) ** (t - 1) - rente for t in range(1, n + 1)] V_emph = fin.van([0.0] + flux_emph, r) V_rentes = rente * fin.facteur_va_annuite(rp, n) V_reprise = V * (1 + g) ** n * fin.facteur_va(rp, n) V_prop = V_rentes + V_reprise return {"n": n, "V_pleine": V, "V_emph": V_emph, "V_rentes": V_rentes, "V_reprise": V_reprise, "V_prop": V_prop, "somme": V_emph + V_prop, "part_emph": V_emph / V, "rente": rente, "noi_1": noi_1} def grille_emphyteose(noi_1: float, rente: float, durees=(60, 50, 45, 40, 35, 30, 20, 10, 5), **kw) -> pd.DataFrame: return pd.DataFrame([emphyteose(noi_1, rente, n, **kw) for n in durees]).set_index("n") def rne_cas3() -> float: """RNE normalisé de première année du cas 3 (traite.cas3), seul chiffre du cas repris ici.""" from traite import cas3 return float(cas3.tout()["base"]["rne"]) # ----------------------------------------------------------------------------- indivision, servitude, hypothèque, mutations, vice def part_indivise(V: float, quote_part: float, decote: float) -> dict: """Valeur d'une part indivise : quote-part de la valeur du tout, diminuée d'une décote d'illiquidité déclarée.""" brute = quote_part * V return {"quote_part": quote_part, "valeur_proportionnelle": brute, "decote": decote, "valeur_part": brute * (1 - decote)} def servitude_terrain(V: float, terrain_m2: float, grevee_m2: float, elasticite: float) -> dict: """Effet d'une servitude qui soustrait grevee_m2 à l'usage du terrain, par l'élasticité prix-terrain du modèle hédonique : V_avec = V ((T - T_g)/T)^beta ; approximation déclarée (la servitude n'est pas une réduction de lot).""" ratio = (terrain_m2 - grevee_m2) / terrain_m2 V_avec = V * ratio ** elasticite return {"terrain_m2": terrain_m2, "grevee_m2": grevee_m2, "elasticite": elasticite, "V_sans": V, "V_avec": V_avec, "delta": V - V_avec, "delta_pct": 1 - V_avec / V} def rang_hypothecaire(produit_net: float, creances: list[float]) -> list[dict]: """Distribution du produit net d'une vente sous contrôle de justice selon le rang (art. 2660, 2945).""" reste = produit_net; out = [] for i, c in enumerate(creances, 1): paye = min(c, max(reste, 0.0)); reste -= paye out.append({"rang": i, "creance": c, "recouvre": paye, "perte": c - paye, "taux_recouvrement": paye / c if c else 1.0}) out.append({"rang": "débiteur", "creance": 0.0, "recouvre": max(reste, 0.0), "perte": 0.0, "taux_recouvrement": float("nan")}) return out def droit_mutation(base: float, tranches=TRANCHES_2026, taux_municipal: list[tuple[float, float]] | None = None) -> dict: """Droit sur les mutations immobilières (D-15.1, art. 2) : taux par tranche sur la base d'imposition. taux_municipal : liste optionnelle de (seuil_inferieur, taux) remplaçant 1,5 % au-dessus de 500 000 $ (facultatif, max 3 %).""" detail = []; prec = 0.0; total = 0.0 for plafond, taux in tranches: if base <= prec: break assiette = min(base, plafond) - prec montant = assiette * taux detail.append({"de": prec, "a": min(base, plafond), "taux": taux, "montant": montant}); total += montant; prec = plafond supplement = 0.0 if taux_municipal: for seuil, taux in taux_municipal: if base > seuil: borne_sup = min([s for s, _ in taux_municipal if s > seuil] + [float("inf")]) supplement += (min(base, borne_sup) - seuil) * (taux - 0.015) return {"base": base, "detail": detail, "droit_base": total, "supplement_municipal": supplement, "total": total + supplement, "taux_effectif": (total + supplement) / base if base > 0 else 0.0} def quantum_vice(prix: float, cout_reparation: float, plus_value: float, prejudice_autre: float = 0.0, vendeur_connaissait: bool = False) -> dict: """Mesure usuelle du préjudice d'un vice caché : coût des réparations diminué de la plus-value qu'elles procurent (réduction de prix, art. 1726-1727) ; dommages-intérêts additionnels seulement si le vendeur connaissait le vice (art. 1728).""" reduction = cout_reparation - plus_value dommages = prejudice_autre if vendeur_connaissait else 0.0 return {"prix": prix, "reduction_prix": reduction, "dommages": dommages, "total": reduction + dommages, "prix_ajuste": prix - reduction, "part_prix": reduction / prix} def budget_registre(nb_comparables: int, documents_par_vente: int = 3) -> dict: """Budget de consultation du registre foncier pour vérifier des ventes comparables (1 $ par document).""" n_doc = nb_comparables * documents_par_vente return {"documents": n_doc, "cout": n_doc * TARIFS_REGISTRE["consultation_document"]} # ----------------------------------------------------------------------------- impression def _d(x: float) -> str: return f"{x:,.0f} $".replace(",", " ") def main() -> None: print("=== Chapitre 6 — démembrements et modalités (DONNÉES SYNTHÉTIQUES / hypothèses déclarées) ===\n") tm = table_mortalite() print("Table de mortalité SYNTHÉTIQUE (Gompertz–Makeham A=%.4f B=%.6f c=%.3f) :" % (GM_A, GM_B, GM_C)) print(tm.round(3).to_string(), "\n") c1 = valeur_cas1() V = c1["V"] print(f"Cas 1 : prédiction hédonique {c1['prediction_hedonique']:.2f} $ → V retenu {_d(V)} ; L = (r-g)V = {_d(loyer_net(V))} / an " f"({loyer_net(V)/12:.0f} $/mois) avec r = {R_ACTU:.1%}, g = {G_LOYER:.1%}\n") for n in (10, 20, 30): u = usufruit_terme(V, n) print(f"Usufruit à terme {n} ans : U = {_d(u['U'])} ; NP = {_d(u['NP'])} ({u['part_NP']:.1%}) ; vérif. NP formule {_d(u['NP_formule'])}") print() for x in (55, 65, 75, 85): u = usufruit_viager(V, x) a = approximation_terme_espere(V, x) print(f"Usufruit viager, {x} ans : e_x = {u['esperance_vie']:.1f} ; U = {_d(u['U'])} ; NP = {_d(u['NP'])} ({u['part_NP']:.1%}) ; " f"NP direct = {_d(u['NP_direct'])} ; annuité viagère {u['annuite_viagere']:.3f} ; rente équiv. {_d(rente_viagere_equivalente(V, x))}/an ; " f"approx. terme espéré n_eff = {a['n_eff']:.2f} → NP = {_d(a['NP'])} ({a['part_NP']:.1%}), écart {_d(u['NP'] - a['NP'])}") print("\nGrille NP/V (âge × r) :"); print(grille_usufruit(V).round(3).to_string(), "\n") noi = rne_cas3() e = emphyteose(noi, RENTE_EMPH, N_EMPH) print(f"Emphytéose : RNE_1 cas 3 = {noi:.2f} $ ; r = {R_EMPH:.1%}, g = {G_EMPH:.1%} → V pleine = {_d(e['V_pleine'])} ; rente {_d(RENTE_EMPH)}/an ; n = {N_EMPH}") print(f" droit de l'emphytéote {_d(e['V_emph'])} ({e['part_emph']:.1%}) ; propriétaire : rentes {_d(e['V_rentes'])} + reprise {_d(e['V_reprise'])} = {_d(e['V_prop'])} ; somme {_d(e['somme'])}") e2 = emphyteose(noi, RENTE_EMPH, N_EMPH, r_prop=0.055) print(f" avec r_prop = 5,5 % : propriétaire {_d(e2['V_prop'])} ; somme {_d(e2['somme'])} (> V pleine de {_d(e2['somme']-e2['V_pleine'])})") print("\nGrille emphytéose selon la durée restante :") ge = grille_emphyteose(noi, RENTE_EMPH)[["V_emph", "V_rentes", "V_reprise", "V_prop", "part_emph"]].copy() ge["part_emph"] = (ge["part_emph"] * 100).round(1); ge[["V_emph", "V_rentes", "V_reprise", "V_prop"]] = ge[["V_emph", "V_rentes", "V_reprise", "V_prop"]].round(0) print(ge.to_string(), "\n") for q, d in ((0.5, 0.0), (0.5, 0.10), (0.5, 0.20), (1 / 3, 0.15)): p = part_indivise(V, q, d) print(f"Part indivise {q:.3f} décote {d:.0%} : {_d(p['valeur_proportionnelle'])} → {_d(p['valeur_part'])}") s = servitude_terrain(V, c1["terrain_m2"], 120, c1["elasticite_terrain"]) print(f"\nServitude de non-construction sur 120 m² de {c1['terrain_m2']} m² (élasticité {s['elasticite']:.4f}) : V_avec = {_d(s['V_avec'])} ; ΔV = {_d(s['delta'])} ({s['delta_pct']:.2%})") print("\nRang hypothécaire, produit net 560 000 $, créances 455 000 / 90 000 / 40 000 :") for r in rang_hypothecaire(560_000, [455_000, 90_000, 40_000]): print(" ", r) m = droit_mutation(V) print(f"\nDroit de mutation 2026 sur {_d(V)} : " + " ; ".join(f"{t['taux']:.1%} sur {_d(t['a']-t['de'])} = {t['montant']:.2f} $" for t in m["detail"]) + f" → {m['total']:.2f} $ ({m['taux_effectif']:.3%})") m2 = droit_mutation(750_000, taux_municipal=[(500_000, 0.02)]) print(f"Droit de mutation 2026 sur 750 000 $ avec taux municipal 2 % au-delà de 500 000 $ : base {m2['droit_base']:.2f} + {m2['supplement_municipal']:.2f} = {m2['total']:.2f} $") qv = quantum_vice(520_000, 46_000, 9_000, 6_500, True) print(f"\nVice caché : prix 520 000 $, réparations 46 000 $, plus-value 9 000 $ → réduction {_d(qv['reduction_prix'])} ({qv['part_prix']:.1%}) ; " f"si vendeur connaissait : + {_d(qv['dommages'])} = {_d(qv['total'])}") b = budget_registre(5) print(f"\nRegistre foncier : 5 comparables × 3 documents = {b['documents']} documents → {b['cout']:.0f} $ (1 $/document)") if __name__ == "__main__": main()