feat: règles métier T2A Phase 1 — exclusions diagnostiques, sévérité CMA et alertes codage
Ajout des règles d'exclusion symptôme (R00-R99) vs diagnostic précis (Chapitres I-XIV), détection heuristique de sévérité CMA sur 25 racines CIM-10, et affichage des alertes de codage dans le viewer Flask. 153 tests, 0 régression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
src/medical/severity.py
Normal file
201
src/medical/severity.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""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) -> list[str]:
|
||||
"""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:
|
||||
Liste d'alertes de sévérité générées.
|
||||
"""
|
||||
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
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user