Part IV — Modeling · Chapter 9

Decision Trees

~40 min read5 interactive widgets3 plates

In this chapter

  1. The models covered in this lecture
  2. A first decision tree
  3. Reading the tree: plot_tree and feature importance
  4. Petal vs sepal: two views of the data
  5. Feature selection: checking correlations
  6. Tuning max_depth: what do you expect?
  7. Decision boundaries from depth 1 to 6
  8. Accuracy vs depth: the learning curve
  9. Check your understanding

1. The models covered in this lecture

With the workflow of Chapter 8 in place, the deck turns to the models: "Let's train some machine learning models." Three models are covered in this lecture:

The lecture's rule

It is important to understand the model dynamicsnot only the final result. Actually, it is mandatory for the exam. This is why every model here is studied through its decision boundaries: the picture of where the model draws its regions shows how it behaves, not just how well it scores.

2. A first decision tree

The first model is a decision tree with max_depth=2 — the same five-step pattern of Chapter 8:

from sklearn.tree import DecisionTreeClassifier   # Import the model
from sklearn.metrics import accuracy_score

clf = DecisionTreeClassifier(max_depth=2, random_state=seed) # Instantiate the model
clf.fit(X_train, y_train)   # Train the model
y_pred = clf.predict(X_test) # Predict new values
accuracy_score(y_test, y_pred) # Evaluate the model (on the test set)
0.9666666666666667

29 of the 30 test flowers are classified correctly — and the one that is not is a versicolor that the tree confuses with virginica, as the boundary plots later show. The deck now asks the question that matters: what does this tree actually look like?

3. Reading the tree: plot_tree and feature importance

Two tools open the black box:

from sklearn.tree import plot_tree
plt.figure(figsize=(4, 3))
plot_tree(clf, feature_names=df.columns, class_names=['setosa', 'versicolor', 'virginica'], filled=True);

The depth-2 tree found on Iris has exactly two splits, and both use the same feature:

THE DEPTH-2 IRIS TREE (plot_tree) petal length ≤ 2.45 gini = 0.667 · 120 samples 50 setosa · 50 versicolor · 20 virginica True → setosa False → next split leaf: setosa 50 samples, gini = 0.0 all 50 setosa — perfect petal length ≤ 4.75 gini = 0.5 · 70 samples 50 versicolor · 20 virginica True → versicolor leaf: versicolor (48) leaf: virginica (22) petal length alone drives both splits: the tree ignores the sepal features entirely.
Plate 9.1 — The tree behind the 0.9667 score, reconstructed from the deck's plot_tree call. One feature, two thresholds: petal length below 2.45 cm is setosa; between 2.45 and 4.75 cm is versicolor; above is mostly virginica.

Then the deck checks feature relevance with feature_importances_:

feature_importance_df = pd.DataFrame({'Feature': X.columns, 'Importance': clf.feature_importances_})
feature_importance_df = feature_importance_df.sort_values(['Importance', 'Feature'], ascending=[False, True])
feature_importance_df
FeatureImportance
2petal length (cm)1.0
3petal width (cm)0.0
0sepal length (cm)0.0
1sepal width (cm)0.0

All the importance is on petal length. This is the direct consequence of the two splits in Plate 9.1: a depth-2 tree has exactly two decisions, and both were made on the same feature.

Widget — feature importance

Click each feature to see why it scored what it scored.

4. Petal vs sepal: two views of the data

The deck now plots the data twice — the 2 most important features and the 2 least important features:

plot(df, clf, feature_importance_df['Feature'].iloc[:2].tolist())   # plot the 2 most important features
plot(df, clf, feature_importance_df['Feature'].iloc[2:].tolist())   # plot the 2 least important features
PETALS SEPARATE · SEPALS OVERLAP petal length petal width petal L × petal W — separable sepal length sepal width sepal L × sepal W — overlapping ● setosa ● versicolor ● virginica — the same 150 flowers, two feature pairs.
Plate 9.2 — The deck's two views. On petal features the three species form three well-separated clouds (a tree can separate them with straight axis-aligned cuts); on sepal features the clouds overlap so heavily that no simple rule separates them — which is exactly what the tree discovered on its own.

