Boosted Trees: Gradient Boosting Algorithm & Implementation

Michael BrenndoerferJuly 3, 202544 min read

Part of Machine Learning from Scratch

Covers boosted trees and gradient boosting, covering ensemble learning, loss functions, sequential error correction, and scikit-learn implementation.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Boosted Trees

Boosted trees, also known as gradient boosting, is a powerful ensemble learning method that builds a strong predictive model by sequentially combining multiple weak learners (typically decision trees). Unlike Random Forest, which trains trees independently and combines them through voting or averaging, boosted trees train each new tree to correct the mistakes made by the previous trees in the ensemble. This sequential, error-correcting approach allows boosted trees to achieve exceptional predictive performance, often outperforming other machine learning algorithms.

The core idea behind boosted trees is to start with a simple model (often just the mean of the target variable) and then iteratively add new trees that focus on the residuals.the errors made by the current ensemble. Each new tree is trained to predict these residuals, and when added to the ensemble, it helps correct the previous mistakes. This process continues until a stopping criterion is met, resulting in a powerful ensemble that can capture complex patterns in the data.

In simple terms, boosted trees work like a student learning from their mistakes: they start with a basic understanding, identify where they went wrong, focus on correcting those specific errors, and gradually build up their knowledge through this iterative improvement process. This approach typically produces models that are highly accurate and can handle complex, non-linear relationships in the data.

Advantages

Boosted trees offer several practical advantages and rank among the most accurate machine learning algorithms available. First, they often achieve state-of-the-art predictive performance across a wide variety of problems, frequently outperforming other algorithms including Random Forest, neural networks, and linear models. This exceptional performance makes boosted trees particularly valuable when accuracy is the primary concern.

Additionally, boosted trees can handle both numerical and categorical features effectively without requiring extensive preprocessing, and they can automatically capture complex non-linear relationships and feature interactions without explicit feature engineering. The algorithm is also stable to outliers and can handle missing values.

Another significant advantage is that boosted trees provide built-in feature importance measures, allowing you to understand which features are most influential in making predictions. The algorithm also offers flexibility in the choice of loss functions, making it adaptable to different types of problems (regression, classification, ranking, etc.). Finally, boosted trees can be fine-tuned extensively through hyperparameter optimization, allowing you to achieve optimal performance for your specific problem.

Disadvantages

Despite their exceptional performance, boosted trees have some limitations that are important to consider. One of the main drawbacks is that they are prone to overfitting, especially when using too many trees or when the learning rate is too high. This requires careful tuning of hyperparameters and often necessitates early stopping or other regularization techniques to prevent the model from memorizing the training data.

Another limitation is that boosted trees can be computationally expensive to train, especially when using many trees or when dealing with large datasets. The sequential nature of the algorithm means that trees cannot be trained in parallel (unlike Random Forest), which can make training time significantly longer for large models.

Boosted trees are also sensitive to noisy data and outliers, which can cause the algorithm to focus too much on correcting errors that may not be representative of the true underlying pattern. This sensitivity can lead to poor generalization performance if not properly managed through regularization or data cleaning.

Additionally, while boosted trees provide feature importance measures, the individual trees in the ensemble are not easily interpretable, and the sequential nature of the algorithm makes it difficult to understand the exact decision-making process. The models can also be quite large and memory-intensive, especially when using many trees, which can impact deployment and inference speed in production environments.

Formula

The mathematical foundation of boosted trees involves understanding how the algorithm sequentially builds an ensemble by minimizing a loss function through gradient descent. Let's break this down step by step, starting with the basic structure and progressing to the complete algorithm formulation.

The Ensemble Model

The final boosted tree model is an ensemble of MM decision trees, where each tree is trained sequentially to correct the errors of the previous trees. The prediction for a given input x\mathbf{x} is:

y^=m=1Mγmhm(x)\hat{y} = \sum_{m=1}^{M} \gamma_m h_m(\mathbf{x})

where:

  • y^\hat{y} is the final prediction
  • MM is the total number of trees in the ensemble
  • γm\gamma_m is the learning rate (step size) for tree mm
  • hm(x)h_m(\mathbf{x}) is the prediction from the mm-th tree for input x\mathbf{x}

The Loss Function

Boosted trees minimize a loss function L(y,y^)L(y, \hat{y}) that measures how well the model's predictions match the true values. Common loss functions include:

For Regression (Mean Squared Error):

We've already covered these loss functions in the regression chapters, so we are keeping the explanation brief.

The most common loss function for regression tasks in gradient boosting is the Mean Squared Error (MSE). It measures the average squared difference between the actual target value yy and the predicted value y^\hat{y}:

L(y,y^)=12(yy^)2L(y, \hat{y}) = \frac{1}{2}(y - \hat{y})^2

The factor of 12\frac{1}{2} is included for mathematical convenience because when you take the derivative of 12(yy^)2\frac{1}{2}(y - \hat{y})^2 with respect to y^\hat{y}, the 2 from the exponent cancels with the 12\frac{1}{2}, leaving just (y^y)(\hat{y} - y); this makes the gradient expression simpler and avoids carrying an extra constant through the calculations.

During training, the algorithm tries to minimize this loss by adjusting the model so that predictions y^\hat{y} get as close as possible to the true values yy. The gradient of this loss with respect to y^\hat{y} is simply (y^y)(\hat{y} - y), which means each new tree in the ensemble is trained to predict the residuals (errors) of the current model.

