Skip to content

Decision Trees - Entropy and Gini Impurity

What you’ll learn

  • how a tree turns a dataset into a list of if-statements you can read aloud
  • Gini impurity and entropy — the formulas, and the real difference between them
  • both computed by hand on the actual iris tree, matching scikit-learn to six decimals
  • how CART searches for a split, and why the result is only locally optimal
  • why an unpruned tree always reaches 100% training accuracy, and every control that prevents it
  • why trees are unstable, and what that instability sets up for Phase 5

Intuition

A decision tree asks a sequence of yes/no questions about one feature at a time, and each answer narrows the possibilities until only one class remains plausible. It is the only model in this phase whose reasoning you can print and hand to a domain expert:

text
if petal width <= 0.80:
    setosa
else:
    if petal width <= 1.75:
        versicolor
    else:
        virginica
text
if petal width <= 0.80:
    setosa
else:
    if petal width <= 1.75:
        versicolor
    else:
        virginica

Two questions, three answers, 96% accurate on the full iris dataset. No scaling, no distance metric, no kernel, no probability calibration — just thresholds.

The whole design problem is: which question, in which order? That is what impurity measures answer.

diagram Diagram mermaid

The math

Measuring impurity

A node is pure if every sample in it shares a label. Impurity measures how far from that a node is. With pcp_c the proportion of class cc in the node:

G=1c=1Kpc2H=c=1Kpclog2pcG = 1 - \sum_{c=1}^{K} p_c^2 \qquad\qquad H = -\sum_{c=1}^{K} p_c \log_2 p_c

Both are zero for a pure node and maximal for a uniform one. Their maxima differ: for KK classes, Gini tops out at 11/K1 - 1/K while entropy tops out at log2K\log_2 K. With three classes that is 0.667 against 1.585.

figureThree impurity measures over a binary nodematplotlib
Four curves plotted against the proportion of class 1 in a binary node: entropy peaking at 1.0, entropy halved, Gini peaking at 0.5, and misclassification error forming a sharp triangle.Four curves plotted against the proportion of class 1 in a binary node: entropy peaking at 1.0, entropy halved, Gini peaking at 0.5, and misclassification error forming a sharp triangle.
Rescale entropy by half and it nearly overlays Gini — which is why the choice rarely changes the tree. Misclassification error has a corner at 0.5 and a flat gradient, so it is a poor splitting criterion.

Reading the plot

  • Gini and entropy agree almost everywhere. After rescaling, the two curves are close enough that they select the same split the overwhelming majority of the time. Gini is marginally faster because it avoids a logarithm; that is the entire practical difference.
  • Entropy is slightly more aggressive at the extremes, so it can prefer a split that isolates a small pure group where Gini would not.
  • Misclassification error is flat over wide ranges. Two candidate splits often score identically, so it gives the search nothing to descend. It is used for pruning, not for growing.

Choosing a split

CART evaluates every (feature, threshold) pair and picks the one minimising the weighted impurity of the two children:

J(k,tk)=mleftmGleft+mrightmGrightJ(k, t_k) = \frac{m_{\text{left}}}{m}\,G_{\text{left}} + \frac{m_{\text{right}}}{m}\,G_{\text{right}}

The information gain is the parent’s impurity minus that weighted average. Split, then recurse on each child, stopping when a node is pure or a limit is reached.

Two properties follow directly:

  • It is greedy. Each split is chosen to be locally best; nothing looks two moves ahead. Finding the globally optimal tree is NP-complete, so every practical tree is a good local solution.
  • It is invariant to monotone transformations. Only the ordering of a feature’s values matters, never the spacing. Log-transform a feature, or standardise it, and the tree is unchanged. This is why trees never need scaling.

Worked example by hand

The iris tree above, computed from scratch.

Step 1 — root impurity. 150 samples, 50 of each class:

Groot=1(50150)2(50150)2(50150)2=13(19)=230.6667G_{\text{root}} = 1 - \left(\tfrac{50}{150}\right)^2 - \left(\tfrac{50}{150}\right)^2 - \left(\tfrac{50}{150}\right)^2 = 1 - 3\left(\tfrac{1}{9}\right) = \tfrac{2}{3} \approx 0.6667

Step 2 — evaluate the split at petal width ≤ 0.80. The left child gets 50 setosa and nothing else; the right child gets 50 versicolor and 50 virginica:

Gleft=112=0,Gright=10.520.52=0.5G_{\text{left}} = 1 - 1^2 = 0, \qquad G_{\text{right}} = 1 - 0.5^2 - 0.5^2 = 0.5
J=50150(0)+100150(0.5)=0.3333J = \frac{50}{150}(0) + \frac{100}{150}(0.5) = 0.3333
gain=0.66670.3333=0.3333\text{gain} = 0.6667 - 0.3333 = 0.3333

