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

Kies drempels op validatie en houd ze daarna vast

Onderzoek een begrensd drempelgrid met publieke foutkosten en een expliciet criterium en beoordeel de vaste keuze op andere rijen.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je combineert twee groepsgrids en selecteert binnen een gekozen empirische grens op publieke foutkosten.
  • Je houdt gekozen drempels vast op afzonderlijke gegevens en onderscheidt validatie-uitkomsten van een algemene fairnessgarantie.

Een gevonden modelscore is nog geen besluit. Bij welke score krijgt een melding extra aandacht? Een lagere drempel vindt vaak meer urgente meldingen, maar kan ook meer gewone meldingen onterecht selecteren.

Stel je kaartjes op volgorde van hun score voor. Een verschuifbare scheidslijn bepaalt welke kaartjes doorgaan. Je wilt zien welke fouten én groepsverschillen veranderen voordat je één beleid kiest.

Je onderzoekt twee volledig verzonnen groepen op een klein, vast drempelgrid. De voorbeelden maken een afweging zichtbaar; ze bepalen niet welke groepsspecifieke behandeling maatschappelijk of juridisch passend is. Kies op validatiegegevens en beoordeel de vastgehouden keuze daarna op andere rijen.

1. Maak eerst de mogelijke beslissingen zichtbaar

Groep a heeft labels 0, 0, 1, 1 en scores 0,1; 0,4; 0,35; 0,8. Een score gelijk aan de drempel telt als positief. Het grid bevat hier vier vooraf gekozen drempels: nul, 0,3, 0,5 en één.

Bij drempel 0,3 gaan de scores 0,4; 0,35 en 0,8 door. Dat levert twee TP en één FP op. Bij 0,5 blijft alleen de score 0,8 over, waardoor één urgente melding wordt gemist.

cuts = [0.,.3,.5,1.]
a = threshold_profile(
    [0,0,1,1],[.1,.4,.35,.8],cuts)
print(a[1]['tp'],a[1]['fp'])
print(a[2]['fn'])
# 2 1
# 1

Een hogere drempel kan bij dezelfde labels en scores geen extra positieve voorspellingen creëren. De profielen bewaren daarom dezelfde groepsgrootte en dezelfde aantallen positieve en negatieve labels. Een lege groep houdt tellingen nul; haar conditionele maten kunnen onbekend blijven.

01
CHECKPOINTReken een vooraf gekozen drempelgrid door

2. Combineer groepsdrempels en maak kosten expliciet

Groep b heeft één urgente en drie gewone meldingen. Je onderzoekt daar hetzelfde grid. Vier keuzes voor a en vier voor b geven samen zestien kandidaten; dit is een begrensd onderzoek, geen zoektocht door alle denkbare beslisregels.

Noem de publieke foutkosten c_FP en c_FN. De totale FP en FN tellen fouten over beide groepen, en C is hun gewogen som. Alle kandidaten gebruiken dezelfde validatierijen, zodat hun kosten op dezelfde gegevens worden vergeleken.

C=cFP·FP+cFN·FN

Kies een kost van één per FP en twee per FN. Bij drempel 0,3 voor beide groepen zijn er samen twee FP en geen FN: C is twee. Het TPR-verschil is nul; het FPR-verschil is een half min een derde, ongeveer 0,167.

b = threshold_profile(
    [0,0,0,1],[.05,.2,.6,.7],cuts)
groups = [
    {'id':'a','profiles':a},
    {'id':'b','profiles':b}]
grid = combine_policies(groups,1.,2.)
hand = [c for c in grid
        if c['id']=='1:1'][0]
print(len(grid))
print(hand['weighted_cost'])
print(round(hand[
    'equalized_odds_gap'],3))
# 16
# 2.0
# 0.167
02
CHECKPOINTCombineer twee groepsgrids en tel publieke foutkosten

3. Kies één criterium en leg de grens vast

Verander in het beeld de foutkosten, de maximale toegestane gap en het vergeleken criterium. Selection vergelijkt selectieratio’s; opportunity vergelijkt TPR; equalized_odds vergelijkt het grootste TPR-/FPR-verschil. Het gekozen criterium bepaalt welke kandidaten door het filter komen.

VERKEN HET ZELFKies op validatie, houd de drempels vast bij beoordeling