For Classification (Logistic Loss):

For binary classification problems, gradient boosting typically uses the logistic loss (also called log-loss or binary cross-entropy). This loss is specifically designed for situations where the target yy is 0 or 1, and the model's output y^\hat{y} is a real number (not a probability, but a "logit" or "log-odds" score).

The logistic loss for a single data point is:

L(y,y^)=ylog(1+ey^)+(1y)log(1+ey^)L(y, \hat{y}) = y \log(1 + e^{-\hat{y}}) + (1-y) \log(1 + e^{\hat{y}})

In gradient boosting, the logistic loss is central to the entire training process. At each boosting iteration (i.e., for each new tree that is added to the ensemble), the algorithm evaluates the logistic loss for every training example using the current prediction y^\hat{y} (which is the sum of the outputs from all previous trees).

The gradients of this loss with respect to y^\hat{y} are then computed for each example, and the next tree is trained to predict these gradients (also called "pseudo-residuals"). This means that the logistic loss is used repeatedly, at every boosting step and for every data point, to guide the model's updates and help it focus on the examples that are currently hardest to classify.

Let's look at how this loss function connects to log-odds and probabilities, step by step.

  1. Model Output (y^\hat{y}): In gradient boosting for classification, the model does not directly output a probability. Instead, it outputs a real number y^\hat{y} for each example. This number is called the logit or log-odds.

  2. From Log-Odds to Probability: To get a probability pp from y^\hat{y}, we use the sigmoid (logistic) function:

    p=11+ey^p = \frac{1}{1 + e^{-\hat{y}}}

    This maps any real value to a number between 0 and 1.

  3. From Probability to Log-Odds: If you start with a probability pp, you can get back to the log-odds (the value the model outputs) by inverting the sigmoid:

    y^=log(p1p)\hat{y} = \log\left(\frac{p}{1-p}\right)

    So, the model is really building up an estimate of the log-odds for each example.

Out[3]:
Visualization
Sigmoid curve from log-odds minus six to six to probability, with five annotated example points, a horizontal probability 0.5 decision boundary, and a vertical zero-log-odds reference.
Sigmoid transformation from log-odds to probability. The curve maps every real-valued model score to a probability between zero and one. The five labeled examples, the p = 0.5 decision boundary, and the corresponding zero log-odds reference show how boosted-tree classification scores become interpretable probabilities.
Logit curve from probability 0.01 to 0.99 to log-odds, with five annotated example points, a vertical probability 0.5 reference, and a horizontal zero-log-odds reference.
Logit transformation from probability to log-odds. This inverse mapping returns probabilities to the score space used by gradient boosting. The five labeled examples and matching p = 0.5 and zero-log-odds reference lines make the inverse relationship explicit.
  1. Why does the loss use y^\hat{y} and not pp?
    You might notice that the loss function is expressed in terms of y^\hat{y} (the log-odds), while the sigmoid function uses pp (the probability). This is intentional: the model predicts and updates values in the log-odds space, not directly in probability space.

    This is because the logistic loss is derived from the negative log-likelihood of the Bernoulli distribution. Let's walk through the full derivation:

  2. Start with the Bernoulli likelihood: First, we write down the likelihood function for a single data point, which gives the probability of observing y{0,1}y \in \{0, 1\} given the predicted probability pp.

    P(yp)=py(1p)1yP(y|p) = p^y (1-p)^{1-y}
  3. Take the negative log-likelihood: Next, we take the negative logarithm of the likelihood to obtain the loss function that we want to minimize.

    logP(yp)=log(py(1p)1y)=ylogp(1y)log(1p)-\log P(y|p) = -\log\left(p^y (1-p)^{1-y}\right) = -y \log p - (1-y) \log (1-p)
  4. Express pp in terms of the model output y^\hat{y}: Then, we rewrite the probability pp in terms of the model's output y^\hat{y} (the log-odds), since this is what the model predicts.

    p=11+ey^p = \frac{1}{1 + e^{-\hat{y}}}

    and

    1p=ey^1+ey^1 - p = \frac{e^{-\hat{y}}}{1 + e^{-\hat{y}}}
  5. Substitute pp and 1p1-p into the loss: Now, we substitute the expressions for pp and 1p1-p in terms of y^\hat{y} back into the loss function.

    ylogp(1y)log(1p)=ylog(11+ey^)(1y)log(ey^1+ey^)-y \log p - (1-y) \log (1-p) = -y \log\left(\frac{1}{1 + e^{-\hat{y}}}\right) - (1-y) \log\left(\frac{e^{-\hat{y}}}{1 + e^{-\hat{y}}}\right)
  6. Simplify the expression: Finally, we simplify the resulting expression to obtain the standard form of the logistic loss in terms of y^\hat{y}.

    =y[log(1+ey^)](1y)[y^log(1+ey^)]= -y \left[ -\log(1 + e^{-\hat{y}}) \right] - (1-y) \left[ -\hat{y} - \log(1 + e^{-\hat{y}}) \right] =ylog(1+ey^)+(1y)[y^+log(1+ey^)]= y \log(1 + e^{-\hat{y}}) + (1-y) \left[ \hat{y} + \log(1 + e^{-\hat{y}}) \right] =ylog(1+ey^)+(1y)y^+(1y)log(1+ey^)= y \log(1 + e^{-\hat{y}}) + (1-y)\hat{y} + (1-y)\log(1 + e^{-\hat{y}}) =log(1+ey^)+(1y)y^= \log(1 + e^{-\hat{y}}) + (1-y)\hat{y}

    Alternatively, you may see the loss written as:

    L(y,y^)=ylog(1+ey^)+(1y)log(1+ey^)L(y, \hat{y}) = y \log(1 + e^{-\hat{y}}) + (1-y) \log(1 + e^{\hat{y}})

    which is algebraically equivalent.

