Data Science & Machine Learning
ConceptIntermediate 28 min· Machine learning · 12 of 20

Decision trees, random forests and gradient boosting

Learn how decision trees, random forests and gradient boosting work, why trees overfit, and how to compare and tune them in scikit-learn. These are the standard models for tabular data.

For tables of data (rows and columns, like a spreadsheet or database), the most successful classic models are built from decision trees. Random forests and gradient boosting are the usual winners in Kaggle competitions on tabular data and the first things many companies try on customer, sales and risk data. This lesson explains how they work in plain terms and how to use them in scikit-learn.

Decision trees: a flowchart that learns itself

A decision tree is a flowchart of yes/no questions about the features. To predict, you start at the top and follow the answers down to a leaf, which gives the prediction. Training means finding the best questions.

At each step the algorithm tries many possible questions ("is worst radius at most 16.8?") and picks the one that separates the classes best, using a measure of impurity such as Gini. A group is pure if it contains one class only. It then repeats on each half until it stops.

A small tree you can read
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=42, stratify=data.target
)

tree = DecisionTreeClassifier(max_depth=2, random_state=42)
tree.fit(X_train, y_train)
print(export_text(tree, feature_names=list(data.feature_names)))
print("Test accuracy:", round(tree.score(X_test, y_test), 3))

export_text prints the learned rules. Output shown is from scikit-learn on the breast cancer dataset.

Output
|--- worst radius <= 16.80
|   |--- worst concave points <= 0.14
|   |   |--- class: 1
|   |--- worst concave points >  0.14
|   |   |--- class: 0
|--- worst radius >  16.80
|   |--- texture error <= 0.47
|   |   |--- class: 1
|   |--- texture error >  0.47
|   |   |--- class: 0

Test accuracy: 0.895

You can read this like a rulebook: a tumour with a small radius and a low concave-points value is predicted benign (class 1). This explainability is the main strength of a single tree. Other strengths: no feature scaling needed, handles numbers and (after encoding) categories, and captures curved and interacting patterns that a straight-line model cannot.

The big weakness: left alone, a tree keeps asking questions until every training row is in its own pure leaf. That memorises the data (overfitting, see *Train, validation, test and overfitting*). Small changes in the data can also produce a very different tree, which is why single trees are unstable.

Main settings that limit a tree

SettingWhat it doesEffect on overfitting
max_depthMaximum number of questions in a rowSmaller means simpler
min_samples_leafMinimum rows in a leafLarger means smoother
min_samples_splitMinimum rows needed to split a nodeLarger means simpler
max_featuresFeatures considered at each splitUsed mainly in forests

Random forest: many different trees, one vote

One tree is unstable, but the average of many different trees is stable and accurate. A random forest builds hundreds of trees and combines them (majority vote for classification, average for regression). To make the trees different from each other, each one:

- is trained on a bootstrap sample: rows drawn at random with replacement, so each tree sees a slightly different version of the data; - considers only a random subset of features at each split.

Think of asking 200 people who each saw part of the evidence, instead of one person who saw everything. Their individual mistakes are different, so they cancel out. This is called bagging, and it mainly reduces variance (overfitting).

Gradient boosting: trees that fix each other's mistakes

Boosting builds trees one after another. The first tree makes a rough prediction. The second tree is trained to correct the errors of the first. The third corrects what is still wrong, and so on. Each tree is small and weak on its own (often just a few levels deep), and the learning rate controls how much each new tree adds. Many small corrections add up to a strong model.

In scikit-learn, HistGradientBoostingClassifier is the fast, modern choice and it accepts missing values directly. Popular libraries with the same idea are XGBoost, LightGBM and CatBoost, which you can install with pip.

Forest versus boosting

Random forestGradient boosting
How trees are builtIndependently, in parallelOne by one, each fixing the previous errors
Main effectReduces varianceReduces bias, then needs care to avoid overfitting
Individual treeDeep, fully grownShallow, weak
Tuning effortLow; good defaultsMedium; learning rate, number of trees and depth interact
Risk of overfittingLowHigher if too many trees; use early stopping
Typical accuracy on tablesVery goodOften the best

Compare them on the same data

Cross-validated comparison
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.model_selection import cross_val_score

models = {
    "Single tree, no limit": DecisionTreeClassifier(random_state=42),
    "Single tree, depth 3": DecisionTreeClassifier(max_depth=3, random_state=42),
    "Random forest": RandomForestClassifier(n_estimators=200, random_state=42),
    "Gradient boosting": HistGradientBoostingClassifier(random_state=42),
}

for name, model in models.items():
    scores = cross_val_score(model, X_train, y_train, cv=5)
    print(f"{name:24s} {scores.mean():.3f} (+/- {scores.std():.3f})")

Run it after the first cell. Uses X_train and y_train from there.

