Werkplaats / De werkplaats
In jouw eigen tempo
← AI in de wereld
MODULE 02 · LES 04

Onderzoek naamloze velden en herkenbare combinaties

Projecteer fictieve tabellen, bouw getypeerde equivalentieklassen en meet hoeveel rijen onder een gekozen minimum blijven.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt gegevensminimalisatie van herkenbaarheid via gecombineerde kenmerken.
  • Je bouwt zelfstandige projecties en equivalentieklassen en berekent een rijgewogen grootteverslag zonder anonimiteitsclaim.

Een team wil een AI-router verbeteren met helpdeskmeldingen. De namen verdwijnen uit een export, maar combinaties van zone, leeftijdscategorie en dienst blijven staan. Hoeveel rijen kun je daarmee nog van elkaar onderscheiden?

Denk aan een bak kaartjes. Met alleen een zone zijn veel kaartjes hetzelfde; met telkens een extra kenmerk kunnen de stapeltjes kleiner worden. De vraag welke velden je nodig hebt, komt daarom vóór de vraag hoe groot zo’n stapeltje is.

Je gebruikt uitsluitend zes verzonnen rijen. De oefening meet herkenbaarheid tegenover gekozen kenmerken en een gekozen minimumgrootte. Dat is geen certificaat dat willekeurige externe informatie niemand kan herkennen.

1. Begin met de vraag die je wilt beantwoorden

Voor de vraag “hoeveel meldingen komen uit elke zone?” heb je leeftijd en dienst niet nodig. Gegevensminimalisatie (data minimization) begint met dat doel. Een veld weglaten voorkomt dat deze export de weggelaten waarde rechtstreeks meestuurt.

| Rij | Zone | Leeftijd | Dienst | |---|---|---|---| | 1 | noord | 20-29 | vroeg | | 2 | noord | 20-29 | laat | | 3 | noord | 30-39 | vroeg | | 4 | zuid | 20-29 | vroeg | | 5 | zuid | 20-29 | laat | | 6 | zuid | 30-39 | vroeg |

Een projectie selecteert de gevraagde kolommen. Je behoudt de rijvolgorde, maar maakt nieuwe dictionaries zodat een latere wijziging de oorspronkelijke tabel niet beschadigt. Hier kies je bewust alleen de zone.

data = [
 ('noord','20-29','vroeg'),
 ('noord','20-29','laat'),
 ('noord','30-39','vroeg'),
 ('zuid','20-29','vroeg'),
 ('zuid','20-29','laat'),
 ('zuid','30-39','vroeg')]
fields = ['zone','age','shift']
rows = [dict(zip(fields,values))
        for values in data]
minimal = project_fields(rows,['zone'])
print(minimal[0])
print('age' in minimal[0])
# {'zone': 'noord'}
# False

De rijlabels in de tabel helpen ons tellen; ze zijn geen namen. Ook een projectie die een direct identificerend veld weglaat, kan nog indirect herkenbare kenmerken (quasi-identifiers) bevatten. Wat elders over iemand bekend is, staat niet in deze tabel en wordt hier dus niet volledig gemodelleerd.

01
CHECKPOINTBewaar alleen expliciet gekozen velden

2. Onderzoek welke rijen gelijk blijven

Kies eerst welke kenmerken een waarnemer kent. Rijen met op die velden dezelfde waarden vormen een equivalentieklasse. Verander de selectie in het beeld en kijk hoe twee stapels van drie uit elkaar vallen.

VERKEN HET ZELFMeer kenmerken maken kleinere stapels

Kies welke kenmerken een waarnemer kent. Bekijk de overblijvende velden, de groepen met dezelfde combinatie en hoeveel rijen onder jouw minimum vallen.