Onderzoek een vast grid voor twee verzonnen groepen. Filter op een expliciet criterium, vergelijk publieke foutkosten en beoordeel pas daarna de gekozen drempels op andere rijen.

Validatiekeuze
03,857,700,510:0: gap 0, kosten 50:1: gap 0,667, kosten 30:2: gap 0,667, kosten 30:3: gap 1, kosten 41:0: gap 0,5, kosten 41:2: gap 0,167, kosten 21:3: gap 1, kosten 32:0: gap 1, kosten 52:1: gap 0,5, kosten 32:2: gap 0,5, kosten 32:3: gap 0,5, kosten 43:0: gap 1, kosten 73:1: gap 1, kosten 53:2: gap 1, kosten 53:3: gap 0, kosten 61:1: gap 0,167, kosten 2Kosten CGekozen gap · streep = grens

Groen: past binnen de grens · grote stip: gekozen. Punten kunnen samenvallen; de tabel toont iedere kandidaat.

Kandidaten
16
Binnen gekozen grens
4
Gekozen ID
1:1
Validatiekosten C
2

Gekozen 1:1: drempel a 0,3, b 0,3; validatiegap 0,167. Kies op deze validatiegegevens, voordat je de andere rijen bekijkt.

Alle kandidaten op dezelfde validatierijen. ID is het nul-gebaseerde gridpaar; C telt publieke FP-/FN-kosten. Onbekende gaps blijven onbekend.
IDDrempels a / bKosten CGap
0:00 / 050
0:10 / 0,330,667
0:20 / 0,530,667
0:30 / 141
1:00,3 / 040,5
1:1gekozen0,3 / 0,320,167
1:20,3 / 0,520,167
1:30,3 / 131
2:00,5 / 051
2:10,5 / 0,330,5
2:20,5 / 0,530,5
2:30,5 / 140,5
3:01 / 071
3:11 / 0,351
3:21 / 0,551
3:31 / 160

Pas de vaste keuze toe op andere rijen

Je bekijkt nu alleen validatie. De knop houdt de gekozen drempels vast en vergrendelt de keuzevelden.

Dit is een beperkt deterministisch grid op vaste oefendata. Een kleine gap bewijst geen algemene fairness of statistische zekerheid. Dezelfde oefenbeoordeling wordt bij terugkeer hergebruikt; na bekijken is zij geen nieuwe onafhankelijke eindtest.

Noem de gekozen gap Δ en de publieke tolerantie τ. Een kandidaat mag mee als zijn gap bekend is en niet groter is dan τ. Gelijkheid aan de grens is toegestaan; onbekend wordt niet als nul behandeld.

Δ≤τ

Daarna kies je binnen dit grid de laagste gewogen foutkosten. Bij gelijke kosten gaat de kleinere gekozen gap voor; bij opnieuw gelijkheid gebruik je lexicografisch de drempels in de vaste groepsvolgorde. Die laatste stap maakt een gelijke uitkomst reproduceerbaar, zonder haar inhoudelijk beter te verklaren.

Met equalized_odds en tolerantie 0,2 kiest ons voorbeeld kandidaat 1:1. De indices in dat id beginnen bij nul: index één verwijst naar de tweede drempel, dus 0,3. De groepsdrempels zijn hier toevallig gelijk; de zoekruimte laat ook ongelijke drempels toe.

choice = choose_policy(
    grid,'equalized_odds',.2)
chosen_id = choice['chosen_id']
selected = [c for c in grid
            if c['id']==chosen_id][0]
print(chosen_id)
print(list(selected[
    'thresholds'].values()))
# 1:1
# [0.3, 0.3]

Een andere kostenkeuze kan andere fouten aantrekkelijk maken. Een strengere gapgrens kan alle kandidaten uitsluiten, of alleen kandidaten met hoge foutkosten overhouden. Geen passende kandidaat is een geldige uitkomst, geen aanleiding om onbekende maten weg te poetsen.

03
CHECKPOINTSelecteer binnen een expliciete empirische grens

4. Houd de keuze vast op afzonderlijke gegevens

Een validatie-uitkomst voorspelt geen zekerheid over nieuwe gegevens. Houd beide gekozen drempels op 0,3 en beoordeel andere rijen. Kies in deze beoordeling niet opnieuw een gunstig beleid uit.

test = [
 ('a',[1,1,0,0],[.1,.2,.7,.8]),
 ('b',[1,0,0,0],[.9,.1,.2,.25])]