5. Feature selection: checking correlations

The deck now checks whether the features are redundant, using the Pearson correlation of the training set:

X_train.corr(method='pearson', numeric_only=True)
sepal lengthsepal widthpetal lengthpetal width
sepal length1.000000-0.1069260.8621750.801480
sepal width-0.1069261.000000-0.432089-0.369509
petal length0.862175-0.4320891.0000000.962577
petal width0.801480-0.3695090.9625771.000000

Two pairs stand out: petal length vs petal width at 0.96 and sepal length vs petal length at 0.86 — the petal features are strongly correlated with each other (and with sepal length). Highly correlated features carry largely the same information, so a model can often drop some of them with little loss. This is a feature selection argument: the tree already made the same discovery by ignoring the sepal features entirely.

Widget — the correlation matrix

Click a cell to read its meaning.

6. Tuning max_depth: what do you expect?

The tree has one obvious hyperparameter: max_depth. Before running anything, the deck stops and asks: what do you expect?

Expectation: one single split — a single straight, axis-aligned cut across the petal plane. It will isolate the easiest group (setosa) and lump the other two together, so the accuracy will be mediocre.

What happens: the split lands on petal length ≈ 2.45. Setosa is perfectly isolated; versicolor and virginica share the rest. This is underfitting by construction: the model is too shallow to see the versicolor/virginica boundary.

Expectation: three cuts (a depth-3 tree can use up to 7 regions). Now versicolor and virginica should get their own region — the boundary that depth 1 and 2 could not draw.

What happens: the test accuracy reaches 1.0. The tree separates the three species completely: with the petal features, three axis-aligned regions suffice. Deeper trees can only add fragments around single points.

Expectation: a much more fragmented boundary — up to 63 regions, each fit to tiny clusters of training points. The danger is overfitting: the boundary starts following individual points and noise.

What happens: the training accuracy is 1.0 and the test accuracy stays 1.0 — on this clean dataset the fragmentation is harmless. But on noisy data the same depth produces the wiggly, extrapolating shapes of the xkcd strip from Chapter 8.

The pedagogical trick: the deck asks for a prediction before showing the plots, because predicting the model's behavior is exactly the skill the exam tests. Try it on the widget in the next section: set the depth, guess the boundary, then reveal it.

7. Decision boundaries from depth 1 to 6

The deck plots the decision boundaries for max_depth = 1, 2, 2 (seed 1), 3, 6 on the petal plane. The sequence tells the whole story of this chapter in pictures:

BOUNDARY GROWTH WITH DEPTH depth 1 2 regions depth 2 3 regions · 0.9667 depth 3 4 regions · 1.0 depth 6 fragmented regions each depth adds one more level of axis-aligned cuts: from one straight split to a mosaic that hugs individual points. depth controls capacity → too shallow underfits, too deep risks overfitting.
Plate 9.3 — The boundary progression (reconstructed from the deck's plot_boundary outputs). One straight cut; a second cut; a third; then fragmentation. The model's "behavior" is this picture — the score alone cannot show it.

Widget — grow the tree

This widget trains a real greedy decision tree (the same CART logic as scikit-learn) on the actual 150 Iris flowers, projected on the petal plane — exactly the deck's plots. Move the depth slider and watch the boundary, the splits, and the training accuracy change.

8. Accuracy vs depth: the learning curve

Finally the deck makes the trade-off quantitative, plotting training and test accuracy for max_depth from 1 to 9:

max_depths = range(1, 10)
train_accuracies, test_accuracies = [], []
for max_depth in max_depths:
    clf = DecisionTreeClassifier(max_depth=max_depth, random_state=seed)
    clf.fit(X_train, y_train)
    train_acc = accuracy_score(y_train, clf.predict(X_train))
    test_acc = accuracy_score(y_test, clf.predict(X_test))
    train_accuracies.append(train_acc)
    test_accuracies.append(test_acc)
plt.plot(max_depths, train_accuracies, label="Training", marker='o')
plt.plot(max_depths, test_accuracies, label="Test", marker='o')
plt.xlabel("max_depth"); plt.ylabel("Accuracy"); plt.legend(); plt.grid(True)

The values below are the actual numbers this code produces (seed 42, the deck's setup — the depth-2 test value reproduces the printed 0.9666666666666667 exactly):

max_depth123456789
Training0.6750.9500.95830.9750.99171.01.01.01.0
Test0.63330.96671.01.01.01.01.01.01.0

The classic pattern, in miniature: at depth 1 the tree underfits (both curves low — the model cannot even separate setosa from the rest cleanly); around depth 2-3 both curves are high; from depth 6 the training curve reaches 1.0 while the test curve stops improving. On this clean dataset the plateau is flat, but the shape is the warning from Chapter 8: once the training curve keeps climbing while the test curve stalls, additional capacity is buying memory, not generalization.

Widget — accuracy vs depth

The same curves, computed from the real runs. Hover the markers to read the exact values.

Check your understanding

Which three models are covered in this lecture, and what is the lecture's rule about them?

Decision tree, random forest, k-NN. The rule: it is important to understand the model dynamics, not only the final result — actually, it is mandatory for the exam. This is why the deck studies each model through its decision boundaries.

Write the five lines that train and evaluate the depth-2 decision tree, and give its accuracy.

from sklearn.tree import DecisionTreeClassifier; clf = DecisionTreeClassifier(max_depth=2, random_state=seed); clf.fit(X_train, y_train); y_pred = clf.predict(X_test); accuracy_score(y_test, y_pred)0.9666666666666667 (29 of 30 test flowers correct).

Describe the depth-2 iris tree and its feature importances.

The tree makes exactly two splits, both on petal length: first at 2.45 (True → setosa leaf with 50 samples), then at 4.75 on the remaining 70 samples (True → versicolor, False → virginica). Consequently the feature importances are petal length 1.0 and everything else 0.0: a depth-2 tree has only two decisions and both used the same feature.

What does the "petal vs sepal" comparison show?

Plotting the 2 most important features (petal length × petal width) shows the three species as three well-separated clouds — a tree can separate them with axis-aligned cuts. Plotting the 2 least important (sepal length × sepal width) shows heavy overlap — no simple rule separates them. The tree discovered this on its own by never using the sepal features.

Read the Pearson correlation matrix of the iris training set.

The strong positive pairs are petal length vs petal width (0.9626) and sepal length vs petal length (0.8622); sepal width is almost uncorrelated with the others (e.g. -0.1069 with sepal length). Strongly correlated features carry largely the same information, so some can be dropped — a feature-selection argument that matches the tree's behavior.

What does the sequence of boundaries from depth 1 to 6 show?

Depth 1: one straight cut at petal length ≈ 2.45, isolating setosa. Depth 2: a second cut at ≈ 4.75 separating versicolor from virginica (test accuracy 0.9667). Depth 2 with seed 1: a different boundary for the same depth — tree learning has randomness and the seed controls it. Depth 3: test accuracy 1.0. Depth 6: many small rectangular regions hugging individual points — the fragmentation that on noisy data becomes overfitting.

Describe the accuracy-vs-depth curves and what they diagnose.

Training accuracy rises from 0.675 (depth 1) to 1.0 (depth 6); test accuracy rises from 0.6333 to 1.0 by depth 3 and then plateaus. The pattern: depth 1 underfits (both curves low); around depth 2-3 both are high; beyond, training keeps climbing while test stalls — the signature of capacity buying memory, not generalization. On this clean dataset the plateau is flat; on noisy data it turns downwards (the overfitting shape).