Random Forest

Learn ensemble methods, bagging, and feature importance with random forests

intermediate50 min
On this page

The Big Picture — What Random Forest Does & Why

A Random Forest is an ensemble learning algorithm that combines predictions from multiple decision trees. Instead of trusting a single tree (which can overfit), Random Forest grows many trees on slightly different subsets of the data and features, then aggregates their predictions.

Core Insight: A single decision tree is like asking one expert—they might be biased or have blind spots. A random forest is like asking 100 independent experts and taking a majority vote. Diversity + aggregation = better predictions.

Why Random Forests Work

Random Forests solve two fundamental problems in machine learning:

Problem 1: Overfitting

A single deep tree memorizes noise in training data, performing poorly on new data.

Solution: Grow multiple trees on different data samples (bagging) with limited feature access. Variance cancels out when averaging.

Problem 2: Feature Dominance

Multiple trees may all use the same "obvious" features, creating correlated predictions.

Solution: Force each tree to randomly select from a subset of features (random subspace). This decorrelates predictions.

The Two Sources of Randomness

Source What's Random Why It Matters
Bootstrap Sampling Each tree sees a different random sample of rows (with replacement) Reduces variance through averaging independent predictions
Feature Subspace Each tree splits on a random subset of columns Decorrelates tree predictions, amplifying variance reduction
AdTech Context: When predicting click-through rates for Bing Ads, a single tree might learn "users from Seattle always click ads at 2pm on Tuesdays." A random forest learns robust patterns across many user segments and time windows, avoiding overfitting to quirks in your training data.

AdTech Example: Click-Through Rate Prediction for Bing Ads

Imagine you work at Bing Ads and need to predict whether a user will click on an ad. Your dataset has:

Features:
  • User age
  • Device type (mobile, desktop, tablet)
  • Time of day
  • Geographic location
  • Search query category
  • Ad position on page
  • Ad relevance score
Target:
  • Click (1) or No Click (0)

Goal: Predict CTR accurately so you can rank ads, set bid prices, and allocate budget efficiently.

Why Single Tree Fails

A single decision tree might learn:

if age < 25 and device == "mobile":
    if time_of_day < 14:
        return high_click_probability
    else:
        return low_click_probability

This overfits to patterns in your training data that don't generalize. Maybe there was a viral trend among 25-year-olds on mobile at 1pm, but it won't happen again.

Why Random Forest Works

Instead, a Random Forest grows 100+ trees, each seeing:

  • Bootstrap Sample: 80% of your training data (with replacement), so each tree sees slightly different users
  • Random Features: Each split only considers 3-4 random features out of 7, forcing trees to learn diverse patterns

The ensemble predictions are more stable because:

  • Tree 1 learns "mobile users click more" (based on 70% of the data)
  • Tree 2 learns "afternoon ads get fewer clicks" (based on a different 70%)
  • Tree 3 learns "high relevance score drives clicks" (forced to ignore mobile/time)
  • Averaging all 100 trees balances overfitting in any single tree
Real-World Impact: Random Forest CTR predictions reduce overfitting by ~15-30% compared to single trees, leading to better bid strategies and higher revenue per impression.

From Single Tree to Forest — Bagging & Bootstrap

Bagging (Bootstrap Aggregating) is the foundational technique behind Random Forests. The idea: train multiple models on random samples of the training data, then average their predictions.

Bootstrap Sampling Explained

For each tree in the forest, we create a bootstrap sample: a new dataset drawn from the original data with replacement.

Original Data (n=5) A B C D E Bootstrap Sample 1 (with replacement) A A C E C Bootstrap Sample 2 (with replacement) B D E B D Same size as original, but some samples repeated, others missing

Why Bootstrap Sampling Works for Variance Reduction

Mathematically, when we average predictions from uncorrelated estimators:

Var(average) = Var((y₁ + y₂ + ... + yₙ)/n) = σ²/n Where: σ² = variance of each individual predictor n = number of predictors

If we have 100 trees with variance σ² = 0.25, then:

Var(RF prediction) = 0.25/100 = 0.0025 vs. single tree: Var = 0.25 Reduction factor: 100×
Critical Caveat: This 1/n reduction only works if predictions are uncorrelated. If all 100 trees make the same mistakes (high correlation), variance reduction is minimal. This is where feature randomness comes in (Section 5).

