MPS311/439 - Machine Learning
Dr. Wei Xing
University of Sheffield
November 2025
By the end of this lecture, you will be able to:
DecisionTreeClassifierLast week, we explored Linear Discriminant Analysis (LDA) and Quadratic Discriminant Analysis (QDA). These methods make specific assumptions about our data:
Both methods work well when these assumptions hold. But what happens when our data doesn't fit these neat distributional assumptions?
Consider the classic XOR (exclusive OR) problem. We have two features and two classes arranged in a very specific pattern:

Figure 1: The XOR problem - a classic example where linear classifiers fail. The four clusters cannot be separated by any straight line.
The problem: No single straight line can separate the blue points from the red points! Even though this pattern is conceptually simple (diagonal clustering), linear methods like logistic regression or LDA fail miserably.
Here's another challenging scenario - nested or circular boundaries:

Figure 2: Nested class boundaries challenge linear methods. Class A (blue) forms an inner circle, while Class B (red) forms an outer ring.
Even QDA struggles here because the decision boundary isn't a simple quadratic curve - it's fundamentally about distance from the origin.
Here's a key insight: Humans don't make decisions using linear combinations of features. Instead, we use hierarchical sequences of yes/no questions:
Example: Should I go for a run today?
Notice how this decision process naturally forms a tree structure. Each question is a node, and we follow different branches based on the answers until we reach a final decision (the leaves).
Other examples of tree-like thinking:
The key question: Can we teach machines to make decisions the same way?
The answer: Yes! That's exactly what decision trees do.
A decision tree is a flowchart-like structure where:
Think of it as a game of "20 questions" that the model plays with itself to arrive at a prediction.
Let's look at a real decision tree trained on the famous Iris flower dataset:

Figure 3: A decision tree (depth=3) for classifying Iris flowers. The tree asks questions about petal width and petal length to determine the species.
How to read this tree:
Each box (node) contains:
Color coding: The color intensity shows the majority class and how pure the node is. Darker colors mean more samples belong to the majority class.
Let's walk through an example:
Suppose we have a flower with petal width = 1.5 cm and petal length = 4.5 cm:
Let's define the key components more precisely:
Root node: The topmost node containing all training data
Internal nodes (or decision nodes): Nodes that split the data based on a feature test
Edges (or branches): The outcomes of the tests (typically "True" or "False" for binary splits)
Leaf nodes (or terminal nodes): Nodes with no children that output final predictions
Depth: The length of the longest path from root to any leaf
Splitting rule: The feature and threshold chosen at each internal node
Mathematical notation:
Why are decision trees so popular in machine learning?
1. Interpretability 🔍
2. Non-linearity 🌊
3. No feature scaling needed 📏
4. Handles mixed data types 🎯
5. Implicit feature selection ✨
But (there's always a but!), trees have some serious drawbacks too - we'll discuss these later when we talk about overfitting.
Now comes the crucial question: How do we actually build a decision tree?
Decision trees are built using a greedy, recursive algorithm called CART (Classification and Regression Trees):
Algorithm (high-level):
1. Start with all training data at the root node
2. FOR each node (starting from the root):
a. Find the "best" feature and threshold to split on
b. Split the data into two child nodes based on this split
c. Recursively apply steps 2a-2c to each child node
3. STOP when a stopping criterion is met
Key term: "Greedy" means we make the locally optimal choice at each step, without looking ahead. We choose the split that looks best right now, even though a different split might lead to a better overall tree. This is computationally efficient but doesn't guarantee the globally optimal tree.
The critical question is: How do we measure the quality of a split?
Goal: We want splits that create pure child nodes.
Purity means:
Intuition: If we can split the data so that one child has mostly blues and the other has mostly reds, we've made progress! The more pure the children, the better the split.
The Gini impurity measures how "mixed" a node is. It's the probability of misclassifying a randomly chosen element if we randomly label it according to the class distribution in the node.
Formula:
For a node containing samples from classes, where is the proportion of samples belonging to class :
For binary classification ():
where is the proportion of class 1 samples.
Interpretation:
Example calculations:
Pure node: 80 samples of Class A, 0 samples of Class B
Maximum impurity: 40 samples of Class A, 40 samples of Class B
Moderate impurity: 60 samples of Class A, 20 samples of Class B
Entropy is an alternative measure borrowed from information theory. It measures the "disorder" or "uncertainty" in a node.
Formula:
Convention: (by limit definition)
Interpretation:
Information Gain measures how much a split reduces entropy:
where the weighted entropy is:
Example calculations (same nodes as before):
Pure node: [80, 0]
Maximum impurity: [40, 40]
Moderate impurity: [60, 20]
Let's visualize the difference between these two measures:

Figure 4: Comparison of Gini impurity and Entropy for binary classification as functions of class probability. Both curves are similar in shape, with maximum impurity at p=0.5.
Key observations:
Practical differences:
When they differ: Entropy tends to produce slightly more balanced trees because it penalizes impurity more heavily. Gini tends to isolate the most frequent class in its own branch.
Recommendation: Start with Gini (default). Switch to entropy only if you have a specific reason or if experimentation shows it performs better on your data.
When we split a node, we create two children of potentially different sizes. We need to account for this when evaluating split quality.
Weighted Gini impurity for a split:
where:
Goal: Choose the split that minimizes weighted impurity (or equivalently, maximizes information gain).
Example:
Parent node has 100 samples: [60 Class A, 40 Class B]
Consider split: "Feature X ≤ 5"
Weighted Gini:
Gini reduction =
This split reduces impurity, so it's helpful! We'd compare this to all other possible splits and choose the one with the largest reduction.
The tree-building algorithm is recursive - it keeps splitting nodes into children. But when should we stop?
Common stopping criteria:
Maximum depth reached (max_depth)
max_depth=5 means maximum 5 levels from root to leafMinimum samples for split (min_samples_split)
min_samples_split=20 means don't split nodes with <20 samplesMinimum samples per leaf (min_samples_leaf)
min_samples_leaf=10 ensures every leaf has ≥10 samplesPerfect purity achieved
No improvement in impurity (min_impurity_decrease)
min_impurity_decrease=0.01 requires significant splitsWithout any stopping criteria, the tree grows until every leaf is pure (or has one sample). This leads to overfitting - we'll discuss this problem in detail later!
Let's build a tiny decision tree manually to really understand the process.
Toy dataset (8 samples, 2 features, 2 classes):
| Sample | Feature 1 | Feature 2 | Class |
|---|---|---|---|
| 1 | 2.5 | 3.0 | A |
| 2 | 3.0 | 3.5 | A |
| 3 | 1.5 | 2.0 | A |
| 4 | 5.0 | 4.0 | B |
| 5 | 5.5 | 4.5 | B |
| 6 | 6.0 | 5.0 | B |
| 7 | 4.0 | 3.0 | B |
| 8 | 3.5 | 2.5 | A |
Step 1: Root node impurity
All 8 samples: 4 Class A, 4 Class B
Step 2: Try all splits on Feature 1
Possible thresholds (midpoints between sorted values): 2.0, 2.75, 3.25, 3.75, 4.5, 5.25, 5.75
Let's try Feature 1 ≤ 3.0:
No need to check other splits - we found a perfect one!
Gini reduction = (maximum possible)
Step 3: Create split
Our tree after one split:
[8 samples: 4A, 4B]
Feature 1 ≤ 3.0?
/ \
Yes No
/ \
[4 samples: 4A, 0B] [4 samples: 0A, 4B]
Predict: A Predict: B
Result: Both children are pure, so we stop! This tree achieves 100% accuracy on the training data with just one split.
Key insight: The CART algorithm would find this split by systematically trying every feature and every threshold, computing the weighted Gini for each, and selecting the best one.
Now let's see how to actually build and use decision trees with scikit-learn. The good news: sklearn handles all the complexity we just discussed!
The standard machine learning workflow applies:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
# 1. Load your data
# X = features, y = labels
# 2. Split into train/test sets
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 3. Create and train the tree
clf = DecisionTreeClassifier(max_depth=3, random_state=42)
clf.fit(X_train, y_train)
# 4. Make predictions
y_pred = clf.predict(X_test)
# 5. Evaluate
train_acc = clf.score(X_train, y_train)
test_acc = clf.score(X_test, y_test)
print(train_acc, test_acc)
Key parameters:
max_depth: Maximum tree depth (start with 3-5)criterion: 'gini' (default) or 'entropy'min_samples_split: Minimum samples to split a node (default: 2)min_samples_leaf: Minimum samples in a leaf (default: 1)random_state: For reproducibilityThat's it! Just a few lines of code to train a decision tree.
One of the best features of decision trees is that we can visualize them:
from sklearn.tree import plot_tree
import matplotlib.pyplot as plt
plt.figure(figsize=(20, 10))
plot_tree(clf, filled=True, feature_names=['f1', 'f2'])
plt.show()
How to interpret the visualization:
Each node shows:
Node colors: Darker color = higher purity (more samples from majority class)
For 2D data, we can visualize the decision regions:

Figure 5: Decision boundaries for shallow (depth=2) vs deep (depth=8) trees on a moons dataset. Notice how deeper trees create more complex, axis-aligned boundaries.
Key observations:
Code to generate this (for 2D data):
import numpy as np
# Create a mesh
h = 0.02
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
# Predict on mesh
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.show()
Decision trees automatically tell us which features are most important for making predictions!
Feature importance measures how much each feature contributes to reducing impurity across all splits in the tree.
Access in sklearn:
# Get feature importances
importances = clf.feature_importances_
print(importances)
Interpretation:
Example output:
[0.654 0.346 0.000 0.000]
This tells us that the first feature has 65.4% importance, second feature 34.6%, and the last two features aren't used at all!
Visualization:
import matplotlib.pyplot as plt
plt.bar(range(len(importances)), importances)
plt.xlabel('Feature')
plt.ylabel('Importance')
plt.show()
Here's a full working example on the Iris dataset:
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
# Load data
iris = load_iris()
X, y = iris.data, iris.target
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# Train tree
clf = DecisionTreeClassifier(max_depth=3, random_state=42)
clf.fit(X_train, y_train)
# Evaluate
print(clf.score(X_train, y_train))
print(clf.score(X_test, y_test))
# Feature importances
print(clf.feature_importances_)
Expected output:
0.981
0.956
[0.000 0.000 0.551 0.449]
Interpretation: The tree achieves ~96% test accuracy using only petal measurements (features 2 and 3). Sepal features (0 and 1) aren't needed.
Decision trees are powerful, but they have a critical weakness: they are prone to overfitting.
Let's run an experiment. Train two trees on the same data:
max_depth=3 (shallow, constrained)max_depth=None (unrestricted, grows until pure)Typical results:
Wait, what? 🤔
Tree 2 is perfect on training data but worse on test data! This is the hallmark of overfitting.
An unrestricted decision tree will keep splitting until either:
The problem: The tree learns specific quirks of the training data rather than general patterns:
Analogy: It's like memorizing answers to practice exam questions instead of understanding the concepts. You'll ace the practice exam but fail the real exam with slightly different questions.
Why does this happen?

Figure 6: Comprehensive demonstration of overfitting in decision trees. Top row shows decision boundaries for shallow (depth=2) vs deep (depth=15) trees. Bottom row shows how accuracy and impurity change with tree depth.
Key insights from this figure:
Top-left (Shallow tree):
Top-right (Deep tree):
Bottom-left (Accuracy vs Depth):
Bottom-right (Gini Impurity):
This is a fundamental concept in machine learning:
Bias (underfitting):
Variance (overfitting):
For decision trees:
| Tree Depth | Bias | Variance | Typical Behavior |
|---|---|---|---|
| Very shallow (depth=1-2) | High | Low | Underfits, misses patterns |
| Moderate (depth=4-6) | Medium | Medium | Sweet spot! |
| Very deep (depth>10) | Low | High | Overfits, memorizes noise |
Goal: Find the depth that minimizes total error = Bias² + Variance
Fortunately, sklearn provides several "knobs" to control tree complexity:
max_depth What it does: Limits maximum depth of the tree
Effect:
Typical values: 3-10 for most problems
Example:
clf = DecisionTreeClassifier(max_depth=5, random_state=42)
min_samples_split What it does: Minimum number of samples required to split a node
Effect:
Typical values: 2-50
Example:
clf = DecisionTreeClassifier(min_samples_split=20, random_state=42)
min_samples_leaf What it does: Minimum number of samples required in each leaf node
Effect:
Typical values: 1-20
Example:
clf = DecisionTreeClassifier(min_samples_leaf=5, random_state=42)
Relationship: min_samples_split must be at least 2 × min_samples_leaf
max_features What it does: Number of features to consider when looking for the best split
Effect:
Typical values:
None: Use all features (default for trees)'sqrt': Use features where is total features'log2': Use featuresExample:
clf = DecisionTreeClassifier(max_features='sqrt', random_state=42)
Note: This is more commonly used with Random Forests (covered in optional section).
min_impurity_decrease What it does: Minimum impurity decrease required to make a split
Effect:
Typical values: 0.0-0.01
Example:
clf = DecisionTreeClassifier(min_impurity_decrease=0.01, random_state=42)
Start here:
clf = DecisionTreeClassifier(
max_depth=5,
min_samples_split=10,
min_samples_leaf=5,
random_state=42
)
Then tune based on results:
If underfitting (both train and test accuracy are low):
max_depthmin_samples_split and min_samples_leafIf overfitting (train accuracy high, test accuracy low):
max_depthmin_samples_split and min_samples_leafmin_impurity_decreaseAlways remember:
How do we find the best hyperparameters systematically?
Problem: A single train/test split might be lucky (or unlucky)
Solution: K-fold cross-validation
How it works:
Visual:
Fold 1: [Test] [Train] [Train] [Train] [Train]
Fold 2: [Train] [Test] [Train] [Train] [Train]
Fold 3: [Train] [Train] [Test] [Train] [Train]
Fold 4: [Train] [Train] [Train] [Test] [Train]
Fold 5: [Train] [Train] [Train] [Train] [Test]
sklearn implementation:
from sklearn.model_selection import cross_val_score
clf = DecisionTreeClassifier(max_depth=5, random_state=42)
scores = cross_val_score(clf, X, y, cv=5)
print(scores)
print(scores.mean())
Example output:
[0.867 0.900 0.867 0.933 0.900]
0.893
Interpretation: The model achieves about 89.3% accuracy on average. This gives us much more confidence than a single 90% test accuracy!
Problem: Too many hyperparameters to try manually!
Solution: Grid Search - systematically try all combinations
How it works:
sklearn implementation:
from sklearn.model_selection import GridSearchCV
param_grid = {
'max_depth': [3, 5, 7, 10],
'min_samples_split': [2, 10, 20],
'min_samples_leaf': [1, 5, 10]
}
grid = GridSearchCV(
DecisionTreeClassifier(random_state=42),
param_grid,
cv=5
)
grid.fit(X_train, y_train)
print(grid.best_params_)
print(grid.best_score_)
best_clf = grid.best_estimator_
print(best_clf.score(X_test, y_test))
Example output:
{'max_depth': 5, 'min_samples_leaf': 5, 'min_samples_split': 10}
0.892
0.907
How many models did we train?
Tips:
RandomizedSearchCV for very large gridsDecision trees are not always the best choice. Here's when they shine and when they struggle:
Advantages ✓
Highly interpretable
No feature scaling needed
Handles mixed data types
Non-linear by nature
Fast training and prediction
Automatic feature selection
Disadvantages ✗
High variance (unstable)
Prone to overfitting
Axis-aligned boundaries only
Biased with imbalanced classes
Not globally optimal
Extrapolation is poor
How do decision trees compare to methods we've learned?
| Aspect | Logistic Regression | LDA/QDA | Decision Trees |
|---|---|---|---|
| Decision boundary | Linear | Linear/Quadratic | Axis-aligned, complex |
| Assumptions | Linear in log-odds | Gaussian | None |
| Interpretability | High | Medium | High |
| Feature scaling | Required | Not strictly | Not required |
| Non-linearity | Manual | Quadratic only (QDA) | Automatic |
| Overfitting tendency | Low | Low | High |
Rule of thumb:
1. Start with a shallow tree
clf = DecisionTreeClassifier(max_depth=3, random_state=42)
2. Always visualize (if not too large)
from sklearn.tree import plot_tree
plot_tree(clf, filled=True)
3. Check feature importances
print(clf.feature_importances_)
4. Use cross-validation
from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X, y, cv=5)
5. Monitor both training and test
print(clf.score(X_train, y_train))
print(clf.score(X_test, y_test))
6. Consider ensemble methods if single tree underperforms (covered in optional section for MPS439)
Let's apply everything we've learned to a complete real-world problem!
Source: UCI Machine Learning Repository / sklearn built-in datasets
Task: Predict wine quality category from chemical properties
Features: 13 chemical measurements
Target: 3 wine classes (cultivar types)
Samples: 178 wines
from sklearn.datasets import load_wine
import numpy as np
wine = load_wine()
X, y = wine.data, wine.target
print(X.shape)
print(wine.feature_names)
print(wine.target_names)
print(np.bincount(y))
Output:
(178, 13)
['alcohol', 'malic_acid', 'ash', ...]
['class_0' 'class_1' 'class_2']
[59 71 48]
Observations:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
print(len(X_train), len(X_test))
Output:
124 54
from sklearn.tree import DecisionTreeClassifier
clf_simple = DecisionTreeClassifier(max_depth=3, random_state=42)
clf_simple.fit(X_train, y_train)
print(clf_simple.score(X_train, y_train))
print(clf_simple.score(X_test, y_test))
Output:
0.960
0.907
Analysis: Good baseline! 96% train, 91% test. Small gap suggests we're not overfitting badly.
from sklearn.model_selection import GridSearchCV
param_grid = {
'max_depth': [2, 3, 4, 5, 7],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
grid = GridSearchCV(
DecisionTreeClassifier(random_state=42),
param_grid,
cv=5
)
grid.fit(X_train, y_train)
print(grid.best_params_)
print(grid.best_score_)
best_clf = grid.best_estimator_
print(best_clf.score(X_test, y_test))
Output:
{'max_depth': 4, 'min_samples_leaf': 2, 'min_samples_split': 2}
0.919
0.926
Analysis: Tuning improved test accuracy from 90.7% to 92.6%!
importances = best_clf.feature_importances_
feature_names = wine.feature_names
for i in np.argsort(importances)[::-1]:
if importances[i] > 0:
print(feature_names[i], importances[i])
Typical output:
flavanoids 0.421
proline 0.267
color_intensity 0.154
od280/od315_of_diluted_wines 0.089
alcohol 0.069
Interpretation:
from sklearn.metrics import confusion_matrix
y_pred = best_clf.predict(X_test)
cm = confusion_matrix(y_test, y_pred)
print(cm)
Output:
[[17 1 0]
[ 1 19 1]
[ 0 1 14]]
Interpretation: Most errors are between class_1 and class_2
What did the tree learn?
From feature importances:
"The decision tree primarily uses flavanoid content to distinguish wines. High flavanoids strongly suggest class_0. For wines with moderate flavanoids, proline content becomes the key discriminator. Finally, color intensity helps separate remaining cases."
This is the kind of interpretable insight that makes decision trees valuable!
Could we have done better?
Probably! Some options:
But 93% accuracy is already quite good!
From this example, we learned:
Note: This section is optional and targeted at MPS439 students. If you're in MPS311, feel free to skip this section or read it for enrichment!
We've seen that single decision trees have a major flaw: high variance.
What does high variance mean?
Example: Train the same tree 10 times with slightly different random subsamples:
The key insight: What if we could reduce this variance by combining multiple trees?
Ensemble learning means combining predictions from multiple models to get better overall performance.
Intuition: "Wisdom of crowds"
For decision trees:
Two main approaches:
Random Forest is the most popular bagging method for decision trees.
Algorithm (simplified):
1. Create N "bootstrap" datasets by sampling with replacement
2. For each dataset:
a. Train a decision tree
b. When splitting nodes, only consider random subset of features
3. To predict:
- Classification: Vote from all trees (majority wins)
- Regression: Average predictions from all trees
Key features:
Why does this work?
Random Forests are incredibly easy to use in sklearn:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
print(rf.score(X_train, y_train))
print(rf.score(X_test, y_test))
Key parameters:
n_estimators: Number of trees (default: 100)
max_features: Features to consider per split
'sqrt' for classification'log2' or specific numbermax_depth: Maximum depth per tree
None (trees grow until pure)n_jobs: Number of parallel jobs
-1 to use all CPU coresThat's it! Random Forests are almost as easy as single trees.
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
wine = load_wine()
X, y = wine.data, wine.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
print(rf.score(X_train, y_train))
print(rf.score(X_test, y_test))
print(rf.feature_importances_)
Typical output:
1.000
0.963
[0.112 0.029 0.012 0.032 0.025 0.051 0.144 0.011 0.031 0.152 0.096 0.131 0.174]
Observations:
Advantages ✓:
Disadvantages ✗:
Gradient Boosting is a different ensemble approach. Instead of training trees independently (bagging), we train them sequentially, each trying to correct the previous tree's mistakes.
Intuition: "Learn from mistakes"
Algorithm (simplified):
1. Train a shallow tree on the data
2. Find samples where this tree makes errors
3. Train a new tree focusing on those errors
4. Add this tree to the ensemble (with small weight)
5. Repeat steps 2-4 for N iterations
6. Final prediction = weighted sum of all trees
Key differences from Random Forest:
The "gradient" comes from optimization theory:
You don't need to understand the math deeply - just know that it's a principled way to build sequential ensembles.
XGBoost (eXtreme Gradient Boosting) is the most popular and powerful gradient boosting implementation.
Why XGBoost dominates:
Installation:
pip install xgboost
Basic usage:
import xgboost as xgb
xgb_clf = xgb.XGBClassifier(n_estimators=100, max_depth=5, random_state=42)
xgb_clf.fit(X_train, y_train)
print(xgb_clf.score(X_train, y_train))
print(xgb_clf.score(X_test, y_test))
Key parameters:
n_estimators: Number of boosting rounds (trees)
max_depth: Maximum depth per tree
learning_rate (or eta): Shrinkage parameter
subsample: Fraction of samples used per tree
import xgboost as xgb
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
wine = load_wine()
X, y = wine.data, wine.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
xgb_clf = xgb.XGBClassifier(
n_estimators=100,
max_depth=5,
learning_rate=0.1,
random_state=42
)
xgb_clf.fit(X_train, y_train)
print(xgb_clf.score(X_train, y_train))
print(xgb_clf.score(X_test, y_test))
Typical output:
1.000
0.981
Wow! 98.1% test accuracy vs 96.3% for Random Forest vs 93% for single tree!
XGBoost has many parameters. Here's a practical tuning strategy:
Step 1: Tune number of trees with early stopping
xgb_clf = xgb.XGBClassifier(
n_estimators=1000,
max_depth=5,
learning_rate=0.1,
random_state=42
)
xgb_clf.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
verbose=False
)
print(xgb_clf.best_iteration)
This automatically finds the optimal number of trees.
Step 2: Tune tree depth and learning rate
from sklearn.model_selection import GridSearchCV
param_grid = {
'max_depth': [3, 5, 7],
'learning_rate': [0.01, 0.1, 0.3]
}
grid = GridSearchCV(
xgb.XGBClassifier(n_estimators=100, random_state=42),
param_grid,
cv=5
)
grid.fit(X_train, y_train)
print(grid.best_params_)
Step 3: Fine-tune other parameters (if needed)
Usually the defaults work well!
Let's compare all three methods on the wine dataset:
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
import xgboost as xgb
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
wine = load_wine()
X, y = wine.data, wine.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# Single tree
dt = DecisionTreeClassifier(max_depth=5, random_state=42)
dt.fit(X_train, y_train)
print("Decision Tree:", dt.score(X_test, y_test))
# Random Forest
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
print("Random Forest:", rf.score(X_test, y_test))
# XGBoost
xgb_clf = xgb.XGBClassifier(n_estimators=100, random_state=42)
xgb_clf.fit(X_train, y_train)
print("XGBoost:", xgb_clf.score(X_test, y_test))
Typical results:
Decision Tree: 0.926
Random Forest: 0.963
XGBoost: 0.981
Summary:
| Method | Test Accuracy | Interpretability | Training Time | Prediction Speed |
|---|---|---|---|---|
| Decision Tree | 92.6% | ⭐⭐⭐⭐⭐ | Fast | Fastest |
| Random Forest | 96.3% | ⭐⭐⭐ | Medium | Slow |
| XGBoost | 98.1% | ⭐⭐⭐ | Slow | Medium |
Use Single Decision Tree when:
Use Random Forest when:
Use XGBoost when:
Example use cases:
To go deeper with ensemble methods:
XGBoost Documentation:
Kaggle:
Key Papers:
Books:
Practice Tips:
Let's recap what we've learned about decision trees:
Core Concepts:
max_depth control model complexityPractical Skills Acquired (MPS311):
Advanced Skills Acquired (MPS439):
When Decision Trees Excel ✨:
When Trees Struggle 🚧:
The Solution (for MPS439): Ensemble methods!
Where we've been (Supervised Learning - Weeks 2-6):
Linear Methods Non-linear Methods
| |
|--- Linear Regression |--- Decision Trees
|--- Logistic Regression |--- Random Forests (optional)
|--- LDA/QDA |--- XGBoost (optional)
|
└→ Simple, interpretable, assume linearity
└→ Complex, flexible, no assumptions
The supervised learning landscape:
Big transition: From supervised to unsupervised learning!
What changes:
Week 8: Principal Component Analysis (PCA)
The new challenge:
PCA's answer:
Connection to decision trees:
What to review before Week 8:
Practice what you learned:
For everyone (MPS311):
Try decision trees on a new dataset
Experiment with hyperparameters
max_depth?min_samples_leaf affect overfitting?Visualize your trees
plot_tree() to understand what it learnedFor MPS439 students:
Apply XGBoost to a Kaggle competition
Try hyperparameter tuning
Read the XGBoost paper (optional)
Prepare for PCA:
sklearn Documentation:
Interactive Visualizations:
Books:
Videos:
XGBoost Resources:
Papers:
Kaggle:
Advanced Topics (if interested):
Key Formulas:
Gini Impurity:
Entropy:
Weighted Impurity:
Key Code Snippets:
# Train tree
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(max_depth=5, random_state=42)
clf.fit(X_train, y_train)
# Visualize
from sklearn.tree import plot_tree
plot_tree(clf, filled=True)
# Feature importance
importances = clf.feature_importances_
# Grid search
from sklearn.model_selection import GridSearchCV
grid = GridSearchCV(clf, param_grid, cv=5)
grid.fit(X_train, y_train)
# Random Forest (MPS439)
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100)
# XGBoost (MPS439)
import xgboost as xgb
xgb_clf = xgb.XGBClassifier(n_estimators=100)
End of Week 6 Lecture Notes
Next week: Principal Component Analysis (PCA) - Dimensionality Reduction
MPS311/439 - Machine Learning
Dr. Wei Xing
University of Sheffield
November 2025