This full derivation shows how the logistic loss arises naturally from the probabilistic model, and why it is expressed in terms of y^\hat{y}. This formulation is preferred because it simplifies the optimization process and makes the gradients easy to compute, which is essential for efficient training.

To recap, the logistic loss is derived from the negative log-likelihood of the Bernoulli distribution, and is expressed in terms of y^\hat{y} (the log-odds) because the model predicts and updates that value at each boosting step.

Gradients and Pseudo-Residuals

The boosting algorithm computes the gradient of the loss with respect to y^\hat{y} (where y^\hat{y} is the log-odds score for the model's prediction, or the raw score output by the model for a given input x\mathbf{x}) for each training example. For logistic loss, this gradient is:

Ly^=(yp)\frac{\partial L}{\partial \hat{y}} = - (y - p)

This means that the "pseudo-residuals" (the targets for the next tree) are simply the difference between the true label and the current predicted probability.

  • If the model predicts pp close to yy (e.g., p=0.9p=0.9 and y=1y=1), the gradient is small, so the next tree makes only a small correction.
  • If the model predicts pp far from yy (e.g., p=0.1p=0.1 and y=1y=1), the gradient is large, so the next tree tries to make a bigger correction.

This process is repeated, with each new tree focusing on the mistakes of the current model, gradually improving the fit.

Log-loss has several properties that make it well-suited for classification tasks in gradient boosting. First, it is a strictly proper scoring rule, which means it encourages the model to produce well-calibrated probability estimates. It is also differentiable, so gradient-based optimization methods can be applied directly. Another useful property is that log-loss imposes a strong penalty on predictions that are both confident and incorrect, discouraging the model from making overconfident mistakes.

In gradient boosting for classification, the logistic loss provides a principled way to measure prediction error and guide the model's updates. At each step, the model fits a new tree to the difference between the true label and the current predicted probability, steadily reducing the overall loss and improving classification accuracy.

The Gradient Boosting Algorithm

The key insight of gradient boosting is to use the gradient of the loss function to guide the training of each new tree. Here's how it works:

Step 1: Initialize the model
Start with a simple model, typically the mean of the target variable for regression or the log-odds for classification:

F0(x)=argminγi=1nL(yi,γ)F_0(\mathbf{x}) = \arg\min_{\gamma} \sum_{i=1}^{n} L(y_i, \gamma)
  • Here, F0(x)F_0(\mathbf{x}) is the initial prediction for all inputs x\mathbf{x}.
  • L(yi,γ)L(y_i, \gamma) is the loss function, measuring the error between the true value yiy_i and a constant prediction γ\gamma.
  • nn is the number of training examples.
  • argminγ\arg\min_{\gamma} means we choose the value of γ\gamma that minimizes the total loss over all training examples.

For regression with mean squared error (MSE) loss, this simplifies to:

F0(x)=1ni=1nyiF_0(\mathbf{x}) = \frac{1}{n} \sum_{i=1}^{n} y_i

That is, the initial prediction is just the average of the target values.

Step 2: For each iteration m=1,2,,Mm = 1, 2, \ldots, M:

  • mm is the index of the current boosting iteration (tree), and MM is the total number of boosting rounds (trees).

2a. Calculate the negative gradient (pseudo-residuals)
For each training example ii, calculate the negative gradient of the loss function with respect to the current model's prediction:

rim=L(yi,Fm1(xi))Fm1(xi)r_{im} = -\frac{\partial L(y_i, F_{m-1}(\mathbf{x}_i))}{\partial F_{m-1}(\mathbf{x}_i)}
  • rimr_{im} is the pseudo-residual for example ii at iteration mm.
  • Fm1(xi)F_{m-1}(\mathbf{x}_i) is the prediction for example ii from the previous iteration.
  • The negative gradient tells us how to adjust our prediction to reduce the loss.

This gives us the "pseudo-residuals" that tell us in which direction and by how much we need to adjust our predictions.

2b. Train a new tree on the pseudo-residuals
Train a decision tree hm(x)h_m(\mathbf{x}) to predict the pseudo-residuals:

hm=argminhi=1n(rimh(xi))2h_m = \arg\min_{h} \sum_{i=1}^{n} (r_{im} - h(\mathbf{x}_i))^2
  • hm(x)h_m(\mathbf{x}) is the new tree fitted at iteration mm.
  • The tree is trained to predict rimr_{im} from the input features xi\mathbf{x}_i.
  • The sum of squared differences means we fit the tree to the pseudo-residuals using least squares.

2c. Find the optimal step size
For each leaf jj in the new tree, find the optimal step size that minimizes the loss:

γjm=argminγxiRjmL(yi,Fm1(xi)+γ)\gamma_{jm} = \arg\min_{\gamma} \sum_{\mathbf{x}_i \in R_{jm}} L(y_i, F_{m-1}(\mathbf{x}_i) + \gamma)
  • γjm\gamma_{jm} is the optimal value to add to the prediction for all examples in leaf jj of tree mm.
  • RjmR_{jm} is the set of training examples that fall into leaf jj of tree mm.
  • This step ensures that each leaf's output is chosen to minimize the loss for the examples it contains.

2d. Update the model
Add the new tree to the ensemble:

Fm(x)=Fm1(x)+γmhm(x)F_m(\mathbf{x}) = F_{m-1}(\mathbf{x}) + \gamma_m h_m(\mathbf{x})
  • Fm(x)F_m(\mathbf{x}) is the updated prediction after adding the mm-th tree.
  • γm\gamma_m is the step size (sometimes called the learning rate or shrinkage parameter) for this iteration. In some implementations, each leaf has its own γjm\gamma_{jm}, and the update is performed leaf-wise.
  • hm(x)h_m(\mathbf{x}) is the prediction of the new tree.

Mathematical Properties

Boosted trees possess several key mathematical properties that help explain their effectiveness. One of the most important is the connection to gradient descent optimization. The algorithm can be viewed as performing gradient descent in function space, where each tree represents a step in the direction of steepest descent of the loss function.

The bias-variance decomposition also applies to boosted trees, but in a different way than Random Forest. Early in the training process, the model has high bias (underfitting) but low variance. As more trees are added, the bias decreases but variance increases. The optimal number of trees balances this trade-off to minimize the total error.

Another important property is the concept of shrinkage, controlled by the learning rate γ\gamma. A smaller learning rate means smaller steps in the optimization process, which can lead to better generalization but requires more trees to achieve the same level of performance. This is often expressed as:

y^=m=1Mνhm(x)\hat{y} = \sum_{m=1}^{M} \nu h_m(\mathbf{x})

where ν\nu is the shrinkage parameter (typically between 0.01 and 0.1) that controls the learning rate.

Out[4]:
Visualization
Training and test mean squared error curves over one to 196 trees, with a dashed vertical optimal-tree reference and a star at the minimum test error.
Training and test mean squared error as trees are added. Training error decreases throughout, while the star and dashed line mark the number of trees that minimizes test error. This is gradient descent in function space made visible: each additional learner reduces the current residual error, but validation performance determines when to stop.
Bias squared, variance, and total-error curves over one to 196 trees, plus a horizontal irreducible-error reference and a marked optimal tree count.
Bias-variance decomposition across the same boosting sequence. Bias squared falls as model flexibility grows, variance rises, and total error includes the irreducible noise level. The marked optimum identifies the tree count that best balances the competing error components.

Visualizing Boosted Trees

Let's create visualizations that demonstrate how boosted trees work and how they differ from other ensemble methods.

Out[5]:
Visualization
Scatter plot of one-dimensional regression training data with four differently styled step functions labeled after one, two, three, and four boosted trees.
Sequential learning in a boosted-tree regressor. The four prediction functions show the ensemble after each of its first four shallow trees. Each new stage changes the current function to correct residual errors, while the complete training dataset remains visible for context.
Scatter plot of the same training data with a dashed single-tree prediction and a solid boosted-ensemble prediction using one hundred trees.
A single depth-three decision tree compared with a 100-tree boosted ensemble on the same training data. The single tree is visibly stepwise, whereas repeated residual correction produces a more detailed ensemble prediction.

Now let's visualize how the number of trees and learning rate affect the boosting performance:

Out[6]:
Visualization
Training and test mean squared error curves from 10 to 200 boosted trees, with a star and dashed vertical reference marking the tree count with minimum test error.
Training and test error for gradient-boosted regression at learning rate 0.1 as the ensemble grows from 10 to 200 trees. Training error decreases steadily, while the marked minimum of the test curve identifies the best generalization performance in this range. The widening gap after that point signals increasing overfit.

Example

Let's work through a detailed step-by-step example of boosted trees using a simple regression dataset. Suppose we want to predict house prices based on house size and number of bedrooms.

Given Data:

We have a small dataset of houses, each described by its size (in square feet), number of bedrooms, and price (in thousands):

  • House Size (sq ft): [1200, 1500, 1800, 2000, 2200, 2500, 2800, 3000]
  • Bedrooms: [2, 3, 3, 4, 4, 4, 5, 5]
  • House Price (thousands): [180, 220, 260, 300, 340, 380, 420, 460]

Step 1: Initialize the model

The first step in gradient boosting is to start with a simple model. For regression, this is typically just the mean of the target variable. This initial prediction is our baseline before any trees are added.

F0(x)=18i=18yi=180+220+260+300+340+380+420+4608=320F_0(\mathbf{x}) = \frac{1}{8} \sum_{i=1}^{8} y_i = \frac{180 + 220 + 260 + 300 + 340 + 380 + 420 + 460}{8} = 320

So, at this point, our model predicts 320 for every house, regardless of its features.

Step 2: Calculate pseudo-residuals for the first tree

Next, we measure how well our initial model is doing by calculating the errors (residuals) for each data point. In gradient boosting, these are called "pseudo-residuals" and, for mean squared error (MSE) loss, they are simply the difference between the true value and the current prediction.

ri1=yiF0(xi)=yi320r_{i1} = y_i - F_0(\mathbf{x}_i) = y_i - 320

Calculating these for each house:

  • House 1: 180 - 320 = -140
  • House 2: 220 - 320 = -100
  • House 3: 260 - 320 = -60
  • House 4: 300 - 320 = -20
  • House 5: 340 - 320 = 20
  • House 6: 380 - 320 = 60
  • House 7: 420 - 320 = 100
  • House 8: 460 - 320 = 140

These residuals represent the errors our current model makes, and the next tree will try to predict these errors.

Step 3: Train the first tree on pseudo-residuals

We now fit a decision tree to these pseudo-residuals. The tree tries to find patterns in the features (house size and bedrooms) that explain the errors made by the initial model. For simplicity, suppose the tree splits the data as follows:

  • If House Size ≤ 2000: predict -80
  • If House Size > 2000: predict 80

This means the tree has learned that smaller houses tend to be under-predicted (so it adds a negative correction), and larger houses are over-predicted (so it adds a positive correction).

Step 4: Find optimal step size

For each leaf of the tree, we determine how much to move the prediction in the direction suggested by the tree. This is done by finding the step size (also called the "shrinkage" or "learning rate") that minimizes the loss for the data points in that leaf. For the left leaf (House Size ≤ 2000):

γ11=argminγi:sizei2000(yi(320+γ(80)))2\gamma_{11} = \arg\min_{\gamma} \sum_{i: \text{size}_i \leq 2000} (y_i - (320 + \gamma \cdot (-80)))^2

This optimization finds the best value of γ\gamma to reduce the error for the houses in this group. The same process is repeated for the right leaf.

Step 5: Update the model

We update our model by adding the predictions from the first tree, scaled by the optimal step size:

F1(x)=F0(x)+γ1h1(x)=320+γ1h1(x)F_1(\mathbf{x}) = F_0(\mathbf{x}) + \gamma_1 h_1(\mathbf{x}) = 320 + \gamma_1 h_1(\mathbf{x})

Here, h1(x)h_1(\mathbf{x}) is the prediction from the first tree for input x\mathbf{x}, and γ1\gamma_1 is the step size we just computed.

Step 6: Calculate new pseudo-residuals

With the updated model, we again compute the pseudo-residuals, which now reflect the errors remaining after the first tree's corrections:

ri2=yiF1(xi)r_{i2} = y_i - F_1(\mathbf{x}_i)

These new residuals are typically smaller in magnitude, as the model has improved.

Step 7: Repeat the process

We repeat steps 3-6 for each subsequent tree. Each new tree is trained to predict the current residuals (errors), and the model is updated accordingly. Over time, the ensemble of trees becomes better at capturing the patterns in the data, as each tree focuses on the mistakes of the previous ones.

Final Prediction:

After training MM trees, the final prediction for a new data point is the sum of the initial prediction and the contributions from all the trees:

y^=F0(x)+m=1Mγmhm(x)=320+m=1Mγmhm(x)\hat{y} = F_0(\mathbf{x}) + \sum_{m=1}^{M} \gamma_m h_m(\mathbf{x}) = 320 + \sum_{m=1}^{M} \gamma_m h_m(\mathbf{x})

In summary, this example illustrates how boosted trees work: starting from a simple model, they iteratively add trees that correct the errors of the current ensemble, gradually building a more accurate predictor by focusing on the hardest-to-predict cases at each step.

Implementation in Scikit-learn

Boosted trees are implemented in scikit-learn through the GradientBoostingClassifier and GradientBoostingRegressor classes. Let's implement a complete example with proper preprocessing and evaluation:

In[7]:
Code
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score, train_test_split

np.random.seed(42)
n_samples = 1000

# Generate features for classification
age = np.random.normal(40, 15, n_samples)
age = np.clip(age, 18, 80)

income = np.random.normal(60, 20, n_samples)
income = np.clip(income, 20, 150)

experience = np.random.normal(10, 8, n_samples)
experience = np.clip(experience, 0, 30)

# Create target based on realistic relationship
log_odds = -5 + 0.02 * age + 0.01 * income + 0.05 * experience
prob_purchase = 1 / (1 + np.exp(-log_odds))
y_class = np.random.binomial(1, prob_purchase, n_samples)

X_class = np.column_stack([age, income, experience])  # Combine features

X_train_class, X_test_class, y_train_class, y_test_class = train_test_split(
    X_class, y_class, test_size=0.2, random_state=42, stratify=y_class
)

gb_classifier = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
)

