Werkplaats / De werkplaats
In jouw eigen tempo
← Klassieke machine learning
MODULE 03 · LES 09

Stel steeds één gerichte vraag

Bereken Gini-onzuiverheid, kies een split en bouw een kleine recursieve beslisboom.

75 min4 checkpoints
DIT GA JE ONTDEKKEN
  • Je kiest een splitsing op basis van gewogen Gini-onzuiverheid.
  • Je bouwt en gebruikt een echte boom met een begrensde diepte.

Een tuinder wil planten indelen met hun hoogte: “lager dan deze grens?” Een beslisboom (decision tree) stelt zulke vragen achter elkaar. Het antwoord stuurt een plant naar een volgende vraag of naar een blad met een klasse.

Je bouwt hier een echte boom voor één numerieke feature en integerlabels. Eerst zoek je een nuttige vraag; daarna herhaal je hetzelfde werk in kleinere groepen. Met meerdere features zou een boom ook moeten kiezen over welke feature hij vraagt.

1. Tel hoe gemengd een groep is

Een groep met alleen gezonde planten vraagt geen verdere splitsing. Een groep met half gezonde en half zieke planten is minder duidelijk. Gini-onzuiverheid vat die menging samen: trek de som van gekwadrateerde klasseaandelen af van één.

G=1−∑pc2

c loopt over de aanwezige klassen; p is hun aandeel. Bij labels [0,0,0,1] zijn de aandelen 3/4 en 1/4: G = 1−9/16−1/16 = 6/16 = 0.375. Alleen klasse 0 geeft 1−1² = 0.

labels = [0, 0, 0, 1]
telling = {}
for label in labels:
    n = telling.get(label, 0)
    telling[label] = n + 1
kwadraten = 0
for n in telling.values():
    kwadraten = kwadraten + n * n
g = 1 - kwadraten / len(labels) ** 2
print(g)  # 0.375

Een dictionary bewaart aantallen per label: .get(label,0) geeft de bestaande telling of nul als dit label nog ontbreekt; .values() geeft alle aantallen. De code telt eerst gekwadrateerde aantallen en deelt daarna door het gekwadrateerde totaal: dat is dezelfde formule, met minder afrondingsstappen. Maak gini(labels) en gebruik voor de lege groep de rekenconventie 0.

Dit criterium helpt trainingsgroepen vormen; het zegt nog niets over prestaties op onbekende planten.

01
CHECKPOINTMeet hoe gemengd een groep is

2. Weeg de kinderen naar hun grootte

Neem hoogtes [1,2,3,4] met labels [0,0,1,1]. Een grens t = 2.5 maakt twee zuivere groepen: links gaat x kleiner dan t, rechts gaat de rest. De gewogen onzuiverheid is nul.

Bij t = 1.5 bevat links één label 0 en rechts [0,1,1]. Rechts is G = 4/9, maar die groep vertegenwoordigt drie van vier planten. De splitscore is daarom 1/4 × 0 + 3/4 × 4/9 = 1/3.

S=nLNGL+nRNGR

N is het totale aantal; n geeft het aantal links of rechts. Veelgemaakte fout: beide Gini-waarden ongewogen middelen. Dan krijgt een klein kind ten onrechte evenveel invloed als een groot kind.

links = 0.0
rechts = 1 - (1/3)**2 - (2/3)**2
score = (1 * links + 3 * rechts) / 4
print(score)  # ongeveer 0.333333

Maak split_gini(xs,ys,t): verzamel links en rechts de labels met een lus, roep gini aan en weeg. Weiger lege of ongelijk lange invoer en een split met een leeg kind. Het diagram heeft acht planten; vergelijk daar grenzen 3.5 en 4.5 en verklaar het verschil.

HOE ZUIVER WORDEN DE KINDEREN?Verschuif één boomgrens

Cirkels zijn klasse 0, vierkanten klasse 1. Links gaat x<t; rechts x≥t. De ouder heeft Gini 0.5.

12345678x
Links (4)0.375Rechts (4)0.375Gewogen Gini0.375
02
CHECKPOINTWeeg beide kinderen naar hun grootte

3. Vergelijk alleen grenzen die iets veranderen