Out-of-Bag Samples (~37%)

In bootstrap sampling, each sample is drawn with replacement. Mathematically, the probability that a row is not selected in a bootstrap sample of size n is:

P(not selected) = (1 - 1/n)^n ≈ 0.368 ≈ 37%

These "out-of-bag" (OOB) samples can be used for free cross-validation (see Section 9).

AdTech Application: For Bing Ads CTR prediction, bootstrap samples ensure each tree trains on a slightly different mix of user demographics, time periods, and ad types. This prevents overfitting to anomalies in any particular time window or user segment.

How the First Tree Gets Created (with Bagging)

Let's walk through the step-by-step process of growing a single tree in a Random Forest. We'll use the Bing Ads example:

Step 1: Create a Bootstrap Sample

Draw n samples with replacement from the original training data. If you have 10,000 ad impressions, create a bootstrap sample of 10,000 rows (some rows appear multiple times, others not at all).

# Pseudo-code
bootstrap_sample = original_data.sample(n=len(original_data), replace=True)
# Result: 10,000 rows, ~3,700 unique rows from original

Step 2: At Each Node, Randomly Select Features

For classification tasks like CTR prediction, Random Forest considers only m = √p random features at each split, where p is the total number of features.

With 7 features (age, device, time, location, category, position, relevance):

m = √7 ≈ 2.6 ≈ 3 features (rounded)

So at the root node of Tree 1, we randomly select 3 out of 7 features and find the best split among only those 3.

Select 3 from 7 features randomly Available Features: 1. Age 2. Device 3. Time of Day 4. Location 5. Category 6. Position 7. Relevance Selected for Tree 1: Device ✓ Time ✓ Relevance ✓ Step 3: Find Best Split Among Selected Features Compute Gini impurity reduction for: • Device == "mobile"? • Time < 14 (2pm)?

Step 3: Find Best Split Among Selected Features

Using a metric like Gini impurity (for classification) or variance (for regression), find which of the 3 selected features creates the purest child nodes.

Best Split = argmax(information_gain)

Example: If "Device == mobile" separates clicks from non-clicks better than the other two features, that becomes the root split.

Step 4: Recursively Grow the Tree

At each child node, repeat Steps 2-3: randomly select m features and find the best split among them. Continue until:

  • Max depth is reached (e.g., depth=15), or
  • Node is pure (all samples have same target), or
  • Min samples per node reached (e.g., min_samples_split=2)

Important: Unlike some tree algorithms, Random Forest trees are typically grown fully without pruning. The ensemble averaging prevents overfitting.

Key Difference from Bagging: Bagging alone (like in Bagged Decision Trees) only randomizes the training data. Random Forest adds feature randomness at each split, further decorrelating trees.

How Different Trees Are Grown — Feature Randomness (Random Subspace Method)

The secret sauce of Random Forests is feature randomness. By forcing each tree to use a random subset of features at each split, we ensure that different trees learn different patterns, which decorrelates their predictions.

Contrast: Bagging vs. Random Forest

Bagging (No Feature Randomness)

Each tree sees different rows but all features.

Result: Trees may all prefer "Device" for root split since it's the strongest feature. Predictions are correlated → variance reduction is weak.

Random Forest (Feature Randomness)

Each tree sees different rows AND different features at each split.

Result: Tree 1 splits on Device, Tree 2 on Time, Tree 3 on Relevance. Predictions are decorrelated → variance reduction is strong (near optimal 1/n).

Visualizing 3 Different Trees

Tree 1 (Random Features: Device, Time) Device? Time? Click Tree 2 (Random Features: Time, Category) Time? Category? No Click Tree 3 (Random Features: Relevance, Location) Relevance? Click Location? Predictions on a New Ad: Tree 1 Output Device=Mobile → Click? Probability: 0.75 Tree 2 Output Time=Afternoon → No Click Probability: 0.40 Tree 3 Output Relevance=High → Click Probability: 0.80 Ensemble (Average of 3 Trees) Final Prediction: (0.75 + 0.40 + 0.80) / 3 = 0.65 (Click)

Why Feature Randomness Decorrelates Trees

When you force trees to use different features, they learn different decision boundaries:

  • Tree 1: "Mobile users click more" (Device-centric)
  • Tree 2: "Afternoon ads get fewer clicks" (Time-centric)
  • Tree 3: "High relevance drives clicks" (Quality-centric)

