fix(phase2): Corrections audit 30 fichiers — FP stop words, villes, établissements, noms composés
- Ajout 10 stop words FP (bouffee, discontinue, respimat, lyoc, probnp, bpco, colle, gsc, masse, selle) - Ajout 8 villes stop words (saint-palais, tarnos, hendaye, dax, orthez, oloron, pau, cambo) - Protection "Examen Clinique" contre masquage [ETABLISSEMENT] (lookbehind négatif) - Ajout Pharmacie et Centre Médical dans RE_HOPITAL_VILLE - Masquage "Ville, le [date]" dans en-têtes courrier (Bayonne, le 12/03/2024) - Noms composés avec espace (DI LULLO, LE MOIGNE) via _add_compound - Contacts Trackare lowercase + capture 3e token (vandestock/michele) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -538,6 +538,7 @@ _MEDICAL_STOP_WORDS_SET = {
|
||||
# FP audit OGC 17 CRH
|
||||
"mode", "retraitee", "retraité", "retraitée", "régression", "regression", "tel",
|
||||
"strasbourg", "bordeaux", "toulouse", "paris", "lyon", "marseille", "bayonne", "anglet",
|
||||
"saint-palais", "tarnos", "hendaye", "dax", "orthez", "oloron", "pau", "cambo",
|
||||
# Spécialités/services récurrents comme FP NOM
|
||||
"cancérologie", "cancerologie", "réanimation", "reanimation",
|
||||
"urologie", "néphrologie", "nephrologie", "hématologie", "hematologie",
|
||||
@@ -630,6 +631,11 @@ _MEDICAL_STOP_WORDS_SET = {
|
||||
"bronchite", "accueil", "cadre", "transfert", "relecture", "examens",
|
||||
"traitements", "traitement", "infectiologie", "cancérologie", "cancerologie",
|
||||
"maternité", "orale", "sachet", "absence",
|
||||
# FP audit 30 fichiers Phase 2 (mars 2026)
|
||||
"bouffee", "bouffée", "discontinue", "respimat", "lyoc",
|
||||
"probnp", "pro-bnp", "nt-probnp",
|
||||
"bpco", "colle", "gsc", "masse",
|
||||
"selle", "selles",
|
||||
}
|
||||
# Enrichissement automatique avec les ~4000 noms de médicaments d'edsnlp
|
||||
_MEDICAL_STOP_WORDS_SET.update(_load_edsnlp_drug_names())
|
||||
@@ -790,9 +796,10 @@ RE_ETABLISSEMENT = re.compile(
|
||||
r")",
|
||||
)
|
||||
RE_HOPITAL_VILLE = re.compile(
|
||||
r"(?<![Ee]xamen )"
|
||||
r"\b((?:[Hh]ôpital|[Cc]linique|[Pp]olyclinique|[Cc]entre\s+[Hh]ospitalier"
|
||||
r"|[Cc]entre\s+[Dd]e\s+[Ss]oins|[Mm]aison\s+[Dd]e\s+[Ss]anté"
|
||||
r"|[Mm]aison\s+[Dd]e\s+[Rr]etraite|[Rr]ésidence|[Ff]oyer)"
|
||||
r"|[Cc]entre\s+[Mm][ée]dical|[Cc]entre\s+[Dd]e\s+[Ss]oins|[Mm]aison\s+[Dd]e\s+[Ss]anté"
|
||||
r"|[Mm]aison\s+[Dd]e\s+[Rr]etraite|[Rr]ésidence|[Ff]oyer|[Pp]harmacie)"
|
||||
r"\s+(?:de\s+|d['']\s*|du\s+|des\s+)?(?:la\s+|le\s+|l['']\s*|les\s+)?"
|
||||
r"(?:[A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇ][A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇa-zéèàùâêîôûäëïöüç\-']+)"
|
||||
r"(?:\s+(?:de\s+|d['']\s*|du\s+|des\s+)?(?:la\s+|le\s+|l['']\s*|les\s+)?"
|
||||
@@ -1230,6 +1237,23 @@ def _mask_line_by_regex(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict
|
||||
return PLACEHOLDERS["MASK"]
|
||||
line = RE_SERVICE.sub(_repl_service, line)
|
||||
|
||||
# Ville en en-tête de courrier : "Bayonne, le 12/03/2024" → masquer la ville
|
||||
# Le contexte "Mot, le [date]" est fiable (virgule obligatoire)
|
||||
# Autorise les mots de liaison minuscules (de, du, la, sur, en, lès)
|
||||
_re_ville_date = re.compile(
|
||||
r"^(\s*)"
|
||||
r"([A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇ][a-zéèàùâêîôûäëïöüç\-]+"
|
||||
r"(?:\s+(?:de|du|la|sur|en|lès|les|l['']\s*)?"
|
||||
r"[A-ZÉÈÀÙÂÊÎÔÛÄËÏÖÜÇa-zéèàùâêîôûäëïöüç\-]+)*)"
|
||||
r"(\s*,\s+le\s+\d{1,2})",
|
||||
re.MULTILINE,
|
||||
)
|
||||
def _repl_ville_date(m: re.Match) -> str:
|
||||
ville = m.group(2).strip()
|
||||
audit.append(PiiHit(page_idx, "VILLE", ville, PLACEHOLDERS["VILLE"]))
|
||||
return m.group(1) + PLACEHOLDERS["VILLE"] + m.group(3)
|
||||
line = _re_ville_date.sub(_repl_ville_date, line)
|
||||
|
||||
# Champs structurés : Lieu de naissance, Ville de résidence (masquage direct, sans filtre stop words)
|
||||
_re_lieu = re.compile(r"(Lieu\s+de\s+naissance\s*:\s*)(\S.+)")
|
||||
def _repl_lieu(m: re.Match) -> str:
|
||||
@@ -1341,10 +1365,17 @@ def _extract_trackare_identity(full_text: str) -> Tuple[set, List[PiiHit]]:
|
||||
force_names: set = set() # noms issus de contextes structurés (DR., Signé, etc.) → bypass stop words
|
||||
|
||||
def _add_name(s: str):
|
||||
for tok in s.split():
|
||||
s = s.strip()
|
||||
parts = s.split()
|
||||
for tok in parts:
|
||||
tok = tok.strip(" .-'(),")
|
||||
if len(tok) >= 2 and tok[0].isupper():
|
||||
names.add(tok)
|
||||
# Garder aussi le nom composé complet (DI LULLO, LE MOIGNE, etc.)
|
||||
if len(parts) >= 2:
|
||||
compound = " ".join(t.strip(" .-'(),") for t in parts if len(t.strip(" .-'(),")) >= 2)
|
||||
if len(compound) >= 5:
|
||||
names.add(compound)
|
||||
|
||||
# Termes non-noms fréquents dans les contextes Signé/DR./Note d'évolution
|
||||
_FORCE_EXCLUDE = _MEDICATION_WHITELIST | {
|
||||
@@ -1429,15 +1460,29 @@ def _extract_trackare_identity(full_text: str) -> Tuple[set, List[PiiHit]]:
|
||||
|
||||
# --- Contacts structurés ---
|
||||
# Pattern: Relation NOM PRENOM [ADRESSE] [TEL]
|
||||
# Accepte les minuscules (Trackare écrit parfois "Conjoint vandestock michele")
|
||||
# Capture jusqu'à 3 tokens pour les noms composés (le moigne christophe)
|
||||
for m in re.finditer(
|
||||
r"(?:Conjoint|Concubin|Epoux|Epouse|Parent|Père|Mère|Fils|Fille|Frère|Soeur|Tuteur)\s+"
|
||||
r"([A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôû][A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôûä\-']+)"
|
||||
r"(?:\s+([A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôû][A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôûä\-']+))?"
|
||||
r"(?:\s+([A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôû][A-ZÉÈÀÙÂÊÎÔÛa-zéèàùâêîôûä\-']+))?",
|
||||
full_text,
|
||||
):
|
||||
_add_name(m.group(1))
|
||||
if m.group(2):
|
||||
_add_name(m.group(2))
|
||||
contact_parts = [g.strip(" .-'(),") for g in (m.group(1), m.group(2), m.group(3)) if g]
|
||||
# Ajouter chaque token >= 3 chars (pas les articles courts comme "le", "di")
|
||||
for tok in contact_parts:
|
||||
if len(tok) >= 3 and tok.lower() not in _MEDICAL_STOP_WORDS_SET:
|
||||
names.add(tok)
|
||||
if tok[0].islower():
|
||||
names.add(tok.capitalize())
|
||||
# Ajouter aussi le composé complet (pour "le moigne", "di lullo")
|
||||
if len(contact_parts) >= 2:
|
||||
compound = " ".join(contact_parts)
|
||||
if len(compound) >= 5:
|
||||
names.add(compound)
|
||||
# Version capitalisée pour propagation
|
||||
names.add(" ".join(t.capitalize() for t in compound.split()))
|
||||
|
||||
# --- Prescripteurs / Exécutants (trackare) ---
|
||||
for m in re.finditer(
|
||||
@@ -1592,7 +1637,16 @@ def _extract_document_names(full_text: str, cfg: Dict[str, Any]) -> Tuple[set, s
|
||||
names: set = set()
|
||||
force_names: set = set()
|
||||
|
||||
def _add_compound(match_str: str):
|
||||
"""Ajoute le nom composé complet en plus des tokens individuels (DI LULLO, LE MOIGNE)."""
|
||||
parts = [t.strip(" .-'") for t in match_str.split() if len(t.strip(" .-'")) >= 2]
|
||||
if len(parts) >= 2:
|
||||
compound = " ".join(parts)
|
||||
if len(compound) >= 5:
|
||||
names.add(compound)
|
||||
|
||||
def _add_tokens(match_str: str):
|
||||
_add_compound(match_str)
|
||||
for token in match_str.split():
|
||||
token = token.strip(" .-'")
|
||||
if len(token) < 3:
|
||||
@@ -1605,6 +1659,7 @@ def _extract_document_names(full_text: str, cfg: Dict[str, Any]) -> Tuple[set, s
|
||||
|
||||
def _add_tokens_force_all(match_str: str):
|
||||
"""Bypass stop words pour TOUS les tokens (contexte Patient: très fiable)."""
|
||||
_add_compound(match_str)
|
||||
for token in match_str.split():
|
||||
token = token.strip(" .-'")
|
||||
if len(token) < 2:
|
||||
@@ -1616,6 +1671,7 @@ def _extract_document_names(full_text: str, cfg: Dict[str, Any]) -> Tuple[set, s
|
||||
|
||||
def _add_tokens_force_first(match_str):
|
||||
"""Comme _add_tokens mais force le 1er token (contexte Dr/Mme fort)."""
|
||||
_add_compound(match_str)
|
||||
tokens = match_str.split()
|
||||
for i, token in enumerate(tokens):
|
||||
token = token.strip(" .-'")
|
||||
|
||||
Reference in New Issue
Block a user