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>
156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
#!/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())
|