When predicting, these trees make mistakes in different ways. Tree 1 might fail on desktop users, Tree 2 might fail on morning ads, and Tree 3 might fail on low-quality ads. When averaged, these errors cancel out.

Correlation Effect on Variance Reduction: If trees are uncorrelated (ρ=0): Var(RF) = σ²/n ← ideal, variance reduces by factor of n If trees are highly correlated (ρ→1): Var(RF) = σ² ← little benefit from ensemble Feature randomness pushes ρ closer to 0
Practical Insight: The parameter max_features controls how many features are randomly selected at each split. Smaller max_features = more decorrelation but possibly higher bias. Larger max_features = less decorrelation but lower bias. (Tuning discussed in Section 7.)

Splitting Metrics Recap (Gini, Entropy, Variance Reduction)

At each node, Random Forest needs to decide which feature and threshold creates the best split. This is measured using impurity metrics.

For Classification (CTR Prediction): Gini Impurity

Gini Impurity measures the probability of misclassifying a randomly chosen sample if we randomly label it according to class distribution.

Gini(S) = 1 - Σ(p_i)² Where: S = set of samples at node p_i = proportion of class i in S Range: 0 (pure, all one class) to 0.5 (for binary, equal split)

Example: Gini at Root Node (CTR Data)

Suppose at the root node, 40% of impressions are clicks, 60% are non-clicks:

Gini(root) = 1 - (0.4² + 0.6²) = 1 - (0.16 + 0.36) = 1 - 0.52 = 0.48

After splitting on "Device==Mobile", left node has 70% clicks, right node has 20% clicks:

Gini(left) = 1 - (0.7² + 0.3²) = 1 - 0.58 = 0.42 Gini(right) = 1 - (0.2² + 0.8²) = 1 - 0.68 = 0.32 Information Gain = Gini(parent) - (n_left/n_total × Gini(left) + n_right/n_total × Gini(right)) = 0.48 - (0.5 × 0.42 + 0.5 × 0.32) = 0.48 - 0.37 = 0.11
Interpretation: Splitting on Device==Mobile reduces impurity by 0.11. If another feature (Time) would reduce impurity by only 0.08, then Device is the better split.

For Regression: Variance Reduction

If predicting continuous values (e.g., predicted CTR as a decimal 0.0-1.0), use variance reduction instead of Gini:

Variance_Reduction = Var(parent) - (n_left/n_total × Var(left) + n_right/n_total × Var(right))

For Classification: Entropy (Alternative to Gini)

Some implementations use entropy instead of Gini (very similar results):

Entropy(S) = -Σ(p_i × log₂(p_i)) Range: 0 (pure) to 1 (for binary, equal split)
Metric Use Case Formula Pros
Gini Impurity Classification (default in scikit-learn) 1 - Σ(p_i)² Faster, simpler computation
Entropy Classification (information theory) -Σ(p_i × log(p_i)) Theoretically motivated, similar results
Variance Regression Σ((y_i - mean)²)/n Intuitive for continuous targets
AdTech Note: For Bing Ads CTR prediction (binary: click or no-click), Gini is the standard choice. The algorithm automatically selects features and thresholds that minimize misclassification.

Key Parameters to Tune

Random Forest has many hyperparameters. Here are the most important ones for controlling bias-variance and preventing overfitting:

Parameter Default What It Does Higher = ?
n_estimators 100 Number of trees in forest More trees = better (diminishing returns after ~100-200). No overfitting risk; more computation.
max_features 'sqrt' for classif., 'log2' for regression Number of features sampled at each split Higher = more correlation between trees, weaker ensemble. Lower = stronger ensemble but higher bias.
max_depth None (trees grow fully) Maximum depth of tree Shallower trees = lower variance but higher bias. Deeper = overfitting risk.
min_samples_split 2 Minimum samples required to split a node Higher = simpler trees, reduces overfitting. Too high = underfitting.
min_samples_leaf 1 Minimum samples required at leaf node Higher = removes small leaf nodes, reduces overfitting.
bootstrap True Use bootstrap sampling (with replacement) False = each tree uses entire dataset without replacement (rarely used).
oob_score False Compute out-of-bag error True = get free validation score (Section 9). Adds computation.
random_state None Random seed for reproducibility Set to integer (e.g., 42) for reproducible results.

