- Cache persistant JSON thread-safe pour les résultats Ollama (invalidation par modèle) - Parallélisation des appels Ollama (ThreadPoolExecutor, 2 workers) - 6 nouvelles règles de filtrage DAS parasites (doublons, ponctuation, OCR, labo, fragments) - Client Ollama centralisé (mode JSON natif + retry) - Module GHM (estimation CMD/sévérité) - Module contrôle CPAM (parser + contre-argumentation RAG) - Export RUM (format RSS) - Viewer enrichi (détail dossier) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
6.5 KiB
Python
207 lines
6.5 KiB
Python
"""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
|