Output
Single tree, no limit    0.910 (+/- 0.019)
Single tree, depth 3     0.925 (+/- 0.024)
Random forest            0.960 (+/- 0.019)
Gradient boosting        0.971 (+/- 0.018)

The pattern is typical: a limited tree beats an unlimited tree, a forest beats both, and boosting is slightly ahead. With differences this small (the +/- shows the spread across folds), do not claim one is certainly better on a dataset of 455 rows. On this small, clean dataset even logistic regression does well. The gap between linear models and tree ensembles is usually larger on messy business data with interactions and mixed columns.

Feature importance

A forest can tell you which features it used most. feature_importances_ measures how much each feature reduced impurity across all trees.

Which features matter?
import pandas as pd

forest = RandomForestClassifier(n_estimators=200, random_state=42)
forest.fit(X_train, y_train)

importance = pd.Series(forest.feature_importances_, index=data.feature_names)
print(importance.sort_values(ascending=False).head(5).round(3))
print("Test accuracy:", round(forest.score(X_test, y_test), 3))

Prints the five most important features and the test accuracy.

Output
worst perimeter         0.133
worst area              0.128
worst concave points    0.108
mean concave points     0.094
worst radius            0.091
dtype: float64
Test accuracy: 0.956

Importance is not causation

Impurity-based importance favours features with many distinct values and splits credit between correlated features (here perimeter, area and radius all measure size). Treat it as a hint, not proof. For a more reliable view use sklearn.inspection.permutation_importance on validation data. And importance never shows cause and effect.

Boosting with early stopping

Boosting can keep adding trees and start to overfit. Early stopping holds back part of the training data, watches the score on it, and stops when it has not improved for several rounds.

Early stopping
gb = HistGradientBoostingClassifier(
    learning_rate=0.1,        # how much each new tree contributes
    max_iter=300,             # maximum number of trees
    early_stopping=True,      # stop when a held-out part of the training data stops improving
    validation_fraction=0.15,
    n_iter_no_change=10,
    random_state=42,
)
gb.fit(X_train, y_train)
print("Trees actually used:", gb.n_iter_)
print("Test accuracy:", round(gb.score(X_test, y_test), 3))

n_iter_ shows how many trees were actually built. Results can vary slightly between versions.

Output
Trees actually used: 46
Test accuracy: 0.947

When to use what

Practical guide

  1. Start with a simple baseline (logistic regression or a dummy model).
  2. Try a random forest next: few settings, hard to get badly wrong.
  3. Try gradient boosting when you want the best accuracy on tabular data and can spend time on tuning.
  4. Use a small decision tree when you must show people the rules.
  5. Do not expect trees to beat deep learning-style methods on images, audio or free text; those need neural networks.
  6. Remember trees cannot extrapolate: a tree predicting house prices never predicts above the highest price it saw.

How this comes up in interviews

Common questions: "How does a decision tree decide where to split?" (the split that most reduces impurity), "Why do trees overfit and how do you stop it?", "Bagging versus boosting", "Why is a random forest better than one tree?" (averaging many different trees reduces variance) and "Do trees need feature scaling?" (no). If asked about XGBoost, say it is an optimised implementation of gradient boosting.

Prompt · Explain my model's rules
Here is the output of export_text for my decision tree, which predicts [target]:

[paste the export_text output]

Explain in plain English what rules the tree learned, which features it relies on most, and whether any rule looks suspicious or like it could be caused by leakage or noise. I am a beginner.

What to expect: A good answer rewrites the branches as sentences and flags splits that seem to use information not available at prediction time.

Try it: Tune a forest

On the same train/test split, loop over n_estimators in [10, 50, 200] and max_depth in [3, 6, None] for a RandomForestClassifier. Print the 5-fold cross-validated accuracy for each of the nine combinations. Does adding trees help? Does limiting depth help?

Try it: Draw the tree

Run from sklearn.tree import plot_tree, then import matplotlib.pyplot as plt, create a figure with plt.figure(figsize=(12, 6)) and call plot_tree(tree, feature_names=data.feature_names, class_names=data.target_names, filled=True). Compare it with the text output.

Key takeaways

  1. A decision tree is a learned flowchart. It is easy to explain but overfits unless you limit its depth.
  2. A random forest averages many trees built on random samples of rows and features, which reduces overfitting.
  3. Gradient boosting builds small trees in sequence, each correcting the previous errors, and is often the most accurate on tables.
  4. Trees need no feature scaling and handle interactions; they cannot extrapolate beyond the values seen.
  5. Feature importance is a hint, not proof: it is biased and never shows cause.
  6. Use early stopping for boosting, and compare models with cross-validation, not a single split.

Check yourself

Finishing the quiz marks the lesson as done.

1. Why does an unrestricted decision tree usually overfit?

2. What makes the trees in a random forest different from each other?

3. Which statement best describes gradient boosting?

4. Which model type needs feature scaling to work well?

5. A random forest says feature A is the most important. What can you conclude?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free