"""Validation ATIH des codes extraits. Prend un JSON d'extraction produit par `pipeline/extract.py` et l'enrichit d'une section `_validation` par champ de code médical (CIM-10, CCAM, GHM, GHS) avec : - `valid` : le code existe dans le référentiel ATIH 2018 - `suggestion` : si invalide, code le plus proche par Levenshtein ≤ 1 (CIM-10) - `libelle_ref` : libellé officiel ATIH (CIM-10) pour audit Plus des cross-checks (GHS ∈ ghm_to_ghs(GHM)) pour détecter des incohérences de groupage. Principes : - Lecture seule sur le JSON source — on produit une COPIE enrichie. - Ne supprime / ne corrige RIEN automatiquement ; seule une suggestion est annotée. La correction reste à la discrétion d'un humain (overlay) ou d'un prochain pass automatique. """ from __future__ import annotations from copy import deepcopy from typing import Any from .referentials import ( get_cim10_libelle, ghm_to_ghs, is_valid_ccam, is_valid_cim10, is_valid_ghm, is_valid_ghs, nearest_cim10, ) # ============================================================ # Helpers # ============================================================ def _check_cim10(code: str) -> dict: """Valide un code CIM-10 et suggère une correction si invalide.""" code = (code or "").strip() if not code: return {"code": "", "valid": None} valid = is_valid_cim10(code) entry = {"code": code, "valid": valid} if valid: entry["libelle_ref"] = get_cim10_libelle(code) else: sug = nearest_cim10(code, max_distance=1) if sug: entry["suggestion"] = sug entry["suggestion_libelle"] = get_cim10_libelle(sug) return entry def _check_ccam(code: str) -> dict: code = (code or "").strip() if not code: return {"code": "", "valid": None} return {"code": code, "valid": is_valid_ccam(code)} def _check_ghm(code: str) -> dict: code = (code or "").strip() if not code: return {"code": "", "valid": None} entry = {"code": code, "valid": is_valid_ghm(code)} if entry["valid"]: entry["ghs_possibles"] = ghm_to_ghs(code) return entry def _check_ghs(code: str) -> dict: code = (code or "").strip() if not code: return {"code": "", "valid": None} return {"code": code, "valid": is_valid_ghs(code)} # ============================================================ # Validation d'un bloc codage (etab ou reco) # ============================================================ def _validate_codage(codage: dict) -> dict: """Valide un bloc codage_etab ou codage_reco.""" if not isinstance(codage, dict): return {} out = { "dp": _check_cim10(codage.get("dp", "")), "dr": _check_cim10(codage.get("dr", "")), } das_list = codage.get("das") or [] if isinstance(das_list, list): out["das"] = [_check_cim10(d.get("code", "")) if isinstance(d, dict) else _check_cim10(str(d)) for d in das_list] return out def _validate_actes(actes: Any) -> list[dict]: if not isinstance(actes, list): return [] return [_check_ccam(a.get("code", "")) if isinstance(a, dict) else _check_ccam(str(a)) for a in actes] # ============================================================ # Cross-checks GHM ↔ GHS # ============================================================ def _cross_check_ghm_ghs(ghm: str, ghs: str) -> dict: """Vérifie qu'un GHS observé est listé parmi les GHS possibles du GHM.""" ghm = (ghm or "").strip() ghs = (ghs or "").strip() if not ghm or not ghs: return {"checked": False, "reason": "ghm ou ghs manquant"} if not is_valid_ghm(ghm): return {"checked": False, "reason": "GHM invalide"} possibles = ghm_to_ghs(ghm) # Normalisation simple : on compare la fin (au cas où l'un est tronqué) ok = ghs in possibles or any(p.endswith(ghs) or ghs.endswith(p) for p in possibles) return { "checked": True, "coherent": ok, "ghs_extrait": ghs, "ghs_possibles": possibles, } # ============================================================ # Point d'entrée # ============================================================ def validate_recueil(recueil: dict) -> dict: """Retourne un dict résumé des validations pour la page recueil.""" v = { "codage_etab": _validate_codage(recueil.get("codage_etab", {})), "codage_reco": _validate_codage(recueil.get("codage_reco", {})), "actes_etab": _validate_actes(recueil.get("actes_etab", [])), "actes_reco": _validate_actes(recueil.get("actes_reco", [])), "ghm_etab": _check_ghm(recueil.get("ghm_etab", "")), "ghs_etab": _check_ghs(recueil.get("ghs_etab", "")), "ghm_reco": _check_ghm(recueil.get("ghm_reco", "")), "ghs_reco": _check_ghs(recueil.get("ghs_reco", "")), "cross_checks": { "etab": _cross_check_ghm_ghs( recueil.get("ghm_etab", ""), recueil.get("ghs_etab", "")), "reco": _cross_check_ghm_ghs( recueil.get("ghm_reco", ""), recueil.get("ghs_reco", "")), }, } v["summary"] = _summarize(v) return v def _summarize(validation: dict) -> dict: """Compte les codes valides / invalides dans une section _validation.""" valid, invalid, empty = 0, 0, 0 def _count_entry(e): nonlocal valid, invalid, empty if e.get("valid") is True: valid += 1 elif e.get("valid") is False: invalid += 1 else: empty += 1 for section in ("codage_etab", "codage_reco"): sec = validation.get(section, {}) or {} _count_entry(sec.get("dp", {})) _count_entry(sec.get("dr", {})) for d in sec.get("das", []) or []: _count_entry(d) for actes_key in ("actes_etab", "actes_reco"): for a in validation.get(actes_key, []) or []: _count_entry(a) for g in ("ghm_etab", "ghs_etab", "ghm_reco", "ghs_reco"): _count_entry(validation.get(g, {})) cc = validation.get("cross_checks", {}) incoherent = sum(1 for v in cc.values() if v.get("checked") and not v.get("coherent")) return { "valid": valid, "invalid": invalid, "empty": empty, "total_codes": valid + invalid, "ghm_ghs_incoherents": incoherent, } def annotate(extraction: dict) -> dict: """Annote un JSON d'extraction complet avec validation ATIH. Retourne une COPIE enrichie d'un bloc `_validation` à la racine de chaque page structurée. N'efface / ne corrige aucune valeur. """ out = deepcopy(extraction) ext = out.get("extraction") or {} if "recueil" in ext and isinstance(ext["recueil"], dict): ext["recueil"]["_validation"] = validate_recueil(ext["recueil"]) # Concertation 2 : valider les 3 GHS if "concertation_2" in ext and isinstance(ext["concertation_2"], dict): c2 = ext["concertation_2"] c2["_validation"] = { "ghs_initial": _check_ghs(c2.get("ghs_initial", "")), "ghs_avant_concertation": _check_ghs(c2.get("ghs_avant_concertation", "")), "ghs_final": _check_ghs(c2.get("ghs_final", "")), } return out if __name__ == "__main__": # Test rapide sur OGC 7 import json, sys path = sys.argv[1] if len(sys.argv) > 1 else "output/v2/OGC 7.json" with open(path) as f: data = json.load(f) annotated = annotate(data) rec_v = annotated["extraction"]["recueil"]["_validation"] print(json.dumps(rec_v["summary"], indent=2)) print("\ncross_checks:", json.dumps(rec_v["cross_checks"], indent=2, ensure_ascii=False))