Chapter 16 — Decision Tree Classification
Chapter overview
Decision tree classification turns data into a hierarchy of if-then rules. At every internal node, the algorithm selects a feature and threshold that separates the training labels as effectively as possible. A new observation follows one branch at a time until it reaches a leaf, where the tree returns a predicted class and an estimated class probability.
Trees are popular because their logic can be displayed, they capture nonlinear relationships and interactions, and they usually do not need feature scaling. Their flexibility is also their central weakness: an unrestricted tree can memorize small details, change substantially after a modest data perturbation, and generalize poorly. This chapter therefore treats complexity control and pruning as essential parts of tree modeling.
| CENTRAL IDEA A decision tree repeatedly asks a simple question. The quality of the model depends on which questions are chosen, when splitting stops, and whether weak branches are removed. |
Learning objectives
- Identify the root node, internal nodes, decision rules, branches, and leaf nodes.
- Explain how a classification tree converts leaf class counts into predictions and probabilities.
- Calculate and interpret Gini impurity, entropy, weighted impurity, and information gain.
- Explain how feature thresholds are evaluated during greedy recursive partitioning.
- Control complexity with max_depth, min_samples_split, min_samples_leaf, and max_leaf_nodes.
- Use cost-complexity pruning and validation data to select ccp_alpha.
- Visualize a fitted tree and trace the decision path for individual observations.
- Recognize the advantages, limitations, and common failure modes of a single decision tree.
Chapter map
Table 16.1. Chapter structure
Section | Focus | Student outcome |
|---|---|---|
16.1 | Tree structure | Read nodes, rules, branches, leaves, classes, and probabilities |
16.2 | Split selection | Compare candidate thresholds with impurity reduction |
16.3 | Complexity | Regularize growth and prune weak branches |
16.4 | Advantages | Identify situations where trees are useful |
16.5 | Limitations | Recognize variance, instability, and greedy-search risks |
Lab | Train, visualize, prune | Select a tree without using the test set for tuning |
16.1 Tree structure
A classification tree organizes decisions as a top-down hierarchy. Every observation begins at the same root, follows rules through internal nodes, and ends in exactly one leaf.
Figure 16.1. Anatomy of a small classification tree
ROOT NODE age <= 42.5 | |||
LEFT INTERNAL NODE income <= 38,000 | RIGHT INTERNAL NODE account_age <= 3.5 | ||
LEAF A class 0 | LEAF B class 1 | LEAF C class 1 | LEAF D class 0 |
Root node
The root node contains every training observation available to the tree. Its rule creates the first and usually most globally influential partition. A root such as mean radius <= 15.05 sends observations satisfying the condition to the left child and the remaining observations to the right child. The first rule is not automatically the most important causal factor; it is simply the split that best improves the chosen training criterion at that moment.
Internal node and decision rule
An internal node is a nonterminal region that can still be divided. Its decision rule normally has the form feature <= threshold for continuous features. The rule is local: it is evaluated only for observations that have already reached that node. This is why the same feature may appear more than once and why a feature can have different effects in different regions of the tree.
| INTERACTION WITHOUT A PRODUCT TERM If the tree checks feature B only after feature A sends an observation down one branch, the effect of B depends on A. The hierarchy therefore represents a feature interaction naturally. |
Branches
A branch is the connection created by an outcome of a decision rule. In a binary tree, the left branch conventionally represents a true <= condition and the right branch represents the complementary > condition. A complete root-to-leaf path is a conjunction of rules: all conditions along the path must hold for the observation to enter that leaf.
Leaf node, predicted class, and class probability
A leaf node is terminal: no additional split is applied. The predicted class is normally the class with the largest training count, or weighted count, among observations in that leaf. The class probability is estimated from the class proportions inside the leaf. With 30 training observations in a leaf, of which 24 are class 1 and 6 are class 0, the estimated probability of class 1 is 24 / 30 = 0.80.
LEAF CLASS PROBABILITY n(m,k) is the number of training observations of class k in leaf m; n(m) is the total number in that leaf. |
| PROBABILITY CAUTION A leaf proportion is a model estimate, not a guarantee. Very small leaves can produce extreme probabilities such as 0 or 1 even when uncertainty is substantial. Evaluate probability calibration separately when probabilities drive decisions. |
Trace one prediction
PYTHON • Train a small tree and inspect predictions from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier
data = load_iris(as_frame=True) X_train, X_test, y_train, y_test = train_test_split( data.data, data.target, test_size=0.25, random_state=42, stratify=data.target, ) |
PYTHON • Train a small tree and inspect predictions — continued
tree_model = DecisionTreeClassifier( max_depth=3, min_samples_leaf=3, random_state=42, ) tree_model.fit(X_train, y_train)
query = X_test.iloc[[0]] print("Predicted class:", tree_model.predict(query)[0]) print("Class probabilities:", tree_model.predict_proba(query)[0]) |
Read the learned rules as text
PYTHON • Export an interpretable rule listing from sklearn.tree import export_text
rules = export_text( tree_model, feature_names=list(X_train.columns), decimals=2, ) print(rules) |
Table 16.2. Reading a classification tree
Element | Question it answers | Interpretation |
|---|---|---|
Root | Where does every case begin? | The full training population before any split |
Internal node | What local question is asked? | A subset that may still be partitioned |
Rule | How is the subset divided? | A feature-threshold condition |
Branch | Which outcome occurred? | True/false route from a rule |
Leaf | Where does the path end? | Terminal subgroup with a prediction |
Predicted class | Which label wins? | Largest class count or weighted count |
Probability | How mixed is the leaf? | Class proportion among leaf training cases |
16.2 How a tree selects splits
At each node, the algorithm compares candidate feature-threshold pairs and chooses a partition that produces purer child nodes. This is a greedy decision: it optimizes the current split without solving for the globally best possible tree.
Figure 16.2. Greedy split selection at one node
1 CANDIDATES | 2 PARTITIONS | 3 SCORE | 4 CHOOSE |
Feature-threshold pairs | Left and right child groups | Weighted child impurity | Largest impurity reduction |
Purity and impurity
A node is pure when all training observations inside it have the same class label. Impurity increases as the class distribution becomes more mixed. A useful split decreases impurity by separating observations into children whose class distributions are more homogeneous than the parent distribution.
Gini impurity
Gini impurity can be interpreted as the probability of a label disagreement under a simple random-label thought experiment based on the node proportions. It is zero for a pure node. For a balanced binary node with proportions 0.5 and 0.5, Gini impurity is 0.5, its binary maximum.
GINI IMPURITY p(m,k) is the proportion of class k among the observations that reached node m. |
Entropy
Entropy measures uncertainty in the class distribution. A pure node has entropy zero because its label is certain. A balanced binary node has maximum entropy of 1 bit when logarithms use base 2. Entropy places a different numerical emphasis on mixed distributions than Gini impurity, but the two criteria often produce similar trees.
ENTROPY Terms with p(m,k) = 0 contribute zero. In scikit-learn, entropy and log_loss both use Shannon information gain. |
Worked impurity example
Suppose a parent node contains 10 observations: 6 positive and 4 negative. Candidate split A produces a left child with 4 positive and 0 negative observations and a right child with 2 positive and 4 negative observations. The parent Gini impurity is 0.48. The left child impurity is 0, and the right child impurity is approximately 0.444. Weighting by child sizes gives 0.267, so the Gini reduction is approximately 0.213.
Table 16.3. Manual Gini calculation
Node | Class counts | Gini | Weight | Weighted contribution |
|---|---|---|---|---|
Parent | 6 positive / 4 negative | 0.480 | 1.00 | 0.480 |
Left | 4 positive / 0 negative | 0.000 | 0.40 | 0.000 |
Right | 2 positive / 4 negative | 0.444 | 0.60 | 0.267 |
Children total | — | — | 1.00 | 0.267 |
Reduction | parent - children | — | — | 0.213 |
PYTHON • Verify Gini and entropy numerically import numpy as np
def gini(counts): proportions = np.array(counts) / np.sum(counts) return1 - np.sum(proportions ** 2)
def entropy(counts): proportions = np.array(counts) / np.sum(counts) nonzero = proportions[proportions > 0] return -np.sum(nonzero * np.log2(nonzero)) |
PYTHON • Verify Gini and entropy numerically — continued
parent_gini = gini([6, 4]) left_gini = gini([4, 0]) right_gini = gini([2, 4]) weighted_children = 0.4 * left_gini + 0.6 * right_gini
print("Parent Gini:", round(parent_gini, 3)) print("Weighted child Gini:", round(weighted_children, 3)) print("Gini reduction:", round(parent_gini - weighted_children, 3)) print("Parent entropy:", round(entropy([6, 4]), 3)) |
Weighted impurity and information gain
A candidate split must be evaluated using both children. The child impurities are weighted by the proportion of node observations sent to each child; otherwise a tiny pure child could make a poor split look attractive. Information gain is the parent impurity minus the weighted child impurity. The tree selects a candidate with the largest improvement under the configured criterion and constraints.
WEIGHTED CHILD IMPURITY nL and nR are child sample counts and n is the parent sample count. |
IMPURITY REDUCTION A larger positive value indicates a more useful local split under the chosen criterion. |
Feature thresholds
For a continuous feature, candidate thresholds are considered between ordered training values that reach the current node. A threshold partitions the cases into feature <= threshold and feature > threshold. The selected threshold depends on the labels and on any sample or class weights; it is not simply the feature mean or median.
| THRESHOLDS ARE DATA-DEPENDENT A printed threshold may contain decimals even when the observed feature values are integers because a midpoint between adjacent candidate values can define the same partition. |
Gini versus entropy
Table 16.4. Comparing classification criteria
Aspect | Gini | Entropy / log loss |
|---|---|---|
Formula | 1 - sum of squared class proportions | Negative sum of p log p |
Pure node | 0 | 0 |
Binary maximum | 0.5 | 1 bit with log base 2 |
Typical result | Often similar to entropy | May choose a different split in close cases |
| scikit-learn value | criterion='gini' | criterion='entropy' or 'log_loss' |
PYTHON • Compare Gini and entropy on the same split from sklearn.tree import DecisionTreeClassifier
for criterion in ["gini", "entropy"]: model = DecisionTreeClassifier( criterion=criterion, max_depth=4, min_samples_leaf=4, random_state=42, ) model.fit(X_train, y_train) print( criterion, "depth=", model.get_depth(), "leaves=", model.get_n_leaves(), "test_accuracy=", round(model.score(X_test, y_test), 3), ) |
Recursive partitioning is greedy
After choosing the best available split at the root, the algorithm repeats the search independently inside each child. It does not usually revisit the root after discovering later branches. This greedy strategy makes tree induction computationally practical, but a locally best early decision may prevent a better overall structure. Different training samples or equal-quality candidate splits can therefore lead to different trees.
16.3 Tree complexity
Tree growth must be controlled. Complexity parameters can stop weak or overly specific branches before they are created, while cost-complexity pruning removes branches after an initial tree has been grown.
Figure 16.3. Controlling tree complexity
UNRESTRICTED | PRE-PRUNED | POST-PRUNED |
Many deep branches | Growth stops early | Grow, then remove weak branches |
Maximum depth
max_depth limits the number of successive decisions from the root to the deepest leaf. A shallow tree is easier to explain and normally has lower variance, but it can miss real structure. An unrestricted depth allows the algorithm to continue until another stopping condition applies and can produce a large, highly specific tree.
Minimum samples per split
min_samples_split specifies how many training observations an internal node must contain before it may be split. Raising the value prevents the algorithm from dividing very small nodes. It does not guarantee large leaves by itself because a permitted split can still send only a few observations to one child.
Minimum samples per leaf
min_samples_leaf requires each resulting leaf to contain at least a specified number of training observations. This directly prevents tiny terminal regions and usually smooths class probability estimates. Both min_samples_split and min_samples_leaf can be integers or fractions of the training size in scikit-learn.
Maximum number of leaf nodes
max_leaf_nodes places a global limit on the number of terminal regions. In scikit-learn, the tree grows in best-first fashion under this constraint, prioritizing nodes with the strongest relative impurity reduction. Limiting leaves can be more directly connected to the number of distinct rule-based segments than limiting depth.
Table 16.5. Main complexity controls
Parameter | What it limits | Increasing regularization |
|---|---|---|
max_depth | Longest root-to-leaf path | Use a smaller value |
| min_samples_split | Ability to split small nodes | Use a larger value |
min_samples_leaf | Smallest permitted terminal group | Use a larger value |
max_leaf_nodes | Total terminal regions | Use a smaller value |
| min_impurity_decrease | Minimum gain required to split | Use a larger value |
ccp_alpha | Post-pruning penalty | Use a larger value |
Study depth and leaf-size effects
PYTHON • Create a controlled complexity comparison import pandas as pd from sklearn.tree import DecisionTreeClassifier
records = [] for max_depth in [2, 3, 4, 6, None]: for min_leaf in [1, 5, 15]: model = DecisionTreeClassifier( max_depth=max_depth, min_samples_leaf=min_leaf, random_state=42, ) model.fit(X_train, y_train) |
PYTHON • Create a controlled complexity comparison — continued records.append({ "max_depth": max_depth, "min_samples_leaf": min_leaf, "tree_depth": model.get_depth(), "leaves": model.get_n_leaves(), "train_accuracy": model.score(X_train, y_train), "test_accuracy": model.score(X_test, y_test), })
comparison = pd.DataFrame(records) print(comparison.round(3)) |
Cost-complexity pruning
Minimal cost-complexity pruning balances empirical fit against the size of the subtree. scikit-learn exposes this trade-off through ccp_alpha. With ccp_alpha = 0, no cost-complexity pruning is applied. As alpha increases, branches with insufficient improvement relative to their complexity are removed, so node count and depth generally decrease in steps.
COST-COMPLEXITY OBJECTIVE R(T) measures leaf impurity, |leaves(T)| measures subtree size, and alpha controls the complexity penalty. |
Selecting a pruning strength
| SELECTION RULE Do not choose ccp_alpha by maximizing the test score. Generate candidates on training data, compare them with validation or cross-validation, freeze the choice, and evaluate the test set once. |
PYTHON • Generate candidate pruning strengths from sklearn.tree import DecisionTreeClassifier
unpruned = DecisionTreeClassifier(random_state=42) path = unpruned.cost_complexity_pruning_path(X_train, y_train)
# The final alpha normally collapses the tree to one root node. candidate_alphas = path.ccp_alphas[:-1] print("Number of candidate alphas:", len(candidate_alphas)) print("First candidates:", candidate_alphas[:5]) |
Pre-pruning versus post-pruning
Table 16.6. Complexity-control strategies
Approach | Mechanism | Strength | Caution |
|---|---|---|---|
Pre-pruning | Stop growth with depth, sample, leaf, or gain limits | Fast and directly controls the initial tree | A useful branch may be stopped too early |
Post-pruning | Grow a tree and remove weak branches with ccp_alpha | Compares nested subtrees along a pruning path | Alpha still requires honest validation |
Combined | Use sensible growth limits plus ccp_alpha | Can prevent extreme trees before pruning | More choices increase tuning complexity |
16.4 Advantages
A single decision tree is often a valuable first nonlinear classifier because its mechanism is transparent, its preprocessing demands are modest, and its learned rules can be inspected directly.
Easy to visualize
A small tree can be drawn as a flow of rules from the root to leaves. The diagram can show feature thresholds, impurity, sample counts, class distributions, and predicted classes. This visibility supports teaching, debugging, stakeholder discussion, and discovery of suspicious leakage features. Very large trees, however, cease to be meaningfully interpretable even if they remain technically drawable.
Nonlinear relationships and feature interactions
Axis-aligned rules divide feature space into regions, allowing a tree to represent nonlinear class boundaries. Hierarchical rules also create interactions: a threshold for one feature may matter only after another feature has placed an observation in a specific branch. No explicit polynomial or interaction term is required.
Scaling is usually unnecessary
A tree compares one feature with a threshold at a time. Multiplying a feature by a positive constant changes the numerical threshold but does not change the ordering of observations, so standardization normally does not improve the split geometry. This differs from KNN, support vector machines, and regularized linear models, whose calculations depend directly on feature scale.
| NO SCALING DOES NOT MEAN NO PREPROCESSING Raw text labels and inconsistent categories still require preparation. Missing values, categorical encoding, invalid measurements, leakage, and train-test discipline remain important. |
Mixed feature patterns
After appropriate encoding, a tree can combine binary indicators, counts, continuous measurements, ordinal values, and one-hot encoded categories in different branches. It can also ignore features that do not provide useful impurity reduction. This flexibility is especially attractive for tabular problems containing thresholds, exceptions, and conditional rules.
Table 16.7. Practical advantages with caveats
Advantage | Why it matters | Boundary of the advantage |
|---|---|---|
Visual rules | Supports explanation and debugging | Only small trees remain easy to read |
| Nonlinear structure | Captures thresholds and irregular regions | Axis-aligned splits may need many nodes |
Interactions | Conditional effects arise naturally | Interactions can be sample-specific |
| No standardization | Simplifies numeric preprocessing | Categories and missing data still need care |
Fast prediction | Each case follows one root-to-leaf path | Deep trees and large batches still have cost |
Verify scale invariance
PYTHON • Confirm invariance to a positive scale change import pandas as pd from sklearn.tree import DecisionTreeClassifier
X_scaled_copy = X_train.copy() X_scaled_copy.iloc[:, 0] = X_scaled_copy.iloc[:, 0] * 1000
original_tree = DecisionTreeClassifier(max_depth=4, random_state=42) rescaled_tree = DecisionTreeClassifier(max_depth=4, random_state=42)
original_tree.fit(X_train, y_train) rescaled_tree.fit(X_scaled_copy, y_train) |
PYTHON • Confirm invariance to a positive scale change — continued
query_original = X_test.copy() query_rescaled = X_test.copy() query_rescaled.iloc[:, 0] = query_rescaled.iloc[:, 0] * 1000
same_predictions = ( original_tree.predict(query_original) == rescaled_tree.predict(query_rescaled) ).all() print("Same predictions:", same_predictions) |
16.5 Limitations
The interpretability of a small tree should not obscure the statistical weaknesses of a single deep tree. High variance, instability, greedy search, and piecewise-constant predictions require careful validation.
High variance and overfitting
An unrestricted tree can keep splitting until leaves are pure or too small to divide. Training accuracy may then approach 100%, while validation accuracy stops improving or declines. The tree has learned local accidents, mislabeled observations, and sampling noise rather than stable structure. Complexity controls reduce this variance at the cost of added bias.
Instability to small data changes
A small change in the training sample can alter the best root split. Because every later split depends on the earlier hierarchy, the entire tree may reorganize. Two trees with similar predictive accuracy can therefore present noticeably different rules and feature importances. Fixing random_state improves reproducibility for a given dataset, but it does not remove sampling instability.
Greedy optimization
The algorithm selects the best current split and recurses. It does not explore every possible future tree because exhaustive global search would be impractical. A locally strong split can block a better later structure, and correlated features may substitute for one another depending on small criterion differences.
Additional practical limitations
- Axis-aligned partitions: complex diagonal boundaries may require many step-like regions.
- Biased importance measures: impurity-based importance can favor variables with many possible split points.
- Weak probability estimates: small or pure leaves can produce overconfident class proportions.
- Poor extrapolation: a tree predicts from existing leaf regions rather than extending a smooth trend.
- Class imbalance: impurity improvement and majority-class leaves can under-serve rare classes unless metrics and weights are chosen carefully.
Table 16.8. Diagnosing common tree problems
Failure signal | Likely cause | Possible response |
|---|---|---|
| Training score far above validation | Tree is too flexible | Limit depth/leaves, enlarge leaves, or prune |
| Rules change across resamples | High sampling variance | Cross-validate stability or use an ensemble |
| Many tiny pure leaves | Memorization | Increase min_samples_leaf or ccp_alpha |
| One feature dominates importance | Leakage, many thresholds, or correlation | Audit availability and use permutation importance |
| Minority recall is weak | Imbalance or unsuitable thresholding | Use class-aware metrics and consider class_weight |
| Tree is accurate but unreadable | Excessive structural complexity | Prune or provide constrained summary rules |
Measure instability with repeated splits
PYTHON • Compare root features across random splits from collections import Counter from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier
root_features = [] for seed inrange(20): X_part, _, y_part, _ = train_test_split( data.data, data.target, test_size=0.25, |
PYTHON • Compare root features across random splits — continued random_state=seed, stratify=data.target, ) model = DecisionTreeClassifier(max_depth=4, random_state=42) model.fit(X_part, y_part) root_index = model.tree_.feature[0] root_features.append(data.feature_names[root_index])
print(Counter(root_features)) |
| MODEL-SELECTION PERSPECTIVE A constrained tree is a strong interpretable candidate, but its validation score, stability, subgroup behavior, probability quality, and operational cost should be compared fairly with other model families. |
Practical lab — Train, visualize, and prune a decision tree
Students build a classification tree for the Breast Cancer Wisconsin diagnostic dataset, compare training and validation performance, visualize an initial tree, generate a cost-complexity pruning path, select ccp_alpha without touching the test set, refit the chosen tree, and report final test results. The dataset is educational and must not be treated as a clinical decision system.
Lab objectives
- Create reproducible 60/20/20 training, validation, and test subsets with stratification.
- Train an unrestricted baseline and inspect its depth, leaves, nodes, and accuracy gap.
- Visualize the upper levels of the tree with feature and class names.
- Generate candidate ccp_alpha values from the training data only.
- Compare subtree size, training accuracy, and validation accuracy across alpha values.
- Select the simplest best-validation tree and freeze its configuration.
- Refit on development data and evaluate once on the untouched test set.
- Trace one test observation through the final tree and inspect feature importance cautiously.
Experimental design
Table 16.9. Practical lab design
Element | Choice | Reason |
|---|---|---|
Dataset | Breast Cancer Wisconsin (diagnostic) | Built-in numerical binary-classification data |
Split | 60% train / 20% validation / 20% test | Separate model selection from final evaluation |
Stratification | Use y in both splits | Preserve malignant/benign proportions approximately |
Scaling | None | Tree thresholds depend on order, not feature magnitude |
Selection | Validation accuracy, then fewer nodes | Prefer a simpler tree when the best score ties |
Random state | 42 | Reproducible splitting and tie behavior |
| IMPORTANT Accuracy is used here to isolate the pruning exercise. A real health-related application requires clinically appropriate metrics, external validation, calibration analysis, and expert oversight. |
Step 1 — Load and inspect the data
PYTHON • Load the dataset and required tools import numpy as np import pandas as pd import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer from sklearn.metrics import classification_report, confusion_matrix from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
data = load_breast_cancer(as_frame=True) X = data.data y = data.target
print("Shape:", X.shape) print("Class names:", list(data.target_names)) print(y.value_counts().sort_index()) |
In this dataset, target 0 represents malignant and target 1 represents benign. Attach these meanings to every class-specific result so that recall or error counts are not interpreted for the wrong class.
Step 2 — Create protected subsets
PYTHON • Create 60/20/20 stratified subsets X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.40, random_state=42, stratify=y, )
X_valid, X_test, y_valid, y_test = train_test_split( X_temp, |
PYTHON • Create 60/20/20 stratified subsets — continued y_temp, test_size=0.50, random_state=42, stratify=y_temp, )
print("Train:", X_train.shape) print("Validation:", X_valid.shape) print("Test:", X_test.shape) |
Step 3 — Train an unrestricted baseline
PYTHON • Measure the initial overfitting gap baseline_tree = DecisionTreeClassifier(random_state=42) baseline_tree.fit(X_train, y_train)
print("Depth:", baseline_tree.get_depth()) print("Leaves:", baseline_tree.get_n_leaves()) print("Nodes:", baseline_tree.tree_.node_count) print("Training accuracy:", round(baseline_tree.score(X_train, y_train), 3)) print("Validation accuracy:", round(baseline_tree.score(X_valid, y_valid), 3)) |
| INTERPRETATION PROMPT A perfect training score is not proof of a good classifier. Compare it with validation performance and inspect how many nodes were needed to achieve it. |
Step 4 — Visualize the upper tree
PYTHON • Plot the first three levels plt.figure(figsize=(16, 8)) plot_tree( baseline_tree, max_depth=3, feature_names=list(X.columns), class_names=list(data.target_names), filled=True, rounded=True, proportion=True, precision=2, fontsize=8, ) plt.title("Unrestricted tree — first three levels") plt.tight_layout() plt.show() |
Limiting max_depth inside plot_tree changes only the drawing, not the fitted baseline_tree. The hidden lower branches still exist and still affect predictions. Record the root rule, the dominant classes in the first children, and any feature that appears repeatedly.
Step 5 — Generate the pruning path
PYTHON • Find effective alpha candidates path_model = DecisionTreeClassifier(random_state=42) path = path_model.cost_complexity_pruning_path(X_train, y_train)
# Exclude the final alpha that usually leaves only the root node. candidate_alphas = np.unique(path.ccp_alphas[:-1])
if len(candidate_alphas) > 40: positions = np.linspace( 0, len(candidate_alphas) - 1, 40, dtype=int, ) candidate_alphas = candidate_alphas[positions]
print("Candidate alphas:", len(candidate_alphas)) print(candidate_alphas[:10]) |
Step 6 — Evaluate every candidate on validation data
PYTHON • Record accuracy and subtree size records = []
for alpha in candidate_alphas: model = DecisionTreeClassifier( random_state=42, ccp_alpha=float(alpha), ) model.fit(X_train, y_train) records.append({ "ccp_alpha": alpha, |
PYTHON • Record accuracy and subtree size — continued "node_count": model.tree_.node_count, "depth": model.get_depth(), "leaves": model.get_n_leaves(), "train_accuracy": model.score(X_train, y_train), "validation_accuracy": model.score(X_valid, y_valid), })
results = pd.DataFrame(records) print(results.round(4)) |
Step 7 — Plot the pruning trade-off
PYTHON • Visualize accuracy and node count fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
axes[0].plot( results["ccp_alpha"], results["train_accuracy"], marker="o", label="Training", ) axes[0].plot( results["ccp_alpha"], results["validation_accuracy"], marker="o", label="Validation", ) axes[0].set_xlabel("ccp_alpha") axes[0].set_ylabel("Accuracy") axes[0].set_title("Accuracy across pruning strengths") |
PYTHON • Visualize accuracy and node count — continued axes[0].grid(alpha=0.25) axes[0].legend()
axes[1].plot( results["ccp_alpha"], results["node_count"], marker="o", drawstyle="steps-post", ) axes[1].set_xlabel("ccp_alpha") axes[1].set_ylabel("Number of nodes") axes[1].set_title("Tree size across pruning strengths") axes[1].grid(alpha=0.25)
plt.tight_layout() plt.show() |
Questions for the pruning curves
- How does node count change as ccp_alpha increases?
- Where is the gap between training and validation accuracy largest?
- Does validation accuracy improve after removing some branches?
- Is the highest validation score achieved by one alpha or a plateau of candidates?
- At what point does excessive pruning reduce both training and validation performance?
Step 8 — Select alpha without using the test set
PYTHON • Prefer the smallest tree among top validation scores best_validation = results["validation_accuracy"].max()
best_candidates = results.loc[ results["validation_accuracy"] == best_validation ]
selected_row = best_candidates.sort_values( ["node_count", "ccp_alpha"], ascending=[True, False], ).iloc[0]
selected_alpha = float(selected_row["ccp_alpha"]) print("Selected alpha:", selected_alpha) print("Validation accuracy:", round(best_validation, 3)) print("Selected node count:", int(selected_row["node_count"])) print("Selected depth:", int(selected_row["depth"])) |
| TIE POLICY The lab prefers fewer nodes when several candidates have the same validation accuracy. Define the tie rule before examining the final test result. |
Step 9 — Refit and evaluate once
PYTHON • Fit the final pruned tree on development data X_development = pd.concat([X_train, X_valid]) y_development = pd.concat([y_train, y_valid])
final_tree = DecisionTreeClassifier( random_state=42, ccp_alpha=selected_alpha, ) final_tree.fit(X_development, y_development)
test_predictions = final_tree.predict(X_test)
|
PYTHON • Fit the final pruned tree on development data — continued print("Confusion matrix:") print(confusion_matrix(y_test, test_predictions)) print() print("Classification report:") print(classification_report( y_test, test_predictions, target_names=data.target_names, digits=3, )) print("Final test accuracy:", round(final_tree.score(X_test, y_test), 3)) |
Step 10 — Visualize and export the pruned tree
PYTHON • Draw the final tree plt.figure(figsize=(18, 10)) plot_tree( final_tree, feature_names=list(X.columns), class_names=list(data.target_names), filled=True, rounded=True, proportion=True, precision=2, fontsize=8, |
PYTHON • Draw the final tree — continued ) plt.title("Final cost-complexity-pruned decision tree") plt.tight_layout() plt.show()
print(export_text( final_tree, feature_names=list(X.columns), decimals=2, )) |
Step 11 — Trace one test observation
PYTHON • Inspect the decision path query = X_test.iloc[[0]] path_matrix = final_tree.decision_path(query) leaf_id = final_tree.apply(query)[0]
visited_nodes = path_matrix.indices print("Visited node IDs:", visited_nodes) print("Final leaf ID:", leaf_id) print("Predicted class:", final_tree.predict(query)[0]) print("Probabilities:", final_tree.predict_proba(query)[0]) |
Step 12 — Inspect feature importance cautiously
PYTHON • List the strongest impurity-based importances importance = pd.Series( final_tree.feature_importances_, index=X.columns, ).sort_values(ascending=False)
print(importance.head(10))
importance.head(10).sort_values().plot( kind="barh", figsize=(8, 5), title="Top impurity-based feature importances", ) plt.xlabel("Importance") plt.tight_layout() plt.show() |
| IMPORTANCE CAUTION Impurity-based feature importance is not causality and can be unstable or biased. Compare it with validation behavior, domain knowledge, and model-agnostic methods later in the course. |
Expected observations
- The unrestricted tree usually achieves extremely high training accuracy and uses more nodes than the selected pruned tree.
- Increasing ccp_alpha removes branches in discrete steps rather than continuously shrinking every rule.
- Moderate pruning may preserve or improve validation accuracy while substantially simplifying the tree.
- Excessive pruning eventually produces a tree that is too small and underfits.
- The final test score can be below the best validation score because selection favored some validation-specific noise.
- Leaf probabilities are constant for all observations that enter the same leaf.
Lab deliverables
- A reproducible notebook containing the complete workflow and outputs.
- A diagram of the initial tree's first three levels.
- A table of ccp_alpha, node count, depth, leaves, training accuracy, and validation accuracy.
- Two labeled pruning curves: accuracy versus alpha and node count versus alpha.
- The selected alpha and the predeclared tie-breaking rule.
- A final confusion matrix, classification report, and complete pruned-tree visualization.
- A 200–300 word interpretation of overfitting, pruning, final complexity, and limitations.
Extension challenges
- Replace the single validation set with StratifiedKFold cross-validation and report mean plus standard deviation for each alpha.
- Compare cost-complexity pruning with max_depth and min_samples_leaf under the same validation procedure.
- Use class_weight='balanced' and compare malignant-class recall, precision, and F1 rather than accuracy alone.
- Bootstrap the development data, refit several pruned trees, and measure how often the root feature changes.
- Compare impurity-based feature importance with permutation importance on validation data.
Common mistakes and corrections
Table 16.10. Frequent decision-tree errors
Mistake | Why it causes trouble | Correction |
|---|---|---|
| Report training accuracy only | A deep tree can memorize training cases | Use validation or cross-validation |
| Tune on the test set | The final estimate becomes optimistic | Freeze complexity before test evaluation |
| Plot only three levels and call the tree shallow | The fitted model may contain hidden lower branches | Check get_depth and tree_.node_count |
| Interpret a split causally | Predictive partitions do not prove causes | Use careful predictive language |
| Assume no scaling means no preprocessing | Raw categories and invalid data remain problematic | Prepare features inside a leakage-safe workflow |
| Choose alpha from training score | Training performance favors larger trees | Use held-out or cross-validated performance |
| Trust tiny-leaf probabilities | Small counts create extreme unstable estimates | Increase leaf size and evaluate calibration |
| Treat feature importance as stable truth | Correlation and many thresholds can distort rankings | Check resampling and permutation importance |
Knowledge check
Choose one answer for each question. Complete the questions before consulting the answer key.
1. What does every observation encounter first in a decision tree?
- A. A leaf node
- B. The root node
- C. A confusion matrix
- D. A scaling transformation
2. What normally determines the predicted class of a leaf?
- A. The class with the largest leaf count
- B. The deepest feature
- C. The global feature mean
- D. A random label
3. A pure classification node has Gini impurity equal to:
- A. -1
- B. 0
- C. 0.5 for every problem
- D. The sample size
4. Information gain compares:
- A. Test and training size
- B. Parent impurity with weighted child impurity
- C. Feature scales
- D. Prediction time and fit time
5. Why must child impurity be weighted?
- A. Children may contain different numbers of observations
- B. Every feature needs standardization
- C. Entropy cannot use probabilities
- D. Trees require equal class counts
6. Which change usually makes leaves larger and the tree smoother?
- A. Decrease min_samples_leaf
- B. Increase min_samples_leaf
- C. Remove random_state
- D. Standardize every feature
7. What does a larger ccp_alpha generally encourage?
- A. More branches
- B. A smaller pruned subtree
- C. More features in the dataset
- D. A different target
8. Why can a single tree be unstable?
- A. It never uses labels
- B. Small data changes can alter early splits and later branches
- C. Scaling always changes class labels
- D. It averages thousands of trees
9. Which statement about scaling is most accurate?
- A. Trees always fail without StandardScaler
- B. Trees usually do not need scaling, but other preprocessing may still be required
- C. Scaling removes overfitting
- D. Scaling converts classification to regression
10. When should the final test set be evaluated?
- A. During every alpha comparison
- B. Before selecting a criterion
- C. Once after the modeling choices are frozen
- D. To decide the train-validation split
Answer key and explanations
Table 16.11. Knowledge-check solutions
Answer | Explanation |
|---|---|
1 — B | Every observation begins at the root before following any branch. |
2 — A | The winning leaf class is based on the largest class count or weighted count. |
3 — B | A pure node contains one class and has zero impurity. |
4 — B | Gain is the parent impurity minus the sample-weighted impurity of the children. |
5 — A | A large child must contribute more than a very small child to the split score. |
6 — B | A larger minimum leaf size blocks tiny terminal regions and usually smooths predictions. |
7 — B | A larger pruning penalty removes branches whose improvement does not justify their complexity. |
8 — B | Changing an early split changes which observations and candidate rules appear downstream. |
9 — B | Feature magnitude does not control a one-feature threshold split, but data preparation still matters. |
10 — C | The test set is reserved for final evaluation after criterion and complexity choices are fixed. |
Score interpretation
- 9–10 correct: ready to train, interpret, and prune a classification tree with honest validation.
- 7–8 correct: solid understanding; revisit impurity reduction or complexity controls where needed.
- 5–6 correct: repeat the worked split calculation and pruning-path lab.
- 0–4 correct: review tree anatomy, leaf predictions, and the distinction between training and validation performance.
Chapter summary
- A decision tree routes each observation from a root through internal rules and branches to one terminal leaf.
- The leaf predicts the class with the largest training count or weighted count and estimates probabilities from leaf proportions.
- Gini impurity and entropy quantify class mixing; useful splits reduce weighted impurity.
- Split search is greedy and recursive, so a locally best decision is not guaranteed to produce a globally optimal tree.
- Maximum depth, minimum split size, minimum leaf size, and maximum leaves control growth before pruning.
- Cost-complexity pruning uses ccp_alpha to trade fit against subtree size and must be tuned without the test set.
- Trees capture nonlinear relationships and interactions and normally do not require feature scaling.
- A single tree can overfit, vary substantially across samples, and produce unstable rules or importance rankings.
- Interpretability depends on restraint: a small validated tree is understandable; a huge tree is merely visible.
| ONE SENTENCE TO REMEMBER A classification tree is a hierarchy of locally chosen rules, so honest validation and complexity control are as important as the rules themselves. |
Key vocabulary
Table 16.12. Essential terminology
Term | Meaning |
|---|---|
Root node | First node containing all training observations available to the tree |
Internal node | Nonterminal subgroup where another decision rule is applied |
Branch | Route corresponding to an outcome of a decision rule |
Leaf node | Terminal region that returns a prediction |
Threshold | Numeric cut point used to create left and right child groups |
Impurity | Degree of class mixing within a node |
Gini impurity | One minus the sum of squared class proportions |
Entropy | Uncertainty measure based on negative p log p |
Information gain | Reduction from parent impurity to weighted child impurity |
Pre-pruning | Stopping or limiting growth before weak branches are created |
| Cost-complexity pruning | Removing weak branches using a fit-versus-size penalty |
ccp_alpha | scikit-learn parameter controlling cost-complexity pruning strength |
High variance | Sensitivity of the learned tree to changes in the training sample |
Further reading
- scikit-learn: DecisionTreeClassifier
- scikit-learn user guide: decision trees
- scikit-learn: plot_tree
- scikit-learn example: cost-complexity pruning
What’s next?
The next chapter studies ensemble classification models. Random forests reduce the variance of individual trees by combining many randomized trees, while gradient boosting builds trees sequentially to correct earlier errors. The contrast explains why one transparent but unstable learner can become the foundation of highly accurate ensemble methods.