Lesson 8 of 11

Decision trees

A decision tree classifies by asking one yes or no question after another. Build one by hand on a classic dataset, watch one grow on its own, and learn when to stop it.

Beginner20 min

Builds on Overfitting and generalization

In this lesson you will

  • Describe how a decision tree classifies by asking a sequence of questions
  • Compute Gini impurity, entropy, and information gain, and use them to choose a question
  • Explain what greedy growth means and where it fails
  • Control overfitting by limiting how deep a tree grows

Think of the game Twenty Questions, or a doctor’s checklist: is there a fever? If yes, has it lasted more than three days? Each answer narrows the possibilities until one remains. A Decision treeA model that classifies (or predicts a number) by asking a sequence of simple questions about the input, following one branch per answer down to a leaf that gives the prediction.Open in glossary classifies the same way. It asks a question about the input, follows the branch for the answer, asks another question, and stops at a leaf that names a class.

Trees are popular for a reason you can see at a glance: a person can read one. You can follow exactly why it made a decision, which is rarely true of a neural network. In this lesson you will build a tree by hand, then watch an algorithm grow one, and find out why a tree that is perfect on its training data is usually a bad tree.

A famous small dataset

In 1986 the computer scientist J. Ross Quinlan published “Induction of decision trees” in the first volume of the journal Machine Learning. It described ID3, an algorithm that builds a tree from examples, and illustrated it with a small training set of fourteen weather records. Each is described by four attributes and belongs to one of two classes, P or N. Later textbooks reuse a version of this data as a question of whether to play tennis.

#OutlookTemperatureHumidityWindyClass
1sunnyhothighfalseN
2sunnyhothightrueN
3overcasthothighfalseP
4rainmildhighfalseP
5raincoolnormalfalseP
6raincoolnormaltrueN
7overcastcoolnormaltrueP
8sunnymildhighfalseN
9sunnycoolnormalfalseP
10rainmildnormalfalseP
11sunnymildnormaltrueP
12overcastmildhightrueP
13overcasthotnormalfalseP
14rainmildhightrueN

Nine records are P and five are N. Which question should come first? A good question splits the records into groups that are each as close to one class as possible.

Measuring how mixed a group is

To compare questions you need a number for how mixed a group is, called impurity. Two are common. If pkp_k is the share of the group in class kk:

  • EntropyA measure of how uncertain a probability distribution is, in bits. Zero means one outcome is certain; k bits is as uncertain as a fair choice among 2 to the power k outcomes.Taught in Inside a real language modelOpen in glossary, in bits: H=−∑kpklog⁡2pkH = -\sum_k p_k \log_2 p_k. A pure group has entropy 0; an even two-class mix has 1 bit.
  • Gini impurityA measure of how mixed the classes in a group are: the chance that two members drawn at random (with replacement) belong to different classes. 0 for a pure group, 0.5 for an even two-class mix.Open in glossary: G=1−∑kpk2G = 1 - \sum_k p_k^2, the chance that two members drawn at random (with replacement) belong to different classes. Pure is 0; an even two-class mix is 0.5.

A question splits a group into smaller groups. Its value is how much it lowers impurity, averaging the children by their size. With entropy this drop is called Information gainHow much a question lowers the entropy of the class labels, averaging the resulting groups by size. Decision tree algorithms such as ID3 ask the question with the largest gain.Open in glossary:

gain=H(parent)−∑childrennchildnparent H(child)\text{gain} = H(\text{parent}) - \sum_{\text{children}} \frac{n_{\text{child}}}{n_{\text{parent}}}\, H(\text{child})

For the weather data, the fourteen labels have entropy H(9,5)=0.940H(9, 5) = 0.940 bits. Asking about outlook gives sunny (2 P, 3 N), overcast (4 P, 0 N), and rain (3 P, 2 N). The average entropy afterwards is 514(0.971)+414(0)+514(0.971)=0.694\tfrac{5}{14}(0.971) + \tfrac{4}{14}(0) + \tfrac{5}{14}(0.971) = 0.694 bits, so the gain is about 0.2470.247 bits. Quinlan’s paper reports 0.246, because it subtracts values already rounded to three places.

Build a tree by hand

