Målet
Persondata (navn, adresse, e-mail, telefon, CPR) skal fjernes eller generaliseres, før en AI ser kundeklubdata — men købshistorik, køn, region og aldersbånd skal bevares, så analysen stadig kan laves. Kravet var mindst 75 % præcision på personnavne, og at kundens egne brand-navne bevares, også når de lyder som personnavne.
Arkitekturen: flere værktøjer, route efter type
Ét værktøj kan ikke både fange CPR, danske navne og undtage brand. Derfor routes hver type til det værktøj, der er målt bedst:
- Strukturerede id'er (CPR, e-mail, tlf, CVR, konto, IBAN, kort, IP) → deterministisk kode (regex + kontrolsum).
- Personnavne i fri tekst → NER-ensemble (opf + ScandNER) + gazetteer-bekræftelse.
- Brand (også navn-agtigt) → brand-undtagelse i kode (696 brands fra kundens egen brand-liste). En sprogdom kan ikke skelne brand fra navn.
- Adresse → deterministisk dansk adresse-detektor + opf.
- Brugernavn → @-handles i kode; bare handles via Jev (gate 0,9).
- Tvivlsomme navne/kollisioner → Jev med kontekst (gate 0,9).
- Særlige kategorier (helbred, religion, fagforening, straf) → flagges til menneske, maskeres ikke.
Værktøjssammenligning på samme cases
90 cases (60 normale + 30 svære navne uden for gazetteeret). fp_brand = brand-tegn maskeret ved en fejl.
Svære navne (30)
| Metode | Præcision | Recall | Brand-falsk | Andre falske |
|---|---|---|---|---|
| opf (rå) | 0,525 | 0,459 | 118 | 15 |
| ScandNER (rå) | 0,734 | 1,000 | 116 | 0 |
| Navne-gazetteer alene | 0,800 | 0,188 | 15 | 0 |
| Pipeline (opf) | 1,000 | 0,441 | 0 | 0 |
| Pipeline (opf + ScandNER) | 1,000 | 0,784 | 0 | 0 |
| Pipeline (+ betro ScandNER) | 1,000 | 1,000 | 0 | 0 |
Normale navne (60)
| Metode | Præcision | Recall | Brand-falsk | Andre falske |
|---|---|---|---|---|
| opf (rå) | 0,672 | 0,693 | 245 | 40 |
| ScandNER (rå) | 0,651 | 0,864 | 377 | 14 |
| Navne-gazetteer alene | 0,854 | 0,778 | 112 | 0 |
| Pipeline (opf) | 1,000 | 0,876 | 0 | 0 |
| Pipeline (opf + ScandNER) | 0,996 | 0,902 | 0 | 3 |
| Pipeline (+ betro ScandNER) | 0,996 | 0,902 | 0 | 3 |
Læsning: ingen enkelt model har både høj præcision og recall. Koden (brand-undtagelse + gazetteer) løfter præcisionen til 1,0 og fjerner alle brand-falske; ScandNER løfter recall.
Præcision pr. datatype
| Type | Id-kerne P | Id-kerne R | Survey P | Survey R | Extra hard P | Extra hard R |
|---|---|---|---|---|---|---|
| PERSON | – | – | 0,975 | 0,973 | 0,881 | 0,942 |
| 1,000 | 1,000 | 1,000 | 1,000 | 1,000 | 0,167 | |
| PHONE | 0,600 | 1,000 | 1,000 | 1,000 | 1,000 | 0,750 |
| ADDRESS | – | – | 0,984 | 1,000 | 1,000 | 0,845 |
| CPR | – | – | 1,000 | 1,000 | 1,000 | 1,000 |
| CVR | – | – | 1,000 | 1,000 | – | – |
| ACCOUNT | – | – | 1,000 | 1,000 | 1,000 | 1,000 |
| CARD | – | – | 1,000 | 1,000 | 0,543 | 1,000 |
| IP | – | – | 1,000 | 1,000 | – | – |
| USERNAME | – | – | 1,000 | 0,827 | 1,000 | 0,618 |
| DOB | – | – | – | – | – | 0,000 |
Id-kerne = deterministisk kerne på 33 målrettede cases. Survey = 56 fiktive surveysvar med PII midt i sætningen. Extra hard = 35 varierede cases med forkortelser, obfuskerede mails, små bogstaver, typo og udenlandsk adresse. – betyder at typen ikke optræder i det sæt.
Feltbaseret data
De strukturerede felter maskeres deterministisk. Verifikation på hele datasættet (120 fiktive kunder):
- Kundens navn helt væk fra fritekstnoten (token-niveau): 52/53
- Brand-navne bevaret i noterne: 70/70
- Navne-rest: 1 — brand/navn-kollisionen et brand der også er et almindeligt dansk efternavn.
- Strukturerede felter: navn, aldersbånd, postnummer-prefix, adresse, fjernet e-mail/telefon/by = 120/120.
- Residual-scan af hele den maskerede fil: 0 e-mails, 0 CPR, 0 telefonnumre.
Extra hard felter (24 poster, 96 tjek)
PII i det forkerte felt, andre datoformater, postnummer med bynavn, dobbelt mellemrum, VERSALER, initialer, NBSP, tomme felter, brand-navn som kundenavn.
- Beståede tjek: 96/96 (fejl: 0)
- Residual efter maskering: {'email': 0, 'cpr': 0, 'tlf': 0}
Hvor det brister — ærligt
- Obfuskerede mails ("mette (at) gmail (dot) com") fanges ikke (recall 0,17 i extra hard).
- Telefon i streg-format ("12-34-56-78") fanges ikke (recall 0,75).
- Fødselsdato i fritekst ("1. januar 1980") fanges ikke.
- Udenlandske adresser ("10 Downing Street, London") fanges ikke — det er en dansk adresse-detektor.
- Bare brugernavne uden @ (recall 0,62). Jev kan lukke hullet (18/18 ved gate ≥0,9), men det er ikke koblet ind.
- Brand/navn-kollision: et efternavn der også står på brand-listen bevares, fordi brand-undtagelsen vinder. Politikken kan vendes, men så maskeres brandet i stedet.
- Ordrenumre der tilfældigt består Luhn maskeres som kort (1 falsk positiv i extra hard). Den sikre fejl er over-maskering.
- Tip: over-maskering af slægtskabsord ("min mand Henrik" → "min mand <PERSON>") koster lidt præcision.
Huller der kan lukkes (målt, ikke koblet ind endnu)
De tre svageste steder er testet med en smal Jev-dom (sætningskontekst, gate ved sikkerhed ≥ 0,9). Alle tre bestod, men er endnu ikke i pipelinen:
- Obfuskerede e-mails ("mette (at) gmail (dot) com") → Jev 16/16 ved gate 0,9.
- Bare brugernavne ("henrik_vinter") → Jev 18/18 ved gate 0,9.
- Telefon vs. andet 8-cifret tal ("Konto 12345678") → Jev 15/15 ved gate 0,9.
Reglen fra projektet: kendte mønstre og kendte entiteter hører i koden; kun det tvetydige går til Jev — og kun ved høj sikkerhed.
Sådan er det målt
- Facit registreres ved opbygningen af data (type + præcise tegnintervaller), ikke gættet bagefter.
- Score er tegn-niveau: præcision = forudsagte tegn af typen der er rigtige; recall = facit-tegn af typen der blev fanget.
- Alle data er fiktive (faker da_DK + kundens brand-liste + håndskrevne hard-cases). Ingen rigtige kunder.
- Genkørsel:
python3 eval/detector_bench.py,python3 eval/pii_type_accuracy.py,python3 eval/survey_bench.py,python3 eval/hard_bench.py. - Værktøjer: opf-danish (finetunet privacy-filter), ScandNER (saattrupdan/nbailab-base-ner-scandi, MIT), dansk navne-gazetteer, kundens brand-liste, samt egen deterministisk kerne og Jev (gate 0,9).
Genereret 29.09.2026 af maskeringsprojektet. 120 kunder, 695 brands.