gb_classifier.fit(X_train_class, y_train_class)

y_pred_class = gb_classifier.predict(X_test_class)
y_pred_proba_class = gb_classifier.predict_proba(X_test_class)[:, 1]

# Use a more robust cross-validation approach
import warnings

from sklearn.model_selection import StratifiedKFold

# Suppress the specific warning about failed fits
warnings.filterwarnings("ignore", category=UserWarning, module="sklearn")

# Check class distribution to ensure we have both classes
unique_classes, class_counts = np.unique(y_train_class, return_counts=True)
print(
    f"Class distribution in training set: {dict(zip(unique_classes, class_counts))}"
)

# Use StratifiedKFold with error handling
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_scores = cross_val_score(
    gb_classifier, X_train_class, y_train_class, cv=skf, scoring="roc_auc"
)
Out[8]:
Console
Classification Performance:
Accuracy: 0.945
ROC AUC: 0.668

Feature Importance:
Age: 0.415
Income: 0.329
Experience: 0.256

Sample Predictions:
Age: 21.8, Income: 57.0, Exp: 2.7 -> Prob: 0.007, Actual: 0
Age: 39.8, Income: 90.6, Exp: 7.6 -> Prob: 0.061, Actual: 0
Age: 39.6, Income: 64.3, Exp: 8.7 -> Prob: 0.017, Actual: 1
Age: 35.4, Income: 28.3, Exp: 0.0 -> Prob: 0.003, Actual: 0
Age: 52.4, Income: 87.8, Exp: 11.8 -> Prob: 0.027, Actual: 0
Classification Report:

              precision    recall  f1-score   support

           0       0.95      0.99      0.97       190
           1       0.00      0.00      0.00        10

    accuracy                           0.94       200
   macro avg       0.47      0.50      0.49       200
