"""Détection heuristique de sévérité et CMA/CMS pour le codage GHM. Phase 1 : heuristique basée sur des marqueurs textuels et des racines CIM-10. Phase 2 (future) : tables CMA/CMS officielles ATIH. """ from __future__ import annotations import re from dataclasses import dataclass, field from typing import Optional from .cim10_dict import load_dict, normalize_text # --- Marqueurs de sévérité dans le texte --- _SEVERE_MARKERS = { "aigu", "aigue", "severe", "grave", "maligne", "malin", "foudroyant", "foudroyante", "necrosant", "necrosante", "septique", "decompense", "decompensee", "choc", "defaillance", "hemorragique", "fulminant", "fulminante", "massif", "massive", "critique", } _MODERATE_MARKERS = { "modere", "moderee", "moderes", "moderees", "subaigu", "subaigue", "subaiguë", "persistant", "persistante", "recidivant", "recidivante", } _MILD_MARKERS = { "chronique", "leger", "legere", "benin", "benigne", "mineur", "mineure", "superficiel", "superficielle", "stable", } # --- Racines CIM-10 fréquemment CMA (heuristique Phase 1) --- # Ces racines sont connues pour être souvent classées CMA dans les tables ATIH. _HEURISTIC_CMA_ROOTS: set[str] = { # Infectieux "A41", # Sepsis "A40", # Septicémie streptococcique # Hématologie / nutrition "D64", # Anémie "D65", # CIVD "E46", # Dénutrition "E87", # Troubles hydro-électrolytiques "E86", # Déshydratation # Métabolique "E11", # Diabète type 2 (avec complications) "E10", # Diabète type 1 (avec complications) # Cardiovasculaire "I48", # Fibrillation auriculaire "I50", # Insuffisance cardiaque "I26", # Embolie pulmonaire "I80", # Thrombose veineuse # Respiratoire "J18", # Pneumopathie "J96", # Insuffisance respiratoire "J69", # Pneumopathie d'inhalation # Rénal "N17", # Insuffisance rénale aiguë "N18", # Insuffisance rénale chronique "N39", # Infection urinaire # Hépatique "K72", # Insuffisance hépatique # Infectieux nosocomial "T81", # Complications d'actes (infection post-op) "T80", # Complications post-perfusion } @dataclass class SeverityInfo: """Résultat de l'évaluation de sévérité d'un diagnostic.""" est_cma_probable: bool = False niveau_severite: str = "non_evalue" # "leger" | "modere" | "severe" | "non_evalue" marqueurs_trouves: list[str] = field(default_factory=list) def _detect_severity_markers(text: str) -> tuple[str, list[str]]: """Détecte les marqueurs de sévérité dans un texte normalisé. Returns: (niveau, marqueurs_trouves) où niveau est "severe", "modere", "leger" ou "non_evalue". """ text_norm = normalize_text(text) words = set(text_norm.split()) found_severe = words & _SEVERE_MARKERS found_moderate = words & _MODERATE_MARKERS found_mild = words & _MILD_MARKERS all_found = list(found_severe | found_moderate | found_mild) if found_severe: return "severe", all_found if found_moderate: return "modere", all_found if found_mild: return "leger", all_found return "non_evalue", [] def _is_heuristic_cma(code: str) -> bool: """Vérifie si un code CIM-10 est probablement CMA selon les racines heuristiques.""" if not code: return False code_upper = code.upper() for root in _HEURISTIC_CMA_ROOTS: if code_upper.startswith(root): return True return False def evaluate_severity(diagnostic) -> SeverityInfo: """Évalue la sévérité d'un diagnostic (texte + code CIM-10). Args: diagnostic: Objet avec attributs texte, cim10_suggestion. Returns: SeverityInfo avec est_cma_probable, niveau_severite, marqueurs_trouves. """ info = SeverityInfo() # 1. Marqueurs textuels depuis le texte du diagnostic texte = diagnostic.texte or "" niveau, marqueurs = _detect_severity_markers(texte) # 2. Chercher aussi dans le label du dictionnaire CIM-10 code = diagnostic.cim10_suggestion if code: cim10_dict = load_dict() label = cim10_dict.get(code, "") if label: niveau_label, marqueurs_label = _detect_severity_markers(label) # Prendre le niveau le plus sévère severity_order = {"severe": 3, "modere": 2, "leger": 1, "non_evalue": 0} if severity_order.get(niveau_label, 0) > severity_order.get(niveau, 0): niveau = niveau_label marqueurs = list(set(marqueurs + marqueurs_label)) info.niveau_severite = niveau info.marqueurs_trouves = marqueurs # 3. Heuristique CMA basée sur la racine CIM-10 if code and _is_heuristic_cma(code): info.est_cma_probable = True # Un diagnostic sévère avec un code CMA-probable = forte indication if niveau == "severe" and info.est_cma_probable: info.est_cma_probable = True return info def enrich_dossier_severity(dp, das_list: list) -> tuple[list[str], int, int]: """Enrichit les diagnostics d'un dossier avec les informations de sévérité. Modifie les diagnostics en place (attributs est_cma, est_cms, niveau_severite). Args: dp: Diagnostic principal. das_list: Liste des diagnostics associés. Returns: (alertes, cma_count, cms_count). """ alertes = [] # Évaluer le DP if dp and dp.cim10_suggestion: info = evaluate_severity(dp) dp.niveau_severite = info.niveau_severite if info.est_cma_probable: dp.est_cma = True # Évaluer chaque DAS cma_count = 0 cms_count = 0 for das in das_list: if not das.cim10_suggestion: continue info = evaluate_severity(das) das.niveau_severite = info.niveau_severite if info.est_cma_probable: das.est_cma = True cma_count += 1 # CMS = CMA sévère if info.niveau_severite == "severe": das.est_cms = True cms_count += 1 alertes.append( f"CMA probable : '{das.texte}' ({das.cim10_suggestion}) — " f"sévérité {info.niveau_severite}" + (f", marqueurs : {', '.join(info.marqueurs_trouves)}" if info.marqueurs_trouves else "") ) if cma_count >= 2: alertes.insert(0, f"{cma_count} CMA probables détectées — impact potentiel sur le niveau de sévérité GHM") return alertes, cma_count, cms_count