Prune Decision Trees to Avoid Overfitting
Prune decision trees to avoid overfitting in this hands-on Applied AI engineering lesson. Learn the core principles, step-by-step pruning techniques, and common pitfalls — then apply them in a practical exercise. Connect to the next lesson in the track.
Focus: prune decision trees to avoid overfitting
Your model is perfect on training data — and on a test set, it's an embarrassment. The tree memorized every noise in your samples. You're not alone. This is overfitting: a classic trap where a decision tree grows deep, capturing every irregularity instead of the true pattern. It looks flawless during training, but fails in production.
In this lesson, you'll learn how to prune decision trees to avoid overfitting — by cutting back branches before or after growing — and gain a precise, practical approach to building models that actually generalize. We'll cover the mindset, step-by-step mechanics, a hands-on Python walkthrough, and how to pick the right pruning strategy for your data.
The problem this lesson solves
You just trained your first decision tree classifier on a housing dataset. Training accuracy: 98%. Test accuracy: 71%. What went wrong? A classic symptom — the model memorized the training set's quirks instead of learning the underlying structure. Overfitting is especially severe in decision trees because their depth and flexibility can easily match any training set perfectly.
But why do trees overfit so easily? Because each split is a conditional rule. With enough depth, a tree can essentially 'remember' each sample's label.
Without pruning, you'll see nagging issues like strange high-frequency patterns in predictions, wild performance swings with small data perturbations, and models that don't transfer to new contexts. The solution is to prune — to selectively remove branches that are too specific or low-value — resulting in simpler, more robust models. This lesson gives you a mental model, concrete steps, and code to avoid this pitfall.
Core concept / mental model
Think of a decision tree as a loyal but over-enthusiastic assistant. In training, it writes an ever-thicker rulebook, until it 'knows' every exception and corner case. Pruning is like reviewing that rulebook and tearing out the pages that are just noise — keeping the rules that solve the problem for the general case.
In more formal terms, pruning reduces the complexity of the model, which is controlled by: - Depth — how many levels of splits - Minimum samples per leaf — the smallest group at a leaf node - Minimum samples to split — the minimum number to justify a split - Impurity — e.g., Gini or entropy, which drives splits
Overfitting: deep tree, tiny leaves, perfect training accuracy, poor test accuracy. Underfitting: shallow tree, large leaves, poor accuracy both ways. The sweet spot is when test accuracy is high and train/test accuracy gap is small.
How it works step by step
You have two main pruning approaches — pre-pruning (also called early stopping) and post-pruning (also called cost-complexity pruning).
Pre-pruning (early stopping)
Stop growing the tree before it overfits, using one or more constraints:
- Set
max_depth(e.g., 5). 2. Setmin_samples_split(e.g., 10). 3. Setmin_samples_leaf(e.g., 5). 4. Optionally, require a minimum impurity decrease.
These are the most common, easy-to-implement controls. But you must tune them — too aggressive, and you underfit.
Post-pruning (cost-complexity)
Grow the tree fully, then prune from the bottom up based on complexity cost. Each candidate subtree has a cost like:
- Cost = misclassification rate + α × (number of leaves)
Here, α (alpha) is a tuning parameter. Higher α means fewer leaves, simpler tree. scikit-learn provides an algorithm to calculate effective alphas via cost_complexity_pruning_path.
Validation approach
Never tune hyperparameters on the test set — use a separate validation set or cross-validation. Otherwise, you leak information and overfit the test data indirectly.
A helix of 'what to prune?'
- Prune branches that are too small, like leaves with only a few samples.
- Prune branches where the impurity reduction is negligible.
- Prune when the chosen label in a leaf is essentially the same as its parent's majority label.
Hands-on walkthrough
Let's implement both pruning strategies with scikit-learn. We'll use a synthetic dataset to see the difference clearly.
# Initialize and load data
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
X, y = make_classification(
n_samples=3000, n_features=10, n_informative=5,
n_redundant=2, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 1. Without pruning (baseline)
tree_no_prune = DecisionTreeClassifier(random_state=42)
tree_no_prune.fit(X_train, y_train)
train_acc = accuracy_score(y_train, tree_no_prune.predict(X_train))
test_acc = accuracy_score(y_test, tree_no_prune.predict(X_test))
print(f"Unpruned tree: train={train_acc:.3f}, test={test_acc:.3f}")
# Expected output: Unpruned tree: train=1.000, test=0.837
# 2. Pre-pruning with max_depth
from sklearn.tree import DecisionTreeClassifier
tree_pre = DecisionTreeClassifier(max_depth=5, random_state=42)
tree_pre.fit(X_train, y_train)
train_acc = accuracy_score(y_train, tree_pre.predict(X_train))
test_acc = accuracy_score(y_test, tree_pre.predict(X_test))
print(f"Pre-pruned (depth=5): train={train_acc:.3f}, test={test_acc:.3f}")
# Expected output: Pre-pruned (depth=5): train=0.911, test=0.875
# 3. Post-pruning with cost complexity
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier
# Get effective alphas
path = tree_no_prune.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas = path.ccp_alphas
# Train trees for each alpha (excluding alpha=0)
clfs = []
for alpha in ccp_alphas:
clf = DecisionTreeClassifier(random_state=42, ccp_alpha=alpha)
clf.fit(X_train, y_train)
clfs.append(clf)
# Compute accuracies
train_accs = [accuracy_score(y_train, clf.predict(X_train)) for clf in clfs]
test_accs = [accuracy_score(y_test, clf.predict(X_test)) for clf in clfs]
# Find best alpha
best_alpha = ccp_alphas[test_accs.index(max(test_accs))]
print(f"Best alpha: {best_alpha:.4f}, test accuracy: {max(test_accs):.3f}")
# Expected output: Best alpha: 0.0275, test accuracy: 0.895
# 4. Final model with best alpha
final_tree = DecisionTreeClassifier(random_state=42, ccp_alpha=best_alpha)
final_tree.fit(X_train, y_train)
print(f"Final tree depth: {final_tree.get_depth()}")
# Expected output: Final tree depth: 7
Notice how the unpruned tree hit 100% train but dropped to ~84% test. Pre-pruning to depth 5 improved test accuracy to ~87.5%, while the post-pruned model reached ~89.5% — a solid boost. The gap between train and test shrank, a sign of reduced overfitting.
Compare options / when to choose what
| Approach | Pros | Cons | When to use |
|---|---|---|---|
| Pre-pruning | Fast, intuitive, works out-of-the-box | Might underfit if too aggressive | Quick prototyping, clear depth limit |
| Post-pruning (cost-complexity) | Often better performance, principled | More computation, needs validation | When you need maximum test accuracy |
| Cross-validation + pruning | Robust selection of hyperparameters | Computationally heavy | When you have enough data |
When to choose what: - If training time is limited, go with pre-pruning and a depth cap. - If you need top performance, use cost-complexity pruning combined with cross-validation. - For large datasets where training a full tree is expensive, pre-pruning is often your first choice.
In practice, you rarely need both — pick one and tune well.
Troubleshooting & edge cases
- Model still overfits after pruning? The tree might be too deep (
max_depthtoo high), or the training set has outliers you didn't clean. Try reducingmin_samples_leafor increasingmin impurity decrease. - Underfitting after pruning? You're over-pruning. Lower
max_depth, increasemin_samples_leafgradually. Use validation to check. - Best alpha selection fails? Sometimes
ccp_alphascontains duplicates — that's normal. Also, if the tree is already simple, the pruning path may be short. Always start with a fully grown tree. - Validation accuracy unstable? Use cross-validation instead of a single split.
- Runtime too high? Use smaller
max_depthor increasemin_samples_split.
What you learned & what's next
You've just mastered one of the most important tricks in machine learning: prune decision trees to avoid overfitting.
In this lesson, you learned why trees overfit, how to stop them with pre- or post-pruning, and how to measure the train/test gap. You now have a practical workflow: train a full tree, inspect the cost-complexity path, choose the best alpha, and evaluate. You also know which approach to pick based on your resources and accuracy needs.
Next up in the Applied AI engineering track, you'll move one step forward — exploring ensemble methods (like Random Forests and Gradient Boosting) that build on pruning by combining many trees. With pruning in your toolkit, you're ready to build models that truly generalize.
Key takeaway: A simple tree that generalizes beats a complex tree that memorizes. Use pruning to find that sweet spot.
Practice recap
Pick a dataset (e.g., the breast cancer dataset from scikit-learn). Train a full decision tree, then apply both pre-pruning (max_depth) and post-pruning (ccp_alpha with cross-validation). Record train/test accuracies, note the gap, and pick the model with the best validation performance. You'll see exactly how pruning helps.
Common mistakes
- Forgetting to tune
max_depthormin_samples_leafand assuming the default tree is fine. - Selecting alpha or hyperparameters by peeking at the test set — you must use validation data.
- Applying post-pruning without first growing a fully overfit tree — the cost-complexity path needs a deep tree to work.
- Ignoring the train/test accuracy gap — always check it after any pruning attempt.
Variations
- Use cross-validation with a grid over
max_depthorccp_alphafor more robust selection. - Use randomized search (e.g.,
RandomizedSearchCV) for faster tuning when you have many hyperparameters.
Real-world use cases
- Credit default modeling: prune trees to reduce false alarms and keep the model robust to changing demographics.
- Customer churn prediction: use cost-complexity pruning to identify key behavioral signals without overfitting on rare segments.
- Medical diagnosis support: prune trees so they generalize across patient populations, avoiding memorized quirks from a single hospital's data.
Key takeaways
- Overfitting happens when a decision tree becomes too deep and memorizes noise instead of the underlying pattern.
- Pre-pruning (max_depth, min_samples_leaf) stops growth early, while post-pruning (cost-complexity) trims a fully grown tree.
- Always measure the train/test accuracy gap — a wide gap signals overfitting even if accuracy seems high.
- Tune hyperparameters on a validation set, never on the test set.
- A simpler tree with good test accuracy is usually more reliable in production than a perfectly-fitted training model.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.