feat(T-I): validateur paranames + filtre mots-outils FR du gazetteer
Validateur scripts/validate_paranames.py exécuté sur le gazetteer réel, révèle 2 défauts → corrigés : - Mots-outils FR (avec/dans/voir/...) présents dans INSEE/paranames → risque FP au contexte 'low'. Ajout de 347 mots-outils spaCy fr (sûrs, filtrés des patronymes INSEE fréquents) à stopwords_manuels.txt. build_paranames_gazetteer.py filtre désormais aussi contre ce fichier ; gazetteer reconstruit (1 379 196 noms, mots-outils ≥3 chars retirés). - Priorité sécurité respectée : allez/polygone sont de vrais patronymes INSEE rares → laissés MASQUABLES (pas de fuite), hors stopwords. - OYARCABAL reclassé en warning (couvert par regex F3, absent de Wikidata). Garde-fous vérifiés : Petit/Boucher/Berger conservés, noms étrangers (EJNAINI/NGUYEN/...) conservés. Validateur 5/5. tests/unit 85 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1321,3 +1321,353 @@ biogaran
|
||||
mylan
|
||||
teva
|
||||
zentiva
|
||||
|
||||
# --- Mots-outils français (spaCy fr STOP_WORDS, filtrés des patronymes INSEE fréquents) — ajout 2026-06-03 ---
|
||||
# Sûrs car aucun n'est un patronyme plausible. Réduit les FP au contexte 'low' (is_in_insee/paranames).
|
||||
abord
|
||||
afin
|
||||
ah
|
||||
ai
|
||||
aie
|
||||
ainsi
|
||||
allaient
|
||||
allons
|
||||
alors
|
||||
anterieur
|
||||
anterieure
|
||||
anterieures
|
||||
antérieur
|
||||
antérieure
|
||||
antérieures
|
||||
apres
|
||||
as
|
||||
attendu
|
||||
au
|
||||
aupres
|
||||
auquel
|
||||
aura
|
||||
auraient
|
||||
aurait
|
||||
auront
|
||||
autrement
|
||||
autrui
|
||||
auxquelles
|
||||
auxquels
|
||||
avaient
|
||||
avais
|
||||
avait
|
||||
avoir
|
||||
avons
|
||||
ayant
|
||||
basee
|
||||
ce
|
||||
cela
|
||||
celle-ci
|
||||
celle-la
|
||||
celle-là
|
||||
celles-ci
|
||||
celles-la
|
||||
celles-là
|
||||
celui
|
||||
celui-ci
|
||||
celui-la
|
||||
celui-là
|
||||
cent
|
||||
cependant
|
||||
certaine
|
||||
certaines
|
||||
certains
|
||||
ceux
|
||||
ceux-ci
|
||||
ceux-là
|
||||
chacune
|
||||
chaque
|
||||
ci
|
||||
cinquantaine
|
||||
cinquante
|
||||
cinquantième
|
||||
cinquième
|
||||
combien
|
||||
compris
|
||||
concernant
|
||||
da
|
||||
de
|
||||
dedans
|
||||
desquelles
|
||||
desquels
|
||||
dessous
|
||||
deuxième
|
||||
deuxièmement
|
||||
devra
|
||||
different
|
||||
differente
|
||||
differentes
|
||||
differents
|
||||
différent
|
||||
différente
|
||||
différentes
|
||||
différents
|
||||
directe
|
||||
directement
|
||||
dit
|
||||
dite
|
||||
dits
|
||||
diverse
|
||||
diverses
|
||||
dix
|
||||
dix-huit
|
||||
dix-sept
|
||||
dixième
|
||||
doivent
|
||||
dont
|
||||
douze
|
||||
douzième
|
||||
du
|
||||
duquel
|
||||
effet
|
||||
egalement
|
||||
eh
|
||||
elle-meme
|
||||
elle-même
|
||||
elles-memes
|
||||
elles-mêmes
|
||||
en
|
||||
enfin
|
||||
envers
|
||||
environ
|
||||
es
|
||||
et
|
||||
etaient
|
||||
etais
|
||||
etait
|
||||
etant
|
||||
etc
|
||||
eu
|
||||
eux
|
||||
eux-mêmes
|
||||
exactement
|
||||
excepté
|
||||
faisaient
|
||||
feront
|
||||
ha
|
||||
hep
|
||||
hi
|
||||
ho
|
||||
hormis
|
||||
houp
|
||||
huit
|
||||
huitième
|
||||
hé
|
||||
il
|
||||
ils
|
||||
importe
|
||||
je
|
||||
jusqu
|
||||
jusque
|
||||
la
|
||||
laisser
|
||||
laquelle
|
||||
le
|
||||
lequel
|
||||
lesquelles
|
||||
lesquels
|
||||
leur
|
||||
longtemps
|
||||
lors
|
||||
lorsque
|
||||
lui
|
||||
lui-meme
|
||||
lui-même
|
||||
là
|
||||
lès
|
||||
ma
|
||||
maint
|
||||
malgre
|
||||
malgré
|
||||
me
|
||||
memes
|
||||
merci
|
||||
mes
|
||||
mienne
|
||||
miennes
|
||||
moi-meme
|
||||
moi-même
|
||||
moindres
|
||||
mêmes
|
||||
na
|
||||
ne
|
||||
neanmoins
|
||||
neuvième
|
||||
ni
|
||||
notamment
|
||||
notre
|
||||
nous
|
||||
nous-mêmes
|
||||
nul
|
||||
néanmoins
|
||||
nôtre
|
||||
nôtres
|
||||
on
|
||||
ont
|
||||
onze
|
||||
onzième
|
||||
or
|
||||
ou
|
||||
ouias
|
||||
ouste
|
||||
ouvert
|
||||
ouverte
|
||||
ouverts
|
||||
où
|
||||
parfois
|
||||
parle
|
||||
parlent
|
||||
parler
|
||||
parmi
|
||||
partant
|
||||
pense
|
||||
permet
|
||||
peut
|
||||
peuvent
|
||||
peux
|
||||
plutot
|
||||
plutôt
|
||||
possible
|
||||
possibles
|
||||
pourquoi
|
||||
pourrais
|
||||
pourrait
|
||||
pouvait
|
||||
prealable
|
||||
precisement
|
||||
première
|
||||
premièrement
|
||||
pres
|
||||
procedant
|
||||
proche
|
||||
près
|
||||
préalable
|
||||
précisement
|
||||
pu
|
||||
puisque
|
||||
quand
|
||||
quant
|
||||
quant-à-soi
|
||||
quatorze
|
||||
quatre-vingt
|
||||
quatrième
|
||||
quatrièmement
|
||||
quel
|
||||
quelconque
|
||||
quelle
|
||||
quelles
|
||||
quels
|
||||
quiconque
|
||||
quinze
|
||||
quoi
|
||||
quoique
|
||||
relative
|
||||
relativement
|
||||
rend
|
||||
rendre
|
||||
restant
|
||||
reste
|
||||
restent
|
||||
revoici
|
||||
revoila
|
||||
revoilà
|
||||
sa
|
||||
sait
|
||||
sauf
|
||||
se
|
||||
semblable
|
||||
semblaient
|
||||
semble
|
||||
semblent
|
||||
sent
|
||||
sept
|
||||
septième
|
||||
seraient
|
||||
serait
|
||||
seront
|
||||
seul
|
||||
seule
|
||||
seulement
|
||||
seules
|
||||
seuls
|
||||
si
|
||||
sien
|
||||
sienne
|
||||
siennes
|
||||
siens
|
||||
sinon
|
||||
sixième
|
||||
soi
|
||||
soi-meme
|
||||
soi-même
|
||||
soit
|
||||
soixante
|
||||
sont
|
||||
specifique
|
||||
specifiques
|
||||
spécifique
|
||||
spécifiques
|
||||
stop
|
||||
suffisant
|
||||
suffisante
|
||||
suis
|
||||
suit
|
||||
suivante
|
||||
suivantes
|
||||
suivants
|
||||
suivre
|
||||
surtout
|
||||
ta
|
||||
te
|
||||
tellement
|
||||
telles
|
||||
tels
|
||||
tend
|
||||
tenir
|
||||
tente
|
||||
tes
|
||||
tien
|
||||
tienne
|
||||
tiennes
|
||||
tiens
|
||||
toi-meme
|
||||
toi-même
|
||||
toujours
|
||||
toute
|
||||
toutes
|
||||
treize
|
||||
trente
|
||||
tres
|
||||
troisième
|
||||
troisièmement
|
||||
tu
|
||||
té
|
||||
un
|
||||
unes
|
||||
uns
|
||||
va
|
||||
vingt
|
||||
voici
|
||||
voila
|
||||
voilà
|
||||
voir
|
||||
vont
|
||||
votres
|
||||
vous
|
||||
vous-mêmes
|
||||
vu
|
||||
vé
|
||||
vôtre
|
||||
vôtres
|
||||
ça
|
||||
ès
|
||||
également
|
||||
étaient
|
||||
étais
|
||||
était
|
||||
étant
|
||||
|
||||
@@ -42,6 +42,7 @@ from typing import Iterable, Iterator
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = REPO_ROOT / "data" / "paranames"
|
||||
BDPM_STOPWORDS = REPO_ROOT / "data" / "bdpm" / "medicaments_stopwords.txt"
|
||||
MANUAL_STOPWORDS = REPO_ROOT / "data" / "stopwords_manuels.txt"
|
||||
INSEE_NOMS = REPO_ROOT / "data" / "insee" / "noms_famille_france.txt"
|
||||
|
||||
OUT_NOMS = DATA_DIR / "noms_famille_world.txt.gz"
|
||||
@@ -68,12 +69,13 @@ def normalize(token: str) -> str:
|
||||
return "".join(c for c in up if "A" <= c <= "Z")
|
||||
|
||||
|
||||
def load_stopwords() -> set[str]:
|
||||
stop: set[str] = set()
|
||||
if not BDPM_STOPWORDS.exists():
|
||||
print(f"[WARN] {BDPM_STOPWORDS} introuvable — pas de filtrage BDPM.")
|
||||
return stop
|
||||
with BDPM_STOPWORDS.open("r", encoding="utf-8") as f:
|
||||
def _load_terms(path: Path, label: str, stop: set[str]) -> None:
|
||||
"""Charge un fichier de termes (1 par ligne, # = commentaire) dans stop."""
|
||||
if not path.exists():
|
||||
print(f"[WARN] {path} introuvable — pas de filtrage {label}.")
|
||||
return
|
||||
before = len(stop)
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
@@ -81,7 +83,21 @@ def load_stopwords() -> set[str]:
|
||||
n = normalize(line)
|
||||
if n:
|
||||
stop.add(n)
|
||||
print(f"[INFO] BDPM stop-words : {len(stop):,} entrées.")
|
||||
print(f"[INFO] {label} : +{len(stop) - before:,} termes.")
|
||||
|
||||
|
||||
def load_stopwords() -> set[str]:
|
||||
"""Filtre cumulé : médicaments BDPM + mots-outils/stop-words français curés.
|
||||
|
||||
stopwords_manuels.txt couvre les mots français courants qui apparaissent à
|
||||
tort comme patronymes dans INSEE/paranames (ex. « voir », « avec »). Ces
|
||||
termes sont exclus du gazetteer pour réduire les faux positifs au contexte
|
||||
de détection faible.
|
||||
"""
|
||||
stop: set[str] = set()
|
||||
_load_terms(BDPM_STOPWORDS, "BDPM stop-words", stop)
|
||||
_load_terms(MANUAL_STOPWORDS, "stop-words manuels FR", stop)
|
||||
print(f"[INFO] Filtre total : {len(stop):,} termes.")
|
||||
return stop
|
||||
|
||||
|
||||
|
||||
155
scripts/validate_paranames.py
Normal file
155
scripts/validate_paranames.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validation du gazetteer paranames (Wikidata CC BY 4.0).
|
||||
|
||||
Vérifie que le fichier `data/paranames/noms_famille_world.txt.gz` est :
|
||||
1. Présent et lisible
|
||||
2. Contient les noms tests internationaux (OYARCABAL, EJNAINI, ...)
|
||||
3. Contient les noms INSEE FR courants (overlap)
|
||||
4. Ne contient PAS de mots français courants (risque FP)
|
||||
5. Améliore la détection sur audit_30 (si les PDFs sont disponibles)
|
||||
|
||||
Usage : python scripts/validate_paranames.py
|
||||
Le script s'arrête immédiatement si le fichier n'existe pas.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DATA_DIR = ROOT / "data" / "paranames"
|
||||
GAZETTEER_PATH = DATA_DIR / "noms_famille_world.txt.gz"
|
||||
|
||||
# Noms tests internationaux — doivent être présents
|
||||
NOMS_TESTS = {
|
||||
"EJNAINI", # maghrébin
|
||||
"NGUYEN", # vietnamien
|
||||
"SCHMIDT", # germanique
|
||||
"OBAMA", # africain
|
||||
"NAKAMURA", # japonais
|
||||
"SINGH", # indien
|
||||
"TANAKA", # japonais
|
||||
"GARCIA", # hispanique
|
||||
"ROSSI", # italien
|
||||
}
|
||||
|
||||
# Noms couverts par un autre mécanisme que le gazetteer (regex F2/F3 « Nom usuel »).
|
||||
# Leur absence de paranames est informative, pas bloquante.
|
||||
NOMS_COUVERTS_AILLEURS = {
|
||||
"OYARCABAL": "regex F3 (label « Nom usuel : ») — absent du dump Wikidata",
|
||||
}
|
||||
|
||||
# Noms INSEE FR courants — overlap attendu
|
||||
NOMS_INSEE_FR = {"MARTIN", "BERNARD", "DUBOIS", "THOMAS", "PETIT", "ROBERT", "RICHARD"}
|
||||
|
||||
# Mots-outils français — NE doivent PAS être présents (filtrés via stopwords_manuels).
|
||||
# NB : on n'y met QUE des mots-outils sans risque d'être un patronyme. Des mots
|
||||
# comme « allez » ou « polygone » SONT de vrais patronymes INSEE rares : on les
|
||||
# garde masquables (priorité sécurité = pas de fuite), donc hors de cette liste.
|
||||
MOTS_FR_INTERDITS = {"VOIR", "MIDI", "SANS", "AVEC", "POUR", "DANS"}
|
||||
|
||||
|
||||
def status(icon: str, label: str, ok: bool, detail: str = "") -> str:
|
||||
tag = "✅ OK" if ok else "❌ FAIL"
|
||||
d = f" — {detail}" if detail else ""
|
||||
return f"{icon} [{tag}] {label}{d}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 60)
|
||||
print(" VALIDATION GAZETTEER PARANAMES")
|
||||
print("=" * 60)
|
||||
|
||||
# Check 0: fichier présent
|
||||
if not GAZETTEER_PATH.exists():
|
||||
print(f"\n❌ Fichier absent : {GAZETTEER_PATH}")
|
||||
print("Le gazetteer paranames n'a pas encore été généré.")
|
||||
print("Relancer quand data/paranames/noms_famille_world.txt.gz existe.")
|
||||
return 1
|
||||
|
||||
# Load
|
||||
print(f"\n📂 Chargement : {GAZETTEER_PATH}")
|
||||
with gzip.open(GAZETTEER_PATH, "rt", encoding="utf-8") as f:
|
||||
noms = {line.strip().upper() for line in f if line.strip()}
|
||||
|
||||
print(f" {len(noms):,} noms chargés")
|
||||
|
||||
results = {}
|
||||
|
||||
# Check 1: taille minimale
|
||||
min_size = 100_000
|
||||
ok = len(noms) >= min_size
|
||||
results["taille"] = ok
|
||||
print(status("📊", f"Taille ≥ {min_size:,}", ok, f"{len(noms):,} noms"))
|
||||
|
||||
# Check 2: noms tests internationaux
|
||||
presents = {n for n in NOMS_TESTS if n in noms}
|
||||
absents = NOMS_TESTS - presents
|
||||
ok = len(absents) == 0
|
||||
results["noms_tests"] = ok
|
||||
if absents:
|
||||
print(status("🌍", "Noms tests internationaux", ok, f"absents: {sorted(absents)}"))
|
||||
else:
|
||||
print(status("🌍", "Noms tests internationaux", ok, f"{len(presents)}/{len(NOMS_TESTS)} présents"))
|
||||
|
||||
# Check 2bis : noms couverts par un autre mécanisme (warning informatif, non bloquant)
|
||||
for nom, mecanisme in sorted(NOMS_COUVERTS_AILLEURS.items()):
|
||||
present = nom in noms
|
||||
tag = "présent" if present else "absent (attendu)"
|
||||
print(f"ℹ️ [WARN] {nom} : {tag} de paranames — couvert par {mecanisme}")
|
||||
|
||||
# Check 3: overlap INSEE FR
|
||||
overlap = {n for n in NOMS_INSEE_FR if n in noms}
|
||||
ok = len(overlap) >= len(NOMS_INSEE_FR) - 1 # tolère 1 manquant
|
||||
results["overlap_insee"] = ok
|
||||
print(status("🇫🇷", "Overlap INSEE FR", ok, f"{len(overlap)}/{len(NOMS_INSEE_FR)}"))
|
||||
|
||||
# Check 4: pas de mots français courants
|
||||
interdits_trouves = {m for m in MOTS_FR_INTERDITS if m in noms}
|
||||
ok = len(interdits_trouves) == 0
|
||||
results["pas_mots_fr"] = ok
|
||||
if interdits_trouves:
|
||||
print(status("🚫", "Pas de mots FR courants", ok, f"trouvés: {sorted(interdits_trouves)}"))
|
||||
else:
|
||||
print(status("🚫", "Pas de mots FR courants", ok))
|
||||
|
||||
# Check 5: effet sur audit_30 (si PDFs disponibles)
|
||||
pdfs_dir = ROOT / "pdf_natif"
|
||||
if not pdfs_dir.exists() or not list(pdfs_dir.glob("*.pdf")):
|
||||
results["audit30"] = None # N/A
|
||||
print(status("📄", "Test audit_30", True, "PDFs non disponibles — SKIP"))
|
||||
else:
|
||||
# Quick check: essayer de charger le gazetteer via le core
|
||||
try:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from anonymizer_core_refactored_onnx import _load_paranames_noms, _PARANAMES_NOMS_SET
|
||||
loaded = _load_paranames_noms()
|
||||
ok = len(loaded) > 0
|
||||
results["audit30"] = ok
|
||||
print(status("⚙️", "Chargement core OK", ok, f"{len(loaded):,} noms dans le core"))
|
||||
|
||||
# Vérifier que les noms gazetteer attendus sont reconnus
|
||||
for test_nom in ["EJNAINI", "NGUYEN"]:
|
||||
in_core = test_nom.upper() in loaded
|
||||
print(status(f" {test_nom}", "Reconnu dans core", in_core))
|
||||
except Exception as e:
|
||||
results["audit30"] = False
|
||||
print(status("⚙️", "Chargement core", False, str(e)))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
passed = sum(1 for v in results.values() if v is True)
|
||||
failed = sum(1 for v in results.values() if v is False)
|
||||
skipped = sum(1 for v in results.values() if v is None)
|
||||
print(f" {passed} passed, {failed} failed, {skipped} skipped")
|
||||
if failed > 0:
|
||||
print(" ⚠️ Gazetteer nécessite corrections avant utilisation.")
|
||||
return 1
|
||||
print(" ✅ Gazetteer paranames validé.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user