Random Forest


1. What Is a Random Forest?

A Random Forest is an ensemble of B decision trees, each trained on a bootstrap sample of the training data and each node restricted to a random subset of features at split time. Predictions are aggregated by majority vote (classification) or averaging (regression).

Introduced by Leo Breiman in 2001 — building on his own Bagging (1996) and Tin Kam Ho's Random Subspace Method (1998) — Random Forest combines both sources of randomness to produce trees that are diverse enough that their errors cancel upon aggregation.

Property Value
Type Supervised ensemble (bagging family)
Task Classification, Regression, Survival, Anomaly
Base learner CART decision tree (fully grown, unpruned)
Aggregation Majority vote (class) / Mean (regression)
Key randomness Bootstrap samples + random feature subsets
Parallelism Embarrassingly parallel — trees are independent
Convergence Monotone — more trees never hurt
sklearn class RandomForestClassifier, RandomForestRegressor

2. The Two Sources of Randomness — In Full Detail

2.1 Bootstrap Sampling — Full Statistics

Each tree is trained on a bootstrap sample: m samples drawn with replacement from the m training samples.

Probability that sample i is excluded from one bootstrap sample:

P(i not selected in one draw) = 1 − 1/m
P(i not selected in all m draws) = (1 − 1/m)^m

As m → ∞:

(1 − 1/m)^m → e^{−1} ≈ 0.3679

So approximately 63.2% of unique samples appear in each bootstrap. The remaining 36.8% — the out-of-bag (OOB) samples — form a free validation set.

Expected number of duplicates: Each sample appears on average 1 time total, but some appear 0 times (OOB) and some appear 2, 3, or more times. The expected number of times sample i appears in a bootstrap of size m is:

E[count_i] = m · (1/m) = 1
Var[count_i] = m · (1/m) · (1 − 1/m) ≈ 1 − 1/m ≈ 1

So counts follow approximately a Poisson(1) distribution. About 26% of samples appear once, 18% appear twice, 6% appear three times, etc.

Effect on tree diversity: Two bootstrap samples share on average:

E[shared unique samples] ≈ m · (1 − (1 − 1/m)^m)^2 / ... ≈ 0.632² · m ≈ 0.4 · m

Each pair of trees is trained on datasets that overlap by about 40% unique samples — creating meaningful, but not total, diversity.


2.2 Random Feature Subsets — The Full Picture

At each node during tree construction, instead of evaluating all p features, a random subset of max_features features is drawn (without replacement) and only those are candidates for the split.

This happens per node, not per tree — each split decision in the tree considers a fresh random subset.

Default values:

Task Default Rationale
Classification sqrt(p) Breiman's empirical finding
Regression p/3 Standard for continuous targets

Why not per-tree? Restricting per-node provides more diversity than per-tree. A per-tree restriction would still allow the same feature to appear at every level within a tree (once selected). Per-node randomness ensures that even within a single tree, different branches can explore different features.

The diversity mechanism: Even if two trees see the same bootstrap sample, they will likely use different splits at each node because they see different feature subsets. This decorrelates tree predictions — the primary variance reduction mechanism.

The bias cost: Restricting to sqrt(p) features means the best global split is sometimes unavailable at a node. The tree is forced to use a suboptimal split. This increases each tree's bias slightly — but the massive variance reduction (from averaging B decorrelated trees) more than compensates.


3. Mathematical Foundation

3.1 The Ensemble Prediction

Classification — soft voting (default in sklearn):

P̂(y=c | x) = (1/B) · Σ_{b=1}^{B} P̂_b(y=c | x)
ŷ = argmax_c P̂(y=c | x)

Where P̂_b(y=c | x) is the fraction of training samples of class c in the leaf that tree b assigns x to.

Classification — hard voting:

ŷ = mode{ ĥ_1(x), ĥ_2(x), ..., ĥ_B(x) }

Soft voting almost always outperforms hard voting because it uses more information (probability estimates, not just argmax).

Regression:

ŷ = (1/B) · Σ_{b=1}^{B} ĥ_b(x)

3.2 Variance Reduction — Full Derivation

Let ĥ_b(x) be the prediction of tree b at point x. Assume each tree has:

The variance of the forest average:

Var( F̂(x) ) = Var( (1/B) Σ_b ĥ_b(x) )
             = (1/B²) · Var( Σ_b ĥ_b(x) )
             = (1/B²) · [ B·σ² + B(B-1)·ρ·σ² ]
             = σ²/B + (B-1)/B · ρ·σ²
             = ρ·σ² + (1-ρ)·σ²/B

As B → ∞:

Var( F̂(x) ) → ρ·σ²

Interpretation:

The forest variance is always ≥ ρ·σ². Adding trees reduces only the second term. Reducing ρ (by more aggressive feature randomization) reduces the floor itself.


3.3 Bias-Variance Decomposition of the Forest

For the forest F̂(x) predicting a target f*(x):

E[(F̂(x) − f*(x))²] = Bias²(F̂(x)) + Var(F̂(x)) + ε²

Bias(F̂(x)) = E[F̂(x)] − f*(x) = E[ĥ_b(x)] − f*(x)    ← same as single tree bias
Var(F̂(x))  = ρ·σ² + (1-ρ)·σ²/B                        ← reduced by averaging

Key result: The forest's bias equals a single tree's bias — averaging does not change the mean, only the variance. Therefore:

This is the fundamental reason Random Forest works: deep trees have low bias but very high variance — averaging B of them cancels most of the variance while preserving the low bias.


3.4 The Correlation Floor

The minimum achievable variance is ρ·σ². What determines ρ?

Factors increasing ρ (bad — higher floor):

Factors decreasing ρ (good — lower floor):

Empirically for classification with max_features=sqrt(p), ρ ≈ 0.05–0.20 for most datasets. This means the variance floor is only 5–20% of a single tree's variance — a 5–20× improvement.


4. Training Algorithm — Complete Pseudocode

function RandomForest(D, B, max_features, max_depth=None, min_samples_leaf=1):

    forest = []

    for b = 1 to B:

        # Step 1: Bootstrap sample
        D_b = BootstrapSample(D)   # m samples with replacement from D

        # Step 2: Grow a deep, unpruned tree
        tree_b = GrowTree(D_b, max_features, max_depth, min_samples_leaf)

        forest.append(tree_b)

    return forest


function GrowTree(D, max_features, max_depth, min_samples_leaf):

    # Stopping conditions
    if |D| <= min_samples_leaf: return Leaf(majority_class(D))
    if max_depth == 0:          return Leaf(majority_class(D))
    if all_same_class(D):       return Leaf(class(D[0]))

    # Random feature subset — drawn FRESH at each node
    F_subset = RandomSample(all_features, size=max_features, replace=False)

    # Find best split among F_subset only
    best_feature, best_threshold = None, None
    best_gain = -∞

    for f in F_subset:
        for threshold in candidate_thresholds(D, f):
            gain = ImpurityGain(D, f, threshold)    # Gini or Entropy
            if gain > best_gain:
                best_gain = gain
                best_feature, best_threshold = f, threshold

    # If no gain (pure node or all same values), return leaf
    if best_gain <= 0: return Leaf(majority_class(D))

    D_left, D_right = Split(D, best_feature, best_threshold)

    left  = GrowTree(D_left,  max_features, max_depth-1, min_samples_leaf)
    right = GrowTree(D_right, max_features, max_depth-1, min_samples_leaf)

    return Node(best_feature, best_threshold, left, right)


function Predict(forest, x):
    proba = zeros(K)                           # K classes
    for tree in forest:
        leaf = tree.route(x)                   # Follow splits to leaf
        proba += leaf.class_fractions          # Add leaf's class distribution
    proba /= len(forest)                       # Average
    return argmax(proba)

Training complexity:

O(B · m · sqrt(p) · log(m))

Prediction complexity:

O(B · depth)   per sample

5. Out-of-Bag (OOB) Evaluation — Deep Dive

For each training sample xᵢ, approximately 37% of trees did not use xᵢ in their bootstrap sample. These are the OOB trees for sample i.

OOB prediction for sample i:

P̂_OOB(y=c | xᵢ) = (1 / |OOB_trees(i)|) · Σ_{b: xᵢ ∉ D_b} P̂_b(y=c | xᵢ)
ŷᵢ_OOB = argmax_c P̂_OOB(y=c | xᵢ)

OOB error:

OOB_error = (1/m) · Σᵢ 𝟙[ŷᵢ_OOB ≠ yᵢ]

Why OOB is valuable:

OOB convergence: OOB error is reliable only when B is large enough that each sample has been OOB for many trees. Rule of thumb: B ≥ 100 for reliable OOB estimates, B ≥ 200 for small datasets.

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=500, oob_score=True, n_jobs=-1)
rf.fit(X_train, y_train)

print(f"OOB Accuracy:  {rf.oob_score_:.4f}")
print(f"OOB Probabilities shape: {rf.oob_decision_function_.shape}")  # (m, K)

OOB for regression:

from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=500, oob_score=True, n_jobs=-1)
rf.fit(X_train, y_train)
print(f"OOB R²: {rf.oob_score_:.4f}")

6. Feature Importance — Three Tiers

6.1 Mean Decrease in Impurity (MDI)

At each node where feature f is used, the impurity reduction (weighted by the number of samples reaching that node) is attributed to f. MDI importance is the average across all trees. See Feature Importance for comparison with other methods:

MDI(f) = (1/B) · Σ_b Σ_{nodes t in tree b using feature f}
              (n_t / m) · [Impurity(t) − (n_L/n_t)·Impurity(t_L) − (n_R/n_t)·Impurity(t_R)]

Normalization: sklearn normalizes so that Σ_f MDI(f) = 1.

Bias toward high-cardinality features: A continuous feature with many unique values has many more candidate split points than a binary feature → more opportunities to score → artificially inflated MDI.

importances = rf.feature_importances_   # MDI, normalized to sum=1
std = np.std([tree.feature_importances_ for tree in rf.estimators_], axis=0)
# std gives confidence intervals on the importance estimate

6.2 Mean Decrease in Accuracy (MDA / Permutation)

For each feature f:

  1. Compute baseline OOB accuracy
  2. Randomly permute the values of feature f in the OOB samples
  3. Re-compute OOB accuracy on permuted data
  4. MDA(f) = baseline accuracy − permuted accuracy
MDA(f) = OOB_acc(original) − OOB_acc(feature f permuted)

Permuting f breaks its relationship with y. A large accuracy drop means f was important; a small drop means f contributed little.

Advantages over MDI:

from sklearn.inspection import permutation_importance

result = permutation_importance(
    rf, X_val, y_val,
    n_repeats=30,       # Repeat permutation 30 times for stable estimates
    n_jobs=-1,
    random_state=42
)
importances_mda = result.importances_mean
importances_std = result.importances_std

6.3 SHAP Values

TreeSHAP (Lundberg et al., 2018) computes exact SHAP values for tree ensembles in polynomial time. For each prediction, SHAP attributes the output to each feature based on its Shapley value — the average marginal contribution across all possible feature orderings.

import shap

explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X_test)

# For classification: shap_values is a list of arrays, one per class
# shap_values[1][i, j] = contribution of feature j to prediction for sample i (class 1)

# Global summary
shap.summary_plot(shap_values[1], X_test)

# Single prediction waterfall
shap.waterfall_plot(explainer(X_test)[0])

# Feature interaction
shap.dependence_plot('feature_name', shap_values[1], X_test, interaction_index='auto')

6.4 Comparing All Three

Property MDI MDA (Permutation) SHAP
Computation Free (during training) O(p · B · m_OOB) O(B · p² · m) or O(B·m·p)
Bias toward cardinality ❌ Yes ✅ No ✅ No
Handles correlations ❌ Poorly ⚠️ Partially ✅ Yes (all subsets)
Per-sample explanation ❌ No ❌ No ✅ Yes
Global ranking ✅ Fast approximation ✅ More reliable ✅ Most accurate
Used for feature selection ⚠️ Caution ✅ Reliable ✅ Best

Recommendation:


7. Random Forest for Regression

All mechanics are identical, with two differences:

Splitting criterion: Variance reduction instead of Gini/entropy:

Gain(f, t) = Var(D) − [n_L/n · Var(D_L) + n_R/n · Var(D_R)]

Prediction: Mean of leaf values across all trees:

ŷ = (1/B) · Σ_b ĥ_b(x)

where ĥ_b(x) = mean(y_i : x_i in same leaf as x in tree b)

Default max_features: p/3 instead of sqrt(p).

