#!/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())