feat: anonymisation qualité++ — 15 patterns, subparts tirets, fix entity registry
Bloc A: fix sous-parties dans _mappings, filtre NER anti-tag, intégration patterns manquants (DESTINATAIRE, PRESCRIPTION_AUTHOR), whitelist médicaments élargie (+60), villes retirées de whitelist. Bloc B: CRH dedup chars 200-1000, CP_VILLE vrais codes postaux FR, DR_NAME capital par mot, BACTERIO header tolère ligne vide. Bloc C: DR_NAME negative lookahead multi-docteurs même ligne, entity_registry split tirets (RITZ-QUILLACQ), fix early return subparts dans _find_matching_entity, PRESCRIPTION_AUTHOR élargi (Révisé/Traité, variable.), NOTE_AUTHOR élargi (Diététicienne, Kiné, Ergo), + 8 nouveaux patterns (CONTACT_RELATION, MOD_PAR, AIDE_NAME, SIGNATURE_LINE, VALIDE_PAR, INTERNE_SIGNATURE, FOIS_NAME, MALADIE_NAME), adresses inline +ALLEE/IMP, text_cleaner préserve abréviations médicales. Validé sur 6 cas (21, 11, 104, 160, 50, 200). 70 tests OK. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import pytest
|
||||
|
||||
from src.anonymization.entity_registry import EntityRegistry
|
||||
from src.anonymization.regex_patterns import (
|
||||
BACTERIO_NOM_HEADER_PATTERN,
|
||||
CONSULT_ADRESSE_PATTERN,
|
||||
CRH_FOOTER_IPP_EPISODE,
|
||||
CRH_FOOTER_PATIENT_PATTERN,
|
||||
@@ -15,10 +16,14 @@ from src.anonymization.regex_patterns import (
|
||||
EPISODE_PATTERN,
|
||||
FOOTER_PATIENT_PATTERN,
|
||||
IPP_PATTERN,
|
||||
NOM_NAISSANCE_TRACKARE_PATTERN,
|
||||
NOTE_AUTHOR_PATTERN,
|
||||
N_IPP_PATTERN,
|
||||
PAR_NOM_PATTERN,
|
||||
PERSONNE_PREVENIR_PATTERN,
|
||||
PHONE_INTL_PATTERN,
|
||||
PHONE_PATTERN,
|
||||
PRENOM_NAISSANCE_PATTERN,
|
||||
RPPS_PATTERN,
|
||||
VENUE_PATTERN,
|
||||
)
|
||||
@@ -211,18 +216,31 @@ class TestStopWordsAndSubparts:
|
||||
"""Vérifie que les stop words et sous-parties courtes ne sont pas enregistrés."""
|
||||
|
||||
def test_stop_word_not_registered_as_subpart(self):
|
||||
"""'jean' (stop word) ne doit pas être enregistré en sous-partie."""
|
||||
"""Les vrais stop words (sans, dans, avec) ne doivent pas être des sous-parties."""
|
||||
reg = EntityRegistry()
|
||||
reg.register("Jean Martin", "medecin")
|
||||
entities = reg.get_all_entities()
|
||||
assert "jean" not in entities
|
||||
assert "martin" not in entities # < 5 chars → exclu aussi
|
||||
reg.register("Sans Martin", "medecin")
|
||||
assert not reg.is_subpart("sans")
|
||||
|
||||
def test_prenoms_are_subparts(self):
|
||||
"""Les prénoms (jean, paul, marie) sont des PHI et doivent être des sous-parties."""
|
||||
reg = EntityRegistry()
|
||||
reg.register("Jean Dupont", "patient")
|
||||
assert reg.is_subpart("jean")
|
||||
assert reg.is_subpart("dupont")
|
||||
|
||||
def test_short_parts_excluded(self):
|
||||
"""Les sous-parties < 4 chars sont exclues (trop de faux positifs)."""
|
||||
reg = EntityRegistry()
|
||||
reg.register("Ré Dupont", "patient")
|
||||
assert not reg.is_subpart("ré")
|
||||
assert reg.is_subpart("dupont")
|
||||
|
||||
def test_long_subpart_registered(self):
|
||||
"""Les sous-parties >= 5 chars qui ne sont pas des stop words sont enregistrées."""
|
||||
"""Les sous-parties >= 4 chars qui ne sont pas des stop words sont enregistrées."""
|
||||
reg = EntityRegistry()
|
||||
reg.register("Jean Audemar", "medecin")
|
||||
assert reg.is_subpart("audemar")
|
||||
assert reg.is_subpart("jean")
|
||||
|
||||
def test_sans_not_anonymized_when_dr_sans(self):
|
||||
"""'sans' ne doit pas être remplacé quand un médecin s'appelle 'Dr Sans'."""
|
||||
@@ -296,6 +314,60 @@ class TestNewPHIPatterns:
|
||||
assert m is not None
|
||||
assert "15 rue des Lilas" in m.group(1)
|
||||
|
||||
def test_date_naissance_with_dashes(self):
|
||||
"""Date de naissance : DD-MM-YYYY (format Trackare)."""
|
||||
m = DATE_NAISSANCE_PATTERN.search("Date de naissance : 21-01-1948")
|
||||
assert m is not None
|
||||
assert m.group(1) == "21-01-1948"
|
||||
|
||||
def test_phone_intl_pattern(self):
|
||||
"""Téléphone international +33(0)XXXXXXXXX."""
|
||||
m = PHONE_INTL_PATTERN.search("☎ +33(0)156125400")
|
||||
assert m is not None
|
||||
|
||||
def test_phone_intl_with_spaces(self):
|
||||
m = PHONE_INTL_PATTERN.search("+33 1 56 12 54 00")
|
||||
assert m is not None
|
||||
|
||||
def test_personne_prevenir_pattern(self):
|
||||
m = PERSONNE_PREVENIR_PATTERN.search("Personne à prévenir EICHE 06 27 56 38")
|
||||
assert m is not None
|
||||
assert "EICHE" in m.group(1)
|
||||
|
||||
def test_nom_naissance_trackare(self):
|
||||
m = NOM_NAISSANCE_TRACKARE_PATTERN.search("Nom de naissance : EICHE")
|
||||
assert m is not None
|
||||
assert m.group(1).strip() == "EICHE"
|
||||
|
||||
def test_nom_utilise_trackare(self):
|
||||
m = NOM_NAISSANCE_TRACKARE_PATTERN.search("Nom utilisé : DUPONT")
|
||||
assert m is not None
|
||||
assert m.group(1).strip() == "DUPONT"
|
||||
|
||||
def test_prenom_naissance_pattern(self):
|
||||
m = PRENOM_NAISSANCE_PATTERN.search("Prénom de naissance : MARIE")
|
||||
assert m is not None
|
||||
assert m.group(1) == "MARIE"
|
||||
|
||||
def test_prenom_1er_pattern(self):
|
||||
m = PRENOM_NAISSANCE_PATTERN.search("1er prénom de naissance: MARIE")
|
||||
assert m is not None
|
||||
assert m.group(1) == "MARIE"
|
||||
|
||||
def test_bacterio_nom_header_pattern(self):
|
||||
"""En-tête BACTERIO : NOM Prénom avant 'Nom usuel :'."""
|
||||
text = "Compte renduComplet\nURTIZVEREA Marie\nNom usuel : EICHE URGENCES"
|
||||
m = BACTERIO_NOM_HEADER_PATTERN.search(text)
|
||||
assert m is not None
|
||||
assert m.group(1) == "URTIZVEREA Marie"
|
||||
|
||||
def test_bacterio_nom_header_nom_de_naissance(self):
|
||||
"""En-tête BACTERIO : NOM Prénom avant 'Nom de naissance :'."""
|
||||
text = "DUPONT Jean-Pierre\nNom de naissance : MARTIN"
|
||||
m = BACTERIO_NOM_HEADER_PATTERN.search(text)
|
||||
assert m is not None
|
||||
assert m.group(1) == "DUPONT Jean-Pierre"
|
||||
|
||||
|
||||
# --- P2-C : Cohérence pseudonymes ---
|
||||
|
||||
@@ -316,12 +388,52 @@ class TestEntityMatching:
|
||||
p2 = reg.register("MARTIN", "medecin")
|
||||
assert p1 == p2
|
||||
|
||||
def test_no_cross_category_match_via_subset(self):
|
||||
"""Un patient et une adresse ne matchent PAS par sous-ensemble."""
|
||||
reg = EntityRegistry()
|
||||
p1 = reg.register("MARTIN Pierre", "patient")
|
||||
p2 = reg.register("MARTIN Rue", "adresse")
|
||||
# "MARTIN" est commun mais catégories incompatibles → pas de match
|
||||
assert p1 != p2
|
||||
|
||||
def test_person_categories_compatible(self):
|
||||
"""Les catégories de personnes (patient/medecin/soignant) sont compatibles."""
|
||||
reg = EntityRegistry()
|
||||
p1 = reg.register("DUPONT", "patient")
|
||||
p2 = reg.register("DUPONT Pierre", "medecin")
|
||||
# Catégories de personnes compatibles → même pseudo
|
||||
assert p1 == p2
|
||||
|
||||
def test_fix_double_brackets(self):
|
||||
"""Les doubles crochets sont corrigés."""
|
||||
"""Les doubles crochets fermants sont corrigés."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
result = Anonymizer._fix_brackets("Mme[PERSONNE_14]]")
|
||||
assert result == "Mme [PERSONNE_14]"
|
||||
|
||||
def test_fix_double_open_brackets(self):
|
||||
"""Les doubles crochets ouvrants sont corrigés."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
result = Anonymizer._fix_brackets("Dr [[PERSONNE_7]")
|
||||
assert result == "Dr [PERSONNE_7]"
|
||||
|
||||
def test_fix_orphan_digits(self):
|
||||
"""Les chiffres orphelins entre crochets sont supprimés."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
result = Anonymizer._fix_brackets("[PERSONNE_6]10]")
|
||||
assert result == "[PERSONNE_6]"
|
||||
|
||||
def test_fix_orphan_underscore_digits(self):
|
||||
"""Les _N] orphelins sont supprimés."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
result = Anonymizer._fix_brackets("[PERSONNE_4]_2]")
|
||||
assert result == "[PERSONNE_4]"
|
||||
|
||||
def test_fix_glued_tags(self):
|
||||
"""Deux tags collés ][ reçoivent un espace."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
result = Anonymizer._fix_brackets("[MEDECIN_5][MEDECIN_6]")
|
||||
assert result == "[MEDECIN_5] [MEDECIN_6]"
|
||||
|
||||
def test_fix_glued_bracket(self):
|
||||
"""Un tag collé à un mot reçoit un espace."""
|
||||
from src.anonymization.anonymizer import Anonymizer
|
||||
|
||||
Reference in New Issue
Block a user