Parameter Tuning Strategy

Start with Defaults

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=100,
    max_features='sqrt',
    random_state=42
)

Tune max_features First

This is the most critical parameter. It controls tree decorrelation. Test a few values:

for mf in ['sqrt', 'log2', 0.5, None]:
    rf = RandomForestClassifier(max_features=mf, random_state=42)
    rf.fit(X_train, y_train)
    score = rf.score(X_val, y_val)
    print(f"max_features={mf}: {score}")

Interpretation:

  • 'sqrt' or 'log2': Conservative, strong decorrelation
  • 0.5 (50% of features): Medium decorrelation
  • None (all features): Weak decorrelation, may overfit

Then Tune Depth & Leaf Size

for depth in [5, 10, 15, 20, None]:
    for min_leaf in [1, 5, 10]:
        rf = RandomForestClassifier(
            max_depth=depth,
            min_samples_leaf=min_leaf,
            random_state=42
        )
        rf.fit(X_train, y_train)
        score = rf.score(X_val, y_val)

Increase n_estimators (if computation allows)

More trees always help (no harm). Stop when validation score plateaus.

Effect of max_features on Tree Diversity & Accuracy max_features (lower = fewer features/split) Accuracy log2 sqrt 0.5 None Validation Accuracy Best Overfitting Lower max_features (more randomness) decorrelates trees → better ensemble
AdTech Best Practice: For Bing Ads CTR prediction, start with max_features='sqrt' (√7 ≈ 2.6 features), max_depth=10, min_samples_leaf=5. Tune max_features on validation set using cross-validation. Increase n_estimators to 200-500 if computational budget allows.

Bias-Variance Tradeoff — Why RF Reduces Variance

Understanding bias-variance is crucial to knowing when Random Forest excels and when it struggles.

Single Decision Tree

Low Bias

A tree can learn complex patterns in training data. Given enough depth, it can fit any training set perfectly.

High Variance

Small changes in training data (e.g., removing a few rows) can create drastically different trees. Predictions on new data are unreliable.

Random Forest Ensemble

Still Low Bias

By growing many trees and averaging, RF can still learn complex patterns. Ensemble is as flexible as individual trees.

Low Variance!

Averaging predictions from uncorrelated trees reduces variance dramatically. Predictions are stable even with dataset changes.

Mathematical Foundation

Suppose we have k independent (uncorrelated) predictors, each with variance σ². The variance of their average is:

Var(average) = Var((y₁ + y₂ + ... + yₖ)/k) = (1/k²) × Σ Var(yᵢ) = (1/k²) × k × σ² = σ²/k Variance reduction factor: σ² / (σ²/k) = k

With 100 trees:

Var(RF) = σ²/100 = 1% of original variance

Why Feature Randomness Enables This

Mathematically, the variance reduction formula assumes low correlation between predictions. Feature randomness achieves this by:

  • Forcing each tree to use different feature subsets
  • Making trees learn different decision boundaries
  • Ensuring they fail in different ways on new data
  • Errors partially cancel when averaging
Single Tree vs. Random Forest Predictions Single Tree High variance boundary! New point A: Prediction: 0.87 (click) Random Forest (3 Trees) Tree 1 Tree 2 Tree 3 Low variance ensemble boundary! Same point A: Prediction: 0.72 (averaged) Why RF has Lower Variance: • Single tree fits noise in training data → wiggly boundary → different predictions on similar new points • Many trees average out noise → smooth boundary → consistent predictions on similar new points

The Tradeoff

Random Forest slightly increases bias because:

  • Feature randomness restricts which patterns each tree can learn
  • Averaging predictions smooths out subtle patterns

But the variance reduction far outweighs this small bias increase, resulting in lower total error (bias² + variance).

Key Takeaway: Random Forest excels when the underlying problem is low-bias (complex patterns exist), but high-variance (small data changes cause instability). CTR prediction is a perfect fit: patterns are complex, but training data is noisy.

Out-of-Bag Error & Feature Importance

Out-of-Bag (OOB) Error Explained

Recall that in bootstrap sampling, ~37% of data is not included in each bootstrap sample (the "out-of-bag" samples). RF exploits this as free cross-validation.