Prediction intervals: Bootstrap-based prediction intervals can be constructed using the distribution of tree predictions:

# Distribution of predictions across trees
tree_predictions = np.array([tree.predict(X_test) for tree in rf.estimators_])
# shape: (B, n_test)

lower = np.percentile(tree_predictions, 5, axis=0)
upper = np.percentile(tree_predictions, 95, axis=0)
mean  = tree_predictions.mean(axis=0)

This gives a 90% prediction interval from the ensemble variance. Note: this underestimates true uncertainty because all trees share the same training distribution — use jackknife+ or conformal prediction for calibrated intervals.


8. The Proximity Matrix

Two samples are "proximate" if they frequently end up in the same leaf. Define:

Prox(i, j) = (1/B) · Σ_b 𝟙[leaf_b(xᵢ) = leaf_b(xⱼ)]

This is an m×m symmetric matrix with entries in [0, 1]. Values near 1 mean the two samples always land in the same leaf — the forest considers them identical. Values near 0 mean they are always separated.

Computing the proximity matrix:

# Get leaf indices for all samples across all trees
leaf_indices = rf.apply(X_train)   # shape: (m, B)
# leaf_indices[i, b] = leaf node index for sample i in tree b

# Compute proximity (O(m² · B) — expensive for large m)
from sklearn.metrics.pairwise import pairwise_kernels
proximity = (leaf_indices[:, None] == leaf_indices[None, :]).mean(axis=2)
# proximity[i, j] = fraction of trees where i and j land in same leaf

Applications:

# Outlier score: mean proximity to k-nearest neighbors
# Low score = outlier
outlier_scores = 1.0 / proximity.sum(axis=1)

9. Missing Data Imputation via Proximity

Breiman proposed using the proximity matrix for iterative missing value imputation:

Algorithm:
1. Initialize: replace missing values with column medians (numerical) or modes (categorical)
2. Train Random Forest on the filled data
3. Compute proximity matrix
4. For each sample i with missing feature f:
   Impute: x_{i,f} ← Σ_j Prox(i,j) · x_{j,f} / Σ_j Prox(i,j)
   (weighted average of non-missing values, weighted by proximity)
5. Repeat steps 2–4 until convergence (typically 4–6 iterations)

This produces label-aware imputation — the proximity is guided by the target variable, so samples that end up in the same leaf (similar y) contribute more to the imputation. Better than mean/median imputation for complex data.

# sklearn's IterativeImputer with RandomForest estimator achieves similar effect:
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor

imputer = IterativeImputer(
    estimator=RandomForestRegressor(n_estimators=50, n_jobs=-1),
    max_iter=5,
    random_state=42
)
X_imputed = imputer.fit_transform(X_with_nans)

10. Hyperparameters — Complete Reference and Tuning Guide

from sklearn.ensemble import RandomForestClassifier

RandomForestClassifier(
    n_estimators=100,         # Number of trees — more is always better (diminishing returns)
    criterion='gini',         # 'gini', 'entropy', 'log_loss'
    max_depth=None,           # None = fully grown (unpruned)
    min_samples_split=2,      # Min samples to attempt a split
    min_samples_leaf=1,       # Min samples in each leaf
    min_weight_fraction_leaf=0.0,
    max_features='sqrt',      # 'sqrt', 'log2', None (all), int, float
    max_leaf_nodes=None,      # Alternative to max_depth
    min_impurity_decrease=0.0,
    bootstrap=True,           # False = use full dataset (pasting, not bagging)
    oob_score=False,          # Compute OOB error estimate
    n_jobs=None,              # -1 = all cores
    random_state=None,
    verbose=0,
    warm_start=False,         # Reuse previous trees, add more
    class_weight=None,        # 'balanced', 'balanced_subsample', or dict
    ccp_alpha=0.0,            # Post-pruning complexity parameter
    max_samples=None,         # Fraction/number of bootstrap sample size
)

Tuning Priority and Effect

Hyperparameter Effect on Bias Effect on Variance Tuning Range
n_estimators None ↓ (always) 100–2000, more=better
max_features ↑ (less features = higher bias) ↓ (less correlation) 'sqrt' to p
max_depth ↑ (shallower) 3–None
min_samples_leaf ↑ (larger) 1–50
min_samples_split ↑ (larger) 2–20
max_samples ↑ (smaller bootstrap) 0.5–1.0