fixed = []
for name,y,s in test:
    t = selected['thresholds'][name]
    prof = threshold_profile(y,s,[t])
    fixed.append({
        'id':name,'profiles':prof})
check = combine_policies(
    fixed,1.,2.)[0]
print(check['equalized_odds_gap'])
# 1.0

In a worden nu alle urgente meldingen gemist en alle gewone geselecteerd; in b gebeurt het omgekeerde. De gap groeit naar één. Dit voorbeeld maakt concreet waarom een gekozen empirische grens geen algemene fairnessgarantie, statistische zekerheid of bewijs over toekomstige populaties is.

Ook aangeleverde kandidaatkosten en labels verdienen controle: de keuzefunctie bewijst hun herkomst niet. De twee datasets in dit voorbeeld zijn vooraf apart gemaakt. Op echte gegevens vraagt een gewijzigd ontwerp opnieuw een onafhankelijke beoordeling.

Wat je nu kunt

  • Je maakt een begrensd drempelgrid en benoemt publieke foutkosten.
  • Je kiest expliciet een criterium, een inclusieve grens en een reproduceerbare rangorde.
  • Je beoordeelt vastgehouden drempels op afzonderlijke rijen zonder opnieuw te selecteren.

Hierna onderzoek je modeluitleg: welke invoer hangt samen met een voorspelling, en wanneer zegt zo’n verband nog niets over een oorzaak?

DE VOLGENDE STAPMeet permutatiebelang voor één vast model
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
Reken een vooraf gekozen drempelgrid door

Schrijf threshold_profile(labels,scores,thresholds). Gebruik de gegeven _th_inputs. Labels is na np.asarray een 1D-integer-vector (dtype i/u), lengte 0–256, waarden 0/1; voor lengte nul mag numerieke dtype i/u/f. Scores is even lange eindige 1D-vector dtype i/u/f, na zelfstandige floatconversie binnen 0–1. Thresholds is eindige 1D-vector dtype i/u/f met 1–9 waarden, na floatconversie strikt stijgend binnen 0–1. Pure bool/object/complex ongeldig; gemengde bool/int kan numeriek converteren. Geef per drempel een dict met threshold als float en count/tp/fp/tn/fn als integers. Score >= threshold telt positief. Behoud gridvolgorde, lege groepen krijgen tellingen nul. Behoud alle invoer, container-/groepsvolgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoercontainers. Ongeldige invoer geeft ValueError. Een profiel bewaart uitkomsten op deze gedeclareerde labels; geen onzekerheids- of provenancebewijs.

Een hint

Vergelijk iedere score inclusief gelijkheid en behoud steeds dezelfde gelabelde rijen.

02
Combineer twee groepsgrids en tel publieke foutkosten

Schrijf combine_policies(group_profiles,fp_cost=1.,fn_cost=1.). Gebruik de gegeven _th_profiles. Group_profiles is exact list/tuple van precies twee exacte dicts met id/profiles. Ids uniek; profiles is exact list/tuple met 1–9 exacte dicts met threshold/count/tp/fp/tn/fn. Thresholds eindig, geconverteerd binnen 0–1 en strikt stijgend. Count volgt uit de vier cellen. Per groep blijven count, tp+fn en fp+tn gelijk over de drempels; tp en fp mogen niet stijgen. Samen tellen beide groepen 1–256 rijen. Een groep met nul rijen mag. Tellingen zijn Python-/NumPy-integers binnen 0–256 zonder bool, veilig naar Python-int geconverteerd. Groepsnamen zijn exacte Python-str met 1–24 ASCII-tekens: eerste kleine letter, daarna kleine letters/cijfers/underscore/streepje. Scalarvalidators weigeren bool, accepteren Python-/NumPy-int/float, converteren veilig naar Python-float en controleren daarna eindigheid/bounds van die geconverteerde waarde. Fp_cost/fn_cost liggen binnen 0–100. Geef alle cartesiaanse combinaties: eerste groep buitenste lus, tweede groep binnenste. Elke dict bevat id als 0-based indices i:j, thresholds als zelfstandige dict van beide ids naar floats in groepsvolgorde, row_count, error_count=FP+FN als integers, weighted_cost=fp_cost*FP+fn_cost*FN als float, selection_gap, tpr_gap, fpr_gap en equalized_odds_gap. Bereken de eerste drie gaps als absolute verschillen van de passende groepsrates; een ontbrekende noemer geeft None. Equalized odds is max van beide conditionele gaps als die bekend zijn, anders None. Behoud alle invoer, container-/groepsvolgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoercontainers. Ongeldige invoer geeft ValueError. Profielen komen uit dezelfde validatierijen per groep; structurele controle bewijst niet dat deze profielen werkelijk uit scores voortkomen.