OOB Error Calculation Tree 1 Bootstrap: rows {1,3,3,7,9} OOB: rows {2,4,5,6,8} Tree 2 Bootstrap: rows {1,2,5,6,9} OOB: rows {3,4,7,8,10} Tree 3 Bootstrap: rows {2,4,5,7,10} OOB: rows {1,3,6,8,9} For Each Row: Predict Using Only Trees Where It's OOB Row 1: Use Tree 2 & Tree 3 (Tree 1 has it in bootstrap) Row 3: Use Tree 2 & Tree 3 (Tree 1 has it, Tree 2 doesn't) → Predict on ~30-40% of trees per row → Average those predictions

OOB Error Calculation:

  1. For each row i in the training data, find all trees that don't have row i in their bootstrap sample
  2. Use those trees to predict row i
  3. Compare predicted to actual (clicks vs. no-clicks)
  4. Average error across all rows = OOB error

Why This Works: OOB samples are unseen by individual trees, so OOB error is unbiased. No need for separate validation set or k-fold cross-validation.

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=100,
    oob_score=True,
    random_state=42
)
rf.fit(X_train, y_train)

print(f"OOB Score: {rf.oob_score_}")
print(f"Test Score: {rf.score(X_test, y_test)}")

Feature Importance

Random Forest can rank which features are most influential in predictions. Two methods:

1. Impurity-Based Importance (Default)

For each feature, sum up the information gain across all splits where that feature is used, weighted by the number of samples affected by each split.

Importance(f) = Σ(weight × impurity_reduction) Across all nodes where feature f is used

Pros: Fast, built-in, interpretable

Cons: Biased towards high-cardinality features and correlated features

2. Permutation Importance

Randomize the values of a feature, re-predict, and measure how much error increases. If error increases a lot, the feature is important.

from sklearn.inspection import permutation_importance

perm_importance = permutation_importance(
    rf, X_test, y_test, n_repeats=10, random_state=42
)

for feature, importance in zip(feature_names, perm_importance.importances_mean):
    print(f"{feature}: {importance:.4f}")

Pros: Unbiased, captures interactions, handles correlated features

Cons: Slower, requires test set

Example: CTR Prediction Feature Importance

Feature Impurity-Based Permutation Interpretation
Ad Relevance 0.35 0.28 Strongest predictor; most splits use this
Device Type 0.25 0.22 Mobile vs. desktop matters for CTR
Time of Day 0.20 0.19 Time windows affect clicking behavior
Location 0.12 0.15 Geographic effect (permutation shows more impact)
User Age 0.08 0.12 Weak direct effect but important in interactions
AdTech Insight: Feature importance helps Bing Ads understand what drives clicks. If Relevance is 0.35, it suggests investing in better relevance algorithms has high ROI. If Location is surprisingly important (0.15 permutation), geographic targeting could be optimized.

RF vs. Decision Trees vs. XGBoost — When to Use What

Aspect Decision Tree Random Forest XGBoost
Interpretability Excellent (one tree, clear path) Moderate (feature importance, but hard to trace path) Poor (complex ensemble of many trees)
Overfitting High (needs pruning) Low (ensemble + feature randomness) Moderate (regularization helps)
Training Speed Fast Moderate (many trees, but parallelizable) Slower (sequential, needs tuning)
Prediction Speed Very fast Moderate (average of 100+ trees) Moderate (similar to RF)
Handles Non-Linearity Yes Yes (better via ensemble) Yes (best)
Handles Interactions Yes Yes Yes (learns them explicitly)
Feature Scaling Needed No No No
Class Imbalance Handling Poor Moderate (can set class_weight) Excellent (scale_pos_weight parameter)
Tuning Complexity Low (mainly depth/min_samples) Medium (max_features critical) High (many hyperparameters)

Decision Tree: Use When

  • Interpretability is paramount (e.g., loan approval, medical diagnosis)
  • You need to explain decisions to stakeholders
  • Dataset is small and you're not worried about overfitting

Random Forest: Use When

  • You want good predictive accuracy with minimal tuning
  • Dataset is moderate to large (hundreds or thousands of rows)
  • Features are mixed (categorical and continuous)
  • Training time is acceptable (parallelizable)
  • Example: Bing Ads CTR prediction ← RF is a great fit

XGBoost: Use When

  • You need maximum predictive accuracy and can afford tuning time
  • Dataset is very large and competition is fierce (Kaggle, production ML)
  • Class imbalance is severe
  • You need interpretability via SHAP values
  • Example: High-stakes CTR prediction where 1% accuracy improvement saves millions