Tuning workflow:

from sklearn.model_selection import RandomizedSearchCV
import numpy as np

param_dist = {
    'n_estimators':     [100, 300, 500, 1000],
    'max_features':     ['sqrt', 'log2', 0.3, 0.5, 0.7],
    'min_samples_leaf': [1, 3, 5, 10, 20],
    'max_depth':        [None, 10, 20, 30],
    'min_samples_split':[2, 5, 10],
    'class_weight':     [None, 'balanced', 'balanced_subsample']
}

search = RandomizedSearchCV(
    RandomForestClassifier(n_jobs=-1, oob_score=True),
    param_dist,
    n_iter=50,
    cv=5,
    scoring='roc_auc',
    n_jobs=1,      # n_jobs already on the forest
    random_state=42
)
search.fit(X_train, y_train)

11. Parallel Training and Scaling

Random Forest is embarrassingly parallel — trees are independent. sklearn uses Python's joblib to parallelize tree construction:

rf = RandomForestClassifier(n_jobs=-1)   # Use all available CPU cores

Scaling behavior:

Memory management for large models:

# After training, reduce memory by removing unnecessary attributes
import joblib

# Save compressed model
joblib.dump(rf, 'rf_model.pkl', compress=3)

# For prediction-only deployment: can reduce tree complexity
# or use sklearn's warm_start to build incrementally
rf = RandomForestClassifier(n_estimators=100, warm_start=True)
rf.fit(X_train, y_train)
rf.n_estimators = 200
rf.fit(X_train, y_train)   # Adds 100 more trees without rebuilding

12. Class Imbalance Strategies

# Method 1: class_weight='balanced' (adjusts sample weights per class)
rf = RandomForestClassifier(class_weight='balanced')
# Weight for class c = m / (n_classes * count_c)

# Method 2: class_weight='balanced_subsample'
# Recomputes class weights PER BOOTSTRAP SAMPLE — usually better for RF
rf = RandomForestClassifier(class_weight='balanced_subsample')

# Method 3: Adjust decision threshold post-hoc
proba = rf.predict_proba(X_test)[:, 1]
threshold = 0.3   # Lower threshold to increase recall for minority class
y_pred = (proba >= threshold).astype(int)

# Method 4: Balanced Random Forest (imblearn)
from imblearn.ensemble import BalancedRandomForestClassifier
brf = BalancedRandomForestClassifier(
    n_estimators=500,
    sampling_strategy='auto',  # Undersample majority to match minority
    n_jobs=-1
)
brf.fit(X_train, y_train)

# Method 5: max_samples for undersampling in bootstrap
rf = RandomForestClassifier(
    max_samples=minority_count * 2,  # Bootstrap same size as 2× minority class
    class_weight='balanced'
)

13. Convergence as B → ∞

A crucial property distinguishing Random Forest from most ML algorithms:

Theorem (Breiman, 2001): As B → ∞, the generalization error of a Random Forest converges to a limit that depends only on the strength (accuracy) of individual trees and their correlation. The error does not increase — it monotonically decreases and plateaus.

Error(B) = ρ·σ² + (1-ρ)·σ²/B

As B → ∞:  Error(B) → ρ·σ²    (the correlation floor)

Practical consequence: You cannot overfit a Random Forest by adding more trees. Unlike neural networks or single decision trees, training longer (adding trees) cannot hurt. It can only help (up to the correlation floor) or do nothing (after convergence).

This makes Random Forest almost unique in ML: it is monotonically improving with computation. The only constraint is memory and prediction time.

When does performance plateau?

# Find the elbow — when adding trees stops helping
val_scores = []
for n in range(10, 501, 10):
    rf_n = RandomForestClassifier(n_estimators=n, max_features='sqrt',
                                   n_jobs=-1, random_state=42)
    rf_n.fit(X_train, y_train)
    val_scores.append(rf_n.score(X_val, y_val))

import matplotlib.pyplot as plt
plt.plot(range(10, 501, 10), val_scores)
plt.xlabel('n_estimators'); plt.ylabel('Validation Accuracy')
plt.title('Convergence: When Do More Trees Stop Helping?')

