fix: filtre DAS=DP + correction D55.9→D64.9 + enrichissement supplements CIM-10
- Filtre DAS identique au DP (violation règle PMSI) dans extracteur et fusion - Correction automatique D55.9 → D64.9 pour "Anémie" non qualifiée (70 cas) - 17 codes ajoutés aux supplements (K59.0, Z93.1, H92.0, A87.0, D64.9, etc.) - 436 tests OK (+14 nouveaux) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,5 +38,22 @@
|
||||
"F10.6": "Troubles mentaux et du comportement liés à l'utilisation d'alcool, syndrome amnésique",
|
||||
"F10.7": "Troubles mentaux et du comportement liés à l'utilisation d'alcool, trouble résiduel ou psychotique de survenue tardive",
|
||||
"F10.8": "Troubles mentaux et du comportement liés à l'utilisation d'alcool, autres troubles mentaux et du comportement",
|
||||
"F10.9": "Troubles mentaux et du comportement liés à l'utilisation d'alcool, trouble mental ou du comportement, sans précision"
|
||||
"F10.9": "Troubles mentaux et du comportement liés à l'utilisation d'alcool, trouble mental ou du comportement, sans précision",
|
||||
"A87.0": "Méningite à entérovirus",
|
||||
"D64.9": "Anémie, sans précision",
|
||||
"H15.1": "Épisclérite",
|
||||
"H36.0": "Rétinopathie diabétique",
|
||||
"H92.0": "Otalgie",
|
||||
"H92.1": "Otorrhée",
|
||||
"K59.0": "Constipation",
|
||||
"K59.9": "Trouble fonctionnel de l'intestin, sans précision",
|
||||
"M89.5": "Ostéolyse",
|
||||
"N40.0": "Hypertrophie de la prostate sans obstruction urinaire",
|
||||
"Z50.5": "Rééducation de la parole",
|
||||
"Z93.1": "Gastrostomie",
|
||||
"Z93.2": "Iléostomie",
|
||||
"Z93.3": "Colostomie",
|
||||
"Z93.0": "Trachéostomie",
|
||||
"Z98.0": "État consécutif à une dérivation intestinale ou à une anastomose",
|
||||
"Z98.1": "État consécutif à une arthrodèse"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
from .cim10_dict import lookup as dict_lookup, normalize_text, normalize_code, validate_code as cim10_validate
|
||||
from .ccam_dict import lookup as ccam_lookup, validate_code as ccam_validate
|
||||
from .das_filter import clean_diagnostic_text, is_valid_diagnostic_text
|
||||
from .das_filter import clean_diagnostic_text, is_valid_diagnostic_text, correct_known_miscodes
|
||||
from ..config import (
|
||||
ActeCCAM,
|
||||
BiologieCle,
|
||||
@@ -125,6 +125,9 @@ def extract_medical_info(
|
||||
# Post-processing : validation des codes CIM-10 contre le dictionnaire
|
||||
_validate_cim10(dossier)
|
||||
|
||||
# Post-processing : correction des codes systématiquement mal attribués
|
||||
_apply_code_corrections(dossier)
|
||||
|
||||
# Post-processing : exclusions symptôme vs diagnostic précis
|
||||
_apply_exclusion_rules(dossier)
|
||||
|
||||
@@ -134,6 +137,9 @@ def extract_medical_info(
|
||||
# Post-processing : détection non-cumul actes CCAM
|
||||
_apply_noncumul_rules(dossier)
|
||||
|
||||
# Post-processing : retirer DAS dont le code est identique au DP
|
||||
_remove_das_equal_dp(dossier)
|
||||
|
||||
return dossier
|
||||
|
||||
|
||||
@@ -855,6 +861,36 @@ def _apply_severity_rules(dossier: DossierMedical) -> None:
|
||||
logger.warning("Erreur lors de l'évaluation de sévérité", exc_info=True)
|
||||
|
||||
|
||||
def _apply_code_corrections(dossier: DossierMedical) -> None:
|
||||
"""Corrige les codes CIM-10 systématiquement mal attribués par le LLM."""
|
||||
all_diags = []
|
||||
if dossier.diagnostic_principal:
|
||||
all_diags.append(dossier.diagnostic_principal)
|
||||
all_diags.extend(dossier.diagnostics_associes)
|
||||
|
||||
for diag in all_diags:
|
||||
if not diag.cim10_suggestion:
|
||||
continue
|
||||
corrected = correct_known_miscodes(diag.cim10_suggestion, diag.texte)
|
||||
if corrected:
|
||||
logger.info(" Code corrigé : %s → %s pour « %s »", diag.cim10_suggestion, corrected, diag.texte)
|
||||
diag.cim10_suggestion = corrected
|
||||
|
||||
|
||||
def _remove_das_equal_dp(dossier: DossierMedical) -> None:
|
||||
"""Retire les DAS dont le code CIM-10 est identique au DP (violation règle PMSI)."""
|
||||
dp_code = dossier.diagnostic_principal.cim10_suggestion if dossier.diagnostic_principal else None
|
||||
if not dp_code:
|
||||
return
|
||||
before = len(dossier.diagnostics_associes)
|
||||
dossier.diagnostics_associes = [
|
||||
d for d in dossier.diagnostics_associes if d.cim10_suggestion != dp_code
|
||||
]
|
||||
removed = before - len(dossier.diagnostics_associes)
|
||||
if removed:
|
||||
logger.info(" DAS=DP : %d DAS retiré(s) (code %s identique au DP)", removed, dp_code)
|
||||
|
||||
|
||||
def _apply_noncumul_rules(dossier: DossierMedical) -> None:
|
||||
"""Détecte les incompatibilités de non-cumul entre actes CCAM."""
|
||||
try:
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
# Corrections de codes CIM-10 systématiquement mal attribués par le LLM
|
||||
# D55.9 (anémie enzymatique) est proposé pour "Anémie" non qualifiée → D64.9
|
||||
CODE_CORRECTIONS: dict[str, dict] = {
|
||||
"D55.9": {
|
||||
"correct_code": "D64.9",
|
||||
"condition_texte": r"^an[ée]mie$", # uniquement si texte = "Anémie" seul
|
||||
"reason": "Anémie non qualifiée → D64.9 (sans précision), pas D55.9 (enzymatique)",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def clean_diagnostic_text(text: str) -> str:
|
||||
"""Nettoie un texte de diagnostic (newlines, ponctuation trailing, espaces)."""
|
||||
@@ -74,3 +84,17 @@ def is_valid_diagnostic_text(text: str) -> bool:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def correct_known_miscodes(code: str, texte: str) -> str | None:
|
||||
"""Corrige les codes CIM-10 systématiquement mal attribués par le LLM.
|
||||
|
||||
Returns:
|
||||
Le code corrigé, ou None si pas de correction nécessaire.
|
||||
"""
|
||||
correction = CODE_CORRECTIONS.get(code)
|
||||
if not correction:
|
||||
return None
|
||||
if re.match(correction["condition_texte"], texte.strip(), re.IGNORECASE):
|
||||
return correction["correct_code"]
|
||||
return None
|
||||
|
||||
@@ -168,6 +168,13 @@ def merge_dossiers(dossiers: list[DossierMedical]) -> DossierMedical:
|
||||
|
||||
merged.diagnostics_associes = _dedup_diagnostics(all_das)
|
||||
|
||||
# Retirer les DAS dont le code est identique au DP (violation règle PMSI)
|
||||
dp_code = merged.diagnostic_principal.cim10_suggestion if merged.diagnostic_principal else None
|
||||
if dp_code:
|
||||
merged.diagnostics_associes = [
|
||||
d for d in merged.diagnostics_associes if d.cim10_suggestion != dp_code
|
||||
]
|
||||
|
||||
# Actes CCAM
|
||||
all_actes: list[ActeCCAM] = []
|
||||
for d in dossiers:
|
||||
|
||||
@@ -73,3 +73,32 @@ class TestCim10Supplements:
|
||||
is_valid, label = validate_code("e119")
|
||||
assert is_valid
|
||||
assert label # Label non vide
|
||||
|
||||
# --- Nouveaux codes ajoutés (session 2026-02-13) ---
|
||||
def test_k59_0_constipation(self):
|
||||
"""K59.0 (constipation) est reconnu."""
|
||||
is_valid, label = validate_code("K59.0")
|
||||
assert is_valid
|
||||
assert "constipation" in label.lower()
|
||||
|
||||
def test_z93_1_gastrostomie(self):
|
||||
"""Z93.1 (gastrostomie) est reconnu."""
|
||||
is_valid, label = validate_code("Z93.1")
|
||||
assert is_valid
|
||||
assert "gastrostomie" in label.lower()
|
||||
|
||||
def test_h92_0_otalgie(self):
|
||||
"""H92.0 (otalgie) est reconnu."""
|
||||
is_valid, label = validate_code("H92.0")
|
||||
assert is_valid
|
||||
|
||||
def test_a87_0_meningite(self):
|
||||
"""A87.0 (méningite à entérovirus) est reconnu."""
|
||||
is_valid, label = validate_code("A87.0")
|
||||
assert is_valid
|
||||
|
||||
def test_d64_9_anemie(self):
|
||||
"""D64.9 (anémie sans précision) est reconnu."""
|
||||
is_valid, label = validate_code("D64.9")
|
||||
assert is_valid
|
||||
assert "anémie" in label.lower()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.medical.das_filter import clean_diagnostic_text, is_valid_diagnostic_text
|
||||
from src.medical.das_filter import clean_diagnostic_text, is_valid_diagnostic_text, correct_known_miscodes
|
||||
|
||||
|
||||
class TestCleanDiagnosticText:
|
||||
@@ -160,3 +160,35 @@ class TestIsValidDiagnosticText:
|
||||
def test_accept_long_fragment(self):
|
||||
"""Un fragment long commençant par 'Dans' peut être légitime."""
|
||||
assert is_valid_diagnostic_text("Dans le cadre d'une insuffisance rénale chronique terminale")
|
||||
|
||||
|
||||
class TestCorrectKnownMiscodes:
|
||||
"""Tests pour la correction des codes systématiquement mal attribués."""
|
||||
|
||||
def test_d55_9_anemie_simple(self):
|
||||
"""D55.9 + texte 'Anémie' → D64.9."""
|
||||
assert correct_known_miscodes("D55.9", "Anémie") == "D64.9"
|
||||
|
||||
def test_d55_9_anemie_casse(self):
|
||||
"""D55.9 + texte 'anémie' (minuscule) → D64.9."""
|
||||
assert correct_known_miscodes("D55.9", "anémie") == "D64.9"
|
||||
|
||||
def test_d55_9_anemie_spaces(self):
|
||||
"""D55.9 + texte avec espaces → D64.9."""
|
||||
assert correct_known_miscodes("D55.9", " Anémie ") == "D64.9"
|
||||
|
||||
def test_d55_9_anemie_qualifiee_pas_corrige(self):
|
||||
"""D55.9 + texte qualifié → pas de correction."""
|
||||
assert correct_known_miscodes("D55.9", "Anémie ferriprive") is None
|
||||
|
||||
def test_d55_9_anemie_hemolytique_pas_corrige(self):
|
||||
"""D55.9 + texte spécifique → pas de correction."""
|
||||
assert correct_known_miscodes("D55.9", "Anémie hémolytique enzymatique") is None
|
||||
|
||||
def test_code_inconnu_pas_corrige(self):
|
||||
"""Code non listé → pas de correction."""
|
||||
assert correct_known_miscodes("I10", "HTA") is None
|
||||
|
||||
def test_d64_9_pas_corrige(self):
|
||||
"""D64.9 lui-même → pas de correction."""
|
||||
assert correct_known_miscodes("D64.9", "Anémie") is None
|
||||
|
||||
@@ -155,6 +155,37 @@ class TestSourceFilesPopulated:
|
||||
assert result.source_files == ["a.pdf", "b.pdf"]
|
||||
|
||||
|
||||
class TestDasEqualDpRemoved:
|
||||
"""Vérifie que les DAS dont le code est identique au DP sont retirés après fusion."""
|
||||
|
||||
def test_das_same_code_as_dp_removed(self):
|
||||
d1 = DossierMedical(
|
||||
diagnostic_principal=Diagnostic(texte="HTA", cim10_suggestion="I10"),
|
||||
diagnostics_associes=[
|
||||
Diagnostic(texte="Hypertension artérielle", cim10_suggestion="I10"),
|
||||
Diagnostic(texte="Diabète", cim10_suggestion="E11.9"),
|
||||
],
|
||||
)
|
||||
d2 = DossierMedical(
|
||||
diagnostic_principal=Diagnostic(texte="HTA essentielle", cim10_suggestion="I10"),
|
||||
)
|
||||
result = merge_dossiers([d1, d2])
|
||||
das_codes = [d.cim10_suggestion for d in result.diagnostics_associes]
|
||||
assert "I10" not in das_codes, "DAS=DP doit être retiré"
|
||||
assert "E11.9" in das_codes
|
||||
|
||||
def test_das_different_code_kept(self):
|
||||
d1 = DossierMedical(
|
||||
diagnostic_principal=Diagnostic(texte="Cholécystite", cim10_suggestion="K81.0"),
|
||||
diagnostics_associes=[
|
||||
Diagnostic(texte="HTA", cim10_suggestion="I10"),
|
||||
],
|
||||
)
|
||||
result = merge_dossiers([d1])
|
||||
das_codes = [d.cim10_suggestion for d in result.diagnostics_associes]
|
||||
assert "I10" in das_codes
|
||||
|
||||
|
||||
class TestFullMergeCROTrackare:
|
||||
def test_full_merge_cro_trackare(self):
|
||||
"""Cas réel : fusion Trackare + CRO."""
|
||||
|
||||
Reference in New Issue
Block a user