"""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 json import logging import re from dataclasses import dataclass, field from typing import Optional from .cim10_dict import load_dict, normalize_text logger = logging.getLogger(__name__) # --- 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 } _cma_levels: dict[str, int] | None = None def _load_cma_levels() -> dict[str, int]: """Charge les niveaux CMA officiels depuis data/cma_levels.json (lazy-loaded).""" global _cma_levels if _cma_levels is not None: return _cma_levels from ..config import CMA_LEVELS_PATH try: data = json.loads(CMA_LEVELS_PATH.read_text(encoding="utf-8")) _cma_levels = {k: int(v) for k, v in data.items()} logger.debug("CMA levels chargés : %d codes", len(_cma_levels)) except FileNotFoundError: logger.warning("Fichier CMA levels non trouvé : %s", CMA_LEVELS_PATH) _cma_levels = {} except Exception: logger.warning("Erreur chargement CMA levels", exc_info=True) _cma_levels = {} return _cma_levels @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" niveau_cma: int = 1 # 1 (pas CMA), 2, 3 ou 4 (officiel ATIH) 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). Utilise en priorité les niveaux CMA officiels ATIH (2/3/4), avec fallback sur l'heuristique par racines CIM-10. Args: diagnostic: Objet avec attributs texte, cim10_suggestion. Returns: SeverityInfo avec est_cma_probable, niveau_cma, 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. Lookup officiel CMA ATIH (prioritaire) if code: cma_levels = _load_cma_levels() official_level = cma_levels.get(code) if official_level: info.niveau_cma = official_level info.est_cma_probable = True elif _is_heuristic_cma(code): # Fallback heuristique → niveau 2 info.niveau_cma = 2 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 dp.niveau_cma = info.niveau_cma 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 das.niveau_cma = info.niveau_cma if info.est_cma_probable: das.est_cma = True cma_count += 1 # CMS = CMA niveau 4 ou CMA sévère if info.niveau_cma >= 4 or info.niveau_severite == "severe": das.est_cms = True cms_count += 1 alertes.append( f"CMA niveau {info.niveau_cma} : '{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