weighted avg       0.90      0.94      0.92       200


Confusion Matrix:
[[189   1]
 [ 10   0]]

Cross-validation ROC AUC scores:
 [0.51439145 0.58100329 0.55180921 0.52672697 0.48026316]

Mean CV ROC AUC: 0.531 (+/- 0.068)
Out[9]:
Console
Feature Importance in Boosted Tree Classifier:
==================================================
Age         : 0.415
Income      : 0.329
Experience  : 0.256
==================================================
Higher values indicate more influential features.
Out[10]:
Visualization
Boosted-tree receiver operating characteristic curve from false positive rate zero to one and true positive rate zero to one, with the measured AUC in the legend and a dashed random-classifier diagonal.
Receiver operating characteristic curve for the boosted-tree classifier. The curve traces the trade-off between true positive rate and false positive rate across classification thresholds; its legend reports the measured area under the curve. The dashed diagonal retains the random-classifier baseline at AUC = 0.5.

Now let's implement a regression example:

In[11]:
Code
np.random.seed(42)

# Generate features for regression
house_size = np.random.normal(2000, 500, n_samples)
house_size = np.clip(house_size, 800, 4000)

bedrooms = np.random.poisson(3, n_samples)
bedrooms = np.clip(bedrooms, 1, 6)

bathrooms = np.random.poisson(2, n_samples)
bathrooms = np.clip(bathrooms, 1, 4)