Industry Practice: Start with Random Forest for baseline (good accuracy, low tuning). If accuracy plateaus or class imbalance is severe, graduate to XGBoost or LightGBM. Use Decision Tree only for compliance/interpretability.

Python Implementation from Scratch

Let's build a Random Forest from scratch using only NumPy and Pandas. This deepens understanding of how it works under the hood.

Part 1: Decision Tree Class

import numpy as np
from collections import Counter

class DecisionTree:
    def __init__(self, max_depth=15, min_samples_split=2):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.tree = None

    def fit(self, X, y):
        """Grow tree from training data"""
        self.tree = self._build_tree(X, y, depth=0)
        return self

    def _build_tree(self, X, y, depth):
        # Base cases
        if (X.shape[0] < self.min_samples_split or
            depth >= self.max_depth or
            len(np.unique(y)) == 1):
            return {'type': 'leaf',
                    'prediction': Counter(y).most_common(1)[0][0]}

        best_gain = 0
        best_split = None

        # Try all features and thresholds
        for feature_idx in range(X.shape[1]):
            for threshold in np.unique(X[:, feature_idx]):
                # Split
                left_mask = X[:, feature_idx] <= threshold
                right_mask = ~left_mask

                if left_mask.sum() == 0 or right_mask.sum() == 0:
                    continue

                # Gini impurity
                gain = self._information_gain(y, y[left_mask], y[right_mask])

                if gain > best_gain:
                    best_gain = gain
                    best_split = {
                        'feature': feature_idx,
                        'threshold': threshold,
                        'left_mask': left_mask
                    }

        if best_split is None:
            return {'type': 'leaf',
                    'prediction': Counter(y).most_common(1)[0][0]}

        # Recursively build subtrees
        left_mask = best_split['left_mask']
        return {
            'type': 'node',
            'feature': best_split['feature'],
            'threshold': best_split['threshold'],
            'left': self._build_tree(X[left_mask], y[left_mask], depth+1),
            'right': self._build_tree(X[~left_mask], y[~left_mask], depth+1)
        }

    def _gini(self, y):
        # Gini impurity
        proportions = np.bincount(y) / len(y)
        return 1 - np.sum(proportions ** 2)

    def _information_gain(self, parent, left, right):
        # Information gain = parent gini - weighted child gini
        n = len(parent)
        gini_parent = self._gini(parent)
        gini_left = self._gini(left)
        gini_right = self._gini(right)
        gain = gini_parent - (len(left)/n * gini_left + len(right)/n * gini_right)
        return gain

    def predict(self, X):
        return np.array([self._predict_sample(x, self.tree) for x in X])

    def _predict_sample(self, x, node):
        if node['type'] == 'leaf':
            return node['prediction']

        if x[node['feature']] <= node['threshold']:
            return self._predict_sample(x, node['left'])
        else:
            return self._predict_sample(x, node['right'])

Part 2: Random Forest Class

class RandomForestClassifier:
    def __init__(self, n_estimators=100, max_features=None, max_depth=15,
                 min_samples_split=2, bootstrap=True, random_state=None):
        self.n_estimators = n_estimators
        self.max_features = max_features
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.bootstrap = bootstrap
        self.random_state = random_state
        self.trees = []
        self.feature_importances_ = None

    def fit(self, X, y):
        """Train random forest"""
        np.random.seed(self.random_state)

        n_features = X.shape[1]
        if self.max_features is None:
            self.max_features = int(np.sqrt(n_features))

        self.trees = []
        feature_importances = np.zeros(n_features)

        for _ in range(self.n_estimators):
            # Step 1: Bootstrap sample
            if self.bootstrap:
                idx = np.random.choice(X.shape[0], X.shape[0], replace=True)
                X_boot = X[idx]
                y_boot = y[idx]
            else:
                X_boot = X
                y_boot = y

            # Step 2: Train tree (with full features; feature selection happens in tree)
            tree = DecisionTree(max_depth=self.max_depth,
                              min_samples_split=self.min_samples_split)
            tree.fit(X_boot, y_boot)
            self.trees.append(tree)

        return self

    def predict(self, X):
        """Predict by majority vote"""
        predictions = np.array([tree.predict(X) for tree in self.trees])
        # Majority vote: (n_trees, n_samples) → (n_samples,)
        final_predictions = np.array(
            [Counter(predictions[:, i]).most_common(1)[0][0]
             for i in range(X.shape[0])]
        )
        return final_predictions

    def predict_proba(self, X):
        """Predict probabilities (for binary classification)"""
        predictions = np.array([tree.predict(X) for tree in self.trees])
        # Proportion of trees voting for class 1
        proba_class_1 = (predictions == 1).sum(axis=0) / len(self.trees)
        return np.column_stack([1 - proba_class_1, proba_class_1])

    def score(self, X, y):
        """Accuracy score"""
        predictions = self.predict(X)
        return (predictions == y).mean()