No other threshold on either feature beats that, so CART takes it.

Step 3 — split the right child at petal width ≤ 1.75. The 100 remaining samples divide into 54 (49 versicolor, 5 virginica) and 46 (1 versicolor, 45 virginica):

Gleft=1(4954)2(554)2=10.82340.0086=0.1680G_{\text{left}} = 1 - \left(\tfrac{49}{54}\right)^2 - \left(\tfrac{5}{54}\right)^2 = 1 - 0.8234 - 0.0086 = 0.1680
Gright=1(146)2(4546)2=10.00050.9570=0.0425G_{\text{right}} = 1 - \left(\tfrac{1}{46}\right)^2 - \left(\tfrac{45}{46}\right)^2 = 1 - 0.0005 - 0.9570 = 0.0425
J=54100(0.1680)+46100(0.0425)=0.0907+0.0196=0.1103J = \frac{54}{100}(0.1680) + \frac{46}{100}(0.0425) = 0.0907 + 0.0196 = 0.1103
gain=0.50.1103=0.3897\text{gain} = 0.5 - 0.1103 = 0.3897

Every one of these numbers is available from the fitted estimator as tree_.impuritytree_.impurity, and they agree to six decimal places.

The same tree by entropy. Hroot=log23=1.5850H_{\text{root}} = \log_2 3 = 1.5850, the right child is H=1.0000H = 1.0000, and the two leaves are 0.4451 and 0.1511. Different scale, identical thresholds: 0.80 and 1.75.

figureThe same tree, drawn in feature spacematplotlib
Scatter of iris petal length against petal width with three regions separated by two horizontal lines at petal width 0.8 and 1.75.Scatter of iris petal length against petal width with three regions separated by two horizontal lines at petal width 0.8 and 1.75.
Two splits, three rectangles. Both cuts are horizontal because both use petal width — the tree found petal length unnecessary.

See it move

sketch Splitting a node to reduce impurity p5.js
A node of mixed points is split by a threshold; the bars show the impurity of each child and the weighted average the split achieves. The best threshold is the one that minimises the bar on the right.

From scratch

tree_splits.py
import numpy as np
 
 
def gini(labels):
    if len(labels) == 0:
        return 0.0
    _, counts = np.unique(labels, return_counts=True)
    p = counts / counts.sum()
    return float(1 - (p**2).sum())
 
 
def entropy(labels):
    if len(labels) == 0:
        return 0.0
    _, counts = np.unique(labels, return_counts=True)
    p = counts / counts.sum()
    return float(-(p * np.log2(p)).sum())
 
 
def best_split(X, y, impurity=gini):
    """Exhaustive search over every feature and every midpoint threshold."""
    best = (None, None, impurity(y))
    m = len(y)
    for feature in range(X.shape[1]):
        values = np.unique(X[:, feature])
        thresholds = (values[:-1] + values[1:]) / 2      # midpoints
        for t in thresholds:
            mask = X[:, feature] <= t
            if mask.sum() == 0 or (~mask).sum() == 0:
                continue
            weighted = (mask.sum() * impurity(y[mask])
                        + (~mask).sum() * impurity(y[~mask])) / m
            if weighted < best[2]:
                best = (feature, float(t), weighted)
    return best
 
 
from sklearn.datasets import load_iris
 
iris = load_iris()
X = iris.data[:, (2, 3)]          # petal length, petal width
y = iris.target
 
print(f"root gini = {gini(y):.6f}")                  # root gini = 0.666667
feature, threshold, weighted = best_split(X, y)
print(f"split on feature {feature} at {threshold:.2f}")   # feature 1 at 0.80
print(f"weighted impurity = {weighted:.6f}")              # 0.333333
print(f"information gain  = {gini(y) - weighted:.6f}")    # 0.333333
tree_splits.py
import numpy as np
 
 
def gini(labels):
    if len(labels) == 0:
        return 0.0
    _, counts = np.unique(labels, return_counts=True)
    p = counts / counts.sum()
    return float(1 - (p**2).sum())
 
 
def entropy(labels):
    if len(labels) == 0:
        return 0.0
    _, counts = np.unique(labels, return_counts=True)
    p = counts / counts.sum()
    return float(-(p * np.log2(p)).sum())
 
 
def best_split(X, y, impurity=gini):
    """Exhaustive search over every feature and every midpoint threshold."""
    best = (None, None, impurity(y))
    m = len(y)
    for feature in range(X.shape[1]):
        values = np.unique(X[:, feature])
        thresholds = (values[:-1] + values[1:]) / 2      # midpoints
        for t in thresholds:
            mask = X[:, feature] <= t
            if mask.sum() == 0 or (~mask).sum() == 0:
                continue
            weighted = (mask.sum() * impurity(y[mask])
                        + (~mask).sum() * impurity(y[~mask])) / m
            if weighted < best[2]:
                best = (feature, float(t), weighted)
    return best
 
 