# Create target based on realistic relationship
house_price = (
    50000
    + 100 * house_size
    + 20000 * bedrooms
    + 15000 * bathrooms
    + np.random.normal(0, 10000, n_samples)
)
house_price = np.clip(house_price, 100000, 800000)

X_reg = np.column_stack([house_size, bedrooms, bathrooms])

X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(
    X_reg, house_price, test_size=0.2, random_state=42
)

gb_regressor = GradientBoostingRegressor(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
)

gb_regressor.fit(X_train_reg, y_train_reg)
y_pred_reg = gb_regressor.predict(X_test_reg)
Out[12]:
Console
Regression Performance:
R² Score: 0.958
RMSE: 12387.57

Feature Importance:
House Size: 0.688
Bedrooms: 0.249
Bathrooms: 0.063

Sample Predictions:
Size: 2272, Beds: 3, Baths: 1 -> Pred: $356879, Actual: $339322
Size: 2491, Beds: 3, Baths: 3 -> Pred: $405659, Actual: $393120
Size: 1080, Beds: 4, Baths: 2 -> Pred: $274314, Actual: $260181
Size: 1713, Beds: 1, Baths: 4 -> Pred: $297229, Actual: $314224
Size: 1438, Beds: 2, Baths: 1 -> Pred: $248029, Actual: $247561
Out[13]:
Console
Feature Importance in Boosted Tree Regressor:
==================================================
House Size  : 0.688
Bedrooms    : 0.249
Bathrooms   : 0.063
==================================================
Higher values indicate more influential features.
Out[14]:
Visualization
Scatter plot of held-out actual and predicted house prices with dollar-formatted axes, a dashed perfect-prediction diagonal, and the test-set R-squared value in the title.
Predicted versus actual house prices for the boosted-tree regressor. Each point is a held-out house, and the dashed diagonal marks perfect predictions. The displayed R² summarizes overall fit, while vertical distance from the diagonal shows the prediction error for each observation.

Hyperparameter Tuning

Boosted trees have several important hyperparameters that can significantly affect performance:

In[15]:
Code
param_grid = {
    "n_estimators": [50, 100, 200],
    "learning_rate": [0.01, 0.1, 0.2],
    "max_depth": [3, 5, 7],
    "min_samples_split": [2, 5, 10],
    "min_samples_leaf": [1, 2, 4],
}

# Use a smaller subset for faster tuning
X_train_small = X_train_class[:500]
y_train_small = y_train_class[:500]

grid_search = GridSearchCV(
    GradientBoostingClassifier(random_state=42),
    param_grid,
    cv=3,  # 3-fold CV for faster tuning
    scoring="roc_auc",
    n_jobs=-1,
)

grid_search.fit(X_train_small, y_train_small)
Out[16]:
Console
Best parameters:
{'learning_rate': 0.1, 'max_depth': 5, 'min_samples_leaf': 2, 'min_samples_split': 10, 'n_estimators': 50}

Best cross-validation score: 0.654
Test set ROC AUC: 0.621
Out[17]:
Visualization
Training and validation ROC AUC learning curves over ten training-set sizes, with different line styles and markers and shaded one-standard-deviation cross-validation bands around each mean.
Learning curve for the regularized boosted-tree classifier across ten training-set sizes. Solid circles show the mean training ROC AUC, dashed squares show mean validation ROC AUC, and the shaded bands preserve one standard deviation across five stratified folds. The gap between the curves reveals how model fit and generalization change as more observations become available.

