Random Forest (Regression): Principles, Examples and Python Implementation

Random Forest (Régression) : Guide Complet — Principes, Exemples et Implémentation Python

Random Forest (Regression): Complete Guide — Principles, Examples and Python Implementation

Summary

Random Forest applied to regression is one of the most popular ensemble methods in supervised learning. Unlike the classification version which uses majority voting, Random Forest for regression performs a mean of predictions from all the trees in the forest. This approach, introduced by Leo Breiman in 2001, combines the power of bagging (Bootstrap Aggregating) with random feature selection at each split, producing a robust, accurate, and overfitting-resistant model.

This complete guide explores the mathematical principles, intuition, practical Python implementation with scikit-learn, and concrete use cases for Random Forest regression.

Mathematical Principle of Random Forest Regression

Forest Construction

Random Forest regression builds B independent decision trees, each trained on a bootstrap sample (drawing with replacement) of size n from the original training set.

The process breaks down as follows:

1. Bootstrap sampling: For each tree b (from 1 to B), we draw n observations with replacement from the training set. On average, approximately 63.2% of unique observations appear in each bootstrap sample. The remaining observations (~36.8%) form the Out-Of-Bag (OOB) set, used for internal evaluation.

2. Building each tree: At each node of each tree, instead of searching for the best variable among all p available variables as a classical tree would do, we randomly draw a subset of m variables (m < p, typically m = p/3 for regression). The best split is chosen only among these m candidate variables. This random restriction decorrelates the trees from each other, which is essential for the mean of predictions to be effectively more performant than a single tree.

3. Mean prediction: For a new observation x, each tree b produces a prediction f_b(x). The final Random Forest regression prediction is the arithmetic mean of all individual predictions:

$$
\hat{f}{RF}(x) = \frac{1}{B}\sum f_b(x)
$$}^{B

This simple mean is remarkably effective at reducing the variance of the final prediction.

Out-Of-Bag Error (OOB MSE)

The OOB error is computed by evaluating each observation only on the trees that did not see it during training (the trees for which this observation was outside the bootstrap sample). The Mean Squared Error OOB writes:

$$
\mathrm{MSE}{OOB} = \frac{1}{n}\sum(x_i)\right)^2
$$}^{n}\left(y_i – \hat{f}_{RF}^{OOB

where f_RF_OOB(x_i) is the mean of predictions for x_i produced only by trees that were not trained with observation i. This metric provides a bias-free estimate of the generalization error without requiring a separate validation set.

Variance Reduction

The fundamental theorem behind Random Forest is as follows: if each individual tree has a variance $\mathrm{Var}(f_b) = \sigma^2$ and the trees have a correlation $\rho$ between them, then the variance of the mean of $B$ trees is:

$$
\mathrm{Var}(\hat{f}_{RF}) = \rho \sigma^2 + \frac{1 – \rho}{B}\sigma^2
$$

When $B$ tends to infinity, the variance converges to $\rho \sigma^2$. The key is therefore to reduce the correlation $\rho$ between trees, which the random feature selection at each split accomplishes efficiently.

Intuition: Why the Mean Beats the Single Tree

Imagine you need to estimate the price of an apartment. You ask a single real estate agent: they might be wrong because their estimate depends on their personal experience, their cognitive biases, and the few transactions they have in mind.

Now imagine you ask 500 independent real estate agents, each having studied a slightly different sample of the local market with slightly different criteria. If you take the mean of all their estimates, the result will almost certainly be more reliable than that of a single expert. This is exactly the idea of Random Forest regression.

Each tree in the forest is a “partial expert”:

  • It is trained on a random subset of the data (bootstrap), so it doesn’t see the full picture.
  • At each split decision, it only considers a random subset of features, so it doesn’t always rely on the same signals.
  • Individually, each tree can make errors, even overfit.

But when you average all these estimates, errors compensate for each other. Trees that overestimate compensate for those that underestimate. This is called variance reduction by averaging, and it is the central mechanism that makes Random Forest regression so performant.

Python Implementation with scikit-learn

Fundamentals: RandomForestRegressor

import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

# Generate synthetic data
np.random.seed(42)
n_samples = 1000
X = np.random.randn(n_samples, 5)
# Non-linear relationship with interactions
y = (2.5 * X[:, 0]**2
     + 1.8 * np.sin(X[:, 1])
     + 0.7 * X[:, 2] * X[:, 3]
     + 0.3 * X[:, 4]**3
     + np.random.randn(n_samples) * 0.5)

# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Train Random Forest regression
rf = RandomForestRegressor(
    n_estimators=200,
    max_depth=15,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
    n_jobs=-1
)
rf.fit(X_train, y_train)

# Predictions and evaluation
y_pred = rf.predict(X_test)

print(f"MSE  : {mean_squared_error(y_test, y_pred):.4f}")
print(f"RMSE : {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
print(f"MAE  : {mean_absolute_error(y_test, y_pred):.4f}")
print(f"R2   : {r2_score(y_test, y_pred):.4f}")

Comparison with a Single Decision Tree

To understand the contribution of the ensemble approach, let’s compare the Random Forest with a single decision tree:

from sklearn.tree import DecisionTreeRegressor

# Single decision tree
dt = DecisionTreeRegressor(max_depth=15, random_state=42)
dt.fit(X_train, y_train)
y_pred_dt = dt.predict(X_test)

# Random Forest
rf = RandomForestRegressor(n_estimators=200, max_depth=15, random_state=42)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

print("=== Single Decision Tree ===")
print(f"R2 train: {r2_score(y_train, dt.predict(X_train)):.4f}")
print(f"R2 test : {r2_score(y_test, y_pred_dt):.4f}")
diff_dt = r2_score(y_train, dt.predict(X_train)) - r2_score(y_test, y_pred_dt)
print(f"Gap     : {diff_dt:.4f} (overfitting)")
print()
print("=== Random Forest (200 trees) ===")
print(f"R2 train: {r2_score(y_train, rf.predict(X_train)):.4f}")
print(f"R2 test : {r2_score(y_test, y_pred_rf):.4f}")
diff_rf = r2_score(y_train, rf.predict(X_train)) - r2_score(y_test, y_pred_rf)
print(f"Gap     : {diff_rf:.4f} (overfitting)")

The single tree typically shows significant overfitting (excellent R2 on training but considerably lower on test), while the Random Forest reduces this gap considerably through averaging.

Feature Importance

Random Forest provides a natural measure of each feature’s importance based on the mean reduction in impurity (MSE) contributed by each feature across all trees:

import pandas as pd

# Feature importance
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]

feature_names = [f"Feature {i+1}" for i in range(5)]

print("=== Feature Importance ===")
for rank, idx in enumerate(indices, 1):
    print(f"  {rank}. {feature_names[idx]:12s} : {importances[idx]:.4f}")

Impact of the Number of Trees (n_estimators)

One of the major advantages of Random Forest is that increasing the number of trees does not cause overfitting. The error curve simply converges toward a floor:

# Study of n_estimators impact
n_estimators_range = [5, 10, 25, 50, 100, 200, 500]
train_scores = []
test_scores = []

for n in n_estimators_range:
    rf_tmp = RandomForestRegressor(n_estimators=n, max_depth=15, random_state=42, n_jobs=-1)
    rf_tmp.fit(X_train, y_train)
    train_scores.append(r2_score(y_train, rf_tmp.predict(X_train)))
    test_scores.append(r2_score(y_test, rf_tmp.predict(X_test)))

print("=== Impact of n_estimators ===")
for n, tr, te in zip(n_estimators_range, train_scores, test_scores):
    print(f"  n={n:4d}  ->  R2 train: {tr:.4f}  R2 test: {te:.4f}")

We observe that test R2 improves rapidly up to approximately 50-100 trees, then stabilizes. This is why values between 100 and 300 trees constitute a good tradeoff between performance and computation time.

Data with make_friedman1

The Friedman #1 dataset is a classic benchmark for non-linear regression:

from sklearn.datasets import make_friedman1

X_f, y_f = make_friedman1(n_samples=1500, noise=1.0, random_state=42)
print(f"Shape of X: {X_f.shape}")  # (1500, 10)

# Only the first 5 variables are informative
# The last 5 are pure noise

Xf_train, Xf_test, yf_train, yf_test = train_test_split(
    X_f, y_f, test_size=0.2, random_state=42
)

rf_fried = RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1)
rf_fried.fit(Xf_train, yf_train)
y_pred_fried = rf_fried.predict(Xf_test)
print(f"R2 test (Friedman1): {r2_score(yf_test, y_pred_fried):.4f}")

# Verify that RF correctly identifies important features
importances_f = rf_fried.feature_importances_
for i, imp in enumerate(importances_f):
    status = "informative" if i < 5 else "noise"
    print(f"  Variable {i:2d} ({status:11s}): importance = {imp:.4f}")

Variables 5 through 9 (pure noise) should receive importances close to zero, demonstrating the Random Forest’s ability to automatically ignore uninformative features.

Key Hyperparameters of RandomForestRegressor

n_estimators

The number of trees in the forest. Increasing this value improves stability and performance until convergence. More trees = more computation time, but never overfitting. Typical values: 100 to 500.

criterion

The split quality criterion:

  • squared_error (default): minimizes variance (mean squared error). This is the standard choice.
  • absolute_error: minimizes absolute error. More robust to outliers, but often slower to converge.
  • friedman_mse: a variant of MSE with Friedman’s correction, which can produce better splits in certain cases.
  • poisson: suited for count data (non-negative integer values).

max_depth

The maximum depth of each tree. Limiting depth prevents trees from becoming too complex and memorizing noise. For Random Forest, we can often afford fairly deep trees (10-30) since bagging and random selection already control overfitting. Default: None (trees grown until all leaves are pure or contain min_samples_split samples).

min_samples_split

The minimum number of samples required to split an interior node. A higher value (5, 10, 20) makes the model more conservative and reduces the risk of overfitting on local noise.

min_samples_leaf

The minimum number of samples required in a leaf. Controlling this parameter smooths predictions. A leaf with several samples gives a more stable mean. Typical values: 1 to 5.

max_features

The maximum number of features considered at each split. This is the most important parameter for tree decorrelation:

  • sqrt or auto: sqrt(p) variables (standard for classification, sometimes used in regression).
  • log2: log2(p) variables.
  • None: all available features (equivalent to pure bagging, without random selection).
  • A fixed number: for example max_features=3 or a float like 0.3 (30% of features).

For regression, the default is 1.0 (all features), but reducing to sqrt or 1/3 can improve generalization on datasets with many features.

bootstrap

If True (default), each tree is trained on a bootstrap sample. If False, each tree sees the entire training set (yielding Extremely Randomized Trees if max_features is also reduced).

oob_score

If True, the Out-Of-Bag score is computed automatically during training. Useful for evaluation without a separate validation set:

rf_oob = RandomForestRegressor(
    n_estimators=300,
    oob_score=True,
    random_state=42,
    n_jobs=-1
)
rf_oob.fit(X_train, y_train)
print(f"R2 OOB: {rf_oob.oob_score_:.4f}")

random_state

The random seed for reproducibility. Always set it in experimental or production contexts.

Advantages and Limitations of Random Forest Regression

Advantages

  1. No overfitting with more trees: Increasing n_estimators never degrades performance, computation time permitting.
  2. Little preprocessing needed: No need to normalize or standardize data. RF natively handles heterogeneous scales.
  3. Automatic interaction handling: Trees naturally capture non-linear interactions between features without needing to specify them.
  4. Robustness to outliers: The mean of predictions is less sensitive to extreme values than a linear regression.
  5. Built-in feature importance: No external procedure needed to evaluate each feature’s contribution.
  6. Resistant to noise in features: Uninformative features do not significantly affect performance.
  7. Free OOB evaluation: The Out-Of-Bag score provides a reliable estimate of generalization without a validation set.
  8. Works well on tabular data: Often competitive with more complex methods on structured data.

Limitations

  1. No extrapolation: RF cannot predict outside the range of values observed in training. Each leaf returns a mean of training values, so predictions are always bounded by the min/max of the training set.
  2. Step-like predictions: The prediction function is a staircase function (piecewise constant), which can be problematic if a smooth response is expected.
  3. Memory and inference cost: Storing hundreds of deep trees consumes memory. Prediction requires traversing all trees.
  4. Less performant than boosting: On many benchmarks, Gradient Boosting (XGBoost, LightGBM) or neural networks surpass Random Forest in pure precision.
  5. Hard to interpret: Unlike a single tree or linear regression, it is impossible to visualize the complete rule set of a 300-tree RF.
  6. Bias toward high-cardinality features: Continuous features with many unique values tend to receive artificially high importances.

4 Concrete Use Cases

Use Case 1: Real Estate Price Prediction

Estimate the sale price of a property based on features like surface area, number of rooms, year of construction, neighborhood, and distance to public transit. RF excels here thanks to non-linear interactions (e.g., the effect of surface area varies by neighborhood) and robustness to missing data through tree averaging.

Use Case 2: Energy Consumption Prediction

Predict a building’s electricity consumption based on outdoor temperature, humidity, time of day, day of week, and building characteristics. The relationships are highly non-linear (air conditioning consumption is not proportional to temperature), which RF captures naturally.

Use Case 3: Travel Time Estimation

Estimate travel time between two points based on distance, time of day, day of week, weather conditions, and road type. RF can model complex interactions (a traffic jam at 6 PM does not have the same impact as at 2 PM) without requiring manual feature engineering.

Use Case 4: Agricultural Yield Prediction

Estimate crop yield (in tons per hectare) from weather data (rainfall, temperature, sunshine), soil characteristics (pH, nitrogen, organic matter), and farming practices (irrigation, fertilization). RF handles the complex relationships and interactions between many heterogeneous variables well.

Best Practices and Tips

  1. Start with default values: RandomForestRegressor() already works well as a baseline. Then adjust progressively.
  2. Use OOB when your data is limited: oob_score=True avoids wasting data on validation.
  3. Tune max_features first: If you only have time to optimize one hyperparameter, it’s this one.
  4. Increase n_estimators until stabilization: Plot the OOB or validation curve as a function of the number of trees to find the optimal point.
  5. Combine with cross-validation: cross_val_score with KFold gives a reliable and robust estimate of performance.
  6. Don’t normalize unnecessarily: Unlike linear regressions or neural networks, RF doesn’t need it.

See Also