Bij de vier hoogtes zijn 1.5, 2.5 en 3.5 voldoende kandidaten. Binnen een opening tussen twee hoogtes verandert de groepsindeling niet. Dubbele hoogtes mogen niet over de kinderen verdeeld worden.

import math
xs = [1, 2, 3, 4]
waarden = sorted(set(xs))
grenzen = []
for i in range(len(waarden) - 1):
    a, b = waarden[i], waarden[i+1]
    t = a / 2 + b / 2
    if t <= a:
        t = b
    grenzen.append(t)
print(grenzen)  # [1.5, 2.5, 3.5]
scores = [1/3, 0, 1/3]
paren = list(zip(scores, grenzen))
beste = paren[0]
for kandidaat in paren[1:]:
    score, t = kandidaat
    gelijk = math.isclose(
        score, beste[0],
        rel_tol=1e-12, abs_tol=1e-12)
    if score < beste[0] and not gelijk:
        beste = kandidaat
print(beste[1])  # 2.5

set verwijdert dubbele waarden; sorted sorteert de rest. zip koppelt scores en grenzen. Maak beste_grens(xs,ys) met werkelijk berekende scores en geef None terug wanneer alle hoogtes gelijk zijn.

We behandelen scores als gelijk als math.isclose met relatieve én absolute tolerantie 1e−12 dat aangeeft. Zo kan een minuscuul afrondingsverschil de tie-regel niet omdraaien: de grenzen staan oplopend, dus bewaar bij gelijke scores de eerste. Alleen een duidelijk lagere score vervangt de beste kandidaat.

De invoer moet niet-leeg en even lang zijn; gebruik eindige x-waarden. Bij bijna gelijke floats kan het midden naar a afronden: t = b scheidt de twee waarden dan alsnog met dezelfde x-kleiner-dan-t-regel. Deze gretige keuze optimaliseert alleen de volgende vraag, niet alle mogelijke volledige bomen tegelijk.

03
CHECKPOINTZoek alleen betekenisvolle grenzen

4. Herhaal dezelfde vraag binnen elk kind

Een boom kun je bewaren als een dictionary met t, left en right; een blad bevat alleen label. Voor ons voorbeeld is één vraag genoeg. Bij een nieuwe hoogte 2.5 volg je rechts, want de regel links is strikt kleiner.

boom = {"t": 2.5,
        "left": {"label": 0},
        "right": {"label": 1}}

def boom_voorspel(boom, x):
    if "label" in boom:
        return boom["label"]
    if x < boom["t"]:
        kind = boom["left"]
    else:
        kind = boom["right"]
    return boom_voorspel(kind, x)

print(boom_voorspel(boom, 2.5))  # 1

Een functie die zichzelf aanroept gebruikt recursie. Iedere aanroep krijgt een kleiner onderdeel; het blad beëindigt het voorspellen. Bij het bouwen krijgen beide kinderen één diepteniveau minder.

Met labels [0,1,1,0] zijn twee vragen nodig. Hoogte 4 gaat bij 1.5 naar rechts, daarna bij 3.5 opnieuw naar rechts en eindigt bij label 0:

boom2 = {"t": 1.5,
         "left": {"label": 0},
         "right": {"t": 3.5,
                   "left": {"label": 1},
                   "right": {"label": 0}}}
print(boom_voorspel(boom2, 4))  # 0

Maak bouw_boom(xs,ys,diepte): tel labels, kies hun meerderheid en stop bij diepte nul, één klasse of geen bruikbare grens. Anders verdeel je de x- en y-lijsten en bouw je ieder kind opnieuw. Bij gelijke aantallen wint het kleinste label; diepte moet een niet-negatief geheel getal zijn en ongeldige paren geven ValueError.

Een diepe boom kan ruis onthouden. Kies zijn diepte op validatie, ook als hij alle trainingslabels goed voorspelt.

04
CHECKPOINTHerhaal tot een blad of dieptelimiet

Wat neem je mee?

  • Gini meet menging; een splitscore weegt beide groepen naar hun grootte.
  • Middelpunten geven zinvolle kandidaten en een vaste tie-regel maakt de keuze reproduceerbaar.
  • Recursie herhaalt dezelfde bouwstap; stopregels begrenzen de boom.

