The Most Interpretable Machine Learning Model
Department of Computer Science
University of the Philippines Cebu
Lecture 15: CART, Pruning & Interpretation
Learn the CART algorithm and how it splits data using Gini impurity and entropy.
Read tree visualizations and extract business rules from trained models.
Use pre-pruning and post-pruning to prevent overfitting.
Recognize when decision trees outperform other methods.
Three properties that make decision trees uniquely valuable in data analytics.
Visual structure mirrors human decision-making. Non-technical stakeholders can understand the model.
Handles non-linear relationships without feature transformations or polynomial terms.
No feature scaling required. Automatic feature selection. Handles mixed data types natively.
A decision tree is a flowchart of binary decisions, splitting data at each node until reaching a prediction.
A greedy, recursive algorithm that finds the best binary split at each node.
Greedy = picks the best split right now without looking ahead. Fast, but not guaranteed globally optimal.
Binary Split = every node asks one yes/no question (e.g., "Age ≤ 35?"). Data always splits into exactly two groups.
Greedy = picks the best split right now without looking ahead — like choosing the highest-scoring Scrabble move without planning 5 moves ahead. Finding the globally optimal tree is NP-complete, so greedy + pruning is the practical approach.
Given 6 loan applicants, CART evaluates every possible split and picks the one with the lowest weighted Gini.
| Age | Income | Savings? | Approved? |
|---|---|---|---|
| 25 | ₱30K | Yes | Yes |
| 45 | ₱80K | No | No |
| 35 | ₱50K | Yes | Yes |
| 22 | ₱20K | No | No |
| 50 | ₱90K | Yes | Yes |
| 28 | ₱35K | No | No |
Try 1: "Age ≤ 30?" → Left: [Y, N, Y], Right: [N, Y, N] → Weighted Gini = 0.444
L(3): 1−(0.667²+0.333²)=0.444 R(3): same Weighted: (3/6)×0.444+(3/6)×0.444=0.444
Try 2: "Income ≤ ₱50K?" → Left: [Y, N, Y, N], Right: [N, Y] → Weighted Gini = 0.333
Try 3: "Has Savings?" → Left: [Y, Y, Y], Right: [N, N, N] → Weighted Gini = 0.000 ✓
CART picks "Has Savings?" because it produces the lowest weighted Gini. Pure leaves = perfect separation. This is what greedy means — pick the best split NOW.
Weighted Gini: We weight each child's Gini by its proportion of samples: (nleft/n) × Ginileft + (nright/n) × Giniright. CART picks the split that minimizes this value. A weighted Gini of 0 means a perfect split.
For each feature, CART sorts the values and evaluates every midpoint between consecutive values as a candidate threshold.
Income: 5 thresholds + Age: 5 thresholds + Credit: 5 thresholds + Savings: 1 threshold = 16 candidate splits. CART picks the single best: Income ≤ 42.5K (Gini = 0.00). This repeats at every node.
O(p × n log n) per node
p features × sort + scan
What about categorical features? For binary (Yes/No), there's only 1 possible split. For k categories, there are 2k−1−1 possible partitions — exponential! For binary classification, a shortcut exists: sort categories by % positive class and try k−1 ordered splits.
function BuildTree(D, depth):
// Base cases
if all samples in D have same class:
return Leaf(class)
if depth == max_depth or |D| < min_split:
return Leaf(majority_class(D))
// Find best split (brute force)
best_gain = 0
for each feature f in features:
for each threshold t in unique(D[f]):
D_left = {x ∈ D : x[f] ≤ t}
D_right = {x ∈ D : x[f] > t}
gain = Gini(D) - weighted_avg(
Gini(D_left), Gini(D_right))
if gain > best_gain:
best_gain, best_f, best_t = gain, f, t
// Recurse
left = BuildTree(D_left, depth + 1)
right = BuildTree(D_right, depth + 1)
return Node(best_f, best_t, left, right)
This is a greedy recursive algorithm — similar to quicksort's partition step. The "best split" search is an exhaustive scan, not dynamic programming. That's why single trees can't guarantee a globally optimal structure.
CS Connection: The O(n·p·log n) per node comes from sorting each of p features (O(n log n)) then scanning n thresholds. Same bottleneck as quicksort. Total tree training: O(n²·p·log n) worst case.
Let's trace CART on a tiny loan dataset (6 applicants).
| ID | Income | Credit | Approved? |
|---|---|---|---|
| 1 | 80K | 720 | Yes |
| 2 | 35K | 580 | No |
| 3 | 60K | 690 | Yes |
| 4 | 25K | 620 | No |
| 5 | 70K | 550 | No |
| 6 | 55K | 710 | Yes |
Step 1: All 6 at root (3 Yes, 3 No) → Gini = 0.5
Step 2: Try all splits. Best: Credit > 650
Step 3: Left child: {1,3,6} → all Yes → Gini = 0 (pure!)
Step 4: Right child: {2,4,5} → all No → Gini = 0 (pure!)
Step 5: Both leaves pure → stop
Gini = 1 − Σ pi²
Why p²? pi² = chance two random picks are both class i. So 1−Σpi² = chance they disagree = impurity.
Example: 8 samples: 6 cats, 2 dogs. pcat=0.75, pdog=0.25.
Gini = 1 − (0.75² + 0.25²) = 1 − 0.625 = 0.375
Entropy = −Σ pi log2(pi)
Example: Same node. Entropy = −(0.75 × log20.75 + 0.25 × log20.25)
= −(−0.311 + −0.500) = 0.811
Gini vs Entropy: They produce the same tree ~95% of the time. sklearn defaults to Gini because it's faster (polynomial vs logarithm). The only case entropy might differ: multi-class with heavily imbalanced classes.
1. Parent node: 10 samples (6 Yes, 4 No)
Gini = 1 − (0.6² + 0.4²) = 1 − 0.52 = 0.48
2. Split "Age ≤ 30":
Left child (4 samples): 3 Yes, 1 No → Gini = 1 − (0.75² + 0.25²) = 0.375
Right child (6 samples): 3 Yes, 3 No → Gini = 1 − (0.5² + 0.5²) = 0.500
3. Weighted Gini:
(4/10) × 0.375 + (6/10) × 0.500 = 0.15 + 0.30 = 0.45
4. Information Gain = Parent impurity − Weighted child impurity — how much a split reduces disorder.
0.48 − 0.45 = 0.03 (small improvement — CART tries other splits to find a better one)
from sklearn.tree import DecisionTreeClassifier, plot_tree
clf = DecisionTreeClassifier(
max_depth=3,
random_state=42
)
# X_train = feature matrix (rows=samples, cols=features)
# y_train = labels (the class we are predicting)
clf.fit(X_train, y_train)
# Visualize the trained tree
plot_tree(clf,
feature_names=features,
class_names=['Reject', 'Approve'],
filled=True, rounded=True)
Output → The tree diagram on the right. Each node shows the split condition, Gini, sample count, and class distribution.
Each node in a sklearn tree visualization shows five key pieces of information.
Feature & thresholdAge <= 35.5
Impurity measuregini = 0.42
Training examplessamples = 150
Class distributionvalue = [30, 70]
Majority predictionclass = Yes
A fully grown tree is a lookup table, not a model. It memorizes every training sample — perfect training accuracy, terrible generalization. This is the single biggest limitation of decision trees and the reason pruning exists.
Unconstrained decision trees memorize training data, creating overly complex structures that fail to generalize.
Constrain the tree during training with hyperparameters.
Absolute depth limit. Start: 5–10
Min samples to attempt split. Start: 10–20
Min samples for a valid leaf. Start: 5–10
Max features per split. Start: ‘sqrt’
Grow a full tree, then prune branches that don't improve generalization.
Rα(T) = R(T) + α|T|
Example: R(T)=0.15, |T|=8, α=0.01 → Cost = 0.15 + 0.01×8 = 0.23.
Prune to 4 leaves: R(T)=0.18, Cost = 0.18 + 0.01×4 = 0.22 (lower → prune!)
CS Connection: The α parameter is like weight decay (L2 regularization) in neural networks — it penalizes model complexity. Higher α = simpler tree, just like higher λ = smaller weights.
Importance = total reduction in impurity contributed by each feature across all splits.
Each split reduces impurity. Features used in higher, more impactful splits accumulate greater importance scores.
Caution: MDI (Gini-based) importance is biased toward features with many unique values — they get more split opportunities. Permutation importance (shuffle & measure accuracy drop) is more reliable. We'll see this in Random Forest.
Trees aren't always the answer, but in these three scenarios they often outperform.
Excel-style data with mixed types (categorical + continuous). Neural networks struggle here.
Finance, healthcare, insurance — interpretability is legally required.
Trees excel with limited data. Neural networks need massive datasets to generalize.
| Decision Trees / Ensembles | Deep Learning | |
|---|---|---|
| Tabular data | Excellent — handles mixed types natively | Needs heavy feature engineering, embedding layers |
| Sample size | Works with 100s–1000s of samples | Needs 10K+ to generalize |
| Training time | Seconds to minutes (CPU) | Minutes to hours (GPU required) |
| Interpretability | Fully transparent (extract rules) | Black box (needs SHAP/LIME) |
| Images / Text / Audio | Not applicable | Dominant — CNNs, Transformers |
Research evidence: Grinsztajn et al. 2022 (NeurIPS) benchmarked 45 tabular datasets — tree ensembles consistently beat deep learning. Three reasons: robust to uninformative features, preserve axis-aligned structure, handle non-smooth boundaries.
Same tree structure, but leaf values are means instead of class labels. Splits minimize MSE within each partition.
Classification: leaf = majority class vote.
Regression: leaf = mean of training samples in
that partition.
Example: A leaf contains 5 houses priced at: ₱2M, 2.5M, 3M, 2.8M, 2.2M
Prediction = mean = (2 + 2.5 + 3 + 2.8 + 2.2) / 5 = ₱2.5M
MSE: CART finds the cut that minimizes price variance in each child group. Lower variance = more similar houses grouped together.
Trees cannot extrapolate. If training house prices range ₱1M–₱5M, the tree will never predict ₱6M. Leaf values are always within the training range. This is a fundamental limitation vs. linear models or neural networks.
Build an interpretable loan approval model using Philippine bank data. Extract human-readable rules for credit officers.
Decision trees let loan officers explain why an application was approved or rejected — critical for BSP compliance.
Convert the trained tree into actionable IF-THEN rules for stakeholders.
|--- credit_score <= 650 | |--- income <= 250k | | |--- class: Reject | |--- income > 250k | | |--- emp_years <= 2 | | | |--- class: Review | | |--- emp_years > 2 | | | |--- class: Approve |--- credit_score > 650 | |--- class: Approve
Credit score > 650 = Fast-track auto-approve. No need to check other features.
Small changes in training data produce very different trees.
Only perpendicular cuts. Struggles with diagonal decision boundaries.
Makes locally optimal splits, may miss globally optimal structure.
Combine many trees into Ensemble Methods (next lecture).
Key Insight: These four weaknesses are exactly what ensemble methods fix. High variance → averaging (RF). Greedy → sequential correction (Boosting). Axis-aligned → feature combinations across many trees.
A Decision Tree predicting loan defaults achieves 99.8% Training Accuracy but only 62% Validation Accuracy. What is the most likely cause?
Click to reveal answer
Correct: C (Overfitting)
A massive gap between training and validation accuracy is the textbook definition of overfitting. The tree has grown too deep and memorized specific training examples. Solution: Prune it!
1. Decision trees are the most interpretable ML model — stakeholders can read the rules directly.
2. CART uses Gini impurity or entropy to find the best binary split at each node.
3. Pruning (pre & post) prevents overfitting by constraining tree complexity.
4. Single trees have high variance — next lecture: ensemble methods fix this.
Combine many decision trees to overcome the limitations of a single tree.
pip install xgboost lightgbm shapCombining Trees for Superior Performance
Department of Computer Science
University of the Philippines Cebu
Lecture 16: Ensembles, XGBoost & SHAP
Understand bagging, boosting, and stacking paradigms.
Apply Random Forest for robust, low-variance predictions.
Use XGBoost and LightGBM for state-of-the-art tabular ML.
Explain individual model predictions with SHAP values.
Ensemble (French for “together”) = train multiple models and combine their predictions. Like asking 100 doctors instead of 1 — the group answer is usually better than any individual.
Train on random data subsets (with replacement), then average predictions. Reduces variance.
Train models sequentially — each one fixes mistakes of the previous. Reduces bias.
Train different model types, then a “meta-model” learns the best way to combine them.
Analogy: Bagging = ask 100 random people and take a majority vote. Boosting = ask one expert, then ask a second expert specifically about what the first got wrong, then a third about remaining errors.
E[Error] = Bias2 + Variance + σ2irreducible
Bias = error from oversimplifying. A straight line fitting curved data.
Variance = error from being too sensitive. Tiny data changes → wildly different predictions.
σ² = noise in the data itself. No model can remove it.
| Method | Bias | Variance | Strategy |
|---|---|---|---|
| Single Tree | Low | High | Memorizes (Overfits) |
| Bagging | Low | Reduced | Averages many trees |
| Boosting | Reduced | Low | Corrects errors |
CS Connection: Dropout in neural networks is a form of bagging — each forward pass uses a random subnetwork. Residual connections (skip connections) are additive like boosting. Transformer ensembles combine both ideas.
Build many trees on different bootstrap samples, with random feature subsets at each split.
m = √p (classification) | m = p/3 (regression)
m = features per split, p = total features
Bootstrap sample: Draw n rows with replacement from n rows. Some rows repeat; ~37% are never selected — those become the OOB validation set.
More trees never causes overfitting in RF — it just converges. This is the opposite of boosting, where too many rounds can overfit. RF is "embarrassingly parallel" — train all trees on separate CPU cores with n_jobs=-1.
function RandomForest(D, B, m):
// D = dataset, B = num trees, m = features/split
trees = []
for b = 1 to B: // parallelizable!
// Bootstrap sample (sample n with replacement)
D_b = BootstrapSample(D)
// Grow tree with feature randomization
tree_b = BuildTreeRF(D_b, m)
trees.append(tree_b)
return trees
function BuildTreeRF(D, m):
// Same as CART, but at each node:
F_subset = RandomSample(features, size=m)
// Only search F_subset for best split
// (not all features)
... // rest is identical to CART
function Predict(x, trees):
votes = [tree.predict(x) for tree in trees]
return majority_vote(votes) // classification
// or mean(votes) for regression
n_jobs=-1The bootstrap + feature subset creates decorrelated estimators. This is the same principle behind randomized algorithms — injecting randomness reduces systematic error. The m = √p heuristic balances diversity vs. individual tree quality.
Classification default. For p = 100 features, each split only considers 10. This forces trees to explore different paths.
How many trees? 100–500 is typical. More trees is never worse (won't overfit!) — RF just converges. Proof: ensemble variance = ρσ²+(1−ρ)σ²/B. As B→∞, second term vanishes. Diminishing returns after ~300.
With 4 features and m = √4 = 2 random features per split, each tree builds a different model:
Tree 1 gets: {Income, Savings}
Best split: Savings = Yes → Gini = 0.00 (perfect!)
Tree 2 gets: {Age, Credit}
Best split: Credit > 650 → Gini = 0.00 (perfect!)
Tree 3 gets: {Income, Age}
Best split: Income > 42.5K → Gini = 0.28 (weaker)
Tree 3 made a weaker split because it didn't see Savings or Credit. But in the ensemble, the majority vote corrects individual errors — this is how decorrelation reduces variance.
Correlated trees don't help. If one feature dominates (e.g., income), all trees will use it at the root — producing near-identical trees.
Force each split to choose from a random subset of features. This creates diverse, uncorrelated trees whose errors cancel out.
'sqrt' — for classification'log2' or 1/3 — for regressionMore diversity = better ensemble. Random subsets force each tree to learn different patterns.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_estimators=200, # 200 trees
max_features='sqrt', # sqrt(p) features/split
oob_score=True, # free validation
n_jobs=-1, # all CPU cores
random_state=42
)
rf.fit(X_train, y_train)
print(f"OOB Score: {rf.oob_score_:.3f}")
Output: OOB converges around 0.92 (see right). Diminishing returns after ~200 trees.
| Parameter | Default | Recommended Range |
|---|---|---|
n_estimators | 100 | 200–500 (more is safe, just slower) |
max_depth | None (full) | 10–30 or None |
max_features | 'sqrt' | 'sqrt' (classification), 0.33 (regression) |
min_samples_leaf | 1 | 5–20 (reduces overfitting) |
n_jobs=-1 uses all CPU cores — RF is "embarrassingly parallel" since every tree is independent. A 200-tree RF on 8 cores trains ~8× faster.
Built-in, fast, but biased toward high-cardinality features (high-cardinality = many unique values, e.g. ZIP codes).
rf.feature_importances_ — sum of Gini decreases across all trees where the feature is used.
More reliable. Measures accuracy drop when feature values are shuffled.
permutation_importance(rf, X_test, y_test) — model-agnostic, works with any estimator.
MDI for quick screening during development. Permutation for final reports — it’s unbiased and works on any model, not just trees.
Each tree is trained on ~63% of data (bootstrap sample). The remaining ~37% can be used as a free validation set.
P(not selected) = (1 − 1/n)n ⟶ 1/e ≈ 0.368
~63.2% in-bag • ~36.8% out-of-bag (free validation)
No need for a separate validation set! OOB score approximates test performance without holding out data.
The 63.2% proof: P(sample drawn at least once in n draws) = 1 − (1−1/n)n → 1 − 1/e ≈ 0.632 as n→∞. So ~37% of data is "free" validation per tree. OOB replaces cross-validation, but always keep a separate test set.
Each new tree focuses on the mistakes of the previous ensemble.
Weak learner = a model barely better than random guessing (typically a depth-1 tree, called a “stump”).
Residual = actual − predicted. If actual = 100 and predicted = 85, residual = 15.
Bagging = parallel, reduces variance. Boosting = sequential, reduces bias.
Predicting a house price. Actual = ₱3.0M
| Iter | What Tree Learns | Output | Running Total |
|---|---|---|---|
| 0 | Start: mean of all prices | — | ₱2.0M |
| 1 | Residual: 3.0 − 2.0 = 1.0M | +0.8M | ₱2.8M |
| 2 | Residual: 3.0 − 2.8 = 0.2M | +0.15M | ₱2.95M |
| 3 | Residual: 3.0 − 2.95 = 0.05M | +0.04M | ₱2.99M |
Each tree corrects the remaining error. After just 3 iterations: ₱2.0M → ₱2.99M — almost perfect!
Notice: Output < Residual each round. That gap is the learning rate (γ) — it scales how much each tree contributes. (Defined formally on the next slide.)
Why learning_rate = 0.1 beats 1.0: With lr=1.0, each tree fully corrects errors → memorizes noise in ~10 trees. With lr=0.1, each tree only nudges the prediction by 10% → needs ~100 trees but generalizes better. Same tradeoff as step size in gradient descent.
Each new tree only predicts the leftover errors. Final prediction = sum of all trees' contributions.
Fm(x) = Fm−1(x) + γm hm(x)
Example: Old prediction = 85, new tree says error = +12, γ = 0.1 → new prediction = 85 + 0.1 × 12 = 86.2
CS Connection: This is literally gradient descent but in function space. Instead of updating weight parameters θ, we add entire functions (trees). The learning rate γ is the step size.
Why shallow trees (depth 3–6)? Depth d captures up to d-way feature interactions. Depth 1 = additive model. Depth 3 = up to 3-way interactions. Deeper trees overfit to current residuals.
function GradientBoosting(D, M, γ, L):
// D=data, M=num trees, γ=learning rate, L=loss
// Step 1: Initialize with constant
F_0(x) = argmin_c Σ L(y_i, c)
// (mean for regression, log-odds for classification)
for m = 1 to M:
// Step 2: Compute pseudo-residuals
r_i = -∂L(y_i, F_{m-1}(x_i)) / ∂F_{m-1}(x_i)
// (negative gradient of loss function)
// Step 3: Fit a shallow tree to residuals
h_m = BuildTree({(x_i, r_i)}, max_depth=3..6)
// Step 4: Update model (with learning rate)
F_m(x) = F_{m-1}(x) + γ · h_m(x)
return F_M
This is functional gradient descent — instead of updating weights like in neural networks, we add entire functions (trees) to minimize the loss. The learning rate γ is the step size. Small γ + many trees = smoother descent, less overfitting. Same convergence tradeoff as gradient descent in optimization courses.
RF trees are deep and independent. Boosting trees are shallow and sequential. Each boosting tree is a weak learner (depth 3-6), deliberately underfitting. The ensemble's power comes from iteration, not depth.
Fast, regularized, scalable. The go-to algorithm for Kaggle competitions and production ML on tabular data.
Built-in L1/L2 regularization, early stopping, missing value handling, and parallel tree construction.
| n_estimators | Number of boosting rounds (100–1000) |
| learning_rate | Step size shrinkage (0.01–0.3) |
| max_depth | Tree depth (3–10, default 6) |
| early_stopping | Stop when validation loss plateaus |
Choose XGBoost when: you need maximum accuracy on tabular data and can invest time in hyperparameter tuning. Use Random Forest when you need a robust model with minimal tuning.
import xgboost as xgb
model = xgb.XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
early_stopping_rounds=10,
eval_metric='logloss' # penalizes overconfident wrong predictions
)
model.fit(X_train, y_train,
eval_set=[(X_val, y_val)],
verbose=False)
print(f"Best round: {model.best_iteration}")
print(f"Test acc: {model.score(X_test, y_test):.3f}")
Early Stopping = stop training when validation loss stops improving. Prevents overfitting automatically.
logloss = penalizes overconfident wrong predictions (lower = better).
L1 reg = drives small weights to zero (implicit feature selection). L2 reg = shrinks all weights evenly, prevents any one feature dominating.
XGBoost decides whether to split a node using:
Worked Example (λ=1, γ=0.5):
GL=5, HL=10, GR=3, HR=8
Left: 25/(10+1) = 2.27
Right: 9/(8+1) = 1.00
Both: 64/(18+1) = 3.37
Gain = 0.5×(2.27 + 1.00 − 3.37) − 0.5
Gain = 0.5×(−0.10) − 0.5 = −0.55 → Don't split!
Split if impurity (Gini/MSE) improves. No penalty for adding more splits. Trees can grow freely → prone to overfitting.
Split only if Gain > γ. The λ term penalizes large leaf weights, γ penalizes tree complexity. Both act as built-in pruning.
Standard GB uses gradient descent (1st-order). XGBoost uses Newton's method (2nd-order) — the Hessian H acts as a per-sample confidence weight. Uncertain samples (small H) get less influence. This is why XGBoost converges faster.
reg_lambda (λ) — L2 regularization, default 1min_split_loss (γ) — min gain, default 0subsample — row sampling per tree (like RF)colsample_bytree — column sampling (like RF)Don't memorize this formula. Key idea: XGBoost adds a penalty for splitting (γ) and for large leaf weights (λ). Standard GB has neither.
CS Connection: This is Newton's method (2nd-order) vs gradient descent (1st-order). The Hessian H acts as a per-sample confidence weight — uncertain predictions get more influence. Same reason Newton's converges quadratically while GD converges linearly.
Microsoft's gradient boosting library (2017). Key innovations: leaf-wise growth, histogram binning, GOSS sampling.
import lightgbm as lgb
model = lgb.LGBMClassifier(
n_estimators=200,
num_leaves=31, # main complexity control
learning_rate=0.1,
n_jobs=-1
)
model.fit(X_train, y_train)
| Parameter | Default | Notes |
|---|---|---|
num_leaves | 31 | Main complexity knob (replaces max_depth) |
min_child_samples | 20 | Prevents overfitting on small data |
subsample | 1.0 | Row sampling per tree (0.6–0.9) |
colsample_bytree | 1.0 | Column sampling (like RF) |
3 Key Innovations: (1) Leaf-wise growth = split highest-loss leaf only. (2) Histogram binning = 255 bins instead of exact thresholds (10–100× faster). (3) GOSS = keep high-gradient samples, subsample the rest.
Leaf-wise vs Level-wise: XGBoost grows all nodes at same depth. LightGBM grows the leaf that reduces loss the most — faster convergence, but can overfit on small data (<10K rows).
Instead of trying all n thresholds, LightGBM bins continuous features into ~255 buckets. Split search drops from O(n) to O(bins) — a 10-100× speedup on large datasets.
Leaf-wise can overfit on small datasets (< 10K rows) because it creates very deep, asymmetric trees. Use num_leaves ≤ 31 and min_child_samples ≥ 20 to control.
Why 255 bins? 255 = max value of uint8 (8 bits). Storing bins as single bytes instead of 4-byte floats = 4× less memory. This is a systems-level optimization, not a statistical one.
| Aspect | Random Forest | Gradient Boosting |
|---|---|---|
| Training | Parallel (fast) | Sequential (slower) |
| Overfitting | Less prone | More prone |
| Tuning | Easier (fewer params) | More hyperparameters |
| Performance | Good baseline | Often best |
| Best For | Quick baseline, noisy data | Maximum accuracy, competitions |
Start with Random Forest as a baseline. Switch to XGBoost/LightGBM when you need extra performance.
| CART | Random Forest | Gradient Boosting | XGBoost | LightGBM | |
|---|---|---|---|---|---|
| Type | Single tree | Bagging ensemble | Boosting ensemble | Boosting + regularization | Boosting + histogram |
| # Trees | 1 | 100-500 (parallel) | 100-1000 (sequential) | 100-1000 (sequential) | 100-1000 (sequential) |
| Depth | Deep (full) | Deep (full) | Shallow (3-6) | Shallow (3-10) | Leaf-wise (31 leaves) |
| Split Search | All features, all thresholds | Random m features, all thresholds | All features, all thresholds | All features + regularized gain | Histogram bins (~255) |
| Overfit Risk | High | Low | Medium | Low (regularized) | Medium on small data |
| Speed | Fast | Fast (parallelizable) | Slow (sequential) | Medium (optimized C++) | Fastest |
| When to Use | Interpretability, baseline | Robust baseline, noisy data | General boosting | Max accuracy, competitions | Large datasets, speed |
Need interpretability? → CART
Need a robust baseline? → Random Forest
Need max performance? → XGBoost / LightGBM
Large dataset (>100K rows)? → LightGBM
CART = greedy search (like greedy algorithms)
RF = variance reduction via independence (like distributed systems)
GB = bias reduction via iteration (like gradient descent)
XGBoost = 2nd-order optimization (Newton's method)
LightGBM = computational efficiency (histogram approximation)
Rule of thumb: <1K samples → logistic regression | 1K–100K → XGBoost | >100K → LightGBM | Need explainability → single CART tree.
from sklearn.model_selection import RandomizedSearchCV
param_dist = {
'n_estimators': [100, 200, 300, 500],
'max_depth': [3, 5, 7, 10],
'learning_rate': [0.01, 0.05, 0.1],
}
search = RandomizedSearchCV(
xgb.XGBClassifier(), param_dist,
n_iter=20, cv=5, scoring='roc_auc'
)
search.fit(X_train, y_train)
print(search.best_params_)
Use scoring='roc_auc' for imbalanced datasets. RandomizedSearchCV is much faster than GridSearch when the parameter space is large.
cv=5 = 5-fold cross-validation: split data into 5 parts, train on 4, test on 1, repeat 5×, average the scores.
roc_auc = Area Under ROC Curve. Measures class separation ability. 1.0 = perfect, 0.5 = random guessing.
Intuition: Imagine a team project. SHAP figures out each member's (feature's) fair contribution to the final grade (prediction). It asks: “If we remove this feature, how much does the prediction change?”
φi = ∑S⊆N\{i} |S|!(|N|−|S|−1)! ⁄ |N|! · [v(S∪{i}) − v(S)]
Marginal contribution of feature i, averaged over all coalitions (coalition = any subset of features used together)
Feature importance tells you which features matter globally. SHAP tells you why a specific prediction was made.
How to read beeswarm: Red dots right = pushes prediction up. Blue dots left = pushes down. Wider spread = more important.
Think of SHAP like splitting a group project grade fairly — each feature gets credit proportional to its actual contribution to the prediction.
Common Mistake: SHAP ≠ causation. If your model uses ice cream sales to predict drowning risk, SHAP will show high importance for ice cream — but both just correlate with summer.
Base prediction (average across all applicants) = 30% risk. Each feature nudges the prediction up or down.
| Feature | Value | SHAP Impact | Running Total |
|---|---|---|---|
| Base prediction | — | — | 0.30 |
| + Previous Defaults | 2 | +0.22 | 0.52 |
| + Delinquent | Yes | +0.15 | 0.67 |
| + Open Credit Lines | 8 | +0.10 | 0.77 |
| − Income | ₱120K | −0.12 | 0.65 |
| + Debt Ratio | 0.65 | +0.07 | 0.72 |
| Final Prediction | — | — | 0.72 (High Risk) |
The final prediction (72% risk) = base + all SHAP values summed. Each feature has a signed contribution: positive pushes risk up, negative pushes it down.
This is exactly what the waterfall plot on the next slide visualizes.
Each bar shows how one feature shifts the prediction from the base value.
Build a credit scoring model on Philippine data and explain individual rejection decisions using SHAP.
Simulated Philippine credit data: income, employment length, number of open credit lines, previous defaults, delinquency status. Target: default risk (binary).
SHAP explanations satisfy BSP regulatory requirements for transparent credit decisions.
When building a model on a tabular dataset, in which scenario would you strongly prefer XGBoost over Random Forest?
Click to reveal answer
Correct: A (Maximizing Accuracy)
Boosting models (XGBoost) actively correct residuals sequentially, often leading to higher performance on tabular data. Random Forest is preferred when you need lower tuning effort and high stability against outliers.
1. Ensembles combine multiple models to reduce variance (bagging) or bias (boosting).
2. Random Forest uses bagging + feature randomization for robust, easy-to-tune models.
3. XGBoost & LightGBM are state-of-the-art for tabular data, with built-in regularization.
4. SHAP values explain individual predictions — critical for regulated industries.
Build the best predictive model for a Philippine dataset using tree-based methods.
| Algorithm | Tuning Effort | Speed | Accuracy | Your Goal |
|---|---|---|---|---|
| Decision Tree | Low | Fast | Baseline | Set the floor |
| Random Forest | Low–Medium | Fast (parallel) | Good | Beat the baseline |
| XGBoost | Medium–High | Medium | Best | Win the competition |
| LightGBM | Medium | Fastest | Best | Alternative to XGB |
Next Week: Clustering & Segmentation — K-Means, Hierarchical Clustering, DBSCAN, and Customer Analytics.