Bekende kenmerken
Projectie op de gekozen kenmerken. Rijlabels helpen tellen en zijn geen namen.
RijZoneLeeftijd
1noord20-29
2noord20-29
3noord30-39
4zuid20-29
5zuid20-29
6zuid30-39
036K12K1: rij 1, 2, 2 rijenK21K2: rij 3, 1 rijenK32K3: rij 4, 5, 2 rijenK41K4: rij 6, 1 rijenRijen per klasse · streep = minimum 2
  • K1 noord · 20-29Rij 1, 2
  • K2 noord · 30-39Rij 3
  • K3 zuid · 20-29Rij 4, 5
  • K4 zuid · 30-39Rij 6
Rijen
6
Klassen
4
Rijen onder minimum
2
Fractie rijen onder minimum
33,333%

2 van 6 rijen zitten in een klasse kleiner dan 2. Het gekozen minimum is niet overal gehaald. Een klasse van precies 2 telt als groot genoeg.

Alle zes rijen zijn verzonnen. Een gehaald gekozen minimum zegt alleen iets over deze tabel en deze kenmerken; het bewijst geen anonimiteit tegenover externe informatie of juridische naleving.

Noem de klasse rond rij i E_Q(i), waarbij Q de gekozen velden aanduidt. De grootte k_i telt hoeveel rijen inclusief i dezelfde combinatie hebben. De verticale strepen betekenen hier “aantal elementen”.

ki=|EQ(i)|

Alleen zone geeft de klassen noord en zuid, elk met drie rijen. Bij zone plus leeftijd ontstaan vier klassen: de twee noordelijke twintigers, de noordelijke dertiger en dezelfde verdeling in zuid. Hun groottes zijn 2, 1, 2 en 1.

classes = equivalence_classes(
    rows,['zone','age'])
sizes = [c['count'] for c in classes]
print(sizes)
print([c['indices'] for c in classes])
# [2, 1, 2, 1]
# [[0, 1], [2], [3, 4], [5]]

De indices beginnen in Python bij nul; de zichtbare rijlabels beginnen bij één. Voeg ook dienst toe en iedere combinatie wordt uniek in deze zes rijen. Meer kenmerken kunnen een naamloze tabel dus alsnog heel onderscheidend maken.

fine = equivalence_classes(rows,fields)
print([c['count'] for c in fine])
# [1, 1, 1, 1, 1, 1]

De oefening bewaart ook het exacte Python-type: integer 0 en string “0” zijn verschillende categorieën. Harmoniseer zulke codes alleen wanneer je weet dat ze dezelfde betekenis hebben. Dat opschoonbesluit volgt niet automatisch uit een privacymaat.

02
CHECKPOINTTel wie op de gekozen kenmerken gelijk is

3. Tel rijen onder een gekozen minimum

Kies nu een minimumklassegrootte van twee. Een klasse van precies twee haalt die grens. Bij groottes 2, 1, 2 en 1 zitten twee rijen in een te kleine klasse.

Noem het totale aantal rijen N, het aantal rijen in te kleine klassen B en hun fractie r. Je deelt door rijen, omdat iedere rij een bijdrage voorstelt. Twee kleine klassen van vier klassen betekent daarom niet dat de helft van de rijen te klein gegroepeerd is.

r=BN=26=13
report = class_size_report(sizes,2)
print(report['below_minimum_rows'])
print(round(
    report['below_minimum_fraction'],3))
print(report['meets_chosen_minimum'])
# 2
# 0.333
# False

Een grotere klasse voorkomt bovendien niet dat alle leden dezelfde gevoelige waarde kunnen hebben. De gekozen kenmerken, mogelijke koppelingen en verdere gegevensuitgiftes blijven van belang. Presenteer een gehaald minimum daarom steeds met zijn beperkte betekenis.

03
CHECKPOINTMeet de fractie rijen onder een gekozen minimum

Wat je nu kunt

  • Je selecteert velden vanuit een expliciete vraag en bewaart zelfstandige rijcontainers.
  • Je onderzoekt herkenbaarheid tegenover gekozen kenmerken met equivalentieklassen.
  • Je rapporteert een rijgewogen fractie onder een inclusief gekozen minimum, met duidelijke grenzen aan de conclusie.

