Fix faux positifs PDF (EDS_TEL, EDS_VILLE) + détection noms Notes IDE

- Skip EDS_TEL dans PDF (valeurs Pouls détectées comme N° de téléphone)
- Ajout EDS_VILLE au whole-word matching (évite "GEL" dans "GELULE")
- Filtre stop words étendu à EDS_HOPITAL et EDS_VILLE dans la détection NER
- Détection noms soignants dans "Note IDE\nPrenom NOM" (BARGAIN, LACOTE, etc.)
- Stop words : semaine, jour, matin, soir, nuit, midi

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 17:22:38 +01:00
parent 2236fdcd01
commit a138b26738

View File

@@ -301,6 +301,8 @@ _MEDICAL_STOP_WORDS_SET = {
"axa", "ttt", "anionique", "abdomino", "cod", "omi", "urg", "med", "axa", "ttt", "anionique", "abdomino", "cod", "omi", "urg", "med",
"10mg", "20mg", "40mg", "100mg", "300ui", "500ml", "innohep", "coaprovel", "10mg", "20mg", "40mg", "100mg", "300ui", "500ml", "innohep", "coaprovel",
"actiskenan", "simvastatine", "forlax", "actiskenan", "simvastatine", "forlax",
# Mots temporels / contextuels détectés comme EDS_HOPITAL
"semaine", "jour", "matin", "soir", "nuit", "midi",
# Mots clés de contexte document # Mots clés de contexte document
"compétences", "maladies", "inflammatoires", "systémiques", "rares", "compétences", "maladies", "inflammatoires", "systémiques", "rares",
"fret", "fax", "contexte", "résultat", "resultat", "résultats", "resultats", "fret", "fax", "contexte", "résultat", "resultat", "résultats", "resultats",
@@ -888,11 +890,19 @@ def _extract_trackare_identity(full_text: str) -> Tuple[set, List[PiiHit]]:
if m.group(2): if m.group(2):
_add_name(m.group(2)) _add_name(m.group(2))
# --- Noms soignants dans les Notes d'évolution --- # --- Noms soignants dans les Notes d'évolution / Notes IDE / Notes médicales ---
# Pattern: "Note d'évolution PRENOM NOM" ou "NOM HH:MM texte..." # Pattern: "Note IDE\nPrenom NOM" ou "Note d'évolution\nPrenom NOM"
for m in re.finditer(r"Note\s+d'[ée]volution\s+([A-ZÉÈÀÙÂÊÎÔÛ][a-zéèàùâêîôû]+)\s+([A-ZÉÈÀÙÂÊÎÔÛ]{2,})", full_text): for m in re.finditer(
_add_name(m.group(1)) r"Note\s+(?:IDE|AS|d'[ée]volution|m[ée]dicale|kin[ée])\s*\n\s*"
_add_name(m.group(2)) r"([A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôû][a-zéèàùâêîôûäëïöüç]+)\s+"
r"([A-ZÉÈÀÙÂÊÎÔÛ][A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôûäëïöüç\-]+)",
full_text
):
prenom, nom = m.group(1), m.group(2)
if prenom.lower() not in _MEDICAL_STOP_WORDS_SET:
_add_name(prenom)
if nom.lower() not in _MEDICAL_STOP_WORDS_SET:
_add_name(nom)
# --- Noms soignants multi-lignes : "Prénom\nNOM" dans les tableaux de prescriptions/soins --- # --- Noms soignants multi-lignes : "Prénom\nNOM" dans les tableaux de prescriptions/soins ---
for m in re.finditer( for m in re.finditer(
@@ -1174,7 +1184,7 @@ def _mask_with_eds_pseudo(text: str, ents: List[Dict[str, Any]], cfg: Dict[str,
continue continue
# Filtrer les faux positifs NOM/PRENOM (médicaments, acronymes médicaux) # Filtrer les faux positifs NOM/PRENOM (médicaments, acronymes médicaux)
label = e.get("entity_group", "EDS") label = e.get("entity_group", "EDS")
if label in ("NOM", "PRENOM"): if label in ("NOM", "PRENOM", "HOPITAL", "VILLE"):
if w.lower() in _MEDICAL_STOP_WORDS_SET: if w.lower() in _MEDICAL_STOP_WORDS_SET:
continue continue
# Filtrer aussi les tokens multi-mots dont un composant est un stop word # Filtrer aussi les tokens multi-mots dont un composant est un stop word
@@ -1309,10 +1319,10 @@ def redact_pdf_vector(original_pdf: Path, audit: List[PiiHit], out_pdf: Path) ->
by_page.setdefault(h.page, []).append(h) by_page.setdefault(h.page, []).append(h)
# Kinds à ne pas chercher dans le PDF (dates masquées uniquement dans le texte, # Kinds à ne pas chercher dans le PDF (dates masquées uniquement dans le texte,
# pas dans le PDF où elles rendent les tableaux illisibles) # pas dans le PDF où elles rendent les tableaux illisibles)
_VECTOR_SKIP_KINDS = {"EDS_DATE", "EDS_DATE_NAISSANCE", "EDS_SECU"} _VECTOR_SKIP_KINDS = {"EDS_DATE", "EDS_DATE_NAISSANCE", "EDS_SECU", "EDS_TEL"}
# Kinds dont les tokens courts (< 5) risquent le substring matching via page.search_for() # Kinds dont les tokens courts (< 5) risquent le substring matching via page.search_for()
_VECTOR_SHORT_TOKEN_KINDS = {"NOM_GLOBAL", "NOM_EXTRACTED", "EDS_NOM", "EDS_PRENOM", _VECTOR_SHORT_TOKEN_KINDS = {"NOM_GLOBAL", "NOM_EXTRACTED", "EDS_NOM", "EDS_PRENOM",
"EDS_HOPITAL", "ETAB", "ETAB_GLOBAL"} "EDS_HOPITAL", "EDS_VILLE", "ETAB", "ETAB_GLOBAL"}
for pno in range(len(doc)): for pno in range(len(doc)):
page = doc[pno] page = doc[pno]
hits = by_page.get(pno, []) + by_page.get(-1, []) hits = by_page.get(pno, []) + by_page.get(-1, [])
@@ -1365,9 +1375,9 @@ def redact_pdf_raster(original_pdf: Path, audit: List[PiiHit], out_pdf: Path, dp
for pno in range(len(doc)): for pno in range(len(doc)):
page = doc[pno] page = doc[pno]
rects = [] rects = []
_RASTER_SKIP_KINDS = {"EDS_DATE", "EDS_DATE_NAISSANCE", "EDS_SECU"} _RASTER_SKIP_KINDS = {"EDS_DATE", "EDS_DATE_NAISSANCE", "EDS_SECU", "EDS_TEL"}
_RASTER_SHORT_TOKEN_KINDS = {"NOM_GLOBAL", "NOM_EXTRACTED", "EDS_NOM", "EDS_PRENOM", _RASTER_SHORT_TOKEN_KINDS = {"NOM_GLOBAL", "NOM_EXTRACTED", "EDS_NOM", "EDS_PRENOM",
"EDS_HOPITAL", "ETAB", "ETAB_GLOBAL"} "EDS_HOPITAL", "EDS_VILLE", "ETAB", "ETAB_GLOBAL"}
hits = [x for x in audit if x.page in {pno, -1}] hits = [x for x in audit if x.page in {pno, -1}]
for h in hits: for h in hits:
token = h.original.strip() token = h.original.strip()