Een hint

Maak maximaal 9 × 9 kandidaten; gebruik voor TPR en FPR verschillende noemers.

03
Selecteer binnen een expliciete empirische grens

Schrijf choose_policy(candidate_rows,criterion="equalized_odds",tolerance=.1). Gebruik de gegeven _th_candidates. Candidate_rows is exact list/tuple met 1–81 exacte dicts met id, thresholds, weighted_cost, error_count, row_count, selection_gap, tpr_gap, fpr_gap, equalized_odds_gap. Id is uniek i:j met i/j 0–8. Thresholds is exact dict van twee geldige groepsnamen naar eindige geconverteerde floats binnen 0–1; dezelfde ids en volgorde in iedere rij, unieke drempelparen. Groepsnamen zijn exacte Python-str met 1–24 ASCII-tekens: eerste kleine letter, daarna kleine letters/cijfers/underscore/streepje. Tellingen zijn Python-/NumPy-integers binnen 0–256 zonder bool, veilig naar Python-int geconverteerd. Scalarvalidators weigeren bool, accepteren Python-/NumPy-int/float, converteren veilig naar Python-float en controleren daarna eindigheid/bounds van die geconverteerde waarde. Row_count ligt binnen 1–256 en is gelijk voor alle kandidaten; error_count <= row_count. Weighted_cost binnen 0–25600. Iedere gap is None of eindige fractie 0–1; equalized_odds_gap is max(tpr_gap,fpr_gap) als beide bekend zijn, anders None. Criterion is exact str selection/opportunity/equalized_odds; tolerance binnen 0–1. Vergelijk respectievelijk selection_gap/tpr_gap/equalized_odds_gap. Een onbekende gekozen gap wordt afgewezen met reden unknown; gap > tolerance met above-tolerance. Kies onder de rest minimale weighted_cost, daarna kleinere gekozen gap, daarna lexicografisch de drempels in groepsvolgorde. Geef criterion, tolerance als float, chosen_id of None, eligible_ids lexicografisch gesorteerd en rejected als id→reden-map. Behoud alle invoer, container-/groepsvolgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoercontainers. Ongeldige invoer geeft ValueError. Het zijn aangeleverde samenvattingen, geen bewijs van kostenafleiding/provenance. Kies op validatiegegevens; houd gekozen drempels vast op afzonderlijke beoordelingsrijen. Geen algemeen optimum of algemene fairnessgarantie.

Een hint

Filter eerst op de gekozen bekende gap; rangschik daarna pas de geschikte kandidaten.

Vastgelopen? Bekijk de voorbeeldoplossing
import itertools

import math

import re

import numpy as np

# Jouw functies

def threshold_profile(labels, scores, thresholds):
    labels, scores, thresholds = _th_inputs(labels, scores, thresholds)
    result = []
    for threshold in thresholds:
        predicted = scores >= threshold
        result.append({'threshold': float(threshold), 'count': len(labels), 'tp': int(np.count_nonzero((labels == 1) & predicted)), 'fp': int(np.count_nonzero((labels == 0) & predicted)), 'tn': int(np.count_nonzero((labels == 0) & ~predicted)), 'fn': int(np.count_nonzero((labels == 1) & ~predicted))})
    return result

