🌙 30-Day AI Ramadan Series  ·  Day 8 of 30
Deep Dive · Feb 26, 2026 · 16 min read · ML Models

Decision Trees:
The Model You Can Read Like a Book

Decision trees split data by asking yes/no questions — and unlike almost every other ML model, you can read exactly why it made any prediction. That transparency has a price: overfitting.

← Back to Blog ☰ Categories

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:

Internal node
A question about one feature: "Is temperature > 38.5°C?" or "Is income < 50,000?" The tree asks one question per node, about one feature, at one threshold.
Branch
The answer to the question — Yes (go left) or No (go right). Every internal node splits into exactly two branches.
Leaf node
The final prediction at the end of a path. For classification: the most common class among training examples that reached this leaf. For regression: their average value.

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.

🎮 Analogy: 20 Questions

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:

1
Gini Impurity — "how mixed is this node right now?"

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.
The tree wants to minimise Gini in the children it creates. A split that produces two pure leaves (Gini = 0 each) is perfect. A split that produces two 50/50 leaves is useless.
2
Information Gain — "how much did a specific split actually help?"

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.
IG = 0 means the split was completely useless — children are no purer than the parent. IG = parent's Gini means the split created two perfectly pure children — the maximum achievable.
3
Entropy — the alternative impurity measure

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.

1
Start at the root node with all training examples. Compute the node's current impurity (Gini or entropy).
2
Search for the best split. For every feature, and for every possible threshold value of that feature, compute the Information Gain of splitting there. This is an exhaustive search — if you have 10 features each with 100 unique values, that's 1,000 candidate splits evaluated at each node.
For continuous features, sklearn sorts the values and tries every midpoint between adjacent values as a threshold. That's why trees are slower to train on high-cardinality continuous features.
3
Make the best split. Divide the current node's examples into two groups based on the winning feature and threshold. Examples where feature ≤ threshold go left; the rest go right.
4
Recurse on each child node. Repeat steps 1–3 independently for the left child and the right child. Each child only sees the examples that reached it.
5
Stop when 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.
⚠️ The greedy caveat

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.

🔢 The numbers make it vivid

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

Pre-pruning — stop early during growth
Set hard limits before training begins. The tree stops growing when any limit is hit.

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.
Post-pruning — grow first, trim after
Grow the full unlimited tree, then remove branches that don't sufficiently improve validation performance.

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 pruning
ccp_alpha=0.05  → aggressive pruning, much simpler tree

How 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']})")
→ output
From-scratch tree accuracy: 93.3% Root split: feature 3 ≤ 0.8 (gain=0.333)
💡 Reading the output

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))
→ output
Unlimited │ train: 100.0% test: 93.9% depth: 9 max_depth=4 │ train: 98.5% test: 96.5% Top 5 features by importance: worst radius 0.7211 worst concave points 0.1409 mean concave points 0.0721 worst perimeter 0.0431 mean radius 0.0228 Tree rules (depth ≤ 2): |--- worst radius <= 16.80 | |--- worst concave points <= 0.14 | | |--- class: 1 (benign) | |--- worst concave points > 0.14 | | |--- class: 0 (malignant) |--- worst radius > 16.80 | |--- worst texture <= 19.11 | | |--- class: 1 (benign) | |--- worst texture > 19.11 | | |--- class: 0 (malignant)
💡 Reading the output

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.

Use case: feature selection
Train a tree on all features, read the importance scores, drop features with near-zero importance. Often reduces model complexity without hurting accuracy — and exposes which features your model actually relies on.
Use case: data leakage detection
If a feature has suspiciously high importance (say, 90%+ alone), investigate. It might be a target-correlated variable that wouldn't be available at real prediction time — a data leakage bug that would destroy production performance.
Limitation: biased toward high-cardinality features
Features with many unique values have more candidate thresholds and therefore more chances to appear important — even by chance. If you have a mix of binary and continuous features, tree-based importance scores may be misleading. SHAP values (permutation-based) are more reliable in this case.
🔗 Connection to Modern AI

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

💬 For You to Consider

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.