Hierna onderzoek je een ander idee: de invloed van één begrensde bijdrage op een statistiek, en de wiskundige rol van ruis in een ideaal Laplace-mechanisme.

DE VOLGENDE STAPKalibreer ideale privacyverdelingen en tel budgetten
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Bewaar alleen expliciet gekozen velden

Schrijf project_fields(rows,fields). Gebruik de gegeven _privacy_table: rows heeft exact type list of tuple, lengte 1–256; iedere rij heeft exact type dict en 1–16 velden. Veldnamen zijn 1–32 ASCII-tekens, eerste een letter, daarna letters/cijfers/underscore. Alle rijwaarden zijn None, Python-bool, Python-int/float binnen [-1e6,1e6] (floats eindig), of str van maximaal 80 tekens. Geen geneste objecten of NumPy-scalars als categoriewaarde. De validator controleert alle velden, ook niet-geselecteerde. Fields/quasi_fields heeft exact type list of tuple, met 1–8 unieke geldige namen die in iedere rij bestaan. Geef een lijst zelfstandige dicts met precies de gekozen velden in de gevraagde veldvolgorde en dezelfde rijvolgorde; behoud waardetype en waarde. Een wijziging aan een uitgegeven rij mag de oorspronkelijke tabel of een andere uitgegeven rij niet veranderen. Behoud alle invoer en de globale Python-/NumPy-RNG. Ongeldige invoer geeft ValueError. Deze projectie toont minimalisatie; het verwijderen van een naam bewijst geen anonimiteit.

Een hint

Werk eerst het kleine handvoorbeeld uit; volg daarna de expliciete types, grenzen en betekenis van het resultaat.

02
Tel wie op de gekozen kenmerken gelijk is

Schrijf equivalence_classes(rows,quasi_fields). Gebruik de gegeven _privacy_table: rows heeft exact type list of tuple, lengte 1–256; iedere rij heeft exact type dict en 1–16 velden. Veldnamen zijn 1–32 ASCII-tekens, eerste een letter, daarna letters/cijfers/underscore. Alle rijwaarden zijn None, Python-bool, Python-int/float binnen [-1e6,1e6] (floats eindig), of str van maximaal 80 tekens. Geen geneste objecten of NumPy-scalars als categoriewaarde. De validator controleert alle velden, ook niet-geselecteerde. Fields/quasi_fields heeft exact type list of tuple, met 1–8 unieke geldige namen die in iedere rij bestaan. Twee rijen horen bij elkaar als ieder gekozen veld hetzelfde exacte Python-type en dezelfde Python-waarde heeft. False, 0, 0.0 en "0" zijn dus verschillende categorieën; -0.0 en 0.0 horen samen. None is een expliciete categorie. Geef een lijst klassen in volgorde van eerste voorkomen. Elke klasse is een dict met key (zelfstandige dict van gekozen velden/representatieve waarden), indices (zelfstandige oplopende lijst van 0-based rij-indices) en count (integer). Iedere rij komt precies eenmaal voor. Behoud alle invoer en de globale Python-/NumPy-RNG. Ongeldige invoer geeft ValueError. Dit meet equivalentie tegenover deze kenmerken, geen algemene privacygarantie.

Een hint

Werk eerst het kleine handvoorbeeld uit; volg daarna de expliciete types, grenzen en betekenis van het resultaat.

03
Meet de fractie rijen onder een gekozen minimum