Most datasets plateau between 100–500 trees. Beyond that point, the only benefit is smoother probability estimates.


14. The Bias-Variance Profile

Single deep tree:     Low bias, VERY HIGH variance
Random Forest:        Low bias, LOW variance
                      (bias unchanged; variance reduced by ~B·(1-ρ) factor)

Increasing max_features → higher correlation ρ → higher variance floor
Decreasing max_features → lower correlation ρ → lower variance floor, higher bias

Empirical bias-variance estimate:

# Estimate via multiple train-test splits
from sklearn.model_selection import cross_val_score
import numpy as np

scores = cross_val_score(rf, X, y, cv=10, scoring='accuracy')
print(f"Mean accuracy: {scores.mean():.4f}")
print(f"Std (variance proxy): {scores.std():.4f}")

15. Assumptions

Assumption Notes
IID training samples Required for bootstrap validity
Feature relevance At least some features must be informative
No extrapolation Predictions outside training range are clamped to training values
No feature scaling Trees are scale-invariant — no normalization needed
No distributional assumption Non-parametric — no normality, linearity, or homoscedasticity
Weak learners must be better than random Each tree must have accuracy > 50% (nearly always true)

16. Evaluation Metrics

from sklearn.metrics import (accuracy_score, f1_score, roc_auc_score,
                              classification_report, confusion_matrix)

y_pred  = rf.predict(X_test)
y_proba = rf.predict_proba(X_test)

print(classification_report(y_test, y_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba[:, 1]):.4f}")
print(f"OOB Score: {rf.oob_score_:.4f}")

# Calibration check
from sklearn.calibration import calibration_curve
frac_pos, mean_pred = calibration_curve(y_test, y_proba[:, 1], n_bins=10)

Probability calibration: Random Forest probabilities (leaf class fractions averaged across trees) are generally well-calibrated for balanced datasets. For imbalanced datasets, calibration may deteriorate — use CalibratedClassifierCV if precise probabilities matter.


17. Advantages

✅ Robust to Overfitting

More trees = lower variance, never higher. Adding trees always helps or has no effect — there is no "too many trees."

✅ Excellent Default Performance

The defaults (n_estimators=100, max_features='sqrt') are genuinely competitive. Random Forest often gives >90% of maximum possible accuracy with zero tuning.

✅ No Feature Scaling

Decision trees are scale-invariant — no StandardScaler, MinMaxScaler, or normalization step needed.

✅ Handles Mixed Feature Types

Continuous, binary, categorical, ordinal — all handled natively.

✅ Free OOB Validation

Built-in honest generalization estimate without a separate validation set.

✅ Rich Feature Importance

MDI, MDA, and SHAP all available. Three complementary views of feature relevance.

✅ Handles High Dimensionality

With max_features=sqrt(p), Random Forest handles p >> m reasonably well. The random feature subsets prevent any single irrelevant feature from dominating.

✅ Embarrassingly Parallel

n_jobs=-1 uses all CPU cores. Near-linear scaling with cores.

✅ Proximity Matrix for Unsupervised Tasks

Anomaly detection, clustering, imputation — all as byproducts of the trained forest.

✅ Quantifies Uncertainty

The spread of predictions across trees provides a natural uncertainty estimate.


18. Drawbacks & Limitations

❌ Cannot Extrapolate

Predictions outside the training feature range are the mean of the training data in the relevant leaf. This is a hard limit — Random Forest cannot learn "if age > 100, predict X" if it never saw age > 80.

❌ Memory Intensive

500 fully grown trees can require gigabytes of RAM. For deployment, model compression (fewer trees, shallower depth) is often needed.

❌ Slower Prediction Than Single Models

Predicting with 500 trees is 500× slower than a single tree or logistic regression. For latency-sensitive applications, this matters.

❌ Biased MDI for Correlated/High-Cardinality Features

The built-in feature importance is misleading when features are correlated or have many unique values. Always cross-check with permutation importance.

❌ Underperforms Gradient Boosting on Many Tasks

For maximum accuracy on tabular data, XGBoost and LightGBM typically outperform Random Forest — especially on smaller datasets where the bias-variance tradeoff favors boosting's iterative bias reduction.

❌ Suboptimal for Sparse Data

