A doctor sees a patient and thinks:
"Is the fever above 38.5°C? → Yes.
Is there a cough? → Yes.
Is the cough productive? → No.
Duration more than 5 days? → Yes."
Diagnosis: viral upper respiratory infection. Prescribe rest and fluids.
This is not a neural network. This is a decision tree —
and your doctor has been running one for every patient, every day.
01 — What a Decision Tree Is
A decision tree is a model that learns a hierarchy of if-else rules directly from data. It has three types of components:
To classify a new example, you start at the root node, answer each question, follow the appropriate branch, and keep going until you reach a leaf. The leaf's label is the prediction. That's the entire inference process — no matrix multiplication, no gradients, just a sequence of if-else checks.
Unlike every model seen so far, a trained decision tree can be written out as human-readable rules. A doctor can verify it. A judge can audit it. A regulator can approve it. This interpretability is why decision trees remain mandatory in healthcare, finance, and law — even when less interpretable models would be more accurate.
You're playing 20 Questions to guess an animal. You wouldn't start with "Is it a Siberian tiger?" — that wastes a question on one possibility out of millions. A smart player asks "Does it have a backbone?" — instantly eliminating 97% of all species. Then "Does it have fur?" Then "Does it lay eggs?"
Each question is chosen to split the remaining possibilities as cleanly as possible. A decision tree plays 20 Questions with your data — mathematically finding the most informative question at each step. The algorithm's job is to pick those questions.
02 — How the Tree Grows: Gini Impurity & Information Gain
At each node, the algorithm searches for the best split: which feature, and which threshold, produces the purest child nodes? "Purity" means one class dominates — the opposite of a 50/50 mix. Three concepts build on each other here, so walk through them in order:
Formula:
Gini = 1 − Σ(pᵢ²) where pᵢ is the fraction of class i in the node.Worked example. A node has 6 cats and 4 dogs (10 total):
p(cat) = 0.6 · p(dog) = 0.4
Gini = 1 − (0.6² + 0.4²) = 1 − (0.36 + 0.16) = 0.48 — quite mixed.
Two reference points to memorise:
— Pure node (all one class): Gini = 1 − (1.0²) = 0 — perfect, nothing to split.
— Maximally mixed (50/50 binary): Gini = 1 − (0.5² + 0.5²) = 0.5 — worst case.
IG = Gini(parent) − [ (n_left/n) × Gini(left) + (n_right/n) × Gini(right) ]Continuing the same example. We split the 10-animal node. Suppose:
— Left child: 5 cats & 1 dog (6 samples) → Gini = 1 − (0.833² + 0.167²) = 0.278
— Right child: 1 cat & 3 dogs (4 samples) → Gini = 1 − (0.25² + 0.75²) = 0.375
Weighted child impurity = (6/10) × 0.278 + (4/10) × 0.375 = 0.167 + 0.150 = 0.317
IG = 0.48 − 0.317 = 0.163
This split reduced impurity by 0.163. The tree evaluates every possible (feature, threshold) pair and picks the one with the highest Information Gain — the split that most cleans up the node.
Formula:
Entropy = −Σ(pᵢ · log₂(pᵢ))An information-theory measure of uncertainty rather than mislabelling probability. Pure node → Entropy = 0. Maximally mixed binary node → Entropy = 1. When used as the impurity measure inside Information Gain, this is the classic ID3 / C4.5 criterion.
Gini vs Entropy in practice: they produce nearly identical trees on virtually every real dataset. The difference is computational — Gini requires only squaring (fast), entropy requires a logarithm (slower). Gini is the sklearn default. Use
criterion="entropy" only if you specifically want the information-theoretic interpretation in your write-up or need to match a legacy system.
03 — The Tree-Growing Algorithm: Step by Step
The algorithm is recursive — it builds the tree by repeatedly asking "what is the best split I can make at this node?" until a stopping condition is met.
— The node is pure (all examples are the same class) → Gini = 0
—
max_depth is reached— The node has fewer than
min_samples_split examples— No split improves impurity (Information Gain = 0)
At a leaf, predict the majority class (classification) or the mean value (regression) of the examples that reached it.
This algorithm is greedy — it picks the locally best split at each step without considering what splits become possible further down the tree. A slightly worse split at the root might unlock dramatically better splits at depth 2 and 3, producing a better overall tree. Finding the globally optimal decision tree is NP-hard (computationally impossible for real datasets), so greedy is the pragmatic choice. It produces good trees, not perfect ones.
04 — The Overfitting Problem: Memorizing Noise
Left unconstrained, a decision tree will keep growing until every single training example has its own leaf — 100% training accuracy. This is the most textbook example of overfitting in all of machine learning.
On the breast cancer dataset (569 examples, 30 features):
Unlimited tree (depth 9):
Train accuracy: 100.0% — memorized every training example perfectly
Test accuracy: 93.9% — fell apart on new data
Gap: 6.1 percentage points lost to overfitting
Pruned tree (max_depth=4):
Train accuracy: 98.5% — slightly imperfect on training data
Test accuracy: 96.5% — generalizes far better
Gap: only 2 percentage points
The unlimited tree memorized noise — patient-specific quirks that don't generalize. The pruned tree learned the actual pattern. Giving up 1.5% training accuracy bought 2.6% better test accuracy.
05 — Pruning: Two Ways to Fight Overfitting
max_depth=4 → tree can ask at most 4 questions in a row. Start here.min_samples_leaf=5 → every leaf must contain at least 5 training examples. Prevents tiny, noisy leaves.min_samples_split=10 → a node must have at least 10 examples before it can be split.max_features → consider only a random subset of features at each split (also reduces variance).Recommended starting point:
max_depth=3 to visualize the tree, max_depth=5–7 for production. Use cross-validation to tune.
sklearn implements cost-complexity pruning via
ccp_alpha. Higher alpha = more aggressive pruning.ccp_alpha=0.0 → no pruning (default)ccp_alpha=0.01 → light pruningccp_alpha=0.05 → aggressive pruning, much simpler treeHow to choose: use
cost_complexity_pruning_path() to get all candidate alpha values, then pick the one with best cross-validation accuracy.Post-pruning is more principled than pre-pruning but slower — it must train the full tree first.
06 — From Scratch: A Minimal Decision Tree
Building a tree from scratch reveals the recursive structure that sklearn hides. This is the core logic in ~30 lines — no fancy optimizations, just the algorithm.
import numpy as np
from collections import Counter
def gini(y):
"""Gini impurity of a label array."""
n = len(y)
if n == 0: return 0
counts = Counter(y)
return 1 - sum((c / n) ** 2 for c in counts.values())
def best_split(X, y):
"""Find feature + threshold that gives the highest Information Gain."""
best_gain, best_feat, best_thresh = -1, None, None
parent_gini = gini(y)
for feat in range(X.shape[1]): # each feature
thresholds = np.unique(X[:, feat])
for t in thresholds: # each threshold
left = y[X[:, feat] <= t]
right = y[X[:, feat] > t]
if len(left) == 0 or len(right) == 0: continue
# Information Gain = parent impurity − weighted child impurity
n = len(y)
gain = parent_gini - (len(left)/n * gini(left) + len(right)/n * gini(right))
if gain > best_gain:
best_gain, best_feat, best_thresh = gain, feat, t
return best_feat, best_thresh, best_gain
def build_tree(X, y, depth=0, max_depth=3):
"""Recursively build a decision tree. Returns a dict node."""
# Stopping conditions
if depth >= max_depth or len(set(y)) == 1:
return {'leaf': Counter(y).most_common(1)[0][0]}
feat, thresh, gain = best_split(X, y)
if gain <= 0: # no useful split exists
return {'leaf': Counter(y).most_common(1)[0][0]}
mask = X[:, feat] <= thresh
return {
'feature': feat, 'threshold': thresh, 'gain': round(gain, 3),
'left' : build_tree(X[mask], y[mask], depth+1, max_depth),
'right': build_tree(X[~mask], y[~mask], depth+1, max_depth),
}
def predict_one(node, x):
if 'leaf' in node: return node['leaf']
if x[node['feature']] <= node['threshold']:
return predict_one(node['left'], x)
return predict_one(node['right'], x)
# Quick test on a toy dataset
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
tree = build_tree(X_tr, y_tr, max_depth=3)
preds = [predict_one(tree, x) for x in X_te]
accuracy = np.mean(np.array(preds) == y_te)
print(f"From-scratch tree accuracy: {accuracy:.1%}")
print(f"Root split: feature {tree['feature']} ≤ {tree['threshold']} (gain={tree['gain']})")
93.3% accuracy — from 30 lines of pure Python, no libraries. The root split is on feature 3 (petal width) at threshold 0.8 cm, giving Information Gain of 0.333. This is the single question that separates Iris setosa (petal width ≤ 0.8) from the other two species — almost perfectly. That one question alone classifies a third of the dataset with zero error.
The recursive structure is the key insight: build_tree calls itself on the left and right subsets independently. The right side doesn't know anything about the left side — each branch solves its own sub-problem.
07 — sklearn: Overfitting vs Pruned + Feature Importance
The sklearn version handles all optimizations and exposes the full comparison — overfitting vs pruning — plus feature importance scores.
from sklearn.tree import DecisionTreeClassifier, export_text
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import pandas as pd
data = load_breast_cancer()
X, y = data.data, data.target
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
# ── Unlimited tree: maximum overfitting ───────────────────────────────
t1 = DecisionTreeClassifier(random_state=42).fit(X_tr, y_tr)
print(f"Unlimited │ train: {t1.score(X_tr,y_tr):.1%} test: {t1.score(X_te,y_te):.1%} depth: {t1.get_depth()}")
# ── Pre-pruned (max_depth=4): better generalization ───────────────────
t2 = DecisionTreeClassifier(max_depth=4, random_state=42).fit(X_tr, y_tr)
print(f"max_depth=4 │ train: {t2.score(X_tr,y_tr):.1%} test: {t2.score(X_te,y_te):.1%}")
# ── Feature importance: which features matter most? ───────────────────
# Each score = total Gini reduction from all splits using this feature,
# weighted by number of samples, normalized so all scores sum to 1.0
imp = pd.Series(t2.feature_importances_, index=data.feature_names).sort_values(ascending=False)
print("\nTop 5 features by importance:")
print(imp.head(5).to_string())
# ── Human-readable rules for the first 2 levels ───────────────────────
print("\nTree rules (depth ≤ 2):")
print(export_text(t2, feature_names=list(data.feature_names), max_depth=2))
Feature importance scores sum to 1.0. "worst radius" alone accounts for 72.1% of all impurity reduction — it's by far the most powerful predictor. This matches medical knowledge: tumor radius is a primary diagnostic indicator for malignancy.
The rules are auditable. You can read the tree and immediately understand: if worst radius ≤ 16.80 AND worst concave points ≤ 0.14 → classify as benign. A radiologist can verify this logic. A lawyer can challenge it. This is what interpretability actually means in practice.
The overfitting gap: The unlimited tree memorized every quirk in the training data (100% train) but paid a 6.1% penalty on test data. The pruned tree gave up only 1.5% training accuracy and gained 2.6% test accuracy. That trade is almost always worth making.
08 — Feature Importance: The Free Gift
Every trained decision tree delivers feature importance scores as a byproduct. The formula for each feature's importance is:
Importance(feature) = Σ [ (nₜ/n) × ΔGini(t) ] over all nodes t that split on this feature
Where nₜ is the number of samples at node t, n is total training samples, and ΔGini(t) is the Gini reduction from the split. Features used at high-sample, high-impurity nodes (early in the tree) get higher scores. All scores are normalized to sum to 1.0.
Decision trees are the foundation of two of the most powerful ML algorithms that exist: Random Forests (Day 11) — which trains hundreds of trees on random data subsets and aggregates their votes — and XGBoost / Gradient Boosting (Day 13) — which trains trees sequentially, each one correcting the errors of the previous. These ensemble methods dominate tabular data competitions and power recommendation systems at Uber, Airbnb, and Spotify.
SHAP values — the current industry standard for explaining any ML model's predictions — were originally developed in the context of tree ensembles. If you work in ML, you will use SHAP. Understanding the decision tree is understanding where it came from.
09 — A Thought to Sit With
A decision tree is greedy — it picks the locally best split at each step. This doesn't guarantee a globally optimal tree. A different first split might enable dramatically better splits at depth 2 and 3, producing a better overall model. Finding the globally optimal decision tree is NP-hard — computationally impossible for real datasets. So the greedy algorithm is what we use.
Does "locally optimal at each step, globally imperfect" remind you of any other decision-making process — in human organizations, in nature, in life? What does this tell you about the limits of optimization?
10 — Coming Up Next
Tomorrow — Day 9: Bias, Variance & the Tradeoff. The most important concept in all of machine learning. Every model, every hyperparameter, every design choice moves a single dial between two failure modes. Understanding this one idea explains almost every phenomenon you've seen so far — including why the unlimited tree failed and the pruned tree succeeded.
11 — Go Deeper
▶ StatQuest: Decision Trees, Clearly Explained
YouTube · 18 min · The best visual explanation of Gini impurity, Information Gain, and how a tree grows step by step.
▶ Kaggle: Intro to Machine Learning
Free interactive course · Decision trees are the first model in this course — apply them to real datasets in your browser with immediate feedback.
▶ Coursera: Deep Learning Specialization — Andrew Ng
5-course specialization · Covers bias-variance tradeoff rigorously — the concept that explains every overfitting and underfitting phenomenon you've seen across the series. Audit free.
- Structure — internal nodes (questions), branches (answers), leaves (predictions). Classify by traversing root to leaf.
- Gini impurity — 1 − Σ(pᵢ²). Measures how mixed a node is. 0 = pure, 0.5 = maximally mixed (binary).
- Information Gain — parent impurity − weighted child impurity. The tree picks the split with the highest gain at each node.
- Greedy algorithm — picks the locally best split at each step. Fast, but not globally optimal. Finding the globally optimal tree is NP-hard.
- Overfitting — unlimited trees memorize noise (100% train, poor test). The most textbook overfitting example in ML.
- Pre-pruning — max_depth, min_samples_leaf, min_samples_split. Set limits before training. Fast, simple.
- Post-pruning — grow full tree, then trim via ccp_alpha. More principled, slower.
- Feature importance — weighted Gini reduction per feature, normalized to sum to 1.0. Free, fast, but biased toward high-cardinality features.
- Interpretability — the entire model can be printed as human-readable if-else rules. Unique among ML models. Mandatory in healthcare and law.
- Foundation — building block for Random Forests and XGBoost, two of the most powerful ML algorithms for tabular data.
- How does a decision tree choose which feature to split on?
It exhaustively evaluates every (feature, threshold) combination, computes Information Gain for each, and selects the split that most reduces impurity. For N features each with K unique values, that's N×K candidates evaluated at every node. - What is the difference between Gini impurity and entropy?
Both measure node impurity. Gini = 1 − Σ(pᵢ²); entropy = −Σ(pᵢ·log₂pᵢ). Both range from 0 (pure) to their maximum at equal class distribution. They produce nearly identical trees. Gini is faster (no logarithm); entropy gives the Information Gain interpretation. Gini is the sklearn default. - Why do decision trees overfit so easily?
Without constraints, the tree grows until every training example has its own leaf — zero training error. It memorizes every outlier and noise point. A new example that falls between training examples lands in a leaf built for a quirk, not a pattern. Fix: max_depth, min_samples_leaf, or post-pruning via ccp_alpha. - What is feature importance in a decision tree and how is it calculated?
Importance(feature) = Σ[(nₜ/n) × ΔGini(t)] over all nodes that split on this feature, normalized to sum to 1.0. Features used at high-sample, high-impurity nodes early in the tree get higher scores. Biased toward high-cardinality features — use SHAP for more reliable attribution. - What is the difference between pre-pruning and post-pruning?
Pre-pruning stops growth during training via constraints (max_depth, min_samples_leaf). Fast, but you might stop too early. Post-pruning grows the full tree then removes non-improving branches via ccp_alpha. More principled, finds the optimal pruning level, but must train the full tree first.
No attachments for this topic yet.