EduFlow

Section 3 · Classification

Random Forests and Gradient Boosting

20 min3 min readPython exercise

Lesson at a glance

Combine many trees with random forests and gradient boosting, use HistGradientBoostingClassifier’s built-in handling of missing values, and measure feature importance with permutation importance.

  1. 1Random forests: many trees, averaged
  2. 2Gradient boosting: trees that fix each other
  3. 3Both on churn, with the missing column back
  4. 4What do the trees rely on?

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

A single decision tree is easy to read and easy to break: change a few training rows and the branches rearrange. The cure is an old statistical trick. Ask many independent, slightly different estimators and combine their answers. The errors of each one partly cancel, and what’s left is more reliable than any single member. Models built this way are ensembles, and for tabular data like churn they’re what most practitioners reach for after the baselines and a linear model.

Random forests: many trees, averaged

A random forest grows a few hundred deep trees, each on a different bootstrap sample (the training rows sampled with replacement, so each tree sees a slightly different dataset) and, at every split, only a random subset of the features. That second trick stops every tree from opening with the same dominant feature, which makes the trees more different from each other and their mistakes less correlated. To predict, the forest averages the trees’ probabilities.

Gradient boosting: trees that fix each other

Boosting takes the opposite approach. It builds shallow trees one at a time. The first makes rough predictions; the second is fitted to the errors the first one left; the third to what the first two still get wrong, and so on. Each tree’s contribution is scaled down by the learning_rate, so the ensemble creeps towards a good answer instead of lurching. More trees (max_iter) and a higher learning rate fit the training data more closely, and eventually overfit it.

Top row, random forest: three trees trained independently on different samples feed into an average. Bottom row, gradient boosting: tree 1 feeds its errors to tree 2, which feeds its remaining errors to tree 3, and their scaled outputs are summed. Random forest (bagging) tree on sample 1 tree on sample 2 tree on sample 3 average the votes Gradient boosting tree 1 tree 2 on errors tree 3 on errors sum, scaled by learning rate each tree depends on the ones before it
Bagging averages in parallel; boosting corrects in sequence

Both on churn, with the missing column back

So far you’ve left out avg_satisfaction because half its values are blank. HistGradientBoostingClassifier, scikit-learn’s fast boosting implementation, handles missing values natively: at each split it learns which side the blanks should go to. Recent versions of DecisionTreeClassifier and RandomForestClassifier accept NaN too. LogisticRegression and k-NN still don’t; they raise ValueError: Input X contains NaN.

Python
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share",
            "avg_satisfaction"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])

no_gaps = features[:-1]  # logistic regression can't take the NaNs in avg_satisfaction
models = {
    "logistic (no satisfaction)": (make_pipeline(StandardScaler(), LogisticRegression()), no_gaps),
    "random forest": (RandomForestClassifier(n_estimators=300, min_samples_leaf=5,
                                             random_state=42), features),
    "boosting, defaults": (HistGradientBoostingClassifier(random_state=42), features),
    "boosting, gentle": (HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05,
                                                        max_iter=200, random_state=42), features),
}
for name, (model, cols) in models.items():
    model.fit(X_train[cols], y_train)
    print(f"{name:27} train {model.score(X_train[cols], y_train):.2f}  "
          f"test {model.score(X_test[cols], y_test):.2f}")

The forest reaches 0.76 and gently tuned boosting 0.78, against 0.72 for logistic regression and the rule. Boosting with default settings fits the training set almost perfectly (0.99) and does worse than the gentle version: on 298 rows, the defaults are too eager. Shallower trees and a smaller learning rate keep it in check.

Hold these numbers loosely. On a 100-customer test set, two points is two customers. The next lesson adds a metric for ranking quality, and section 4’s cross-validation will tell you whether the ensembles’ edge survives a different shuffle.

What do the trees rely on?

Forests offer feature_importances_, but those numbers are computed on the training data from how much each feature reduced impurity, and they’re biased towards continuous features with many possible thresholds. Permutation importance asks a sharper question on held-out data: if I shuffle this one column, breaking its link with the target, how much does the score drop?

Python
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share",
            "avg_satisfaction"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])
forest = RandomForestClassifier(n_estimators=300, min_samples_leaf=5, random_state=42).fit(X_train, y_train)

result = permutation_importance(forest, X_test, y_test, n_repeats=10, random_state=42)
table = pd.DataFrame({"impurity (train)": forest.feature_importances_,
                      "permutation (test)": result.importances_mean}, index=features)
print(table.sort_values("permutation (test)", ascending=False).round(3))

Impurity importance ranks tenure_days second. Permutation importance on the test set says it barely matters: the forest used it to fit noise in the training data. Shuffling recency costs about 13 points of accuracy; shuffling satisfaction costs nothing (the small negative values are noise). The honest summary for Cartwheel: recency and order count drive the predictions, and the satisfaction survey, in its current half-empty state, adds nothing.

Accuracy has carried you this far, but it treats a missed churner and a false alarm as equally bad, and it ignores the ranking you built for marketing. Next lesson replaces it with metrics that look at each kind of mistake separately.

Key takeaways

  • A random forest trains many deep trees on bootstrap samples with random feature subsets and averages their votes, which cancels much of a single tree’s variance.
  • Gradient boosting builds shallow trees one after another, each fitted to the errors of the ensemble so far; learning_rate and max_iter trade off speed and overfitting.
  • HistGradientBoostingClassifier is scikit-learn’s fast boosting model and accepts missing values (NaN) directly.
  • Impurity-based feature_importances_ favour features with many distinct values; permutation importance on held-out data measures what the model really relies on.
  • On small tabular datasets, ensembles often add only a few points over a good linear model, so compare them against it rather than assuming they win.

Further reading

Check your understanding

4 questions

Question 1 of 4

Why does a random forest usually beat a single unconstrained tree on new data?

Question 2 of 4

In gradient boosting, what does each new tree learn?

Question 3 of 4

avg_satisfaction is blank for half the customers. Which model can you fit on it with no extra preprocessing?

Question 4 of 4

A forest’s feature_importances_ ranks tenure_days second, but permutation importance on the test set gives it almost zero. What’s the most likely explanation?

Practice

Python exercise

Boost with the gaps left in

Fit a gradient boosting model on all nine features, including the half-empty avg_satisfaction, and find out what it relies on.

  1. booster: HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200, random_state=42) fitted on all of X_train (no columns dropped).
  2. test_acc: its test accuracy.
  3. ranking: a list of the nine feature names sorted by permutation importance on the test set (n_repeats=5, random_state=0), most important first.

Data files: churn.csv

import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share",
            "avg_satisfaction"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])

# 1. Gradient boosting on all nine features (missing values included):
#    max_depth=3, learning_rate=0.05, max_iter=200, random_state=42
booster = HistGradientBoostingClassifier(random_state=42)
booster.fit(X_train.drop(columns="avg_satisfaction"), y_train)

# 2. Test accuracy
test_acc = None

# 3. Permutation importance on the TEST set (n_repeats=5, random_state=0),
#    then the feature names sorted from most to least important
ranking = []
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • The model is trained on all nine features
  • The booster uses the gentle settings
  • test_acc is the test accuracy
  • ranking lists all nine features by permutation importance, recency first