Schrijf class_size_report(class_sizes,min_count=3). Class_sizes is na np.asarray een 1D-vector van 1–256 integers, dtype i/u, waarden 1–256, som maximaal 256. Pure bool/float/complex/object ongeldig; gemengde bool/int kan bij conversie integer worden. Min_count is Python-int 1–256 zonder bool. Geef integerwaarden row_count (som), class_count, minimum_class_size, min_count, below_minimum_classes en below_minimum_rows; float below_minimum_fraction=below_minimum_rows/row_count; boolean meets_chosen_minimum. Alleen een klassegrootte strikt onder min_count telt als te klein; below_minimum_rows telt alle rijen in zulke klassen. Behoud alle invoer en de globale Python-/NumPy-RNG. Ongeldige invoer geeft ValueError. Een gehaald gekozen minimum is geen anonimiteits- of juridisch conformiteitsbewijs.

Een hint

Werk eerst het kleine handvoorbeeld uit; volg daarna de expliciete types, grenzen en betekenis van het resultaat.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

import math

import re

# Jouw functies

def project_fields(rows,fields):
    data,fields=_privacy_table(rows,fields)
    return [{field:row[field] for field in fields} for row in data]

def equivalence_classes(rows,quasi_fields):
    data,fields=_privacy_table(rows,quasi_fields);groups={}
    for index,row in enumerate(data):
        # Typed category identity keeps False, 0, 0.0 and '0' explicitly distinct.
        signature=tuple((type(row[field]).__name__,row[field]) for field in fields)
        if signature not in groups:groups[signature]={'key':{field:row[field] for field in fields},'indices':[],'count':0}
        groups[signature]['indices'].append(index);groups[signature]['count']+=1
    return list(groups.values())

def class_size_report(class_sizes,min_count=3):
    values=np.asarray(class_sizes)
    if values.ndim!=1 or not 1<=len(values)<=256 or values.dtype.kind not in 'iu':raise ValueError('Een begrensde vector gehele klassegroottes vereist')
    if np.any(values<1) or np.any(values>256):raise ValueError('Klassegroottes binnen 1–256 vereist')
    sizes=[int(value) for value in values];total=sum(sizes)
    if total>256:raise ValueError('Maximaal 256 fictieve rijen vereist')
    if type(min_count) is not int or not 1<=min_count<=256:raise ValueError('Een gehele gekozen minimumgrootte vereist')
    small=[size for size in sizes if size<min_count];small_rows=sum(small)
    return {'row_count':total,'class_count':len(sizes),'minimum_class_size':min(sizes),'min_count':min_count,
        'below_minimum_classes':len(small),'below_minimum_rows':small_rows,'below_minimum_fraction':small_rows/total,
        'meets_chosen_minimum':not small}

# Gegeven helpers

def _privacy_table(rows,fields):
    if type(rows) not in (list,tuple) or not 1<=len(rows)<=256:raise ValueError('Een tot 256 fictieve rijen vereist')
    if type(fields) not in (list,tuple) or not 1<=len(fields)<=8:raise ValueError('Een tot acht veldnamen vereist')
    if any(type(field) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_]{0,31}',field) is None for field in fields) or len(set(fields))!=len(fields):raise ValueError('Unieke korte veldnamen vereist')
    copied=[]
    for row in rows:
        if type(row) is not dict or not 1<=len(row)<=16:raise ValueError('Begrensde platte rij vereist')
        cleaned={}
        for key,value in row.items():
            if type(key) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_]{0,31}',key) is None:raise ValueError('Korte veldnaam vereist')
            if value is None:pass
            elif type(value) is str:
                if len(value)>80:raise ValueError('Korte categoriewaarde vereist')
            elif type(value) is bool:pass
            elif type(value) is int:
                if not -10**6<=value<=10**6:raise ValueError('Begrensd categorisch integer vereist')
            elif type(value) is float:
                if not -10**6<=value<=10**6 or not math.isfinite(value):raise ValueError('Eindige begrensde categoriewaarde vereist')
            else:raise ValueError('Alleen primitieve oefencategorieën toegestaan')
            cleaned[key]=value
        if any(field not in cleaned for field in fields):raise ValueError('Ieder gekozen veld moet in iedere rij bestaan')
        copied.append(cleaned)
    return copied,list(fields)

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.