Decision Trees
Entropy and Information Gain
Entropy = −Σpₖ·log₂(pₖ) measures node impurity in bits. Information gain =
entropy(parent) − weighted average of child entropies for a proposed split.
Three inline counting loops compute each entropy. Library: NumPy helper
ent() to confirm. RESULT: information gain (rounded).
By hand
parent=['A','A','B','B'] (n=4, balanced): H=1.0 bit. left=['A','A','B'] (n=3, 2A 1B): H=0.9183. right=['B'] (n=1, pure): H=0. IG = 1.0 − (3/4)·0.9183 − (1/4)·0 = 0.3113 bits.
naive.py
Replay: real traced execution (multi-file project)
import math
parent = ['A', 'A', 'B', 'B']
left = ['A', 'A', 'B']
right = ['B']
n_p = len(parent)
cnt_p = {}
for lbl in parent:
cnt_p[lbl] = cnt_p.get(lbl, 0) + 1
h_p = 0.0
for lbl in cnt_p:
p = cnt_p[lbl] / n_p
h_p = h_p - p * math.log2(p)
n_l = len(left)
cnt_l = {}
for lbl in left:
cnt_l[lbl] = cnt_l.get(lbl, 0) + 1
h_l = 0.0
for lbl in cnt_l:
p = cnt_l[lbl] / n_l
h_l = h_l - p * math.log2(p)
n_r = len(right)
cnt_r = {}
for lbl in right:
cnt_r[lbl] = cnt_r.get(lbl, 0) + 1
h_r = 0.0
for lbl in cnt_r:
p = cnt_r[lbl] / n_r
h_r = h_r - p * math.log2(p)
ig = h_p - (n_l / n_p) * h_l - (n_r / n_p) * h_r
print('RESULT:', round(ig, 4))
import math
1import math2parent = ['A', 'A', 'B', 'B']parent ← ['A', 'A', 'B', 'B']
1import math2parent = ['A', 'A', 'B', 'B']3left = ['A', 'A', 'B']values this step['A', 'A', 'B', 'B']parentleft ← ['A', 'A', 'B']
2parent = ['A', 'A', 'B', 'B']3left = ['A', 'A', 'B']4right = ['B']values this step['A', 'A', 'B']leftright ← ['B']
3left = ['A', 'A', 'B']4right = ['B']5n_p = len(parent)values this step['B']rightn_p ← 4
4right = ['B']5n_p = len(parent)6cnt_p = {}values this step4n_pcnt_p ← {}
5n_p = len(parent)6cnt_p = {}7for lbl in parent:values this step{}cnt_plbl ← 'A', cnt_p ← {'A': 1}
pass 1 of 46cnt_p = {}7for lbl in parent:8 cnt_p[lbl] = cnt_p.get(lbl, 0) + 19h_p = 0.0values this step'A'lbl{} → {'A': 1}cnt_pAll 4 passes — pass 1 is the card above pass lblcnt_p1 'A' {} → {'A': 1} 2 — {'A': 1} → {'A': 2} 3 'A' → 'B' {'A': 2} → {'A': 2, 'B': 1} 4 — {'A': 2, 'B': 1} → {'A': 2, 'B': 2} for lbl in parent:
6cnt_p = {}7for lbl in parent:8 cnt_p[lbl] = cnt_p.get(lbl, 0) + 1h_p ← 0.0
8 cnt_p[lbl] = cnt_p.get(lbl, 0) + 19h_p = 0.010for lbl in cnt_p:values this step0.0h_plbl ← 'A', p ← 0.5, h_p ← 0.5
pass 1 of 29h_p = 0.010for lbl in cnt_p:11 p = cnt_p[lbl] / n_p12 h_p = h_p - p * math.log2(p)13n_l = len(left)values this step'B' → 'A'lbl0.5p0.0 → 0.5h_plbl ← 'B', h_p ← 1.0
pass 2 of 29h_p = 0.010for lbl in cnt_p:11 p = cnt_p[lbl] / n_p12 h_p = h_p - p * math.log2(p)13n_l = len(left)values this step'A' → 'B'lbl0.5 → 1.0h_pfor lbl in cnt_p:
9h_p = 0.010for lbl in cnt_p:11 p = cnt_p[lbl] / n_pn_l ← 3
12 h_p = h_p - p * math.log2(p)13n_l = len(left)14cnt_l = {}values this step3n_lcnt_l ← {}
13n_l = len(left)14cnt_l = {}15for lbl in left:values this step{}cnt_llbl ← 'A', cnt_l ← {'A': 1}
pass 1 of 314cnt_l = {}15for lbl in left:16 cnt_l[lbl] = cnt_l.get(lbl, 0) + 117h_l = 0.0values this step'B' → 'A'lbl{} → {'A': 1}cnt_lAll 3 passes — pass 1 is the card above pass lblcnt_l1 'B' → 'A' {} → {'A': 1} 2 — {'A': 1} → {'A': 2} 3 'A' → 'B' {'A': 2} → {'A': 2, 'B': 1} for lbl in left:
14cnt_l = {}15for lbl in left:16 cnt_l[lbl] = cnt_l.get(lbl, 0) + 1h_l ← 0.0
16 cnt_l[lbl] = cnt_l.get(lbl, 0) + 117h_l = 0.018for lbl in cnt_l:values this step0.0h_llbl ← 'A', p ← 0.6666666666666666, h_l ← 0.38997500048077083
pass 1 of 217h_l = 0.018for lbl in cnt_l:19 p = cnt_l[lbl] / n_l20 h_l = h_l - p * math.log2(p)21n_r = len(right)values this step'B' → 'A'lbl0.5 → 0.6666666666666666p0.0 → 0.38997500048077083h_llbl ← 'B', p ← 0.3333333333333333, h_l ← 0.9182958340544896
pass 2 of 217h_l = 0.018for lbl in cnt_l:19 p = cnt_l[lbl] / n_l20 h_l = h_l - p * math.log2(p)21n_r = len(right)values this step'A' → 'B'lbl0.6666666666666666 → 0.3333333333333333p0.38997500048077083 → 0.9182958340544896h_lfor lbl in cnt_l:
17h_l = 0.018for lbl in cnt_l:19 p = cnt_l[lbl] / n_ln_r ← 1
20 h_l = h_l - p * math.log2(p)21n_r = len(right)22cnt_r = {}values this step1n_rcnt_r ← {}
21n_r = len(right)22cnt_r = {}23for lbl in right:values this step{}cnt_rfor lbl in right:
22cnt_r = {}23for lbl in right:24 cnt_r[lbl] = cnt_r.get(lbl, 0) + 1cnt_r ← {'B': 1}
23for lbl in right:24 cnt_r[lbl] = cnt_r.get(lbl, 0) + 125h_r = 0.0values this step{} → {'B': 1}cnt_rfor lbl in right:
22cnt_r = {}23for lbl in right:24 cnt_r[lbl] = cnt_r.get(lbl, 0) + 1h_r ← 0.0
24 cnt_r[lbl] = cnt_r.get(lbl, 0) + 125h_r = 0.026for lbl in cnt_r:values this step0.0h_rfor lbl in cnt_r:
25h_r = 0.026for lbl in cnt_r:27 p = cnt_r[lbl] / n_rp ← 1.0
26for lbl in cnt_r:27 p = cnt_r[lbl] / n_r28 h_r = h_r - p * math.log2(p)values this step0.3333333333333333 → 1.0ph_r = h_r - p * math.log2(p)
27 p = cnt_r[lbl] / n_r28 h_r = h_r - p * math.log2(p)29ig = h_p - (n_l / n_p) * h_l - (n_r / n_p) * h_rfor lbl in cnt_r:
25h_r = 0.026for lbl in cnt_r:27 p = cnt_r[lbl] / n_rig ← 0.31127812445913283
28 h_r = h_r - p * math.log2(p)29ig = h_p - (n_l / n_p) * h_l - (n_r / n_p) * h_r30print('RESULT:', round(ig, 4))values this step0.31127812445913283igstdout ← RESULT: 0.3113
29ig = h_p - (n_l / n_p) * h_l - (n_r / n_p) * h_r30print('RESULT:', round(ig, 4))values this stepRESULT: 0.3113stdout
With NumPy
Helper ent() uses np.unique for counts and −np.sum(p·np.log2(p))
for entropy in bits. abs() guards against −0.0 on pure (single-class) nodes.
library.py
import numpy as np
from dalib.display import set_display
set_display()
def ent(labels):
n = len(labels)
_, counts = np.unique(labels, return_counts=True)
p = counts / n
return abs(float(-np.sum(p * np.log2(p))))
parent = ['A', 'A', 'B', 'B']
left = ['A', 'A', 'B']
right = ['B']
h_p = ent(parent)
h_l = ent(left)
h_r = ent(right)
ig = h_p - (len(left) / len(parent)) * h_l - (len(right) / len(parent)) * h_r
print('h_parent:', round(h_p, 4))
print('h_left:', round(h_l, 4))
print('h_right:', round(h_r, 4))
print('RESULT:', round(ig, 4))
h_parent: 1.0
h_left: 0.9183
h_right: 0.0
RESULT: 0.3113
Implementation notes
- Entropy=0 → pure node; entropy=log₂(k) → maximally mixed k equal classes. Binary maximum = 1 bit at 50/50; parent here is 50/50 → H=1.0.
- IG=0.3113 means this split removes 0.3113 bits of impurity. The right child is pure (H=0) but has only 1 sample, so its weight (1/4) is small.
- log₂(1)=0 in the pure child: the h_r loop computes −1·log₂(1)=0 correctly without a special-case branch.
- Weights (n_left/n, n_right/n) penalise unequal splits — a large impure child contributes more to the weighted average than a small pure one.
- Cross-reference:
gini-impurity(this chapter) for the log-free impurity alternative; the split-selection logic is the same, only the measure differs.