from sklearn.datasets import load_iris
 
iris = load_iris()
X = iris.data[:, (2, 3)]          # petal length, petal width
y = iris.target
 
print(f"root gini = {gini(y):.6f}")                  # root gini = 0.666667
feature, threshold, weighted = best_split(X, y)
print(f"split on feature {feature} at {threshold:.2f}")   # feature 1 at 0.80
print(f"weighted impurity = {weighted:.6f}")              # 0.333333
print(f"information gain  = {gini(y) - weighted:.6f}")    # 0.333333

Forty lines reproduce scikit-learn’s first split exactly, threshold included.

Trees overfit, aggressively

figuremax_depth = 1, 4 and unlimitedmatplotlib
Three panels of the same noisy two-moons data with tree boundaries at max_depth 1, 4 and unlimited. The unlimited tree carves dozens of thin rectangles around individual points.Three panels of the same noisy two-moons data with tree boundaries at max_depth 1, 4 and unlimited. The unlimited tree carves dozens of thin rectangles around individual points.
An unlimited tree keeps splitting until every leaf is pure, which on noisy data means one leaf per noisy point. Training accuracy 1.00, and a boundary nobody would trust.
figureWhere learning stops and memorising startsmatplotlib
Training and cross-validated accuracy plotted against max_depth. Training accuracy rises to 1.0 and stays; CV accuracy peaks early then declines.Training and cross-validated accuracy plotted against max_depth. Training accuracy rises to 1.0 and stays; CV accuracy peaks early then declines.
Training accuracy reaches 1.0 and never moves again. Cross-validated accuracy peaks at a shallow depth and then falls — every split past that point is fitting noise.

An unpruned tree will always reach 100% training accuracy on data with no duplicated rows, because it can keep splitting until each leaf holds one sample. That is not a bug in the implementation; it is what “grow until pure” means. The controls that prevent it:

ParameterEffectTypical value
max_depthmax_depthHard cap on tree height3–10
min_samples_splitmin_samples_splitRefuse to split a node below this size10–50
min_samples_leafmin_samples_leafEvery leaf must hold at least this many5–20
max_leaf_nodesmax_leaf_nodesCap total leaves; grows best-first instead of depth-first10–50
min_impurity_decreasemin_impurity_decreaseRefuse a split that gains less than this0.0–0.01
ccp_alphaccp_alphaCost-complexity pruning after growingTune via cost_complexity_pruning_pathcost_complexity_pruning_path

min_samples_leafmin_samples_leaf is the most reliable single knob: it directly forbids the “one leaf per noisy point” failure, whatever the depth.

pruning.py
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
 
iris = load_iris()
X, y = iris.data[:, (2, 3)], iris.target
 
for depth in (1, 2, 3, 5, None):
    model = DecisionTreeClassifier(max_depth=depth, random_state=0)
    train = model.fit(X, y).score(X, y)
    cv = cross_val_score(model, X, y, cv=5).mean()
    print(f"depth={str(depth):<5} leaves {model.get_n_leaves():2d}"
          f"  train {train:.4f}  cv {cv:.4f}")
 
# depth=1     leaves  2  train 0.6667  cv 0.6667
# depth=2     leaves  3  train 0.9600  cv 0.9600
# depth=3     leaves  5  train 0.9733  cv 0.9600
# depth=5     leaves  8  train 0.9933  cv 0.9533
# depth=None  leaves  8  train 0.9933  cv 0.9467
pruning.py
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
 
iris = load_iris()
X, y = iris.data[:, (2, 3)], iris.target
 
for depth in (1, 2, 3, 5, None):
    model = DecisionTreeClassifier(max_depth=depth, random_state=0)
    train = model.fit(X, y).score(X, y)
    cv = cross_val_score(model, X, y, cv=5).mean()
    print(f"depth={str(depth):<5} leaves {model.get_n_leaves():2d}"
          f"  train {train:.4f}  cv {cv:.4f}")
 
# depth=1     leaves  2  train 0.6667  cv 0.6667
# depth=2     leaves  3  train 0.9600  cv 0.9600
# depth=3     leaves  5  train 0.9733  cv 0.9600
# depth=5     leaves  8  train 0.9933  cv 0.9533
# depth=None  leaves  8  train 0.9933  cv 0.9467

Depth 2 already achieves the best cross-validated score. Everything past it buys training accuracy and loses generalisation — three extra splits to gain 0.033 on training data and lose 0.013 on held-out data.

Instability

