← All tutorials

On this page

1 How a decision tree classifies a point 2 Impurity and information gain 3 Growing a tree greedily 4 Random forests 5 Out-of-bag error 6 Gradient boosting 7 Forests and boosting in practice 8 Strengths, limits and variants of trees 9 References

Decision Trees, Random Forests, and Boosting

A decision tree classifies a point by asking yes-or-no questions about its features.

1 How a decision tree classifies a point

Each question is a threshold on one feature; a leaf votes for the class most common among its training points. A regression tree's leaf predicts its points' mean.

One split on one featurekeep the threshold that leaves the purest sidesGrow the treestop at a max depth; each leaf votesRandom foresteach tree trains on a bootstrap sampleOut-of-bag errorpoints a tree never saw test itGradient boostingeach new tree fits the mistakes leftIn practiceforests and boosting in scikit-learn234567
Figure 1. Each box's marker is the number of the section that teaches it.

A tree (Section 3) is grown once, a forest (Section 4) grows each tree once on its own resample, and boosting (Section 6) adds one small tree per round. One balanced tree costs about points × features × log(points) (scikit-learn documentation).

This schematic follows the notebook's hand-built Iris tree: petal features, three species, root cut at the petal-width median.

petal width ≤ median? no, mostly virginica leaf predicts virginica yes, then petal length ≤ 2.5? setosa + versicolor no yes leaf versicolor leaf setosa 12333
Figure 2. Nodes 12 each ask one question; a leaf 3 votes its training points' majority class.

2 Impurity and information gain

The tree tries every feature and threshold and keeps the split whose children are purest, scored by one of two classic measures.

How a split is scored, and the two classic impurity measures to score it with.
ΔI = I(parent) − Ichildren
The gain ΔI is the impurity before the split minus the impurity after it; with entropy as I it is called the information gain. The tree keeps the split with the biggest gain; a split that separates nothing scores zero.
Ichildren = Σc NcN·I(c)
Each child c counts its impurity I(c) weighted by its share of the points, Nc out of N; Σ adds the children up.
Ientropy = −Σk pk log2 pk
Entropy is measured in bits, where pk is the share of the node's points in class k. A 50/50 node scores 1 bit, a pure node 0. −log2 pk is the surprise of meeting class k, large for a rare class; entropy is the average surprise, so it peaks when the classes are evenly mixed.
IGini = 1 − Σk pk²
Gini, which needs no logarithm, is the chance two points drawn at random from the node disagree, 0.5 for a 50/50 node and 0 for a pure one. In practice both measures grow nearly the same tree.
1Cut on feature 150/50 parent, 1 bitboth sides 50/501 bit, gini 0.5 eachgain 02Cut on feature 250/50 parent, 1 bitboth sides pure0 bits, gini 0 eachgain 1 bit
Figure 3. Cut 1 leaves both sides mixed (gain 0); cut 2 leaves pure sides, gain 1 bit, so it wins.

The Decision_tree notebook in the Machine-Learning-Concepts repo cuts Iris petal width at its median by hand: weighted child gini 0.443. The best root split scores 0.333, a tie between petal length and petal width; either cut separates setosa.

Split scoring in NumPy; f and 1 − f are the children's shares.

import numpy as np

def gini(y):
    p = np.bincount(y) / len(y)
    return 1 - (p ** 2).sum()

def splits(X):
    for j in range(X.shape[1]):
        vals = np.unique(X[:, j])
        for thr in vals[:-1]:
            yield j, thr

def best_split(X, y):
    best = (0.0, None, None)
    for j, thr in splits(X):
        left = X[:, j] <= thr
        f = left.mean()
        gl = gini(y[left])
        gr = gini(y[~left])
        kids = f*gl + (1-f)*gr
        gain = gini(y) - kids
        if gain > best[0]:
            best = (gain, j, thr)
    return best
  1. 1Gini impurity, line 4 of the card in Section 2.
  2. 2Tries every feature and threshold.
  3. 3Weighted impurity of the two children, line 2.
  4. 4Keeps the split with the largest drop, line 1.

The tree recurses on each child until a max depth or until every leaf is pure; depth 1 asks one question. Unchecked it memorizes the noise (depth 125 in the notebook), the high-variance failure of Bias and Variance.

3 Growing a tree greedily

Each point has features x1 and x2 and a class label y. The tree grows greedily: each node keeps its own best split, and earlier splits are never revisited.

Figure 4. Pick the purest split, cut, repeat on each side, then let the leaves vote.

4 Random forests

A random forest trains many deep trees, each on a bootstrap resample (same size as the data, drawn with replacement), and averages their votes.

