AI Knowledge Tree

Impurity criteria and splits

Choose splits with Gini, entropy, or MSE; understand information gain and overfitting on noisy data.

⏱ 26 minPrerequisites:Decision treesShannon entropy

A decision tree's quality depends on its splits, and a split's quality depends on how pure the children are. Three classic impurity measures cover most cases: Gini and entropy for classification, MSE for regression[1]. Each is the local objective that motivates the greedy training of the previous lesson[2].

Gini impurity

For a node with class proportions p1,…,pKp_1, \dots, p_K,

Gini=1−∑k=1Kpk2.\text{Gini} = 1 - \sum_{k=1}^K p_k^2.

It is the probability of misclassification under a stochastic decision rule that predicts class kk with probability pkp_k. Gini is 0 when one class dominates (pk=1p_k = 1 for one kk) and maximized at the uniform distribution (Gini=1−1/K\text{Gini} = 1 - 1/K).

Entropy and information gain

For the same proportions,

H=−∑k=1Kpklog⁡2pk.H = -\sum_{k=1}^K p_k \log_2 p_k.

This is the Shannon entropy in bits. It equals 0 for pure nodes and log⁡2K\log_2 K at the uniform distribution. The information gain of a split is the expected reduction in entropy:

IG=H(parent)−∣SL∣∣S∣H(SL)−∣SR∣∣S∣H(SR).\text{IG} = H(\text{parent}) - \frac{|S_L|}{|S|} H(S_L) - \frac{|S_R|}{|S|} H(S_R).

Trees built with information gain are called ID3 / C4.5 trees in the classical literature.

Gini and entropy track each other closely; the binary case Gini=2p(1−p)\text{Gini} = 2p(1 - p) and H=−plog⁡2p−(1−p)log⁡2(1−p)H = -p\log_2 p - (1-p)\log_2(1-p) both peak at p=0.5p = 0.5. They differ in computational cost (Gini is cheaper, no logarithm) and in marginal sensitivity to small probabilities (entropy is more sensitive). In practice the trees they produce are nearly indistinguishable.

MSE for regression

For a node with target values y1,…,yny_1, \dots, y_n, the mean squared error impurity is the variance:

MSE=1n∑i=1n(yi−yˉ)2.\text{MSE} = \frac{1}{n}\sum_{i=1}^n (y_i - \bar y)^2.

The optimal leaf prediction is the mean yˉ\bar y (which is exactly OLS for a constant predictor). A split reduces MSE by partitioning the data into two groups whose individual variances sum to less than the parent variance.

Mean absolute error (MAE) can replace MSE for more robust regression trees, but the closed-form best leaf becomes the median rather than the mean.

Why information-gain-style criteria favor balanced splits

A split is more useful when its two children are both reasonably large. A split that isolates a single point gives high purity in that child but very small ∣SL∣|S_L| in the weighted sum, so the gain is dominated by the other child. Gain criteria automatically discount tiny-child splits, mitigating one common failure mode.

Overfitting and noise

