#!/usr/bin/env python3 """Chapitre 10 — contrôle automatique de la présence des douze éléments du contenu minimal d'un rapport écrit (NPP de l'OEAQ, norme 2, règle 2.3) dans un texte, et calculs auxiliaires du chapitre (probabilité de détection d'une lacune par révisions indépendantes, incidence d'une hypothèse extraordinaire, échéances réglementaires). Les quatre extraits de rapports analysés ici sont FICTIFS, rédigés pour le traité (aucun immeuble, aucun client, aucun évaluateur réel). Le contrôle est lexical : il détecte la présence de formulations caractéristiques, pas la qualité du contenu — limite discutée au chapitre. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch10_controle_rapport.py""" from __future__ import annotations import re, sys, pathlib, unicodedata, datetime as dt from dataclasses import dataclass sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) ROOT = pathlib.Path(__file__).resolve().parents[3] # ---------------------------------------------------------------------------------------------------------------- # 1. Les douze éléments de la règle 2.3 (paraphrase ; source : NPP OEAQ, norme 2, règle 2.3, éléments 1 à 12) # ---------------------------------------------------------------------------------------------------------------- @dataclass(frozen=True) class Element: no: int cle: str libelle: str strict: tuple # motifs caractéristiques (lexique strict) souple: tuple # motifs supplémentaires, plus larges (lexique souple = strict + souple) def _n(s: str) -> str: """Normalise : minuscules, sans accents, espaces simples, apostrophes droites.""" s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("ascii") s = s.replace("’", "'").lower() return re.sub(r"\s+", " ", s) ELEMENTS: list[Element] = [ Element(1, "identification", "Identification physique et légale de l'immeuble", (r"\blot\s+\d", r"cadastre du quebec", r"designation cadastrale", r"identification (physique|legale)"), (r"\badresse\b", r"\bsitue au\b", r"matricule")), Element(2, "droits", "Droits de propriété évalués", (r"pleine propriete", r"droits? (de propriete )?evalues?", r"droit (reel )?evalue", r"fief simple"), (r"\busufruit\b", r"emphyteo", r"indivis", r"copropriete divise")), Element(3, "but_fin", "But (valeur recherchée) et fin (usage prévu) de l'évaluation", (r"\bbut de l'evaluation", r"\bfin de l'evaluation", r"aux fins (de|d')", r"usage prevu du rapport"), (r"destinataire", r"financement hypothecaire", r"\bmandat\b")), Element(4, "definition_valeur", "Définition de la valeur recherchée", (r"valeur marchande", r"definition de la valeur", r"valeur recherchee", r"prix (sincere )?le plus probable"), (r"\bvaleur\b.{0,40}\bdefini", r"valeur d'investissement", r"valeur reelle")), Element(5, "date", "Date d'évaluation (date effective de l'opinion)", (r"date d'evaluation", r"date effective", r"en date du \d", r"\bau \d{1,2}(er)? [a-z]+ \d{4}\b"), (r"\b(janvier|fevrier|mars|avril|mai|juin|juillet|aout|septembre|octobre|novembre|decembre) \d{4}\b",)), Element(6, "etendue", "Étendue du travail (recherches, vérifications, inspection)", (r"etendue (du travail|des recherches|de la mission)", r"portee du mandat", r"travaux (effectues|realises)", r"inspect(e|ee|ion)"), (r"\bvisite\b", r"recherches? effectuee", r"verifi(e|cation)")), Element(7, "reserves_hypotheses", "Réserves limitant la portée de l'acte et hypothèses de base (dont extraordinaires)", (r"hypothese", r"\breserves?\b", r"condition(s)? limitative"), (r"\bpresum", r"\bsuppos", r"sous reserve")), Element(8, "raisonnement", "Traitement de l'information et raisonnement conduisant à la conclusion", (r"methode (de comparaison|du cout|du revenu|de la parite|des revenus)", r"\bajustements?\b", r"reconciliation", r"raisonnement", r"analyse (des|de) (ventes|donnees|marche)"), (r"\bcomparables?\b", r"taux (global|de capitalisation)", r"\bconclu(sion|ons)\b")), Element(9, "umpp", "Usage le meilleur et le plus profitable (PHMU)", (r"usage le meilleur et le plus profitable", r"plus haute et meilleure utilisation", r"\bumpp\b", r"\bphmu\b"), (r"usage optimal", r"meilleur usage")), Element(10, "rejet_methode", "Explication du rejet d'une méthode traditionnelle", (r"methode[^.]{0,80}(ecartee|rejetee|non retenue|non appliquee|n'a pas ete (retenue|appliquee))", r"rejet de la methode", r"(ecartee|rejetee|non retenue)[^.]{0,40}methode"), (r"n'a pas ete (utilisee|employee)", r"\bnon pertinente\b")), Element(11, "conformite", "Conformité à la substance de la norme 1 et écarts aux directives", (r"conform(e|ement|ite) aux normes de pratique", r"normes de pratique professionnelle", r"ecart[^.]{0,60}directive", r"directive[^.]{0,60}ecart"), (r"\bnormes?\b", r"\boeaq\b", r"ordre des evaluateurs")), Element(12, "attestation", "Attestation signée et datée", (r"j'atteste", r"\batteste(nt)? par la presente", r"\battestation\b"), (r"au mieux de (ma|notre) connaissance", r"\bsign(e|ee|ature)\b")), ] def controler(texte: str, lexique: str = "strict") -> dict: """Retourne, pour chaque élément, (détecté: bool, motif ayant déclenché la détection ou None).""" t = _n(texte) res = {} for e in ELEMENTS: motifs = e.strict + (e.souple if lexique == "souple" else ()) hit = None for m in motifs: if re.search(m, t): hit = m; break res[e.cle] = (hit is not None, hit) return res def taux_conformite(res: dict) -> float: """Équation (10.2) : proportion des douze éléments détectés.""" return sum(1 for d, _ in res.values() if d) / len(ELEMENTS) def manquants(res: dict) -> list[int]: return [e.no for e in ELEMENTS if not res[e.cle][0]] def p_detection(d: float, n: int) -> float: """Probabilité qu'au moins une de n révisions indépendantes, chacune de puissance d, relève une lacune.""" return 1.0 - (1.0 - d) ** n def incidence_hypothese(v_sous_h: float, v_sans_h: float) -> tuple[float, float]: """Incidence d'une hypothèse extraordinaire : écart absolu et relatif (base : valeur sans l'hypothèse).""" return v_sous_h - v_sans_h, (v_sous_h - v_sans_h) / v_sans_h def esperance_sous_incertitude(p: float, v_sous_h: float, v_sans_h: float) -> float: return p * v_sous_h + (1 - p) * v_sans_h def fin_conservation(dernier_service: dt.date, annees: int = 5) -> dt.date: """Règlement sur la tenue des dossiers (C-26, r. 133, art. 6) : au moins 5 ans après le dernier service.""" return dernier_service.replace(year=dernier_service.year + annees) def bilan_formation(heures_total: float, heures_ethique: float, report_anterieur: float = 0.0) -> dict: """Règlement sur la formation continue (C-26, r. 127.1) : 30 h par période de 2 ans dont 3 h éthique/déontologie/normes ; report maximal de 5 h vers la période suivante.""" requis_total, requis_ethique, report_max = 30.0, 3.0, 5.0 dispo = heures_total + min(report_anterieur, report_max) deficit_total = max(0.0, requis_total - dispo) deficit_ethique = max(0.0, requis_ethique - heures_ethique) report_suivant = min(report_max, max(0.0, dispo - requis_total)) return {"deficit_total": deficit_total, "deficit_ethique": deficit_ethique, "report_suivant": report_suivant, "conforme": deficit_total == 0 and deficit_ethique == 0} # ---------------------------------------------------------------------------------------------------------------- # 2. Extraits FICTIFS de rapports (rédigés pour le traité ; immeubles, parties et signataires inexistants) # ---------------------------------------------------------------------------------------------------------------- EXTRAITS = { "A": """EXTRAIT FICTIF A — « Rapport abrégé » Objet : résidence unifamiliale à étages située au 123, rue des Érables (adresse fictive), secteur Hull, Gatineau. Mandat : estimer la valeur de la propriété pour le compte de l'institution prêteuse, aux fins de financement hypothécaire. Description : cottage de deux étages construit en 1998, superficie habitable de 165 m², terrain de 620 m², garage simple, sous-sol aménagé. Le bâtiment est en bon état général selon la visite extérieure effectuée. Analyse : trois ventes comparables du secteur ont été retenues et ajustées pour la superficie, l'âge et le garage. Les prix ajustés s'établissent entre 538 000 $ et 571 000 $. Par la méthode de comparaison, nous estimons la valeur à 555 000 $. Ce montant est valable pour une période de six mois. Signature : Évaluateur fictif, É.A.""", "B": """EXTRAIT FICTIF B — « Rapport narratif, section Réserves et conclusion » Identification : lot 0 000 000 du cadastre du Québec (numéro fictif), circonscription foncière de Hull, bâtiment résidentiel. Droits évalués : pleine propriété, libre de toute charge autre que celles mentionnées au certificat de localisation. But et fin : estimer la valeur marchande, telle que définie aux Normes de pratique professionnelle de l'OEAQ, en date du 1er septembre 2026, aux fins d'une décision d'achat par le client. Usage le meilleur et le plus profitable : l'usage résidentiel unifamilial actuel est conforme au zonage et demeure l'usage optimal du site tel qu'aménagé. Hypothèses : nous présumons que le permis de construction pour l'agrandissement arrière a été obtenu et que les travaux sont conformes au Code. La méthode du coût a également été appliquée. Conclusion : la valeur marchande est de 612 000 $. Le présent rapport est conforme aux normes de pratique professionnelle. J'atteste par la présente, au mieux de ma connaissance, que les faits énoncés sont vrais et exacts.""", "C": """EXTRAIT FICTIF C — « Rapport de mise à jour » Le présent rapport de mise à jour se rapporte au rapport narratif complet portant sur l'immeuble de douze logements situé au 456, avenue Fictive, Montréal (lot 0 000 001 du cadastre du Québec, numéro fictif), préparé par notre firme pour le même client en date du 1er mars 2026. Les réserves et hypothèses de base du rapport original demeurent applicables. L'inspection a été mise à jour le 10 octobre 2026 ; aucun changement physique n'a été constaté. Les loyers en vigueur ont augmenté de 3,1 % et le taux global de capitalisation retenu passe de 5,25 % à 5,40 %. Par la méthode du revenu, l'indication de valeur en date du 15 octobre 2026 est de 2 410 000 $, comparativement à 2 450 000 $ au rapport original. La méthode du coût, écartée au rapport original pour un immeuble de 1965, demeure non retenue. Évaluateur fictif, É.A.""", "D": """EXTRAIT FICTIF D — « Sommaire d'un rapport narratif complet » Identification physique et légale : lot 0 000 002 du cadastre du Québec (numéro fictif), 789, rue Imaginaire, Québec. Droits évalués : pleine propriété. But et fin de l'évaluation : estimer la valeur marchande aux fins d'un règlement successoral ; le rapport est destiné au liquidateur. Définition de la valeur : la valeur marchande est définie comme le prix sincère le plus probable, conformément aux Normes de pratique professionnelle de l'OEAQ. Date d'évaluation : 1er septembre 2026. Étendue du travail : inspection complète de l'immeuble le 28 août 2026, recherche de titres, analyse de quatorze ventes de triplex. Réserves et hypothèses de base : voir l'annexe ; aucune hypothèse extraordinaire. Usage le meilleur et le plus profitable : usage résidentiel locatif actuel. Raisonnement : méthode de comparaison (cinq comparables, ajustements documentés) et méthode du revenu, réconciliation pondérée. La méthode du coût a été écartée en raison de l'âge du bâtiment et de la difficulté de mesurer la dépréciation. Le rapport est conforme à la substance de la norme 1 ; aucun écart aux directives. J'atteste par la présente, au mieux de ma connaissance et de ma conviction, que les faits énoncés sont vrais et exacts. Signé et daté.""", } def main(verbose: bool = True) -> dict: out = {"controle": {}, "taux": {}, "manquants": {}} for nom, txt in EXTRAITS.items(): for lex in ("strict", "souple"): r = controler(txt, lex) out["controle"][(nom, lex)] = r out["taux"][(nom, lex)] = taux_conformite(r) out["manquants"][(nom, lex)] = manquants(r) # probabilités de détection (modèle de révisions indépendantes) grille_d = (0.5, 0.7, 0.9); grille_n = (1, 2, 3) out["p_detection"] = {(d, n): p_detection(d, n) for d in grille_d for n in grille_n} # incidence d'une hypothèse extraordinaire (chiffres FICTIFS de l'extrait B et de l'exercice 10.5) out["incidence_B"] = incidence_hypothese(612000, 575000) out["esperance_B"] = esperance_sous_incertitude(0.6, 612000, 575000) # échéances réglementaires (exercice 10.2) out["conservation"] = fin_conservation(dt.date(2026, 11, 15)) out["formation"] = bilan_formation(heures_total=26, heures_ethique=2, report_anterieur=5) out["formation_b"] = bilan_formation(heures_total=33, heures_ethique=3, report_anterieur=0) if verbose: print("== Contrôle lexical des douze éléments (règle 2.3) — extraits FICTIFS") for nom in EXTRAITS: for lex in ("strict", "souple"): r = out["controle"][(nom, lex)] print(f" Extrait {nom} [{lex:6s}] : {sum(d for d,_ in r.values()):2d}/12 détectés " f"(taux {out['taux'][(nom, lex)]:.3f}) ; manquants : {out['manquants'][(nom, lex)]}") print("== Détail (lexique strict) :") for nom in EXTRAITS: r = out["controle"][(nom, "strict")] print(" ", nom, " ".join(f"{e.no}:{'X' if r[e.cle][0] else '.'}" for e in ELEMENTS)) print("== Probabilité de détection d'une lacune, 1-(1-d)^n :") for d in grille_d: print(" d =", d, " ".join(f"n={n}: {out['p_detection'][(d,n)]:.3f}" for n in grille_n)) dv, dr = out["incidence_B"] print(f"== Incidence de l'hypothèse extraordinaire (extrait B, fictif) : {dv:,.0f} $ ({dr*100:.2f} %) ; " f"espérance à p = 0,6 : {out['esperance_B']:,.0f} $") print("== Fin de conservation minimale d'un dossier clos le 2026-11-15 :", out["conservation"]) print("== Bilan de formation continue (26 h dont 2 h éthique, report 5 h) :", out["formation"]) print("== Bilan de formation continue (33 h dont 3 h éthique) :", out["formation_b"]) return out if __name__ == "__main__": main()