Very sparse high-dimensional data (text) is better handled by linear models. The random feature subset may repeatedly miss the sparse informative features.

❌ No Native Handling of Sequences or Time Series

Random Forest treats all samples as i.i.d. — it cannot natively capture temporal dependencies.


19. Random Forest vs. Extra-Trees vs. Bagging vs. GBT

Property Random Forest Extra-Trees Bagging Classifier GBT (XGBoost)
Randomness source Bootstrap + feat Bootstrap + feat + threshold Bootstrap only None (deterministic)
Threshold selection Optimal Random Optimal Optimal
Bias Low Slightly higher Low Low (iterative)
Variance Low Lower Lower than 1 tree Low (with reg.)
Training speed Fast Fastest Fast Slower (sequential)
Parallelism ✅ Full ✅ Full ✅ Full ❌ Sequential
Accuracy (tabular) ✅ Very good ✅ Good ✅ Good ✅✅ Best
Feature scaling ❌ Not needed ❌ Not needed Depends on learner ❌ Not needed
Overfitting risk Very low Very low Low Medium (needs tuning)

20. Practical Tips & Gotchas

Start Here

from sklearn.ensemble import RandomForestClassifier
import numpy as np

rf = RandomForestClassifier(
    n_estimators=500,
    max_features='sqrt',
    min_samples_leaf=1,
    n_jobs=-1,
    oob_score=True,
    random_state=42
)
rf.fit(X_train, y_train)
print(f"OOB Accuracy: {rf.oob_score_:.4f}")

Diagnose Feature Importance Properly

import pandas as pd
from sklearn.inspection import permutation_importance

# MDI
mdi = pd.Series(rf.feature_importances_, index=feature_names)

# MDA (on validation data)
mda_result = permutation_importance(rf, X_val, y_val, n_repeats=20, n_jobs=-1)
mda = pd.Series(mda_result.importances_mean, index=feature_names)

# Compare
comparison = pd.DataFrame({'MDI': mdi, 'MDA': mda}).sort_values('MDA', ascending=False)
print(comparison.head(20))
# Large MDI, small MDA → feature may be correlated or high-cardinality artifact

Find the Elbow (Optimal n_estimators)

oob_scores = []
for n in [50, 100, 200, 300, 500, 750, 1000]:
    rf_n = RandomForestClassifier(n_estimators=n, oob_score=True, n_jobs=-1)
    rf_n.fit(X_train, y_train)
    oob_scores.append(rf_n.oob_score_)

# Pick n where OOB score stops improving meaningfully

Predict with Uncertainty Quantification

# Standard deviation of tree predictions as uncertainty
tree_probas = np.array([tree.predict_proba(X_test) for tree in rf.estimators_])
# shape: (B, n_test, K)

mean_proba = tree_probas.mean(axis=0)   # Shape: (n_test, K)
std_proba  = tree_probas.std(axis=0)    # Shape: (n_test, K) — uncertainty

# High std_proba → forest is uncertain about this prediction
uncertain_samples = std_proba[:, 1] > 0.2

21. When to Use It

Use Random Forest when:

Use gradient boosting instead when:

Use Extra-Trees instead when:


Summary

┌──────────────────────────────────────────────────────────────────────┐
│                RANDOM FOREST AT A GLANCE                            │
├──────────────────────────────────────────────────────────────────────┤
│  RANDOMNESS    Bootstrap samples + random feature subset per node   │
│  VARIANCE      ρ·σ² + (1-ρ)·σ²/B → ρ·σ² as B → ∞                 │
│  CORRELATION   ρ controlled by max_features (smaller = lower ρ)     │
│  FREE GIFTS    OOB error, proximity matrix, uncertainty estimates   │
│  IMPORTANCE    MDI (fast) → MDA (reliable) → SHAP (best)           │
│  CONVERGENCE   Monotone — adding trees never hurts                  │
│  STRENGTH      Robust, parallel, mixed types, no scaling, OOB       │
│  WEAKNESS      No extrapolation, memory, slower than GBT on accuracy│
│  DEFAULTS      n_estimators=500, max_features='sqrt', min_leaf=1   │
│  BEST FOR      Tabular data, reliable baseline, feature selection   │
└──────────────────────────────────────────────────────────────────────┘
Powered by Forestry.md