Key Parameters

Below are some of the main parameters that affect how Gradient Boosting works and performs.

  • n_estimators: Number of boosting stages (trees) to perform (default: 100). More trees generally improve performance but increase computation time and risk of overfitting. Start with 100 and increase if validation scores improve.
  • learning_rate: Shrinkage parameter that controls the contribution of each tree (default: 0.1). Lower values (0.01-0.1) require more trees but often lead to better generalization. Values between 0.01 and 0.2 work well for most datasets.
  • max_depth: Maximum depth of each tree (default: 3). Controls the complexity of individual trees. Shallow trees (3-5) work well for boosting as they focus on learning simple patterns iteratively.
  • min_samples_split: Minimum samples required to split an internal node (default: 2). Higher values prevent overfitting by requiring more data for splits. Values of 5-10 are commonly used.
  • min_samples_leaf: Minimum samples required in a leaf node (default: 1). Higher values smooth the model and prevent overfitting. Values of 2-4 work well for most problems.
  • subsample: Fraction of samples to use for fitting each tree (default: 1.0). Values less than 1.0 introduce stochastic gradient boosting, which can help prevent overfitting. Try values between 0.5 and 0.8.
  • loss: Loss function to optimize (default: 'log_loss' for classification, 'squared_error' for regression). Different loss functions are appropriate for different problem types.
  • random_state: Seed for reproducibility (default: None). Set to an integer to ensure consistent results across runs.

Key Methods

The following are the most commonly used methods for interacting with Gradient Boosting models.

  • fit(X, y): Trains the gradient boosting model on the training data X and target values y. Each tree is trained sequentially to correct the errors of previous trees.
  • predict(X): Returns predicted class labels (classification) or values (regression) for input data X by summing predictions from all trees in the ensemble.
  • predict_proba(X): Returns probability estimates for each class (classification only). Useful for setting custom decision thresholds or understanding prediction confidence.
  • score(X, y): Returns the mean accuracy (classification) or R² score (regression) on the given test data.
  • staged_predict(X): Returns predictions at each stage of boosting. Useful for determining the optimal number of trees and implementing early stopping.

Practical Implications

Boosted trees excel when predictive accuracy is the primary concern and computational resources allow for thorough hyperparameter tuning. The algorithm performs particularly well on tabular data with complex, non-linear relationships and feature interactions that simpler methods struggle to capture. This makes boosted trees a popular choice in machine learning competitions and production systems where prediction quality justifies the computational investment.

The algorithm handles both numerical and categorical features with minimal preprocessing, and provides built-in feature importance measures for understanding which variables drive predictions. This combination of high accuracy and interpretability makes boosted trees valuable in finance, healthcare, e-commerce, and other domains. However, the sequential training process requires more time than parallelizable methods like Random Forest, and the tendency to overfit noisy data necessitates careful validation and regularization strategies.

Data Requirements and Preprocessing

Unlike distance-based algorithms, boosted trees operate on feature values directly through threshold-based splits, eliminating the need for feature scaling or normalization. This simplifies preprocessing because differences in feature scales or units do not affect the splits.

The algorithm can handle missing values through surrogate splits, but explicit imputation often improves performance. For categorical variables, encoding strategies depend on cardinality: low-cardinality features can be encoded directly, while high-cardinality features benefit from target encoding or frequency encoding to avoid excessive dimensionality. Feature engineering can enhance performance, particularly domain-specific transformations or interaction terms, though the algorithm's ability to learn non-linear relationships means that many interactions emerge naturally during training.

Data quality significantly impacts performance. While boosted trees handle moderate noise reasonably well, they can overfit to outliers or systematic errors in the training data. This sensitivity stems from the sequential error-correction mechanism: if early trees fit to noise, subsequent trees may compound these errors rather than correct them. Careful data cleaning and validation set monitoring help mitigate this issue.

Best Practices

Effective use of boosted trees requires balancing model complexity with generalization through careful hyperparameter selection. Start with conservative settings: 100 trees with a learning rate of 0.1 and shallow trees (depth 3-5). Monitor both training and validation performance, increasing the number of trees if validation scores continue to improve. Lower learning rates (0.01-0.05) often yield better results but require proportionally more trees to achieve comparable performance.

Early stopping provides a principled approach to determining the optimal number of trees. By monitoring validation set performance during training, you can halt the process when additional trees no longer improve generalization. This prevents overfitting while avoiding the need to manually specify the number of trees. Combine early stopping with subsampling (training each tree on 50-80% of the data) to introduce stochasticity that further reduces overfitting risk.

Evaluation should employ multiple metrics appropriate to the problem type. For classification, combine accuracy and ROC AUC with precision and recall to understand performance across different decision thresholds. For regression, examine both R² and RMSE to assess explained variance and prediction error magnitude. Cross-validation provides more reliable performance estimates and helps detect overfitting, particularly when combined with stratified sampling for imbalanced classification problems.

Common Pitfalls

Overfitting represents the most common failure mode for boosted trees, manifesting as excellent training performance but poor generalization. This typically occurs when using too many trees, high learning rates, or insufficient regularization. The sequential nature of boosting means that later trees can memorize training noise rather than learning generalizable patterns. Combat this by implementing early stopping based on validation set performance, using lower learning rates with more trees, and applying tree-level regularization through depth limits and minimum sample requirements.

