SPB Git

Repository Public Research preview

contre_verification.py 36.4 KB 502 lines python

1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""4contre_verification.py — Agent 8, révision indépendante (volet numérique).56Recalcule, SANS importer verification_calculs.py, toutes les valeurs numériques de resultats.json à partir7des seules données d'entrée (recopiées ci-dessous) et les compare au moteur :8  - tolérance 1 $ sur les montants, 0,05 point sur les pourcentages, 0,01 sur les coefficients.9Imprime un tableau « élément | moteur | contre-calcul | écart | OK/ÉCART » et un bilan.10Les contrôles « verifications_originaux » sont eux aussi recalculés de façon autonome.1112Usage : python3 contre_verification.py [--json resultats.json] [--tout]   (--tout : afficher aussi les lignes OK)13"""14from __future__ import annotations15import json, sys, math16from decimal import Decimal, ROUND_HALF_UP17from pathlib import Path1819HERE = Path(__file__).resolve().parent2021# ---------------------------------------------------------------------------------------------22# Outils d'arrondi (convention du cours : dollar ROUND_HALF_UP ligne par ligne ; 5 000 $ pour la conclusion)23# ---------------------------------------------------------------------------------------------24def rd(x, q="1"):25    return Decimal(str(x)).quantize(Decimal(q), rounding=ROUND_HALF_UP)2627def d0(x) -> int:            # au dollar28    return int(rd(x, "1"))2930def p1(x) -> float:          # pourcentage à 0,131    return float(rd(x, "0.1"))3233def p2(x) -> float:          # à 0,0134    return float(rd(x, "0.01"))3536def au_5000(x) -> int:37    return int(rd(x / 5000, "1")) * 50003839# ---------------------------------------------------------------------------------------------40# Comparateur41# ---------------------------------------------------------------------------------------------42LIGNES: list[tuple] = []   # (élément, moteur, contre, écart, ok)4344def cmp(elem: str, moteur, contre, tol: float = 1.0):45    try:46        ecart = float(contre) - float(moteur)47        ok = abs(ecart) <= tol + 1e-948    except (TypeError, ValueError):49        ecart = None50        ok = (moteur == contre)51    LIGNES.append((elem, moteur, contre, ecart, ok))52    return ok5354def fmt(x) -> str:55    if isinstance(x, bool): return "oui" if x else "non"56    if isinstance(x, int): return f"{x:,}".replace(",", " ")57    if isinstance(x, float):58        s = f"{x:,.2f}".replace(",", " ")59        return s60    return str(x)6162# ---------------------------------------------------------------------------------------------63# DONNÉES D'ENTRÉE (recopiées des énoncés — pas du code du moteur)64# ---------------------------------------------------------------------------------------------65DATE_EVAL = (2026, 11)   # 1er novembre 20266667CAT = {68    "temps_mois_pct": 0.17, "aire": 150, "lot": 6, "lot_seuil": 1000,69    "garage": {"aucun": 0, "simple": 30000, "double": 45000},          # valeur cumulée vs « aucun »70    "soussol": {"non fini": 0, "partiel": 10000, "fini": 20000},        # valeur cumulée vs « non fini »71    "sdb": 12000, "salle_eau": 6000, "chambre": 10000, "age": 2000,72    "etat": {"très bon": -8000, "bon": 0, "moyen": 12000}, "loc_pct": 5.0, "piscine": 25000,73}7475SUJET_S8 = dict(annee=2008, aire=1200, lot=6000, chambres=3, sdb=(1, 1), soussol="partiel", garage="simple", etat="bon")76COMPS_S8 = [77    dict(nom="C1", prix=435000, date=(2026, 7), loc="similaire", annee=2007, aire=1250, lot=6200, chambres=3, sdb=(1, 1), soussol="fini", garage="simple", etat="bon"),78    dict(nom="C2", prix=415000, date=(2026, 5), loc="similaire", annee=2010, aire=1150, lot=5500, chambres=3, sdb=(1, 0), soussol="non fini", garage="aucun", etat="bon", non_imm=4000),79    dict(nom="C3", prix=460000, date=(2026, 8), loc="supérieure", annee=2005, aire=1350, lot=7000, chambres=4, sdb=(2, 0), soussol="fini", garage="double", etat="très bon"),80    dict(nom="C4", prix=425000, date=(2026, 3), loc="similaire", annee=2008, aire=1200, lot=6000, chambres=3, sdb=(1, 1), soussol="partiel", garage="simple", etat="moyen"),81]82POIDS_S8 = {"C1": 40, "C4": 35, "C2": 20, "C3": 5}8384SUJET_S9 = dict(annee=2005, aire=1500, lot=5500, chambres=3, sdb=(2, 0), soussol="fini", garage="double", etat="bon")85FIN_C1 = dict(solde=100000, tc=2.0, tm=5.0, n=3)86COMPS_S9 = [87    dict(nom="C1", prix=410000, date=(2026, 2), loc="similaire", annee=2003, aire=1580, lot=5000, chambres=3, sdb=(2, 0), soussol="non fini", garage="simple", etat="bon", financement=FIN_C1),88    dict(nom="C2", prix=445000, date=(2026, 7), loc="similaire", annee=2006, aire=1480, lot=5200, chambres=3, sdb=(1, 0), soussol="fini", garage="double", etat="très bon"),89    dict(nom="C3", prix=425000, date=(2026, 4), loc="similaire", annee=2005, aire=1520, lot=6000, chambres=3, sdb=(2, 0), soussol="fini", garage="double", etat="bon", depenses=15000),90    dict(nom="C4", prix=460000, date=(2026, 8), loc="supérieure", annee=2008, aire=1650, lot=7000, chambres=4, sdb=(2, 0), soussol="fini", garage="double", etat="très bon"),91    dict(nom="C5", prix=438000, date=(2026, 5), loc="similaire", annee=2004, aire=1500, lot=5500, chambres=3, sdb=(2, 0), soussol="partiel", garage="simple", etat="moyen", non_imm=6000),92]93POIDS_S9 = {"C2": 30, "C3": 30, "C1": 20, "C5": 15, "C4": 5}94POIDS_S9_SANS_C4 = {"C2": 32, "C3": 32, "C1": 21, "C5": 15, "C4": 0}9596PAIRES = {"garage_double_vs_simple": (452000, 437000), "localisation_aylmer_vs_hull": (472000, 449000), "soussol_amenage": (441000, 421000)}97REVENTE = (410000, 426600, 24)98REG_AIRES = [1050, 1100, 1120, 1150, 1180, 1200, 1220, 1250, 1280, 1300, 1350, 1400]99REG_RESID = [-9000, 6500, -4000, 11000, -7500, 2000, 9500, -11000, 4000, -3000, 7000, -5500]100101P33 = dict(cg=8.0, fg=7.0, peg=8.0, conting=5.0, permis_par_1000=4.70, permis_min_log=335, gcr_unite=2656.68, gcr_max_log=5,102           pref=4.70, prime=2.05, hono=4.0, assur=0.8, profit=11.0, tps=5.0, tvq=9.975)103SIX = dict(logements=6, hors_sol=5400, sous_sol=1800, duree=14, juridique=12000, terrain=210000,104           divisions=[170000, 145000, 42000, 120000, 65000, 175000, 20000, 48000])105MARCHE = [("M1", 1560000, 215000), ("M2", 1510000, 205000), ("M3", 1595000, 225000), ("M4", 1465000, 200000)]106107# ---------------------------------------------------------------------------------------------108# IMM1003 — grille de comparaison (implémentation indépendante)109# ---------------------------------------------------------------------------------------------110def mois_entre(d_vente, d_eval=DATE_EVAL) -> int:111    return (d_eval[0] - d_vente[0]) * 12 + (d_eval[1] - d_vente[1])112113def equivalence_comptant(solde, tc, tm, n):114    """Solde à intérêts seulement, capital à l'échéance ; avantage = solde − VA au taux du marché."""115    im = tm / 100116    coupon = solde * tc / 100117    fa = sum((1 + im) ** -k for k in range(1, n + 1))          # facteur d'annuité (somme explicite)118    va_cap = solde * (1 + im) ** -n119    va_int = coupon * fa120    return dict(interets_annuels=d0(coupon), facteur_annuite=p2(fa), va_capital=d0(va_cap), va_interets=d0(va_int),121                valeur_actuelle=d0(va_cap + va_int), avantage=d0(solde - va_cap - va_int))122123def grille(sujet, c, cat=CAT):124    """Retourne (lignes transactionnelles [(montant, prix courant)], prix ajusté trans., lignes propriété [montant],125    total propriété, indication, net, brut)."""126    trans = []127    cour = c["prix"]128    # 1 droits129    trans.append((0, cour))130    # 2 financement (avantage pour l'acheteur → on retranche)131    fin = -equivalence_comptant(**c["financement"])["avantage"] if "financement" in c else 0132    cour += fin; trans.append((fin, cour))133    # 3 conditions de vente134    trans.append((0, cour))135    # 4 dépenses après achat136    dep = c.get("depenses", 0); cour += dep; trans.append((dep, cour))137    # 5 temps : prorata simple sur le prix courant138    m = mois_entre(c["date"])139    t = d0(cour * cat["temps_mois_pct"] / 100 * m); cour += t; trans.append((t, cour))140    pat = cour141    # propriété (comparable → sujet : comparable supérieur → négatif)142    prop = []143    loc = {"similaire": 0, "supérieure": -cat["loc_pct"], "inférieure": cat["loc_pct"]}[c["loc"]]144    prop.append(d0(pat * loc / 100))                                                  # 6 localisation145    prop.append((sujet["aire"] - c["aire"]) * cat["aire"])                             # 7a aire146    dl = sujet["lot"] - c["lot"]147    prop.append(dl * cat["lot"] if abs(dl) >= cat["lot_seuil"] else 0)                 # 7b terrain148    prop.append((sujet["annee"] - c["annee"]) * cat["age"])                            # 7c âge (sujet plus récent → +)149    prop.append(cat["etat"][c["etat"]] - cat["etat"][sujet["etat"]])                   # 7d état150    prop.append((sujet["chambres"] - c["chambres"]) * cat["chambre"])                  # 7e chambres151    prop.append((sujet["sdb"][0] - c["sdb"][0]) * cat["sdb"] + (sujet["sdb"][1] - c["sdb"][1]) * cat["salle_eau"])  # 7f152    prop.append(cat["soussol"][sujet["soussol"]] - cat["soussol"][c["soussol"]])       # 7g153    prop.append(cat["garage"][sujet["garage"]] - cat["garage"][c["garage"]])           # 7h154    prop.append(-c.get("non_imm", 0))                                                  # 8 non immobilier155    tot = sum(prop)156    ind = pat + tot157    tous = [a for a, _ in trans] + prop158    net = sum(tous); brut = sum(abs(a) for a in tous)159    return dict(trans=trans, pat=pat, prop=prop, tot=tot, ind=ind, net=net, brut=brut, mois=m,160                net_pct=p1(net / c["prix"] * 100), brut_pct=p1(brut / c["prix"] * 100),161                seuil_net=abs(net) / c["prix"] <= 0.15, seuil_brut=brut / c["prix"] <= 0.25,162                prix_pi2=d0(c["prix"] / c["aire"]), ind_pi2=d0(ind / sujet["aire"]))163164def reconcilier(inds: dict[str, int], poids: dict[str, float]):165    assert sum(poids.values()) == 100166    vp = sum(inds[k] * poids[k] for k in inds) / 100167    v = sorted(inds.values()); n = len(v)168    med = v[n // 2] if n % 2 else (v[n // 2 - 1] + v[n // 2]) / 2169    moy = sum(v) / n170    return dict(vp=d0(vp), conclue=au_5000(vp), moy=d0(moy), moy_arr=au_5000(moy), med=d0(med), mn=v[0], mx=v[-1],171                etendue_pct=p1((v[-1] - v[0]) / vp * 100), vp_brut=vp)172173def ols(x, y):174    n = len(x); mx = sum(x) / n; my = sum(y) / n175    sxx = sum((a - mx) ** 2 for a in x); sxy = sum((a - mx) * (b - my) for a, b in zip(x, y))176    b1 = sxy / sxx; b0 = my - b1 * mx177    sse = sum((b - (b0 + b1 * a)) ** 2 for a, b in zip(x, y)); sst = sum((b - my) ** 2 for b in y)178    s2 = sse / (n - 2); se1 = math.sqrt(s2 / sxx)179    return dict(b0=b0, b1=b1, r2=1 - sse / sst, se1=se1, t=b1 / se1, sigma=math.sqrt(s2))180181# ---------------------------------------------------------------------------------------------182# IMM1033 — chaîne du coût neuf (implémentation indépendante)183# ---------------------------------------------------------------------------------------------184def chaine(six=SIX, p=P33, gcr=None, T=None, i=None):185    nus = sum(six["divisions"])186    cg = d0(nus * p["cg"] / 100); a1 = nus + cg187    fg = d0(a1 * p["fg"] / 100); a2 = a1 + fg188    peg = d0(a2 * p["peg"] / 100); dcours = a2 + peg189    cont = d0(nus * p["conting"] / 100); dc = dcours + cont190    perm_prop = d0(dc * p["permis_par_1000"] / 1000); perm_min = p["permis_min_log"] * six["logements"]191    permis = max(perm_prop, perm_min)192    if gcr is None: gcr = six["logements"] <= p["gcr_max_log"]193    gcr_m = d0(p["gcr_unite"] * six["logements"]) if gcr else 0194    T = six["duree"] if T is None else T195    i = (p["pref"] + p["prime"]) if i is None else i196    inter = d0(dc * (i / 100) * (T / 12) / 2)197    hono = d0(dcours * p["hono"] / 100)198    assur = d0(dc * p["assur"] / 100)199    jur = six["juridique"]200    ind = permis + gcr_m + inter + hono + assur + jur201    assiette = dc + ind202    profit = d0(assiette * p["profit"] / 100)203    total = assiette + profit204    return dict(nus=nus, cg=cg, a1=a1, fg=fg, a2=a2, peg=peg, dcours=dcours, cont=cont, dc=dc, perm_prop=perm_prop, perm_min=perm_min,205                permis=permis, gcr=gcr_m, inter=inter, hono=hono, assur=assur, jur=jur, ind=ind, assiette=assiette, profit=profit, total=total,206                T=T, i=i, cascade_pct=p2((dcours / nus - 1) * 100), ratio_ind_pct=p1(ind / dcours * 100),207                pi2_hs=p2(total / six["hors_sol"]), pi2_brut=p2(total / (six["hors_sol"] + six["sous_sol"])), par_log=d0(total / six["logements"]),208                marge_pct=p2(p["profit"] / (100 + p["profit"]) * 100), part_ind=p1(ind / total * 100), part_profit=p1(profit / total * 100))209210def interets_periode(M, i_pct, T, profil):211    im = i_pct / 100 / 12212    F = (lambda t: t / T) if profil == "lineaire" else (lambda t: 3 * (t / T) ** 2 - 2 * (t / T) ** 3)213    solde = 0.0; tot = 0.0; lignes = []214    for t in range(1, T + 1):215        interet = solde * im                      # sur le solde de début de mois216        deb = M * (F(t) - F(t - 1))               # décaissement en fin de mois217        solde = solde + interet + deb218        tot += interet219        lignes.append(dict(mois=t, dec=d0(deb), cumul=d0(M * F(t)), interet=d0(interet), solde=d0(solde)))220    return dict(total=d0(tot), lignes=lignes, solde_moyen=d0(sum(l["solde"] for l in lignes) / T))221222def taxes(prix, premier=False):223    tps = prix * 0.05; tvq = prix * 0.09975224    if prix <= 350000: rt = min(0.36 * tps, 6300)225    elif prix < 450000: rt = 6300 * (450000 - prix) / 100000226    else: rt = 0.0227    if prix <= 200000: rq = min(0.5 * tvq, 9975)228    elif prix < 300000: rq = 9975 * (300000 - prix) / 100000229    else: rq = 0.0230    rpa = 0.0231    if premier:232        rpa = tps if prix <= 1_000_000 else (tps * (1_500_000 - prix) / 500_000 if prix < 1_500_000 else 0.0)233    rt_eff = max(rt, rpa)234    net = tps + tvq - rt_eff - rq235    return dict(tps=p2(tps), tvq=p2(tvq), brutes=p2(tps + tvq), rt=p2(rt), rpa=p2(rpa), rt_eff=p2(rt_eff), rq=p2(rq), net=p2(net), net_pct=p2(net / prix * 100))236237# ---------------------------------------------------------------------------------------------238# CONTRÔLE239# ---------------------------------------------------------------------------------------------240def controler_grilles(S, sujet, comps, prefix):241    inds = {}242    for c, G in zip(comps, S["grilles"]):243        assert G["nom"] == c["nom"]244        g = grille(sujet, c)245        inds[c["nom"]] = g["ind"]246        n = f"{prefix} {c['nom']}"247        cmp(f"{n} mois écoulés", G["mois"], g["mois"], 0)248        for k, (lib, v, cur) in enumerate(G["transactionnels"]):249            cmp(f"{n} {lib[:34]} ($)", v, g["trans"][k][0]); cmp(f"{n} {lib[:34]} (cumul)", cur, g["trans"][k][1])250        cmp(f"{n} prix ajusté transactionnel", G["prix_ajuste_transactionnel"], g["pat"])251        for k, (lib, v) in enumerate(G["propriete"]):252            cmp(f"{n} {lib[:40]}", v, g["prop"][k])253        cmp(f"{n} total propriété", G["total_propriete"], g["tot"])254        cmp(f"{n} indication", G["indication"], g["ind"])255        cmp(f"{n} net $", G["net"], g["net"]); cmp(f"{n} net %", G["net_pct"], g["net_pct"], 0.05)256        cmp(f"{n} brut $", G["brut"], g["brut"]); cmp(f"{n} brut %", G["brut_pct"], g["brut_pct"], 0.05)257        cmp(f"{n} seuil net ≤ 15 %", G["seuil_net_ok"], g["seuil_net"]); cmp(f"{n} seuil brut ≤ 25 %", G["seuil_brut_ok"], g["seuil_brut"])258        cmp(f"{n} prix/pi²", G["prix_pi2"], g["prix_pi2"]); cmp(f"{n} indication/pi² sujet", G["indication_pi2"], g["ind_pi2"])259    return inds260261def controler_reconc(R, rec, prefix):262    cmp(f"{prefix} poids = 100", 100, sum(R["poids"].values()), 0)263    cmp(f"{prefix} valeur pondérée", R["valeur_ponderee"], rec["vp"]); cmp(f"{prefix} valeur conclue (5 000 $)", R["valeur_conclue"], rec["conclue"])264    cmp(f"{prefix} moyenne simple", R["moyenne_simple"], rec["moy"]); cmp(f"{prefix} moyenne arrondie", R["moyenne_arrondie"], rec["moy_arr"])265    cmp(f"{prefix} médiane", R["mediane"], rec["med"]); cmp(f"{prefix} min", R["min"], rec["mn"]); cmp(f"{prefix} max", R["max"], rec["mx"])266    cmp(f"{prefix} étendue %", R["etendue_pct"], rec["etendue_pct"], 0.05)267    cmp(f"{prefix} conclue dans l'étendue", True, rec["mn"] <= R["valeur_conclue"] <= rec["mx"])268269def main(argv):270    path = HERE / "resultats.json"271    if "--json" in argv: path = Path(argv[argv.index("--json") + 1])272    tout = "--tout" in argv273    R = json.loads(path.read_text(encoding="utf-8"))274275    # ---- catalogue et paramètres : cohérence des entrées recopiées avec celles du moteur276    C = R["catalogue"]277    cmp("catalogue temps %/mois", C["temps_pct_par_mois"], CAT["temps_mois_pct"], 0); cmp("catalogue aire $/pi²", C["aire_par_pi2"], CAT["aire"], 0)278    cmp("catalogue garage simple", C["garage_simple_vs_aucun"], CAT["garage"]["simple"], 0); cmp("catalogue garage double (cumul)", C["garage_simple_vs_aucun"] + C["garage_double_vs_simple"], CAT["garage"]["double"], 0)279    cmp("catalogue sous-sol fini", C["soussol_fini_vs_non_fini"], CAT["soussol"]["fini"], 0); cmp("catalogue sous-sol partiel (cumul)", C["soussol_fini_vs_non_fini"] - C["soussol_fini_vs_partiel"], CAT["soussol"]["partiel"], 0)280    cmp("catalogue localisation %", C["localisation_pct"], CAT["loc_pct"], 0)281282    # =========================== IMM1003 S08 ===========================283    S8 = R["imm1003_s08"]284    inds8 = controler_grilles(S8, SUJET_S8, COMPS_S8, "S08")285    rec8 = reconcilier(inds8, POIDS_S8)286    controler_reconc(S8["reconciliation"], rec8, "S08 réconc.")287    # signes attendus (comparable supérieur → négatif) — cohérence288    cmp("S08 C3 (supérieur partout) total propriété < 0", True, inds8["C3"] < S8["grilles"][2]["prix_ajuste_transactionnel"])289    cmp("S08 C2 (inférieur : sans garage, sous-sol non fini) total propriété > 0", True, S8["grilles"][1]["total_propriete"] > 0)290    ex = S8["exemples_criteres"]291    f3 = equivalence_comptant(100000, 2, 5, 3); f5 = equivalence_comptant(100000, 2, 5, 5)292    for k in ("interets_annuels", "va_capital", "va_interets", "valeur_actuelle", "avantage"):293        cmp(f"S08 ex. financement 3 ans {k}", ex["financement"][k], f3[k]); cmp(f"S08 ex. financement 5 ans {k}", ex["financement_5_ans"][k], f5[k])294    cmp("S08 ex. financement 3 ans facteur", ex["financement"]["facteur_annuite"], f3["facteur_annuite"], 0.01)295    cmp("S08 ex. financement 5 ans facteur", ex["financement_5_ans"]["facteur_annuite"], f5["facteur_annuite"], 0.01)296    cmp("S08 ex. dépenses après achat", ex["depenses_apres_achat"]["equivalent"], 410000 + 15000)297    cmp("S08 ex. temps 12 mois × 0,17 %", ex["temps"]["ajustement"], d0(400000 * 0.17 / 100 * 12)); cmp("S08 ex. temps composé 2 %", ex["temps"]["compose_2pct"], d0(400000 * 0.02))298    cmp("S08 ex. non immobilier net", ex["non_immobilier"]["net"], 440000 - 8000 - 2000)299    cmp("S08 ex. terrain 1 500 pi² × 6 $", ex["terrain"]["ajust"], (5500 - 7000) * 6)300    cmp("S08 ex. servitude 3 %", ex["servitude"]["ajust"], d0(430000 * 0.03)); cmp("S08 ex. succession 4 %", ex["conditions_vente_succession"]["ajust"], d0(405000 * 0.04))301302    # =========================== IMM1003 S09 ===========================303    S9 = R["imm1003_s09"]304    inds9 = controler_grilles(S9, SUJET_S9, COMPS_S9, "S09")305    rec9 = reconcilier(inds9, POIDS_S9)306    controler_reconc(S9["reconciliation"], rec9, "S09 réconc.")307    rec9b = reconcilier(inds9, POIDS_S9_SANS_C4)308    cmp("S09 sans C4 poids = 100", 100, sum(POIDS_S9_SANS_C4.values()), 0)309    cmp("S09 sans C4 valeur pondérée", S9["reconciliation_sans_C4"]["valeur_ponderee"], rec9b["vp"]); cmp("S09 sans C4 valeur conclue", S9["reconciliation_sans_C4"]["valeur_conclue"], rec9b["conclue"])310    cmp("S09 C1 financement = −avantage 3 ans", S9["grilles"][0]["transactionnels"][1][1], -f3["avantage"])311    cmp("S09 C3 : temps calculé sur le prix + dépenses (440 000)", S9["grilles"][2]["transactionnels"][4][1], d0(440000 * 0.0017 * 7))312    # sensibilités313    sens = S9["sensibilite"]314    for gv in (10000, 15000, 20000):315        cat = {**CAT, "garage": {"aucun": 0, "simple": 30000, "double": 30000 + gv}}316        vp = reconcilier({c["nom"]: grille(SUJET_S9, c, cat)["ind"] for c in COMPS_S9}, POIDS_S9)["vp"]317        cmp(f"S09 sensibilité garage double {gv}", sens[f"garage_double_vs_simple_{gv}"], vp)318    for tv in (-0.25, 0.0, 0.17, 0.34):319        cat = {**CAT, "temps_mois_pct": tv}320        vp = reconcilier({c["nom"]: grille(SUJET_S9, c, cat)["ind"] for c in COMPS_S9}, POIDS_S9)["vp"]321        cmp(f"S09 sensibilité temps {tv} %/mois", sens[f"temps_{tv}"], vp)322    for lv in (3.0, 5.0, 8.0):323        cat = {**CAT, "loc_pct": lv}324        vp = reconcilier({c["nom"]: grille(SUJET_S9, c, cat)["ind"] for c in COMPS_S9}, POIDS_S9)["vp"]325        cmp(f"S09 sensibilité localisation {lv} %", sens[f"localisation_{lv}"], vp)326    # régression327    prix = [250000 + 150 * a + e for a, e in zip(REG_AIRES, REG_RESID)]328    reg = S9["regression"]329    cmp("S09 régression : prix reconstitués identiques", True, prix == reg["prix"])330    o = ols(REG_AIRES, prix)331    cmp("S09 régression n", reg["n"], 12, 0); cmp("S09 régression β0", reg["beta0"], d0(o["b0"])); cmp("S09 régression β1", reg["beta1"], p2(o["b1"]), 0.01)332    cmp("S09 régression R²", reg["r2"], p2(o["r2"]), 0.01); cmp("S09 régression se(β1)", reg["se_beta1"], p2(o["se1"]), 0.01)333    cmp("S09 régression t(β1)", reg["t_beta1"], p2(o["t"]), 0.01); cmp("S09 régression σ résidus", reg["ecart_type_residus"], d0(o["sigma"]))334    # paires335    for k, (hi, lo) in PAIRES.items():336        cmp(f"S09 paire {k} écart $", S9["paires"][k]["ecart"], hi - lo); cmp(f"S09 paire {k} écart %", S9["paires"][k]["ecart_pct"], p1((hi - lo) / lo * 100), 0.05)337    # revente338    p0, p1_, m = REVENTE; rv = S9["revente"]339    cmp("S09 revente variation %", rv["variation_pct"], p2((p1_ / p0 - 1) * 100), 0.01)340    cmp("S09 revente taux annuel composé %", rv["taux_annuel_compose_pct"], p2(((p1_ / p0) ** (12 / m) - 1) * 100), 0.01)341    cmp("S09 revente taux mensuel simple %", rv["taux_mensuel_simple_pct"], p2((p1_ / p0 - 1) * 100 / m), 0.01)342    # capitalisation343    cmp("S09 capitalisation garage 150 × 12 / 0,07", S9["capitalisation"]["valeur"], d0(150 * 12 / 0.07))344    # équivalence au comptant C1345    for k in ("interets_annuels", "va_capital", "va_interets", "valeur_actuelle", "avantage"):346        cmp(f"S09 équivalence comptant C1 {k}", S9["equivalence_comptant_C1"][k], f3[k])347    # exemple % vs $348    cmp("S09 ex. 3 % de 420 000", S9["exemple_pct_vs_dollar"]["ajust_dollar"], d0(420000 * 0.03))349    # ordre séquentiel vs additif350    eo = S9["exemple_ordre"]351    seq = d0((400000 - 10000) * 1.02); add = d0(400000 * 1.02 - 10000)352    cmp("S09 ordre : séquence", eo["sequence"], seq); cmp("S09 ordre : additif", eo["additif"], add); cmp("S09 ordre : écart", eo["ecart"], seq - add)353    # valeurs aberrantes354    ab = S9["valeurs_aberrantes"]355    cmp("S09 aberrantes médiane", ab["mediane"], rec9["med"])356    for c in COMPS_S9:357        cmp(f"S09 aberrantes écart {c['nom']} %", ab["ecarts_pct"][c["nom"]], p1((inds9[c["nom"]] - rec9["med"]) / rec9["med"] * 100), 0.05)358    cmp("S09 aberrantes : seul C4 dépasse 10 %", ["C4"], [k for k, v in ab["ecarts_pct"].items() if abs(v) > ab["seuil_pct"]])359360    # =========================== IMM1033 S08 ===========================361    S3 = R["imm1033_s08"]; ch = S3["chaine"]; k = chaine()362    cmp("1033 somme des divisions = 785 000", 785000, k["nus"], 0)363    for a, b in (("directs_nus", "nus"), ("cg", "cg"), ("apres_cg", "a1"), ("fg", "fg"), ("apres_fg", "a2"), ("profit_eg", "peg"), ("directs_cours", "dcours"),364                 ("contingences", "cont"), ("directs_plus_contingences", "dc"), ("permis_proportionnel", "perm_prop"), ("permis_minimum", "perm_min"), ("permis", "permis"),365                 ("gcr", "gcr"), ("interets_intercalaires", "inter"), ("honoraires", "hono"), ("assurance", "assur"), ("juridique", "jur"), ("indirects", "ind"),366                 ("assiette_profit", "assiette"), ("profit_entrepreneurial", "profit"), ("cout_total", "total"), ("cout_par_logement", "par_log")):367        cmp(f"1033 chaîne {a}", ch[a], k[b])368    cmp("1033 taux du prêt (4,70 + 2,05)", ch["taux_pret"], k["i"], 0.001); cmp("1033 durée", ch["duree_mois"], k["T"], 0)369    cmp("1033 majoration en cascade % (attendu 24,8)", ch["majoration_cascade_pct"], k["cascade_pct"], 0.05)370    cmp("1033 cascade = +24,8 % (1,08 × 1,07 × 1,08)", 24.8, p1((1.08 * 1.07 * 1.08 - 1) * 100), 0.05)371    cmp("1033 ratio indirects/directs du cours %", ch["ratio_indirects_directs_cours_pct"], k["ratio_ind_pct"], 0.05)372    cmp("1033 $/pi² hors sol", ch["cout_pi2_hors_sol"], k["pi2_hs"], 0.01); cmp("1033 $/pi² brut", ch["cout_pi2_brut"], k["pi2_brut"], 0.01)373    cmp("1033 marge sur prix % (11/111)", ch["marge_sur_prix_pct"], k["marge_pct"], 0.01); cmp("1033 marge = 9,91 %", 9.91, p2(11 / 1.11), 0.01)374    cmp("1033 part indirects du total %", ch["part_indirects_du_total_pct"], k["part_ind"], 0.05); cmp("1033 part profit du total %", ch["part_profit_du_total_pct"], k["part_profit"], 0.05)375    cmp("1033 GCR non applicable (6 > 5 logements)", ch["gcr_applicable"], 6 <= 5)376    cmp("1033 permis : le proportionnel (4 789) l'emporte sur le minimum (2 010)", ch["permis"], max(k["perm_prop"], k["perm_min"]))377    kg = chaine(gcr=True); chg = S3["chaine_si_gcr_applicable"]378    for a, b in (("gcr", "gcr"), ("indirects", "ind"), ("profit_entrepreneurial", "profit"), ("cout_total", "total")):379        cmp(f"1033 variante GCR {a}", chg[a], kg[b])380    cmp("1033 variante GCR = 6 × 2 656,68", chg["gcr"], d0(6 * 2656.68))381    # intérêts382    cmp("1033 intérêts simplifiée ½", S3["interets_methode_simplifiee"], k["inter"])383    for prof, key in (("lineaire", "interets_par_periode_lineaire"), ("courbe_en_S", "interets_par_periode_courbe_S")):384        P = S3[key]; q = interets_periode(k["dc"], k["i"], k["T"], prof)385        cmp(f"1033 intérêts par période {prof} : montant", P["montant"], k["dc"]); cmp(f"1033 intérêts par période {prof} : total", P["interets_total"], q["total"])386        cmp(f"1033 intérêts par période {prof} : solde moyen", P["solde_moyen"], q["solde_moyen"])387        cmp(f"1033 {prof} : Σ décaissements arrondis = D+C (±0,5 $ × 14 lignes)", k["dc"], sum(l["dec"] for l in q["lignes"]), 7)388        for L, l in zip(P["lignes"], q["lignes"]):389            cmp(f"1033 {prof} mois {L['mois']:2d} décaissement", L["decaissement"], l["dec"]); cmp(f"1033 {prof} mois {L['mois']:2d} cumul", L["cumul_decaisse"], l["cumul"])390            cmp(f"1033 {prof} mois {L['mois']:2d} intérêt", L["interet"], l["interet"]); cmp(f"1033 {prof} mois {L['mois']:2d} solde fin", L["solde_fin"], l["solde"])391    # sensibilités392    for iv in (5.75, 6.75, 7.75, 8.75):393        q = chaine(i=iv); s = S3["sensibilite_taux"][str(iv)]394        cmp(f"1033 sensibilité taux {iv} % intérêts", s["interets"], q["inter"]); cmp(f"1033 sensibilité taux {iv} % total", s["cout_total"], q["total"])395    for tv in (10, 14, 18, 20, 24):396        q = chaine(T=tv); s = S3["sensibilite_duree"][str(tv)]397        cmp(f"1033 sensibilité durée {tv} mois intérêts", s["interets"], q["inter"]); cmp(f"1033 sensibilité durée {tv} mois total", s["cout_total"], q["total"])398    q = chaine(T=20); rt = S3["retard_6_mois"]399    cmp("1033 retard 6 mois intérêts", rt["interets"], q["inter"]); cmp("1033 retard 6 mois surcoût intérêts", rt["surcout_interets"], q["inter"] - k["inter"])400    cmp("1033 retard 6 mois coût total", rt["cout_total"], q["total"]); cmp("1033 retard 6 mois surcoût total", rt["surcout_total"], q["total"] - k["total"])401    # extraction marché402    ex = S3["extraction_marche"]; bats = []403    for (nom, prix_, terr), v in zip(MARCHE, ex["ventes"]):404        bat = prix_ - terr; bats.append(bat)405        cmp(f"1033 marché {nom} bâtiment implicite", v["batiment_implicite"], bat)406        cmp(f"1033 marché {nom} profit extrait %", v["profit_extrait_pct"], p1((bat - k["assiette"]) / k["assiette"] * 100), 0.05)407        cmp(f"1033 marché {nom} prix/logement", v["prix_par_logement"], d0(prix_ / 6))408    sb = sorted(bats); med = (sb[1] + sb[2]) / 2409    cmp("1033 marché médiane bâtiment implicite", ex["batiment_implicite_median"], d0(med)); cmp("1033 marché assiette", ex["assiette"], k["assiette"])410    cmp("1033 marché profit extrait médian %", ex["profit_extrait_median_pct"], p1((med - k["assiette"]) / k["assiette"] * 100), 0.05)411    cmp("1033 marché écart coût total vs marché %", ex["ecart_cout_total_vs_marche_pct"], p1((k["total"] - med) / med * 100), 0.05)412    # taxes413    TX = S3["taxes"]414    for key, prix_, pa in (("occupant_350000", 350000, False), ("occupant_280000", 280000, False), ("occupant_premier_acheteur_650000", 650000, True), ("occupant_premier_acheteur_350000", 350000, True)):415        t = taxes(prix_, pa); T_ = TX[key]416        for a, b in (("tps", "tps"), ("tvq", "tvq"), ("taxes_brutes", "brutes"), ("remb_tps_general", "rt"), ("remb_tps_premier_acheteur", "rpa"), ("remb_tps_retenu", "rt_eff"), ("remb_tvq", "rq"), ("taxes_nettes", "net")):417            cmp(f"1033 taxes {key} {a}", T_[a], t[b], 0.01)418        cmp(f"1033 taxes {key} nettes %", T_["taxes_nettes_pct"], t["net_pct"], 0.01)419    loc = TX["locatif_6_logements"]; base = k["total"] + SIX["terrain"]420    cmp("1033 taxes locatif base JVM", loc["base_jvm_estimee"], base); cmp("1033 taxes locatif TPS", loc["tps"], p2(base * 0.05), 0.01); cmp("1033 taxes locatif TVQ", loc["tvq"], p2(base * 0.09975), 0.01)421    cmp("1033 taxes locatif JVM/logement ≥ 225 000 (→ remb. TVQ nul)", True, base / 6 >= 225000)422    cmp("1033 taxes locatif : note du moteur « ≈ 231 000 $/logement » vs base/6", 231000, d0(base / 6), 1000)423    # honoraires424    H = S3["honoraires_methodes"]425    cmp("1033 honoraires % base", H["pourcentage"]["base"], k["dcours"]); cmp("1033 honoraires % montant", H["pourcentage"]["montant"], d0(k["dcours"] * 0.04))426    cmp("1033 honoraires architecte 2,5 %", H["detail_convention"]["architecte"], d0(k["dcours"] * 0.025)); cmp("1033 honoraires ingénieurs 1,5 %", H["detail_convention"]["ingenieurs"], d0(k["dcours"] * 0.015))427    cmp("1033 honoraires horaire 260 × 145", H["horaire"]["montant"], 260 * 145); cmp("1033 honoraires forfait", H["forfait"]["montant"], 38500)428    # profit vs marge429    PM = S3["profit_vs_marge"]430    cmp("1033 profit/marge coût", PM["cout"], k["assiette"]); cmp("1033 profit 11 % sur coût", PM["profit_11_sur_cout"], k["profit"]); cmp("1033 prix équivalent", PM["prix_equivalent"], k["total"])431    cmp("1033 marge sur prix %", PM["marge_sur_prix_pct"], p2(k["profit"] / k["total"] * 100), 0.02)432    cmp("1033 profit pour marge 11 % sur prix", PM["profit_pour_marge_11_sur_prix"], d0(k["assiette"] * 0.11 / 0.89)); cmp("1033 taux sur coût équivalent %", PM["taux_sur_cout_equivalent_marge_11_pct"], p2(11 / 89 * 100), 0.01)433    # exemples simples434    E = S3["exemples_simples"]435    cmp("1033 ex. intercalaires 800 000 × 7 % × 10/12 × ½", E["intercalaires_800000"]["I"], d0(800000 * 0.07 * 10 / 12 / 2))436    cmp("1033 ex. cascade 100 000 → 124 805", E["cascade_vs_somme"]["cascade"], d0(100000 * 1.08 * 1.07 * 1.08)); cmp("1033 ex. somme 23 %", E["cascade_vs_somme"]["somme_23pct"], 123000)437    cmp("1033 ex. permis 277 500 $", E["permis_exemples"]["travaux_277500"], max(d0(277500 * 4.70 / 1000), 335))438    cmp("1033 ex. permis 800 000 $ / 4 log.", E["permis_exemples"]["travaux_800000_4_log"], max(d0(800000 * 4.70 / 1000), 4 * 335))439    cmp("1033 ex. permis 40 000 $ / 1 log. (minimum)", E["permis_exemples"]["petits_travaux_40000_1_log"], max(d0(40000 * 4.70 / 1000), 335))440    cmp("1033 ex. extraction originale 15,7 %", E["extraction_originale"]["pct"], p1(170000 / (900000 + 180000) * 100), 0.05)441442    # =========================== Vérification des decks originaux (recalcul autonome) ===========================443    V = {c["element"]: c for c in R["verifications_originaux"]}444    def vo(elem, mien, tol=0.5, verdict_attendu=None):445        c = V[elem]446        cmp(f"VO | {elem[:60]} | recalculé", c["valeur_recalculee"], mien, tol)447        if verdict_attendu is not None:448            cmp(f"VO | {elem[:60]} | verdict exact", c["exact"], verdict_attendu)449    # S08 original (5 %/an, prorata mensuel) — la valeur « recalculée » du moteur est arrondie à 100 $450    for nom, prix_, mois in (("C1", 435000, 2), ("C2", 415000, 4), ("C3", 460000, 1), ("C4", 425000, 6)):451        exact = prix_ * 0.05 * mois / 12452        vo(f"temps {nom} (5 %/an)", int(rd(exact / 100)) * 100, 0.5, abs(V[f"temps {nom} (5 %/an)"]["valeur_deck"] - exact) <= 100)453    vo("net propriété C1", -5000 - 2000 - 2000 - 5000, 0.5, True); vo("net propriété C2", 10000 + 5000 + 5000 + 3000 + 12000 + 20000 + 5000, 0.5, True)454    vo("net propriété C3", -(15000 + 15000 + 10000 + 8000 + 10000 + 18000 + 8000 + 8000), 0.5, True); vo("net propriété C4", 10000 + 3000, 0.5, True)455    vo("prix ajusté final C2", 415000 + 6900 + 60000, 0.5, True); vo("moyenne des 4 indications", (424600 + 481900 + 369900 + 448600) / 4, 0.5, True)456    vo("ampleur C3 brut %", p1((1900 + 92000) / 460000 * 100), 0.05, True); vo("ampleur C1 net %", p1((3600 - 14000) / 435000 * 100), 0.05, True)457    vo("financement : reprise 100 000 $ à 2 % vs 5 % (3 ans) — deck ≈ 10 000 $", f3["avantage"], 1, False); vo("financement (5 ans)", f5["avantage"], 1, False)458    # S09 original459    vo("temps C1 : 410 000 × 5 % × 8/12", 410000 * 0.05 * 8 / 12, 0.5, True); vo("indication C1", 410000 + 13667 + 32500, 0.5, True)460    vo("net C1 % (sans le temps)", p1(32500 / 410000 * 100), 0.05, True); vo("brut C1 % (sans le temps)", p1(48500 / 410000 * 100), 0.05, True)461    vo("temps C2 : 445 000 × 5 % × 3/12", 445000 * 0.05 * 3 / 12, 0.5, True); vo("indication C2", 445000 + 5562.5 + 4100, 0.5, True)462    vo("temps C3", 425000 * 0.05 * 6 / 12, 0.5, True); vo("indication C3", 435625 - 5500, 0.5, True)463    vo("temps C4", 460000 * 0.05 * 2 / 12, 0.5, True); vo("localisation C4 −5 %", -0.05 * 463833, 0.5, True); vo("indication C4", 463833 - 74692, 0.5, True)464    vo("net C4 %", p1(74692 / 460000 * 100), 0.05, True); vo("temps C5", 438000 * 0.05 * 5 / 12, 0.5, True); vo("indication C5", 447125 + 37000, 0.5, True)465    vo("quiz net 350 000 $", p1(13000 / 350000 * 100), 0.05, True); vo("quiz brut", p1(43000 / 350000 * 100), 0.05, True)466    vo("capitalisation garage 150 × 12 / 0,07", 150 * 12 / 0.07, 0.5, True)467    # hypothèque assumable 400 000 $ : contrôle du moteur fondé sur un solde de 100 000 $ (5 ans) — on recalcule aussi le vrai cas 400 000 $468    f400 = equivalence_comptant(400000, 2, 5, 5)469    vo("financement : hypothèque 400 000 $ assumable 2 % vs 5 % — deck 12 000 $", f5["avantage"], 1, None)470    cmp("VO | hypothèque 400 000 $ (2 % vs 5 %, 5 ans, intérêts seulement) : avantage réel", 12000, f400["avantage"], 1000)471    # IMM1033 original472    vo("exemple rapide : total 660 000 $", 500000 * 1.2 * 1.1, 0.5, True); vo("honoraires 8 logements", 96000 + 24000 + 36000 + 18000 + 3500 + 48000, 0.5, True)473    vo("honoraires / directs 8 logements %", p1(225500 / 1200000 * 100), 0.05, True); vo("intercalaires 800 000 × 7 % × 10/12 × ½", 800000 * 0.07 * 10 / 12 / 2, 0.5, True)474    vo("TPS 350 000 $", 350000 * 0.05, 0.5, True); vo("TVQ 350 000 $", 350000 * 0.09975, 0.5, True)475    t350 = taxes(350000)476    vo("remboursement TVQ à 350 000 $ (deck : 9 975 $)", t350["rq"], 0.5, False); vo("taxes nettes 350 000 $ (deck : 36 138 $)", t350["net"], 0.5, False)477    vo("assurances/légal 6 logements : 35 500 $", 9000 + 3000 + 4500 + 4000 + 15000, 0.5, True); vo("extraction du profit 15,7 %", p1(170000 / 1080000 * 100), 0.05, True)478    ind = [19425, 5550, 6937.5, 2500, 6475, 2081.25, 2775, 2220, 2500, 2000, 4162.5]479    vo("exemple résidentiel : indirects 56 627 $", sum(ind), 0.5, True); vo("coût total 367 540 $", (277500 + sum(ind)) * 1.10, 0.5, True)480    vo("coût total $/pi² 245", 367540 / 1500, 0.01, True); vo("permis exemple résidentiel (10 $/1 000 $) 2 775 $", 277500 / 100, 0.5, True)481    vo("commercial : indirects 205 375 $", 775000 * 0.265, 0.5, True); vo("commercial : total 1 098 020 $", 980375 * 1.12, 0.5, True)482    vo("6 logements : directs 692 200 $", (28 + 22 + 24 + 12 + 32) * 5400 + 35000 + 20000, 0.5, True); vo("6 logements : 128 $/pi²", 692200 / 5400, 0.01, True)483    cmp("VO | taux directeur 2,25 % : contrôle factuel (non calculable) — verdict ✗ pris tel quel", V["taux directeur 2,25 % (15 juillet 2026)"]["exact"], False)484    cmp("VO | nombre de contrôles originaux", 51, len(R["verifications_originaux"]), 0)485    cmp("VO | nombre de ✗ déclarés par le moteur", 5, sum(1 for c in R["verifications_originaux"] if not c["exact"]), 0)486487    # =========================== Affichage ===========================488    n = len(LIGNES); nko = sum(1 for l in LIGNES if not l[4])489    print(f"{'élément':<78} | {'moteur':>14} | {'contre-calcul':>14} | {'écart':>10} | verdict")490    print("-" * 132)491    for elem, mo, co, ec, ok in LIGNES:492        if ok and not tout: continue493        e = "" if ec is None else f"{ec:+.2f}"494        print(f"{elem[:78]:<78} | {fmt(mo):>14} | {fmt(co):>14} | {e:>10} | {'OK' if ok else 'ÉCART'}")495    print("-" * 132)496    print(f"BILAN : {n} valeurs contrôlées, {n - nko} OK, {nko} écart(s)")497    if not tout and nko == 0: print("(aucune ligne affichée : toutes OK ; --tout pour le détail)")498    return nko499500if __name__ == "__main__":501    sys.exit(1 if main(sys.argv[1:]) else 0)502
Files main
Menu

Theme