def combine_policies(group_profiles, fp_cost=1.0, fn_cost=1.0):
    groups = _th_profiles(group_profiles)
    fp_cost = _th_number(fp_cost, 0, 100)
    fn_cost = _th_number(fn_cost, 0, 100)
    result = []
    for (i, left), (j, right) in itertools.product(enumerate(groups[0]['profiles']), enumerate(groups[1]['profiles'])):
        gaps = {}
        for metric, num_fields, den_fields in [('selection_gap', ['tp', 'fp'], ['count']), ('tpr_gap', ['tp'], ['tp', 'fn']), ('fpr_gap', ['fp'], ['fp', 'tn'])]:
            values = []
            for row in [left, right]:
                den = sum((row[field] for field in den_fields))
                values.append(sum((row[field] for field in num_fields)) / den if den else None)
            gaps[metric] = None if None in values else abs(values[0] - values[1])
        gaps['equalized_odds_gap'] = None if gaps['tpr_gap'] is None or gaps['fpr_gap'] is None else max(gaps['tpr_gap'], gaps['fpr_gap'])
        result.append({'id': f'{i}:{j}', 'thresholds': {groups[0]['id']: left['threshold'], groups[1]['id']: right['threshold']}, 'weighted_cost': fp_cost * (left['fp'] + right['fp']) + fn_cost * (left['fn'] + right['fn']), 'error_count': left['fp'] + right['fp'] + left['fn'] + right['fn'], 'row_count': left['count'] + right['count'], **gaps})
    return result

def choose_policy(candidate_rows, criterion='equalized_odds', tolerance=0.1):
    rows = _th_candidates(candidate_rows)
    if type(criterion) is not str or criterion not in ['selection', 'opportunity', 'equalized_odds']:
        raise ValueError('Een expliciet ondersteund empirisch criterium vereist')
    tolerance = _th_number(tolerance, 0, 1)
    field = {'selection': 'selection_gap', 'opportunity': 'tpr_gap', 'equalized_odds': 'equalized_odds_gap'}[criterion]
    eligible = []
    rejected = {}
    for row in rows:
        if row[field] is None:
            rejected[row['id']] = 'unknown'
        elif row[field] > tolerance:
            rejected[row['id']] = 'above-tolerance'
        else:
            eligible.append(row)
    chosen = min(eligible, key=lambda row: (row['weighted_cost'], row[field], tuple(row['thresholds'].values()), row['id']))['id'] if eligible else None
    return {'criterion': criterion, 'tolerance': tolerance, 'chosen_id': chosen, 'eligible_ids': sorted((row['id'] for row in eligible)), 'rejected': rejected}

# Gegeven helpers

def _th_number(value, low, high):
    if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, float, np.integer, np.floating)):
        raise ValueError('Eindige begrensde scalar vereist')
    try:
        value = float(value)
    except (OverflowError, ValueError, TypeError):
        raise ValueError('Eindige begrensde scalar vereist') from None
    if not math.isfinite(value) or not low <= value <= high:
        raise ValueError('Eindige begrensde scalar vereist')
    return value

def _th_int(value):
    if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)) or (not 0 <= int(value) <= 256):
        raise ValueError('Gehele telling binnen 0–256 vereist')
    return int(value)

def _th_id(value):
    if type(value) is not str or re.fullmatch('[a-z][a-z0-9_-]{0,23}', value) is None:
        raise ValueError('Korte synthetische groepsnaam vereist')
    return value

def _th_inputs(labels, scores, thresholds):
    raw = np.asarray(labels)
    if raw.ndim != 1 or len(raw) > 256 or raw.dtype.kind not in ('iuf' if len(raw) == 0 else 'iu') or np.any((raw != 0) & (raw != 1)):
        raise ValueError('Binaire integer-labels vereist; een lege numerieke vector mag')
    labels = raw.astype(np.int64, copy=True)
    raw = np.asarray(scores)
    if raw.ndim != 1 or len(raw) != len(labels) or raw.dtype.kind not in 'iuf' or (not np.isfinite(raw).all()):
        raise ValueError('Evenveel eindige reële scores vereist')
    scores = raw.astype(float, copy=True)
    if np.any(scores < 0) or np.any(scores > 1):
        raise ValueError('Geconverteerde scores binnen 0–1 vereist')
    raw = np.asarray(thresholds)
    if raw.ndim != 1 or not 1 <= len(raw) <= 9 or raw.dtype.kind not in 'iuf' or (not np.isfinite(raw).all()):
        raise ValueError('Een tot negen eindige drempels vereist')
    thresholds = raw.astype(float, copy=True)
    if np.any(thresholds < 0) or np.any(thresholds > 1) or np.any(np.diff(thresholds) <= 0):
        raise ValueError('Strikt stijgende geconverteerde drempels binnen 0–1 vereist')
    return (labels, scores, thresholds)