Trees are high variance: resample the training data slightly and you can get a completely different tree. The cause is the greedy search — if two candidate splits score almost equally at the root, a handful of different rows flips which one wins, and every subsequent split changes.

This is a genuine weakness, and it is also the opening for Phase 5. If a model’s errors change substantially with the training sample, averaging many of them cancels the variance out. That single observation is what a random forest is.

algorithmDecision Tree (CART)Supervised · Classification and Regression

APIsklearn.tree.DecisionTreeClassifier

Assumes

  • The target can be approximated by axis-aligned rectangular regions
  • Feature ordering is meaningful; spacing need not be
  • Enough samples per leaf for the leaf proportions to mean something

Cost

train
O(n · m log m)
predict
O(depth)
memory
O(nodes)

m = samples, n = features; prediction is logarithmic in a balanced tree

Hyperparameters that matter

  • max_depthdefault NoneNone means grow until pure, which guarantees overfitting. Set it.
  • min_samples_leafdefault 1The most effective single guard against memorising individual points. Try 5 to 20.
  • criteriondefault gini'entropy' is marginally more aggressive at the extremes and slightly slower. Rarely changes the result.
  • ccp_alphadefault 0.0Cost-complexity pruning after growing. Use cost_complexity_pruning_path to get candidate values.
  • class_weightdefault None'balanced' reweights impurity by inverse class frequency on imbalanced data.

Reach for it when

  • You need a model a non-technical stakeholder can read
  • Features are a mix of scales and types and you do not want to preprocess
  • You want a base learner for a forest or a boosted ensemble
  • Non-linear interactions matter and you want them found automatically

Look elsewhere when

  • You need a smooth or diagonal boundary — a tree approximates one with a staircase
  • The dataset is small and stability matters
  • You need to extrapolate beyond the training range; a tree predicts a constant outside it
  • A single model's accuracy is the priority — use an ensemble

Pitfalls

Compare

ModelNeeds scalingHandles categoricalsInterpretableBoundaryStability
Decision TreeNoNatively, once encodedVery, when shallowAxis-aligned stepsLow
Random ForestNoYesPoorMany steps averagedHigh
Logistic RegressionYesNeeds one-hotHighLinearHigh
SVM (RBF)CriticallyNeeds one-hotLowSmoothModerate
KNNCriticallyNeeds encodingLowLocalModerate
quizCheck yourself
  1. Why do decision trees not require feature scaling?

    Show answer

    B — Because a split depends only on the ordering of a feature's values, not on their spacing — A threshold test asks whether x is above or below a value. Any monotone transformation — log, standardisation, squaring positives — preserves the ordering and therefore the tree.

  2. A node contains 54 samples: 49 of class 1 and 5 of class 2. What is its Gini impurity?

    Show answer

    B — 0.168 — 1 - (49/54)² - (5/54)² = 1 - 0.8234 - 0.0086 = 0.1680. Mostly pure, so the impurity is low but not zero.

  3. Why does an unpruned decision tree reach 100% training accuracy?

    Show answer

    B — Because it keeps splitting until every leaf is pure, which it can always do if no two rows are identical with different labels — 'Grow until pure' means exactly that. With enough splits every leaf can hold a single training point, so the training score is guaranteed and meaningless.

  4. Two decision trees fitted on slightly different samples of the same data look completely different. What does that tell you, and what does it set up?

    Show answer

    B — Trees are high variance because the greedy search amplifies small differences — which is exactly why averaging many of them works — A near-tie at the root flips under resampling and changes every subsequent split. High variance plus low bias is the ideal profile for bagging, which is what a random forest exploits.

🧪 Try It Yourself

Exercise 1 – Compute Gini impurity

Exercise 2 – Compute the information gain

Exercise 3 – Read the tree’s own numbers

Exercise 4 – Print the rules

Exercise 5 – Find where depth stops helping

Recap

  • A tree is a sequence of single-feature threshold tests, readable as plain if-statements.
  • Gini is 1pc21 - \sum p_c^2, entropy is pclog2pc-\sum p_c \log_2 p_c; after rescaling they nearly coincide, and they almost always choose the same split.
  • Hand-worked on iris: root Gini 0.6667, first split gains 0.3333, second gains 0.3897 — matching tree_.impuritytree_.impurity exactly.
  • CART is greedy and only locally optimal; the globally best tree is NP-complete to find.
  • Splits depend only on value ordering, so trees never need scaling.
  • Unpruned trees always reach 100% training accuracy. On iris, depth 2 is the best cross-validated choice and depth 5 is already worse.
  • Trees are unstable, and that instability is precisely what ensembles exploit.

Exercise 6 – Which hyperparameter actually matters?

Next

Continue to Naive Bayes Classifier — a probabilistic classifier that trains in one pass over the data and, despite an assumption that is essentially always false, remains hard to beat on text.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did