Quinlan's 14 weather records, each labeled P or N. Pick a node, then pick the question to ask there; the gain of each choice is computed for you.

  • P (play)
  • N (don't play)

Selected: all 14 records (14)

Mixed (9 P, 5 N). Gain from asking each question:

The 14 records (all)
#outlooktemperaturehumiditywindyclass
1sunnyhothighfalse N
2sunnyhothightrue N
3overcasthothighfalse P
4rainmildhighfalse P
5raincoolnormalfalse P
6raincoolnormaltrue N
7overcastcoolnormaltrue P
8sunnymildhighfalse N
9sunnycoolnormalfalse P
10rainmildnormalfalse P
11sunnymildnormaltrue P
12overcastmildhightrue P
13overcasthotnormalfalse P
14rainmildhightrue N
Impurity measure
Questions0
Leaves1
Leaves still mixed1
Records sorted correctly9 of 14

Challenges

0 of 3 complete

  1. Choose the root question with the largest gain. (not yet)
  2. Build a tree that sorts all 14 records into pure leaves using only 3 questions. (not yet)
  3. Put temperature at the root and still sort all 14 records into pure leaves. (not yet)

Try this

  • At the root, compare the four gains before choosing. Which question would you have guessed?
  • Choose outlook, then select the sunny branch: one question finishes it.
  • Switch to Gini. The numbers change, but does the ranking of questions?
  • Start over with temperature at the root and count how many questions you need.
Information gain on Quinlan's weather dataPython with NumPy
import numpy as np

# Quinlan's 14 weather records: outlook, temperature, humidity, windy, class.
rows = """sunny hot high false N
sunny hot high true N
overcast hot high false P
rain mild high false P
rain cool normal false P
rain cool normal true N
overcast cool normal true P
sunny mild high false N
sunny cool normal false P
rain mild normal false P
sunny mild normal true P
overcast mild high true P
overcast hot normal false P
rain mild high true N"""
data = np.array([r.split() for r in rows.splitlines()])
labels = data[:, 4]

def entropy(y):
    _, counts = np.unique(y, return_counts=True)
    p = counts / counts.sum()
    return -np.sum(p * np.log2(p))

def gain(column):
    # Entropy before the split minus the weighted entropy of each branch.
    after = sum(np.mean(column == v) * entropy(labels[column == v])
                for v in np.unique(column))
    return entropy(labels) - after

print(f"entropy of all 14: {entropy(labels):.3f} bits")
for i, name in enumerate(["outlook", "temperature", "humidity", "windy"]):
    print(f"gain({name}) = {gain(data[:, i]):.3f} bits")
entropy of all 14: 0.940 bits
gain(outlook) = 0.247 bits
gain(temperature) = 0.029 bits
gain(humidity) = 0.152 bits
gain(windy) = 0.048 bits

ID3 does exactly what you did: at every node that is still mixed, ask the question with the largest gain, and stop when a node is pure. On this data it finds outlook at the root, humidity under sunny, and windy under rain: three questions that sort all fourteen records. Starting with temperature, the question with the smallest gain, the best possible tree needs seven.

This strategy is greedy: each choice is the best one available right now, and it is never revisited. Searching all possible trees for the smallest one quickly becomes hopeless as data grows, and greedy choices are fast and usually good.

Questions about numbers

Real data often has numbers rather than categories. A tree handles a number by asking a threshold question, “is x≤tx \le t?”. To find the best threshold, sort the training values and try a cut halfway between each pair of neighbors, keeping the cut with the largest drop in impurity. The CART method of Leo Breiman, Jerome Friedman, Richard Olshen, and Charles Stone (Classification and Regression Trees, 1984) grows trees this way, with two-way splits and Gini impurity.

Each threshold cuts the plane with a straight vertical or horizontal line, so a tree divides the plane into boxes. Below, a tree grows on points with two features, across and up.

Grow a decision tree

Each split asks one yes or no question about a point's position. Shading shows the share of class B training points in each leaf.

  • Class A
  • Class B
  • Split

Dashed rings mark points whose label was flipped on purpose.

Tap the plane to follow one point's questions down the tree.

3
Data
Labels flipped at random
Impurity measure
Show
Splits shown7 of 7
Depth reached3
Leaves8
Training accuracy90%
Test accuracy82%

Challenges

0 of 3 complete

  1. With label noise on, grow a tree that gets every training point right. (not yet)
  2. On Circle with 10% noise, find the depth that gets the most test points right. (not yet)
  3. On XOR with no noise, find the smallest depth that gets at least 90% of test points right. (not yet)

Try this

  • Press Grow split by split and watch the order of the questions: the biggest drop in impurity comes first.
  • Raise the maximum depth to 12 with 20% noise and look at the tiny boxes around the ringed points.
  • Switch to test points at depth 12. Which boxes were wrong?
  • Tap a point on the plane to see the questions it answers on the way to its leaf.
Growing a tree by the best Gini splitPython with NumPy
import numpy as np

# Ten points on a plane (across, up) with labels 0 = class A, 1 = class B.
X = np.array([[0.1, 0.2], [0.3, 0.8], [0.2, 0.5], [0.4, 0.3], [0.35, 0.9],
              [0.7, 0.2], [0.8, 0.6], [0.6, 0.85], [0.9, 0.4], [0.55, 0.15]])
y = np.array([0, 1, 0, 0, 1, 1, 1, 1, 1, 0])

def gini(labels):
    p = np.mean(labels) if len(labels) else 0.0
    return 1 - p**2 - (1 - p)**2

def best_split(X, y):
    best = (0.0, None, None)
    for f in range(X.shape[1]):
        values = np.unique(X[:, f])
        for t in (values[:-1] + values[1:]) / 2:  # midpoints between values
            left, right = y[X[:, f] <= t], y[X[:, f] > t]
            after = (len(left) * gini(left) + len(right) * gini(right)) / len(y)
            if gini(y) - after > best[0]:
                best = (gini(y) - after, f, t)
    return best

def grow(X, y, depth, level=0, answer=""):
    pad = "    " * level + answer
    gain, f, t = best_split(X, y)
    if depth == 0 or f is None:  # out of depth, or no split lowers impurity
        print(f"{pad}leaf: {np.sum(y == 0)} A, {np.sum(y == 1)} B")
        return
    print(f"{pad}is {['across', 'up'][f]} <= {t:.3f}? (gain {gain:.3f})")
    mask = X[:, f] <= t
    grow(X[mask], y[mask], depth - 1, level + 1, "yes -> ")
    grow(X[~mask], y[~mask], depth - 1, level + 1, "no  -> ")

grow(X, y, depth=2)
is across <= 0.575? (gain 0.213)
    yes -> is up <= 0.650? (gain 0.444)
        yes -> leaf: 4 A, 0 B
        no  -> leaf: 0 A, 2 B
    no  -> leaf: 0 A, 4 B

When to stop

A tree can keep splitting until every leaf is pure. Unless two identical points carry different labels, it will then get every training point right. That sounds ideal, and it is the same trap you met in Overfitting and generalization. On the Circle data with labels flipped at random 10% of the time, the fully grown tree gets 100% of its training points right but only 83.3% of the test points. Stopped at depth 4, it gets 86.7% of the test points right: its boxes follow the circle without chasing each flipped point.

Depth is a tree’s capacity. Shallow trees are too blocky to follow the real boundary (underfitting); deep trees fit the noise (OverfittingWhen a model fits the quirks and noise of its training data so closely that it does worse on new data. Training error keeps falling while test error rises.Taught in Overfitting and generalizationOpen in glossary). Common ways to stop include a maximum depth, a minimum number of points per leaf, or growing a large tree and then pruning back the branches that do not help on held-out data, as CART does.

That last point, that trees are unstable, matters. Small changes to the training data can change an early split, and every question below it changes with it. The next lesson turns this weakness into a strength by growing many trees and letting them vote.

Key ideas

  • A decision tree classifies by asking a sequence of yes or no questions; each leaf predicts the majority class of the training points that reach it.
  • Impurity (entropy or Gini) measures how mixed a group is. A good question gives the largest drop in impurity, called information gain for entropy.
  • Trees are grown greedily: the best question now, never revisited. This is fast but can miss questions that only help in combination, as on XOR.
  • A fully grown tree memorizes its training data, noise included. Limiting depth or pruning trades a little training accuracy for better accuracy on new data.
  • Trees are easy to read but unstable: small changes to the data can produce a very different tree.

Check yourself

Pick an answer to see why it is right or wrong. Nothing is graded. Your first answer is saved in this browser so the question can come back for review.

1A node holds 4 class A and 4 class B points. A split sends all the A points left and all the B points right. How much does Gini impurity drop?
2On data with some flipped labels, a tree grown until every leaf is pure gets 100% of its training points right. What usually happens on new points?
3Why can a decision tree struggle with XOR data, where opposite corners share a class?
4Which question did ID3 put at the root of the tree for Quinlan's weather data, and why?

Progress is saved in this browser only.

Up nextEnsembles: forests and boosting
Next
How machines learn
  1. 1The learning loop
  2. 2Linear regression and loss
  3. 3Gradient descent
  4. 4Better optimizers
  5. 5Probability and Bayes' rule
  6. 6Classification and probability
  7. 7Overfitting and generalization
  8. 8Decision trees
  9. 9Ensembles: forests and boosting
  10. 10Measuring a classifier
  11. 11Finding groups: k-means

Try "embedding", "softmax", "overfitting", or "backpropagation".