Files
Aivanov_scan_ogc/tests/test_referentials.py
Dom 6df590ae95 feat(referentials): validation ATIH 2018 des codes médicaux
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>
2026-04-24 15:06:01 +02:00

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())