Ajoute une couche de validation post-extraction contre les référentiels
officiels de l'ATIH (Agence Technique de l'Information sur
l'Hospitalisation) pour 2018. Zéro tolérance sur les codes T2A : un
code invalide est flaggé, et une correction par plus proche voisin
(Levenshtein ≤ 1) est proposée.
Contenu :
- pipeline/referentials.py : API publique is_valid_{cim10,ccam,ghm,ghs},
get_cim10_libelle, nearest_cim10, ghm_to_ghs. CLI --build/--test/--stats.
- pipeline/validation.py : annote un JSON d'extraction avec un bloc
`_validation` par page (codes valides/invalides + suggestions + cross-
checks GHM↔GHS).
- referentials/sources/ : données brutes ATIH publiques (CIM-10 ClaML
2019 substitut, CCAM v5 2018, GHM v2018, tarifs fév. 2018).
- referentials/atih_2018.sqlite : base SQLite prête à l'emploi
(11 623 CIM-10 · 8 147 CCAM · 2 593 GHM · 5 329 couples GHM→GHS).
- tests/test_referentials.py : 11 tests unitaires (11/11 passent).
- annotate_validation.py : script qui annote tous les JSONs V2 en
place et produit validation_report.md.
Note CIM-10 : la version 2018 ATIH n'est publiée qu'en PDF, ClaML 2019
est utilisée en substitut (écart connu ≈ 60 codes / 11 600).
Gestion des suffixes PMSI : `*` (CMA exclue par le DP) et `+N`
(extension PMSI) sont strippés avant validation, le code racine seul
est comparé au référentiel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
5.3 KiB
Python
161 lines
5.3 KiB
Python
"""Tests unitaires du module pipeline.referentials.
|
|
|
|
Compatible pytest ET exécution directe (`python tests/test_referentials.py`).
|
|
Nécessite que la base SQLite ait déjà été construite :
|
|
python -m pipeline.referentials --build
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Permet l'exécution directe depuis tests/ sans installer le package.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from pipeline.referentials import ( # noqa: E402
|
|
DB_PATH,
|
|
ghm_to_ghs,
|
|
get_cim10_libelle,
|
|
is_valid_ccam,
|
|
is_valid_cim10,
|
|
is_valid_ghm,
|
|
is_valid_ghs,
|
|
nearest_cim10,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CIM-10
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_cim10_codes_valides():
|
|
"""Quelques codes courants du référentiel PMSI."""
|
|
assert is_valid_cim10("K650") # Péritonite aigüe
|
|
assert is_valid_cim10("T814") # Infection après acte
|
|
assert is_valid_cim10("I10") # Hypertension essentielle
|
|
assert is_valid_cim10("Z515") # Soins palliatifs
|
|
assert is_valid_cim10("C509") # Tumeur maligne du sein
|
|
|
|
|
|
def test_cim10_normalisation():
|
|
"""Tolère le point décimal (K65.0) et la casse (k650)."""
|
|
assert is_valid_cim10("K65.0")
|
|
assert is_valid_cim10("k650")
|
|
assert is_valid_cim10(" K650 ")
|
|
|
|
|
|
def test_cim10_codes_invalides():
|
|
assert not is_valid_cim10("")
|
|
assert not is_valid_cim10("ZZZ99")
|
|
assert not is_valid_cim10("K6501234") # trop long
|
|
assert not is_valid_cim10("1234") # ne commence pas par lettre
|
|
assert not is_valid_cim10("K65X") # suffixe non numérique
|
|
|
|
|
|
def test_cim10_libelle():
|
|
lib = get_cim10_libelle("K650")
|
|
assert lib is not None and "éritonit" in lib.lower() or "peritonit" in lib.lower()
|
|
|
|
|
|
def test_cim10_nearest_correction_ocr():
|
|
# O (lettre) lu au lieu de 0 (chiffre)
|
|
assert nearest_cim10("K65O") == "K650"
|
|
# Code déjà valide : renvoyé tel quel
|
|
assert nearest_cim10("K650") == "K650"
|
|
# Aucune correspondance à distance <= 1
|
|
assert nearest_cim10("ZZZZZ", max_distance=1) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CCAM
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_ccam_codes_valides():
|
|
assert is_valid_ccam("EBFA012")
|
|
assert is_valid_ccam("HFCC003") # Bypass gastrique (mentionné dans la V5)
|
|
assert is_valid_ccam("ebfa012") # casse insensible
|
|
assert is_valid_ccam("EBFA012-1") # extension PMSI tolérée
|
|
|
|
|
|
def test_ccam_codes_invalides():
|
|
assert not is_valid_ccam("AAAA000")
|
|
assert not is_valid_ccam("")
|
|
assert not is_valid_ccam("EBF012") # 3 lettres au lieu de 4
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GHM
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_ghm_codes_valides():
|
|
assert is_valid_ghm("01C031")
|
|
assert is_valid_ghm("01c031")
|
|
|
|
|
|
def test_ghm_codes_invalides():
|
|
assert not is_valid_ghm("99Z99Z")
|
|
assert not is_valid_ghm("")
|
|
assert not is_valid_ghm("ABCDEF")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GHS et couplage GHM→GHS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_ghs_valide():
|
|
assert is_valid_ghs("22")
|
|
assert is_valid_ghs("0022") # zéros de tête tolérés
|
|
assert not is_valid_ghs("99999")
|
|
assert not is_valid_ghs("")
|
|
|
|
|
|
def test_ghm_to_ghs():
|
|
ghs = ghm_to_ghs("01C031")
|
|
assert "22" in ghs
|
|
# GHM inexistant → liste vide
|
|
assert ghm_to_ghs("99Z99Z") == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exécution directe (sans pytest)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _main() -> int:
|
|
import traceback
|
|
tests = [
|
|
("test_cim10_codes_valides", test_cim10_codes_valides),
|
|
("test_cim10_normalisation", test_cim10_normalisation),
|
|
("test_cim10_codes_invalides", test_cim10_codes_invalides),
|
|
("test_cim10_libelle", test_cim10_libelle),
|
|
("test_cim10_nearest_correction_ocr", test_cim10_nearest_correction_ocr),
|
|
("test_ccam_codes_valides", test_ccam_codes_valides),
|
|
("test_ccam_codes_invalides", test_ccam_codes_invalides),
|
|
("test_ghm_codes_valides", test_ghm_codes_valides),
|
|
("test_ghm_codes_invalides", test_ghm_codes_invalides),
|
|
("test_ghs_valide", test_ghs_valide),
|
|
("test_ghm_to_ghs", test_ghm_to_ghs),
|
|
]
|
|
if not DB_PATH.exists():
|
|
print(f"ERREUR : base SQLite manquante ({DB_PATH}).")
|
|
print("Exécute d'abord : python -m pipeline.referentials --build")
|
|
return 2
|
|
|
|
failures = 0
|
|
for name, fn in tests:
|
|
try:
|
|
fn()
|
|
print(f" [OK ] {name}")
|
|
except AssertionError as e:
|
|
print(f" [FAIL] {name} — {e}")
|
|
failures += 1
|
|
except Exception:
|
|
print(f" [ERR] {name}")
|
|
traceback.print_exc()
|
|
failures += 1
|
|
print(f"=== {len(tests) - failures}/{len(tests)} tests OK ===")
|
|
return 0 if failures == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(_main())
|