The forest's vote, and how many features each split may look at.
v(x) = 1T·Σt ht(x)
ht(x) is tree t's call, 0 or 1, on point x, so v(x) is the share of the T trees voting 1.
ŷ = 1 if v(x) > 0.5, else 0
The forest says 1 when more than half the trees do: a majority vote. (scikit-learn averages each tree's class probabilities instead, a soft vote, which usually agrees.)
m = √d
A random forest proper also lets each split look at only m random features out of d, scikit-learn's default for classification. That makes the trees less alike. With d = 2 here, m rounds to 1 feature per split; the tool and the code keep both features, which is plain bagging.

A forest by hand, each tree on its own bootstrap sample idx; X, y are training points and Xte held-out test points.

import numpy as np
from sklearn.tree import DecisionTreeClassifier
Tree = DecisionTreeClassifier
rng = np.random.default_rng(0)
T, n = 20, len(X)
ts = []
for t in range(T):
    idx = rng.integers(0, n, n)
    h = Tree(max_depth=5)
    h.fit(X[idx], y[idx])
    ts.append(h)
P = [h.predict(Xte) for h in ts]
votes = np.mean(P, axis=0)
y_hat = (votes > 0.5).astype(int)
  1. 1Sets the number of trees T.
  2. 2Fits each tree on a bootstrap sample.
  3. 3Averages the votes and calls 1 above 0.5 (Section 4).
Notebook dataone deep tree87.6%20 trees, depth 589.0%Four blobs on this pageone tree89.0%20 trees, depth 1095.0%80%90%100%test accuracy (axis starts at 80%)12
Figure 5. Test accuracy on the notebook's 10,000 synthetic two-class points 1 and four blobs 2: the forest beats one tree.
20 10

Figure 6. Trained on 400 of 500 blob points: one tree's boundary is jagged around stray points; more voting trees smooth it.

5 Out-of-bag error

Each tree sits out about a third of the data.

The chance a point is left out of one bootstrap sample.
P(left out) = (1 − 1N)N
The chance one point never lands in a tree's bootstrap sample. Each of the N draws misses it with chance 1 − 1/N, and the draws are independent, so the misses multiply into the power N. A bigger N means more draws, but each draw is less likely to hit this point, and the two effects nearly cancel.
(1 − 1N)N ≈ e−1 ≈ 0.37
For large N the power settles at 1/e, about 0.37, whatever the data size: roughly a third of the points are never seen by a given tree, and they become its free test set.

The forest's vote on each point, counting only the trees that never saw it, gives the out-of-bag error, an estimate of test error that needs no held-out data.

With oob_score=True the forest stores that accuracy, and one minus it is the out-of-bag error.

from sklearn.ensemble import RandomForestClassifier
RF = RandomForestClassifier
rf = RF(20, max_depth=5)
rf.set_params(oob_score=True)
rf.set_params(max_features=None)
rf.fit(X, y)
oob_error = 1 - rf.oob_score_
  1. 1A 20-tree forest that keeps out-of-bag votes.
  2. 2Fits and reads the out-of-bag error (Section 5).

The Random Forests paper (Breiman, 2001) tested this on sonar returns labeled mine or rock, varying m (scikit-learn's max_features; the paper writes F). Strength is each tree's own accuracy; correlation is how alike two trees' votes are. Figure 7 redraws the paper's Figure 1 from values read off the published plot (PDF).

Strength and correlation0.20.30.40.501020304050correlationstrengthError, percent1416182001020304050out-of-bagtest setm, inputs each split may choose from1234
Figure 7. As m grows, correlation 1 rises, strength 2 flattens, and out-of-bag 3 tracks test 4 error.

6 Gradient boosting

Boosting grows trees in sequence, each fitting the mistakes left so far.

Forest grows trees side by sidedatavoteeach tree sees its own resampleBoosting grows trees one after anothermeaneach new tree fits the mistakes still left12
Figure 8. Forest trees 1 train apart and vote; boosting trees 2 each fit the mistakes so far.

Each small tree fits the residuals, the signed mistakes so far, and nudges the prediction toward it.

Boosting starts from one constant guess and adds small trees, each fitted to what the model still gets wrong.
F0 = mean(y)
The start F0 predicts every point as the mean of the labels y, before any tree exists. This is the start for squared error, as in the notebook; scikit-learn's classifiers start from the log-odds of the classes instead.
ri = yi − F(xi)
The residual ri on point i is its truth yi minus the current prediction F(xi). The next tree is trained to predict these residuals. The residual is the negative gradient of the squared error ½(yi − F)² with respect to F, so each tree is one gradient-descent step on the predictions, hence the name (Friedman, 2001); for classification the tree fits the negative gradient of the log loss instead.
F ← F + lr · h(x)
The ← means "gets replaced by", so F moves a small step, the learning rate lr, toward the new tree h's output, so the boundary corrects itself where it is still wrong. A larger lr fits in fewer rounds and overfits sooner; a smaller one needs more rounds; scikit-learn's default is 0.1.

Boosting by hand with step size LR.

import numpy as np
from sklearn.tree import DecisionTreeRegressor
Tree = DecisionTreeRegressor
LR, ROUNDS = 0.01, 100
F = np.full(len(y), y.mean())
for _ in range(ROUNDS):
    r = y - F
    h = Tree(max_depth=3)
    h.fit(X, r)
    F = F + LR * h.predict(X)
  1. 1Starts from the mean, F0.
  2. 2Residuals r, what the trees still miss.
  3. 3Fits a small tree to r and adds a scaled step (Section 6).

The notebook's 100 rounds at learning rate 0.01 land at 82.7%, still underfit: each round moves the prediction only slightly.

20

Figure 9. Each round fits the mistakes left; halos, rings on points still off by more than 0.45, fall from 400 to 36 by round 20.

7 Forests and boosting in practice

A forest and a boosted model on the same split.

from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.model_selection import train_test_split

n, d = 5000, 20
X, y = make_classification(n, d)
parts = train_test_split(X, y)
Xtr, Xte, ytr, yte = parts
forest = RF(100, oob_score=True)
forest.set_params(n_jobs=-1)
forest.fit(Xtr, ytr)
boost = HGB(learning_rate=0.1)
boost.fit(Xtr, ytr)
oob = forest.oob_score_
acc_rf = forest.score(Xte, yte)
acc_gb = boost.score(Xte, yte)
  1. 1Synthetic data with a test split.
  2. 2A 100-tree forest on all cores.
  3. 3Histogram gradient boosting.
  4. 4Out-of-bag and test accuracy for both.
SettingWhere it entersRaise itLower itDefault
n_estimators, TTrees in the forest voteSmoother vote, more time; adding trees does not raise test error, it stops helpingAt 1, a single jagged tree100
max_depthQuestions on one pathFiner cuts until it memorizes noiseAt 1, one question (a stump)None (grow until pure) in RandomForestClassifier; None in HistGradientBoostingClassifier, which caps each tree at 31 leaves (max_leaf_nodes)
min_samples_leafSmallest allowed leafSmoother, smaller treesAt 1, a leaf can hold one stray point1 in RandomForestClassifier, 20 in HistGradientBoostingClassifier
max_features, mInputs each split may choose fromTrees grow alikeDiverse but weaker trees"sqrt" of the features in RandomForestClassifier
learning_rate, LRStep toward each new tree in the boosting cardFits faster, overfits soonerNeeds many more rounds0.1
max_iterBoosting roundsFits more of the residualUnderfits100, stopping early past 10,000 samples

RandomForestClassifier trains its trees in parallel. HistGradientBoostingClassifier bins each feature into 255 bins for speed and native missing values; its rounds run in order. Permutation importance on held-out data is safer than impurity-based feature_importances_, which favor features with many distinct values.

8 Strengths, limits and variants of trees

Trees suit tabular data with mixed types or missing values, and models you need to read.

feature 1feature 2true boundary, a diagonaltree, one flat step per split12
Figure 10. Every split is vertical or horizontal, so the tree 2 traces the diagonal 1 as a staircase.
CauseSymptomFix
a deep tree memorized the noiseTrain accuracy near 100%, test lowercap the depth (depth limits are regularization for trees), prune, or vote a forest
every split tests one featureStaircase boundary on a diagonaladd a combined feature such as x1 + x2, or use a linear model
greedy splits score one feature at a time, and in an XOR pattern neither helps aloneMisses a pattern that needs two features togethergrow deeper or boost so later splits can pick it up
a leaf can only repeat values it sawFlat predictions past the training rangea linear model when you must extrapolate
MethodWhat changesWhen to use
Extra Treeseach cut point is drawn at random instead of searcheda faster forest that is often just as accurate
AdaBoostreweights the points the last tree got wrongboosting with very small trees
Gradient boostingeach tree fits the residuals (Section 6)tabular data where accuracy matters most
XGBoost, LightGBMhistogram splits, a penalty on leaf weights, column samplingproduction gradient boosting at scale
Pruninggrows deep, then cuts branches that do not pay for themselvesone readable tree that does not memorize

In XGBoost, reg_lambda (default 1) penalizes large leaf weights, so raising it shrinks each tree's push, and colsample_bynode below its default 1 samples columns per split.

from xgboost import XGBClassifier
m = XGBClassifier()
m.set_params(tree_method="hist")
m.set_params(reg_lambda=1.0)
m.set_params(colsample_bynode=0.5)

Trees cut one feature at a time; Support Vector Machines (SVM) draw one boundary with the widest margin between classes.

9 References

  1. Breiman, L., et al. (1984). Classification and Regression Trees. Wadsworth.
  2. Freund, Y., et al. (1997). A decision-theoretic generalization of on-line learning and an application to boosting. Journal of Computer and System Sciences 55(1):119-139.
  3. Breiman, L. (2001). Random Forests. Machine Learning 45(1):5-32. PDF
  4. Friedman, J. H. (2001). Greedy Function Approximation: A Gradient Boosting Machine. Annals of Statistics 29(5):1189-1232.
  5. Geurts, P., et al. (2006). Extremely randomized trees. Machine Learning 63(1):3-42.
  6. Chen, T., et al. (2016). XGBoost: A Scalable Tree Boosting System. KDD. arXiv:1603.02754.
  7. Ke, G., et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NeurIPS.