def _th_profiles(group_profiles):
    if type(group_profiles) not in (list, tuple) or len(group_profiles) != 2:
        raise ValueError('Precies twee expliciete groepsprofielen vereist')
    result = []
    for group in group_profiles:
        if type(group) is not dict or set(group) != {'id', 'profiles'}:
            raise ValueError('Groepsnaam en drempelprofielen vereist')
        name = _th_id(group['id'])
        rows = group['profiles']
        if type(rows) not in (list, tuple) or not 1 <= len(rows) <= 9:
            raise ValueError('Een tot negen drempelprofielen vereist')
        cleaned = []
        for row in rows:
            if type(row) is not dict or set(row) != {'threshold', 'count', 'tp', 'fp', 'tn', 'fn'}:
                raise ValueError('Volledig drempelprofiel vereist')
            item = {'threshold': _th_number(row['threshold'], 0, 1)}
            for key in ['count', 'tp', 'fp', 'tn', 'fn']:
                item[key] = _th_int(row[key])
            if item['count'] != sum((item[key] for key in ['tp', 'fp', 'tn', 'fn'])):
                raise ValueError('Telling moet uit de vier cellen volgen')
            if cleaned:
                previous = cleaned[-1]
                if item['threshold'] <= previous['threshold'] or item['count'] != previous['count'] or item['tp'] + item['fn'] != previous['tp'] + previous['fn'] or (item['fp'] + item['tn'] != previous['fp'] + previous['tn']):
                    raise ValueError('Dezelfde gelabelde groep en stijgende drempels vereist')
                if item['tp'] > previous['tp'] or item['fp'] > previous['fp']:
                    raise ValueError('Een hogere drempel kan niet meer positieve voorspellingen geven')
            cleaned.append(item)
        result.append({'id': name, 'profiles': cleaned})
    if result[0]['id'] == result[1]['id'] or not 1 <= sum((group['profiles'][0]['count'] for group in result)) <= 256:
        raise ValueError('Twee unieke groepen met samen 1–256 rijen vereist')
    return result

def _th_candidates(rows):
    fields = {'id', 'thresholds', 'weighted_cost', 'error_count', 'row_count', 'selection_gap', 'tpr_gap', 'fpr_gap', 'equalized_odds_gap'}
    if type(rows) not in (list, tuple) or not 1 <= len(rows) <= 81:
        raise ValueError('Een tot 81 kandidaat-samenvattingen vereist')
    cleaned = []
    names = None
    seen_ids = set()
    seen_thresholds = set()
    count = None
    for row in rows:
        if type(row) is not dict or set(row) != fields or type(row['id']) is not str or (re.fullmatch('[0-8]:[0-8]', row['id']) is None):
            raise ValueError('Volledige kandidaat met grid-id vereist')
        if row['id'] in seen_ids:
            raise ValueError('Unieke kandidaat-ids vereist')
        thresholds = row['thresholds']
        if type(thresholds) is not dict or len(thresholds) != 2:
            raise ValueError('Twee groepsdrempels vereist')
        ids = [_th_id(value) for value in thresholds]
        if names is None:
            names = ids
        if ids != names:
            raise ValueError('Behoud dezelfde groepsnamen en volgorde')
        thresholds = {name: _th_number(thresholds[name], 0, 1) for name in ids}
        pair = tuple(thresholds.values())
        if pair in seen_thresholds:
            raise ValueError('Unieke drempelparen vereist')
        item = {'id': row['id'], 'thresholds': thresholds, 'weighted_cost': _th_number(row['weighted_cost'], 0, 25600), 'error_count': _th_int(row['error_count']), 'row_count': _th_int(row['row_count'])}
        if not 1 <= item['row_count'] <= 256 or item['error_count'] > item['row_count']:
            raise ValueError('Geldige aantallen rijen en fouten vereist')
        if count is None:
            count = item['row_count']
        if item['row_count'] != count:
            raise ValueError('Dezelfde validatierijen voor alle kandidaten vereist')
        for key in ['selection_gap', 'tpr_gap', 'fpr_gap', 'equalized_odds_gap']:
            item[key] = None if row[key] is None else _th_number(row[key], 0, 1)
        expected = None if item['tpr_gap'] is None or item['fpr_gap'] is None else max(item['tpr_gap'], item['fpr_gap'])
        if item['equalized_odds_gap'] != expected:
            raise ValueError('Equalized-odds-gap volgt uit beide conditionele gaps')
        cleaned.append(item)
        seen_ids.add(item['id'])
        seen_thresholds.add(pair)
    return cleaned

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