Computational efficiency becomes problematic for large datasets or when extensive hyperparameter tuning is required. Unlike Random Forest, which trains trees in parallel, boosted trees must train sequentially, making the process inherently slower. While individual tree training can use multiple cores through n_jobs, the overall boosting process remains serial. For large-scale applications, consider using modern implementations like XGBoost or LightGBM that employ algorithmic optimizations, or reduce dataset size through stratified sampling while ensuring representative coverage of the feature space.

The algorithm's sensitivity to data quality can undermine performance when training data contains outliers or labeling errors. Each tree attempts to correct previous errors, so systematic noise or outliers receive increasing attention as boosting progresses. This can cause the model to fit noise rather than signal. Address this through careful data cleaning, using robust loss functions that downweight extreme residuals, and monitoring feature importance to identify features that may be capturing noise rather than meaningful patterns.

Computational Considerations

Training complexity scales linearly with the number of trees and the size of the dataset, but the sequential nature of boosting prevents full parallelization. Each tree must wait for all previous trees to complete before training can begin. For datasets exceeding 100,000 samples, training time can become substantial, particularly when performing grid search over hyperparameters. Modern implementations like XGBoost and LightGBM address this through histogram-based algorithms and other optimizations that significantly reduce training time without sacrificing accuracy.

Memory requirements depend primarily on the number of trees and their depth. A typical model with 100 trees of depth 5 requires modest memory, but ensembles with thousands of trees or very deep trees can consume substantial resources. During training, the algorithm must store the entire dataset in memory along with residuals and predictions for each boosting iteration. For memory-constrained environments, consider using fewer trees, limiting tree depth, or employing out-of-core learning approaches that process data in batches.

Performance and Deployment Considerations

Inference speed requires evaluating all trees in the ensemble sequentially, with each tree traversing from root to leaf based on feature values. For real-time applications requiring sub-millisecond predictions, this can become a bottleneck. Model compression techniques like tree pruning or knowledge distillation can reduce the ensemble size while maintaining most of the predictive performance. Alternatively, caching predictions for common input patterns or using approximate nearest neighbor lookups can accelerate inference in production systems.

Model maintenance presents challenges because boosted trees cannot be updated incrementally.new data requires complete retraining. This differs from online learning algorithms that can update weights continuously. For applications requiring frequent updates, establish a retraining schedule that balances model freshness with computational cost. Monitor prediction performance on recent data to detect concept drift, which may indicate the need for retraining even before the scheduled update. Feature importance tracking across retraining cycles can reveal evolving patterns in the data and inform feature engineering efforts.

Summary

Boosted trees are a powerful and sophisticated ensemble learning algorithm that achieves exceptional predictive performance by sequentially building an ensemble of decision trees, with each new tree focusing on correcting the errors made by the previous trees. This iterative, error-correcting approach allows boosted trees to capture complex, non-linear relationships and feature interactions that simpler algorithms might miss.

The key strength of boosted trees lies in their ability to achieve state-of-the-art predictive accuracy across a wide variety of problems. The algorithm can handle both numerical and categorical features effectively, provides built-in feature importance measures, and offers flexibility in the choice of loss functions, making it adaptable to different types of problems.

However, boosted trees are prone to overfitting and require careful hyperparameter tuning to achieve optimal performance. The sequential nature of the algorithm makes training computationally expensive, and the models can be quite large and memory-intensive. Additionally, while boosted trees provide feature importance measures, the individual trees in the ensemble are not easily interpretable, and the sequential nature of the algorithm makes it difficult to understand the exact decision-making process.

Despite these limitations, boosted trees remain one of the most useful and effective machine learning algorithms, often achieving the best performance on tabular data problems. They adapt to many problem types while maintaining high predictive accuracy, which makes them particularly useful in machine learning competitions, financial modeling, and other domains where accuracy is important. However, they require careful tuning and validation to prevent overfitting and ensure good generalization performance.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about gradient boosting and boosted trees.

Boosted Trees Quiz

Question 1 of 80 of 8 completed
What is the fundamental difference between gradient boosting and Random Forest ensemble methods?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025boostedtrees, author = {Michael Brenndoerfer}, title = {Boosted Trees: Gradient Boosting Algorithm & Implementation}, year = {2025}, url = {https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-15} }
APAAcademic
Michael Brenndoerfer (2025). Boosted Trees: Gradient Boosting Algorithm & Implementation. Retrieved from https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn
MLAAcademic
Michael Brenndoerfer. "Boosted Trees: Gradient Boosting Algorithm & Implementation." 2026. Web. September 15, 2026. <https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn>.
CHICAGOAcademic
Michael Brenndoerfer. "Boosted Trees: Gradient Boosting Algorithm & Implementation." Accessed September 15, 2026. https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Boosted Trees: Gradient Boosting Algorithm & Implementation'. Available at: https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn (Accessed: September 15, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Boosted Trees: Gradient Boosting Algorithm & Implementation. https://mbrenndoerfer.com/writing/boosted-trees-gradient-boosting-complete-guide-algorithm-implementation-scikit-learn

About the author

Continue with the full handbook

This chapter is part of Machine Learning from Scratch. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Machine Learning from Scratch
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.