Without limits, recursive splitting drives every leaf to perfect purity by isolating noise. Even a single mislabeled point can drive several extra splits. The remedies are the subject of the next lesson:

  • Max depth (cap recursion).
  • Min samples per leaf (refuse small leaves).
  • Min impurity decrease (refuse splits that barely help).
  • Cost-complexity pruning (remove subtrees that don't justify their size).

The right amount of regularization is found by cross-validation.

Node-impurity measures (Gini, entropy, misclassification) all peak at a 50/50 class mix and vanish for a pure node.
Node-impurity measures (Gini, entropy, misclassification) all peak at a 50/50 class mix and vanish for a pure node.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

def gini(labels):
    p = np.bincount(labels) / len(labels)
    return 1 - np.sum(p**2)

def entropy(labels):
    p = np.bincount(labels) / len(labels)
    nz = p[p > 0]
    return -np.sum(nz * np.log2(nz))

# Both impurities peak at the uniform distribution and vanish at purity
for p in [0.0, 0.1, 0.5, 0.9, 1.0]:
    n = 1000
    y = (rng.random(n) < p).astype(int)
    print(f"p={p}: gini={gini(y):.3f}, entropy={entropy(y):.3f} bits")

# Information gain of a useful split vs a useless one
y_parent = np.concatenate([np.zeros(100), np.ones(100)]).astype(int)
y_left_good = np.zeros(100, dtype=int)
y_right_good = np.ones(100, dtype=int)
y_left_bad = np.concatenate([np.zeros(50), np.ones(50)]).astype(int)
y_right_bad = np.concatenate([np.zeros(50), np.ones(50)]).astype(int)

ig_good = entropy(y_parent) - 0.5 * entropy(y_left_good) - 0.5 * entropy(y_right_good)
ig_bad = entropy(y_parent) - 0.5 * entropy(y_left_bad) - 0.5 * entropy(y_right_bad)
print(f"\nperfect split IG = {ig_good:.3f} bits  (= H(parent), one full bit recovered)")
print(f"useless split IG = {ig_bad:.3f} bits  (no information)")

# MSE impurity for regression
y_reg_parent = rng.normal(size=200)
y_reg_left = y_reg_parent[y_reg_parent < 0]
y_reg_right = y_reg_parent[y_reg_parent >= 0]
parent_var = np.var(y_reg_parent)
weighted_child = (len(y_reg_left) * np.var(y_reg_left) + len(y_reg_right) * np.var(y_reg_right)) / len(y_reg_parent)
print(f"\nregression MSE parent: {parent_var:.3f}, after split: {weighted_child:.3f}, reduction: {parent_var - weighted_child:.3f}")

The two classification criteria behave as advertised; the perfect split recovers a full bit of information while the uninformative one yields zero; and the regression split halves the variance on a symmetric distribution.

Exercises

Exercise 1. A node holds 80 examples: 60 of class A and 20 of class B. A split sends 40 examples (30 A, 10 B) left and 40 examples (30 A, 10 B) right. Compute the parent Gini, both child Ginis, and the Gini gain. Explain the result.

Solution

Step 1. Parent proportions pA=60/80=0.75p_A = 60/80 = 0.75, pB=0.25p_B = 0.25:

Gini(S)=1−(0.752+0.252)=1−(0.5625+0.0625)=0.375.\text{Gini}(S) = 1 - (0.75^2 + 0.25^2) = 1 - (0.5625 + 0.0625) = 0.375.

Step 2. Each child has the same 30:10 mix, so pA=0.75p_A = 0.75, pB=0.25p_B = 0.25 in both, giving Gini(SL)=Gini(SR)=0.375\text{Gini}(S_L) = \text{Gini}(S_R) = 0.375 identical to the parent.

Step 3. The weighted reduction is

Δ=0.375−4080⋅0.375−4080⋅0.375=0.375−0.375=0.\Delta = 0.375 - \tfrac{40}{80}\cdot 0.375 - \tfrac{40}{80}\cdot 0.375 = 0.375 - 0.375 = 0.

The split preserves the class proportions in both children, so it carries no information and the gain is zero. A useful split must change the mix.

Exercise 2. Show that for a binary node with class proportions pp and 1−p1 - p, the Gini impurity equals 2p(1−p)2p(1 - p). Then verify it peaks at p=0.5p = 0.5 by setting the derivative to zero.

Solution

Step 1. Substitute the two proportions into the Gini definition:

Gini=1−(p2+(1−p)2).\text{Gini} = 1 - \left(p^2 + (1-p)^2\right).

Step 2. Expand (1−p)2=1−2p+p2(1-p)^2 = 1 - 2p + p^2 and collect terms:

1−(p2+1−2p+p2)=1−1+2p−2p2=2p−2p2=2p(1−p).1 - \left(p^2 + 1 - 2p + p^2\right) = 1 - 1 + 2p - 2p^2 = 2p - 2p^2 = 2p(1-p).

Step 3. Differentiate g(p)=2p−2p2g(p) = 2p - 2p^2: g′(p)=2−4pg'(p) = 2 - 4p. Setting g′(p)=0g'(p) = 0 gives p=0.5p = 0.5, and g′′(p)=−4<0g''(p) = -4 < 0 confirms a maximum. At p=0.5p = 0.5, Gini=2(0.5)(0.5)=0.5=1−1/K\text{Gini} = 2(0.5)(0.5) = 0.5 = 1 - 1/K for K=2K = 2, matching the uniform-distribution bound.

Exercise 3 (code). The lesson claims a split that isolates a single point gives high child purity but little gain, because its weight in the sum is tiny. Verify this: on a 200-point node with a 50/50 class mix, compare the Gini gain of a balanced perfect split against a split that peels off one point.

Solution

Step 1. Build a node with a feature that perfectly separates the two classes (a balanced split) and a second scenario where the threshold isolates one example. Compute the weighted Gini gain for each.

python · numpy
import numpy as np

def gini(labels):
    if len(labels) == 0: return 0.0
    p = np.bincount(labels, minlength=2) / len(labels)
    return 1 - np.sum(p**2)

def gain(y, mask):
    n = len(y)
    return gini(y) - (mask.sum()/n)*gini(y[mask]) - ((~mask).sum()/n)*gini(y[~mask])

y = np.concatenate([np.zeros(100), np.ones(100)]).astype(int)

balanced = np.arange(200) < 100              # left = all class 0, right = all class 1
peel_one = np.arange(200) < 1                # left = one class-0 point only

g_balanced = gain(y, balanced)
g_peel = gain(y, peel_one)

assert np.isclose(g_balanced, 0.5)           # perfect split recovers full Gini 0.5
assert g_peel < 0.01                         # isolating one point barely helps
print(f"balanced perfect split gain: {g_balanced:.4f}")
print(f"peel-one-point gain:         {g_peel:.4f}")

Step 2. The balanced split recovers the full parent Gini of 0.5, while peeling one point yields a gain near 0.0025. The pure singleton child has Gini 0, but its weight 1/2001/200 in the sum makes its contribution negligible, so the weighted criterion discounts tiny-child splits as the lesson states.

The next lesson controls the resulting tree size so it generalizes instead of memorizing.


References

  1. [1]Breiman, L., Friedman, J., Olshen, R., & Stone, C. (1984). Classification and Regression Trees. Wadsworth. Gini impurity for classification and squared-error impurity for regression.↩
  2. [2]Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer. Impurity measures and information gain (doi:10.1007/978-0-387-84858-7).↩