Hierna laat je verschillende bomen samenwerken om minder gevoelig te worden voor toevallige trainingsrijen.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPLaat verschillende bomen samen beslissen
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 / 4
01
Meet hoe gemengd een groep is

Schrijf gini(labels) voor integerlabels.

Een hint

Tel de klassen en kwadrateer hun fracties.

02
Weeg beide kinderen naar hun grootte

Schrijf split_gini(xs,ys,t), met ValueError voor lege/ongelijke invoer of een leeg kind.

Een hint

Maak twee labellijsten en neem hun gewogen gemiddelde onzuiverheid.

03
Zoek alleen betekenisvolle grenzen

Schrijf beste_grens(xs,ys) voor niet-lege gelijke paren: de beste grens of None bij gelijke x. Gebruik eindige x-waarden.

Een hint

Sorteer unieke x-waarden en minimaliseer (split_gini,grens) over middelpunten.

04
Herhaal tot een blad of dieptelimiet

Maak bouw_boom(xs,ys,diepte) en boom_voorspel(boom,x). Diepte is een niet-negatief geheel getal; ongeldige paren of diepte geven ValueError.

Een hint

Verdeel indices met dezelfde x<t-regel; de voorspeller loopt recursief door tot een label.

Vastgelopen? Bekijk de voorbeeldoplossing
def gini(labels):
    if not labels:
        return 0.0
    telling = {}
    for label in labels:
        telling[label] = telling.get(label, 0) + 1
    kwadraten = 0
    for n in telling.values():
        kwadraten = kwadraten + n * n
    return 1 - kwadraten / len(labels) ** 2


def controleer_paren(xs, ys):
    if not xs or len(xs) != len(ys):
        raise ValueError("Niet-lege gelijke paren vereist")


def split_gini(xs, ys, t):
    controleer_paren(xs, ys)
    links, rechts = [], []
    for i in range(len(xs)):
        if xs[i] < t:
            links.append(ys[i])
        else:
            rechts.append(ys[i])
    if not links or not rechts:
        raise ValueError("Leeg kind")
    totaal = len(links) * gini(links) + len(rechts) * gini(rechts)
    return totaal / len(xs)


def beste_grens(xs, ys):
    import math
    controleer_paren(xs, ys)
    waarden = sorted(set(xs))
    kandidaten = []
    for i in range(len(waarden) - 1):
        a, b = waarden[i], waarden[i + 1]
        t = a / 2 + b / 2
        if t <= a:
            t = b
        kandidaten.append((split_gini(xs, ys, t), t))
    if not kandidaten:
        return None
    beste = kandidaten[0]
    for kandidaat in kandidaten[1:]:
        score, t = kandidaat
        gelijk = math.isclose(score, beste[0], rel_tol=1e-12, abs_tol=1e-12)
        if score < beste[0] and not gelijk:
            beste = kandidaat
    return beste[1]


def bouw_boom(xs, ys, diepte):
    controleer_paren(xs, ys)
    if type(diepte) is not int or diepte < 0:
        raise ValueError("Niet-negatieve gehele diepte vereist")
    telling = {}
    for label in ys:
        telling[label] = telling.get(label, 0) + 1
    labels = sorted(telling)
    winnaar = labels[0]
    for label in labels:
        if telling[label] > telling[winnaar]:
            winnaar = label
    if diepte == 0 or len(labels) == 1:
        return {"label": winnaar}
    t = beste_grens(xs, ys)
    if t is None:
        return {"label": winnaar}
    lx, ly, rx, ry = [], [], [], []
    for i in range(len(xs)):
        if xs[i] < t:
            lx.append(xs[i])
            ly.append(ys[i])
        else:
            rx.append(xs[i])
            ry.append(ys[i])
    return {"t": t,
            "left": bouw_boom(lx, ly, diepte - 1),
            "right": bouw_boom(rx, ry, diepte - 1)}


def boom_voorspel(boom, x):
    if "label" in boom:
        return boom["label"]
    if x < boom["t"]:
        kind = boom["left"]
    else:
        kind = boom["right"]
    return boom_voorspel(kind, x)

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