🧠 Check Your Understanding
1. A node contains 8 examples of class A and 2 of class B. What is its Gini impurity? What would it be for a pure node?
p(A) = 8/10 = 0.8, p(B) = 2/10 = 0.2. Gini = 1 − (0.8² + 0.2²) = 1 − (0.64 + 0.04) = 1 − 0.68 = 0.32. This node is fairly impure — 20% of examples are a minority class. For a pure node (all one class): p = 1.0, Gini = 1 − (1.0²) = 1 − 1 = 0. A Gini of 0 means the node is perfectly pure and no split can improve it further — it becomes a leaf.
2. What is Information Gain and how does a decision tree use it to choose splits?
Information Gain = parent node impurity − weighted average impurity of child nodes after the split. It measures how much uncertainty is reduced by making a particular split. The tree evaluates every possible (feature, threshold) combination, computes the Information Gain for each, and selects the split with the highest gain. A gain of 0 means the split doesn't help at all. A gain equal to the parent impurity means the split produces perfectly pure children — the maximum possible gain. The algorithm is exhaustive: for N features each with K unique values, it evaluates N×K candidate splits at every node.
3. An unlimited decision tree achieves 100% training accuracy but only 87% test accuracy. Explain what happened and how you would fix it.
The tree overfitted. Without constraints, it grew deep enough to give every training example its own leaf — memorizing all noise, outliers, and quirks of the training set rather than learning the underlying pattern. On new data, these memorized details are wrong, causing the large accuracy drop. To fix it, apply pruning: set max_depth (e.g. 3–7) to limit how deep the tree grows; set min_samples_leaf (e.g. 5–10) to prevent tiny leaves built on just a few noisy points; or use post-pruning via ccp_alpha to remove branches that don't improve validation performance. The right fix is to tune these hyperparameters using cross-validation.
4. What does a feature importance score of 0.72 mean, and what is one situation where you should not trust it?
A score of 0.72 means that feature was responsible for 72% of the total Gini impurity reduction across all splits in the tree. It is by far the most informative feature the tree found — removing it would dramatically hurt accuracy. One situation not to trust it: when the feature has many unique values (high cardinality). Features with more unique values have more candidate thresholds, so they get more "chances" to appear important even if their true predictive power is modest. A binary feature (only 2 values) can only appear important if it genuinely reduces impurity. A continuous feature with 1000 unique values might appear important just by finding one lucky threshold. Use SHAP permutation importance as a more reliable alternative in mixed-feature settings.
5. What is the difference between pre-pruning and post-pruning? Give one concrete sklearn parameter for each.
Pre-pruning (early stopping) sets hard constraints before training. The tree stops growing whenever a limit is hit — it never builds branches that will be removed. Example: max_depth=4 prevents the tree from asking more than 4 questions in any path. Also: min_samples_leaf=5 prevents any leaf from having fewer than 5 examples. Post-pruning lets the tree grow fully, then removes branches that don't improve validation performance. It's more principled because it sees the full tree before deciding what to remove. Example: ccp_alpha=0.01 in sklearn applies cost-complexity pruning — higher values remove more branches. Post-pruning produces better results but is slower because it trains the full tree first. In practice, pre-pruning (max_depth) is used first for quick experimentation; post-pruning via cross-validated ccp_alpha is used when you want the most accurate possible pruned tree.
⚡ Quick Recap — Decision Trees
  1. Structure — internal nodes (questions), branches (answers), leaves (predictions). Classify by traversing root to leaf.
  2. Gini impurity — 1 − Σ(pᵢ²). Measures how mixed a node is. 0 = pure, 0.5 = maximally mixed (binary).
  3. Information Gain — parent impurity − weighted child impurity. The tree picks the split with the highest gain at each node.
  4. Greedy algorithm — picks the locally best split at each step. Fast, but not globally optimal. Finding the globally optimal tree is NP-hard.
  5. Overfitting — unlimited trees memorize noise (100% train, poor test). The most textbook overfitting example in ML.
  6. Pre-pruning — max_depth, min_samples_leaf, min_samples_split. Set limits before training. Fast, simple.
  7. Post-pruning — grow full tree, then trim via ccp_alpha. More principled, slower.
  8. Feature importance — weighted Gini reduction per feature, normalized to sum to 1.0. Free, fast, but biased toward high-cardinality features.
  9. Interpretability — the entire model can be printed as human-readable if-else rules. Unique among ML models. Mandatory in healthcare and law.
  10. Foundation — building block for Random Forests and XGBoost, two of the most powerful ML algorithms for tabular data.
🎯 Interview Questions
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
📎 Attachments & Further Reading

No attachments for this topic yet.

#DecisionTree#GiniImpurity#Pruning#Overfitting#FeatureImportance#MLModels#30DaysOfAI#Ramadan2026
← Day 7 Naive Bayes