Part 3: Example with Bing Ads CTR Data

import pandas as pd

# Load CTR data
data = pd.DataFrame({
    'age': [25, 35, 22, 45, 32] * 100,
    'device': [0, 1, 0, 1, 0] * 100,  # 0=desktop, 1=mobile
    'time': [9, 14, 18, 10, 16] * 100,
    'relevance': [0.7, 0.5, 0.8, 0.6, 0.9] * 100,
    'clicked': [1, 0, 1, 0, 1] * 100
})

X = data.drop('clicked', axis=1).values
y = data['clicked'].values

# Split into train/test
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# Train Random Forest
rf = RandomForestClassifier(n_estimators=10, max_depth=5, random_state=42)
rf.fit(X_train, y_train)

# Predictions
y_pred = rf.predict(X_test)
accuracy = rf.score(X_test, y_test)

print(f"Accuracy: {accuracy:.4f}")

# Probability predictions
y_proba = rf.predict_proba(X_test)
print(f"CTR (click probability) for first 5 samples: {y_proba[:5, 1]}")

Key Implementation Details

Bootstrap Sampling: Line with `np.random.choice(X.shape[0], X.shape[0], replace=True)` creates bootstrap sample. Each tree sees ~63% unique rows.
Majority Voting: `Counter(predictions[:, i]).most_common(1)[0][0]` aggregates predictions from all trees. For 100 trees, prediction is most common class.
Feature Selection: To implement random feature selection at each split, modify the tree's `_build_tree` method to randomly select `max_features` before the loop: `selected_features = np.random.choice(range(X.shape[1]), self.max_features, replace=False)`.

Production Considerations

  • Use scikit-learn in production: Our implementation is educational; scikit-learn RF is optimized and battle-tested.
  • Parallelize training: `RandomForestClassifier(n_jobs=-1)` trains trees in parallel.
  • Save models: Use `pickle` or `joblib` to save trained models for inference.
  • Monitor OOB score: Track OOB error during training to detect overfitting.
  • Feature preprocessing: Encode categorical variables, handle missing values before fitting.
# Production-grade scikit-learn example
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import joblib

# Train with OOB scoring
rf = RandomForestClassifier(
    n_estimators=200,
    max_features='sqrt',
    max_depth=10,
    min_samples_leaf=5,
    oob_score=True,
    n_jobs=-1,
    random_state=42
)

rf.fit(X_train, y_train)

# Cross-validation
cv_scores = cross_val_score(rf, X_train, y_train, cv=5)
print(f"CV Scores: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
print(f"OOB Score: {rf.oob_score_:.4f}")

# Save model
joblib.dump(rf, "ctr_model.pkl")

Summary

Random Forest is a powerful, practical algorithm that combines the flexibility of decision trees with the stability of ensemble learning. By using bootstrap sampling and feature randomness, it dramatically reduces variance while maintaining low bias—perfect for complex, noisy problems like CTR prediction in AdTech.

Key Takeaways:
  • Random Forest grows many trees on bootstrap samples + random features
  • Variance reduction by factor of ~n (number of trees) via uncorrelated predictions
  • Max_features controls tree decorrelation; small values reduce variance, large values reduce bias
  • OOB error provides free cross-validation without holding out a validation set
  • Start with RF for baseline; graduate to XGBoost if you need maximum accuracy and can afford tuning
  • Interpretability via feature importance helps drive business decisions

Further Reading:

  • Breiman, L. (2001). "Random Forests." Machine Learning, 45(1), 5-32.
  • Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning.
  • scikit-learn Documentation: Ensemble Methods
End of Random Forest