Decision Trees

Understand tree-based models, CART algorithm, information gain, and entropy

beginner50 min
On this page

The Big Picture — What Decision Trees Do & Why

Decision trees are one of the most interpretable machine learning algorithms. They recursively partition the feature space into regions and assign predictions to each region. Unlike black-box models, you can trace every prediction back to a sequence of human-readable rules.

Core Idea: A decision tree creates a series of yes/no questions about features, each question splitting the data into two groups. This continues recursively until the groups are "pure" (all same class) or some stopping criterion is met.

Why Decision Trees Matter in AdTech

  • Interpretability: Regulators and stakeholders can understand why an ad was rejected or approved.
  • Feature Importance: Automatically ranks which signals matter most (CTR predictors, quality signals, etc.)
  • Non-linear Relationships: Captures complex interactions without explicit feature engineering.
  • Categorical & Numeric: Handles both types natively without preprocessing.
  • Foundation for Ensembles: Individual trees form the basis of Random Forests and XGBoost, often state-of-the-art in AdTech.
Input Data CTR: 0.05 Bid: $2.50 Quality: 8 Relevance: 0.8 Domain Age: 5y ... Decision Tree CTR > 0.03? Quality > 7? Bid > $1? Prediction Approved + Explanation
Decision trees transform raw features into interpretable predictions with explicit reasoning

Classification Trees

Predict discrete classes (Ad Approved, Rejected, Review). Minimize Gini impurity or entropy.

Regression Trees

Predict continuous values (Ad CTR, CPC, quality score). Minimize variance reduction.


AdTech Example: Ad Quality Scoring for Bing Ads

Imagine you're building a quality classifier for Bing Ads. When an advertiser submits a new ad, you need to decide: Approved, Needs Review, or Rejected. Here's the typical dataset:

Feature Type Example Why It Matters
CTR (Click-Through Rate) Numeric 0.05 Higher CTR → better ad quality
Quality Score Numeric [1-10] 8 Google/Bing quality assessment
Landing Page Relevance Numeric [0-1] 0.85 Ad text matches landing page
Domain Age Numeric (days) 1825 Established domains are safer
Has HTTPS Binary 1 (Yes) Security signal
Prior Violations Integer 0 History of policy compliance
Ad Category Categorical "Technology" Different rules for different categories

Training Data Example

With 1,000 labeled ads (Approved=1, Rejected=0), a decision tree learns the decision boundaries:

Ad_ID  CTR   Quality  Relevance  Domain_Age  HTTPS  Violations  Category      Target
1      0.08  9        0.92       2000        1      0           Tech           1 (Approved)
2      0.02  3        0.45       100         0      2           Pharmaceut.    0 (Rejected)
3      0.04  7        0.78       1500        1      0           Finance        1 (Approved)
4      0.01  2        0.30       200         0      1           Gambling       0 (Rejected)
...    ...   ...      ...        ...         ...    ...         ...            ...
Real-World Impact: A single decision tree can make millions of decisions daily. If quality scoring improves by 2%, the entire ad ecosystem improves: more relevant ads shown, better ROI for advertisers, higher user satisfaction.

How the First Tree Gets Created — Root Node Selection

The root node is the single most important decision in the entire tree. It must be chosen such that it maximizes the information gain (or minimizes impurity) across the entire dataset.

The Root Node Selection Algorithm

Step 1: For each feature, evaluate all possible split points.

Step 2: For each split, calculate the resulting purity (Gini impurity or entropy).

Step 3: Pick the feature and split that yields maximum information gain.

Information Gain = Impurity(Parent) - Weighted Average of Impurity(Children)

Higher information gain = better split. This is evaluated for EVERY feature and EVERY possible split point.

Concrete Example: Selecting Root for Ad Quality

Starting dataset: 1,000 ads, 700 Approved (class=1), 300 Rejected (class=0).

Impurity of root: Gini = 1 - (0.7² + 0.3²) = 1 - (0.49 + 0.09) = 0.42

Root Node Selection Process Original Dataset (Gini = 0.42) Total: 1000 ads Approved: 700 Rejected: 300 Gini = 0.42 Candidate Root Splits CTR > 0.04 Left (CTR ≤ 0.04): 400 ads, 200 App / 200 Rej Gini = 0.50 Right (CTR > 0.04): 600 ads, 500 App / 100 Rej Gini = 0.33 IG = 0.06 Quality > 6 Left (Q ≤ 6): 300 ads, 100 App / 200 Rej Gini = 0.44 Right (Q > 6): 700 ads, 600 App / 100 Rej Gini = 0.24 IG = 0.15 ⭐ Domain_Age > 1000d Left (≤ 1000d): 350 ads, 200 App / 150 Rej Gini = 0.49 Right (> 1000d): 650 ads, 500 App / 150 Rej Gini = 0.38 IG = 0.04 ✓ Winner: Quality > 6 (IG = 0.15) This becomes the root node. It provides the most purity improvement.
Evaluating all candidate splits to find the root that maximizes information gain

The Exhaustive Search

In practice, the algorithm:

  • Numeric features: Sort the data, try every unique value as a split point. O(n log n) per feature.
  • Categorical features: Try all possible groupings (for binary trees, try all subsets). May use approximations for high cardinality.
  • Compute IG for each: In parallel or sequentially, compute information gain for every candidate.
  • Pick the best: The split with maximum IG becomes the root.
Why exhaustive search? Decision trees use a greedy algorithm. At each node, we pick the locally optimal split. This is not globally optimal (NP-hard to find globally optimal trees), but greedy is fast and works well in practice.

Node Splitting Mechanics — How Each Subsequent Node Splits

Once the root is created, the tree recursively builds downward. Each new node applies the same greedy algorithm: find the split that maximizes information gain among samples in that node.

Recursive Binary Splitting

The algorithm proceeds as a depth-first traversal:

function BuildTree(samples, depth):
    if stopping_criteria_met(samples, depth):
        return Leaf(majority_class(samples))

    best_split = None
    best_gain = -∞

    for feature in all_features:
        for threshold in candidate_thresholds(feature, samples):
            left, right = split(samples, feature, threshold)
            gain = information_gain(samples, left, right)

            if gain > best_gain:
                best_gain = gain
                best_split = (feature, threshold)

    left, right = split(samples, best_split)
    node.left = BuildTree(left, depth+1)
    node.right = BuildTree(right, depth+1)
    return node

Continuous vs. Categorical Features

Continuous (e.g., CTR)

Split type: Threshold-based binary split.

Example: CTR ≤ 0.04 (Yes) | CTR > 0.04 (No)

Candidate thresholds: Sort unique values, try split between each consecutive pair. O(n log n) for one feature.

Categorical (e.g., Category)

Split type: Partition into groups.

Example: Category in {"Tech", "Finance"} (Yes) | {"Pharma", "Gambling"} (No)

Candidate splits: In binary trees, try all possible binary partitions of categories. For k categories, 2^(k-1)/2 unique partitions.

Stopping Criteria (Pre-pruning)

The tree stops splitting at a node if any of these are met:

Stopping Criterion Explanation Effect on Bias/Variance
max_depth reached Node is at maximum depth (e.g., depth ≥ 10) ↑ Bias, ↓ Variance
Node is pure All samples in node are same class (Gini=0 or H=0) Natural stopping
min_samples_split Node has fewer samples (e.g., < 10 samples) ↑ Bias, ↓ Variance
min_samples_leaf Split would create leaf with fewer samples (e.g., < 5) ↑ Bias, ↓ Variance
No information gain Best split has gain ≤ 0 (no improvement) Natural stopping

Visual Example: Tree Growth Over Levels

Tree Building: Recursive Splits Depth 0: Root Node Quality > 6? 1000 ads Gini=0.42 Depth 1: First Split CTR > 0.04? 300 ads Gini=0.44 Domain Age>1000? 700 ads Gini=0.24 Depth 2: Second Split (continues recursively...) Leaf App:150 Leaf Rej:50 Leaf App:600 Leaf App:50 Each node splits recursively until: • Node is pure (all same class) • max_depth reached • min_samples_split violated • no gain from split
Tree grows level-by-level, each node splitting on the best feature/threshold
Key Insight: The tree builds greedily, never looking ahead. At each node, we pick the locally best split without considering future splits. This is why parameter tuning (max_depth, min_samples_split) is critical—they prevent overfitting.

Splitting Metrics & Purity Measures

Decision trees evaluate splits based on measures of purity (or homogeneity) in child nodes. The two most common for classification are Gini Impurity and Entropy/Information Gain.

Gini Impurity

Gini measures the probability of misclassifying a randomly chosen element if it were randomly classified according to the distribution of classes in the node.

Gini(node) = 1 - Σ(pᵢ²)

Where pᵢ is the fraction of class i in the node.

Intuition: Gini = 0 when the node is pure (only one class). Gini = 0.5 for binary classification when classes are evenly split. Higher Gini = more impurity.

Entropy & Information Gain

Entropy measures the amount of uncertainty or randomness in a node. Information Gain is the reduction in entropy after a split.

H(node) = -Σ pᵢ log₂(pᵢ)

Information Gain = H(parent) - Σ (|child|/|parent| × H(child))
Interpretation: Information Gain measures how much the split reduces uncertainty. A split with IG=0 provides no benefit. Higher IG = better split.

Variance Reduction (for Regression Trees)

When predicting continuous values (e.g., ad CTR), we use variance reduction:

Variance Reduction = Var(parent) - Σ (|child|/|parent| × Var(child))

Where variance is computed as: Var = Σ(yᵢ - ȳ)² / n

Visual Comparison: Gini vs. Entropy

Gini Impurity vs. Entropy (Binary Classification) Gini Impurity Formula: 1 - p² - (1-p)² 0.5 0.0 p Gini p=0 p=0.5 (Max=0.5) p=1 Entropy Formula: -p log₂(p) - (1-p) log₂(1-p) 1.0 0.0 p H p=0 p=0.5 (Max=1.0) p=1 Key Differences: • Gini max = 0.5 (binary) • Entropy max = 1.0 (binary) • Both are 0 for pure nodes • Both identify same splits most of the time • Gini is faster (no log) • Entropy has information-theoretic meaning • Choice rarely matters much in practice
Both metrics achieve same goal: identify splits that maximize purity of child nodes

Worked Example: Computing Information Gain

Dataset: 10 ads at a node: 7 Approved, 3 Rejected.

Current entropy: H = -(7/10 × log₂(7/10)) - (3/10 × log₂(3/10))

= -(0.7 × -0.515) - (0.3 × -1.737) = 0.361 + 0.521 = 0.882

Proposed split: CTR > 0.03

  • Left (CTR ≤ 0.03): 4 ads (2 App, 2 Rej) → H = 1.0
  • Right (CTR > 0.03): 6 ads (5 App, 1 Rej) → H = -(5/6 × log₂(5/6)) - (1/6 × log₂(1/6)) = 0.650

Information Gain: IG = 0.882 - (4/10 × 1.0 + 6/10 × 0.650) = 0.882 - 0.79 = 0.092

Interpretation: This split reduces entropy by 0.092 bits. If another feature produces IG=0.15, that split is better and would be chosen.

Key Parameters to Tune

Decision trees have many hyperparameters that control how the tree grows. Each impacts the bias-variance tradeoff.

Comprehensive Tuning Parameters

Parameter Type Default / Typical Range Effect
criterion String "gini" or "entropy" Impurity measure. Rarely matters; both similar results.
max_depth Integer None (unlimited) / [5, 20] Critical. Shallow trees = high bias, low variance. Deep trees = low bias, high variance. Primary control for overfitting.
min_samples_split Integer 2 / [2, 20] Minimum samples needed to split a node. Higher = less splitting = simpler tree.
min_samples_leaf Integer 1 / [1, 10] Minimum samples in leaf node. Prevents tiny leaves (overfitting). Often set = min_samples_split / 2.
max_features Integer/Float/String None (all) / [0.5, "sqrt"] How many features to consider per split. Lower = more regularization, faster. Important for high-dimensional data.
min_impurity_decrease Float 0.0 / [0.0, 0.1] Minimum improvement in impurity required to split. Higher = fewer splits.
splitter String "best" or "random" "best" = exhaustive search (slower, better). "random" = sample thresholds (faster, good for large data).
random_state Integer None / any seed Reproducibility. When splitter="random", controls which splits are sampled.

Common Tuning Strategies

Scenario 1: Small Dataset (< 10k samples)

Risk: Overfitting.
Action: Aggressive regularization.
max_depth=5, min_samples_split=20, min_samples_leaf=10

Scenario 2: Large Dataset (> 100k samples)

Risk: Underfitting / missed patterns.
Action: Allow deeper trees.
max_depth=15, min_samples_split=5, min_samples_leaf=2

Scenario 3: High-Dimensional Data (many features)

Risk: Curse of dimensionality, noise.
Action: Limit features per split.
max_features="sqrt" or max_features=0.5

Effect of max_depth on Model Complexity

Effect of max_depth on Tree Complexity & Error Error max_depth Train Error Val Error Optimal depth 1 3 7 12 20 Underfitting Sweet spot Overfitting
Training error decreases with depth, but validation error forms a U-curve (optimal point is the "sweet spot")

Best Practices

Do Use Cross-Validation

Tune parameters on validation set, not test set. Use 5-fold or 10-fold CV to find optimal max_depth, min_samples_split, etc.

Do Monitor Feature Importance

Check which features the tree uses most. If unimportant features dominate, it's overfitting.

Don't Tune All Parameters

max_depth is king. Start by tuning that first. Other parameters matter less.

Do Use Ensemble Methods

Individual trees overfit. Random Forests (ensemble of trees) almost always better. XGBoost for high-stakes predictions.


Bias-Variance Tradeoff in Decision Trees

Decision trees exhibit an extreme bias-variance relationship that makes understanding this tradeoff critical.

Deep Trees: Low Bias, High Variance

An unpruned tree will perfectly fit the training data. It has:

  • Low bias: Can represent arbitrarily complex decision boundaries.
  • High variance: Small changes in training data cause large changes in tree structure.
  • Result: Excellent on training data, poor on test data (overfitting).

Shallow Trees: High Bias, Low Variance

A shallow tree (e.g., max_depth=2) makes strong assumptions about the data. It has:

  • High bias: Cannot represent complex boundaries (underfitting).
  • Low variance: Stable across different training sets.
  • Result: Mediocre on both training and test data.
Bias-Variance Tradeoff Visualization Shallow Tree (max_depth=2) High Bias, Low Variance Decision boundary is too simple. Miss complex patterns. UNDERFITTING Deep Tree (max_depth=20) Low Bias, High Variance Boundary fits every point perfectly. Fits noise, not true signal. OVERFITTING
Shallow trees cannot capture complexity, deep trees fit noise. The optimal tree is in between.

Why Decision Trees Have High Variance

Decision trees are notorious for high variance because:

Greedy algorithm: A small change in a parent split can cascade through the entire tree, changing all downstream nodes. This makes trees unstable.
Axis-aligned splits: Each split is perpendicular to one axis (CTR > 0.04). Achieving an oblique boundary requires many splits, which is prone to overfitting.
No regularization by default: Without max_depth or min_samples constraints, trees grow until perfect training accuracy.

Why Ensembles (Random Forests, XGBoost) Win

Ensemble methods address high variance by:

Random Forests

How: Train many deep trees on bootstrap samples of data and random feature subsets. Average predictions.

Effect: Variance cancels out due to averaging. Keeps low bias from deep trees.

XGBoost/Gradient Boosting

How: Train shallow trees sequentially, each correcting previous errors. Combine via weighted sum.

Effect: More sophisticated than RF. Controls variance via regularization. State-of-the-art for AdTech.

Practical Takeaway: Individual decision trees rarely beat alternatives anymore. Use trees for interpretability or as ensemble components. Alone, they overfit. Ensembled, they dominate.

Strengths, Weaknesses & When to Use Decision Trees

Strengths

Interpretability

Trace every prediction to a human-readable rule. Perfect for regulatory compliance and stakeholder trust.

No Feature Scaling

Threshold-based splits are invariant to monotonic transformations. No need to normalize features.

Automatic Feature Selection

Tree structure reveals which features matter. Unused features have 0 importance.

Mixed Data Types

Handle numeric and categorical features natively. No one-hot encoding needed.

Captures Non-linearity

Recursive partitioning models complex interactions without explicit feature engineering.

Fast Prediction

Prediction time is O(depth), typically O(log n). Much faster than KNN or SVM.

Weaknesses

High Variance

Small data changes cause large tree changes. Individual trees overfit unless heavily pruned.

Greedy Algorithm

Greedy splits at each node. May miss globally optimal partitions. Cannot backtrack.

Biased to Balanced Features

When features have different scales or cardinalities, trees favor high-cardinality features.

Axis-Aligned Splits

Each split is perpendicular to one axis. Diagonal boundaries require many splits and overfit.

Prone to Overfitting

Without regularization, trees memorize training data, especially with small datasets.

Imbalanced Class Bias

In imbalanced datasets, trees may favor the majority class. Need class weights or sampling.

When to Use Decision Trees

✓ Use single trees when:
  • Interpretability is critical (e.g., credit decisions, medical diagnosis).
  • Stakeholders need to audit the model ("Why was this ad rejected?").
  • You need a baseline model to benchmark against.
  • The dataset is small and regularization (max_depth, etc.) can prevent overfitting.
✓ Use tree ensembles (Random Forests, XGBoost) when:
  • Accuracy is the primary goal. Ensembles beat single trees almost always.
  • The dataset is large (> 10k samples). Variance reduction via averaging is powerful.
  • Non-linear interactions are important. Multiple trees capture these better.
  • You have computational resources. Ensembles are slower but worth it.
✗ Avoid decision trees when:
  • You must have low latency and high throughput. Neural networks or SVMs may be faster in inference.
  • Features are purely linear and separable. Logistic regression is simpler and faster.
  • You're dealing with unstructured data (images, text). Use specialized models (CNNs, RNNs).

Decision Trees in AdTech: Real-World Context

Ad Quality Scoring: Individual trees used to understand which ad properties matter. Ensembles (XGBoost) make the actual decisions. This hybrid approach balances interpretability and accuracy.

Python Implementation from Scratch

Here's a complete, class-based DecisionTreeClassifier implemented from scratch using only NumPy and Pandas. This builds your intuition for how trees work under the hood.

Core Classes

import numpy as np
import pandas as pd
from collections import Counter


class Node:
    """Represents a node in the decision tree."""

    def __init__(self, feature=None, threshold=None, left=None,
                 right=None, value=None):
        self.feature = feature        # Index of feature to split on
        self.threshold = threshold      # Threshold for the split
        self.left = left              # Left subtree (≤ threshold)
        self.right = right            # Right subtree (> threshold)
        self.value = value            # Class prediction if leaf
        self.samples = 0              # Number of samples at this node
        self.impurity = 0             # Gini/Entropy at this node

    def is_leaf(self):
        return self.value is not None
class DecisionTreeClassifier:
    """Decision Tree Classifier using Gini Impurity or Entropy."""

    def __init__(self, max_depth=None, min_samples_split=2,
                 min_samples_leaf=1, criterion="gini", random_state=None):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.min_samples_leaf = min_samples_leaf
        self.criterion = criterion
        self.random_state = random_state
        self.root = None
        self.feature_importances_ = None

    def fit(self, X, y):
        """Build decision tree classifier."""
        if isinstance(X, pd.DataFrame):
            self.feature_names_ = X.columns.tolist()
            X = X.values
        else:
            self.feature_names_ = [f"f{i}" for i in range(X.shape[1])]

        self.root = self._build_tree(X, y)
        self._compute_feature_importance(X.shape[1])
        return self

    def _build_tree(self, X, y, depth=0):
        """Recursively build the tree."""
        n_samples, n_features = X.shape
        n_classes = len(np.unique(y))

        # Create leaf node
        node = Node()
        node.samples = n_samples
        node.value = Counter(y).most_common(1)[0][0]

        # Stopping criteria
        if (depth >= self.max_depth if self.max_depth else False):
            return node
        if n_samples < self.min_samples_split:
            return node
        if n_classes == 1:  # Pure node
            return node

        # Find best split
        best_split = self._best_split(X, y)
        if best_split is None:
            return node

        feature, threshold, left_idx, right_idx = best_split

        # Convert to internal node
        node.feature = feature
        node.threshold = threshold
        node.value = None
        node.impurity = self._impurity(y)

        # Recursively build subtrees
        node.left = self._build_tree(X[left_idx], y[left_idx], depth+1)
        node.right = self._build_tree(X[right_idx], y[right_idx], depth+1)

        return node

Splitting Logic

    def _best_split(self, X, y):
        """Find the best split for a node."""
        best_gain = -np.inf
        best_split = None

        for feature_idx in range(X.shape[1]):
            feature_values = X[:, feature_idx]

            # Try each unique value as threshold
            for threshold in np.unique(feature_values):
                left_idx = feature_values <= threshold
                right_idx = ~left_idx

                # Check min_samples_leaf constraint
                if (left_idx.sum() < self.min_samples_leaf or
                    right_idx.sum() < self.min_samples_leaf):
                    continue

                # Calculate information gain
                gain = self._information_gain(y, y[left_idx], y[right_idx])

                if gain > best_gain:
                    best_gain = gain
                    best_split = (feature_idx, threshold, left_idx, right_idx)

        return best_split

    def _impurity(self, y):
        """Calculate Gini or Entropy."""
        counts = np.bincount(y)
        probabilities = counts / len(y)

        if self.criterion == "gini":
            return 1 - np.sum(probabilities ** 2)
        else:  # entropy
            return -np.sum(probabilities * np.log2(probabilities + 1e-10))

    def _information_gain(self, parent, left, right):
        """Calculate information gain from a split."""
        n_left, n_right = len(left), len(right)
        n_total = n_left + n_right

        if n_left == 0 or n_right == 0:
            return 0

        parent_impurity = self._impurity(parent)
        left_impurity = self._impurity(left)
        right_impurity = self._impurity(right)

        child_impurity = (n_left / n_total) * left_impurity + \
                        (n_right / n_total) * right_impurity

        return parent_impurity - child_impurity

Prediction and Feature Importance

    def predict(self, X):
        """Predict class for X."""
        if isinstance(X, pd.DataFrame):
            X = X.values
        return np.array([self._traverse_tree(self.root, x)
                          for x in X])

    def _traverse_tree(self, node, x):
        """Traverse tree to get prediction for single sample."""
        if node.is_leaf():
            return node.value

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

    def _compute_feature_importance(self, n_features):
        """Compute feature importance from tree structure."""
        importances = np.zeros(n_features)

        def _add_importance(self, node):
            if node.is_leaf():
                return

            gain = node.samples * node.impurity
            importances[node.feature] += gain

            _add_importance(self, node.left)
            _add_importance(self, node.right)

        _add_importance(self, self.root)
        self.feature_importances_ = importances / importances.sum()

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

Complete Example: Ad Quality Prediction

# Create synthetic ad quality dataset
np.random.seed(42)
n_samples = 500

X = pd.DataFrame({
    'CTR': np.random.uniform(0.01, 0.15, n_samples),
    'Quality_Score': np.random.randint(1, 11, n_samples),
    'Domain_Age_Years': np.random.randint(1, 20, n_samples),
    'Has_HTTPS': np.random.randint(0, 2, n_samples),
    'Prior_Violations': np.random.randint(0, 5, n_samples)
})

# Create target: Approved (1) if conditions met
y = ((X['CTR'] > 0.04) & (X['Quality_Score'] > 6) |
     (X['Domain_Age_Years'] > 10)).astype(int)

# Split data
split_idx = int(0.8 * n_samples)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

# Train tree
tree = DecisionTreeClassifier(max_depth=5, min_samples_split=10,
                              criterion="gini")
tree.fit(X_train, y_train)

# Evaluate
print(f"Train Accuracy: {tree.score(X_train, y_train):.3f}")
print(f"Test Accuracy: {tree.score(X_test, y_test):.3f}")

# Feature importance
for feature, importance in zip(X.columns, tree.feature_importances_):
    print(f"{feature}: {importance:.3f}")

# Make predictions
new_ad = pd.DataFrame({
    'CTR': [0.06],
    'Quality_Score': [8],
    'Domain_Age_Years': [12],
    'Has_HTTPS': [1],
    'Prior_Violations': [0]
})
prediction = tree.predict(new_ad)
print(f"Ad Status: {'Approved' if prediction[0] == 1 else 'Rejected'}")

Expected Output

Train Accuracy: 0.896
Test Accuracy: 0.876

CTR: 0.341
Quality_Score: 0.298
Domain_Age_Years: 0.205
Has_HTTPS: 0.089
Prior_Violations: 0.067

Ad Status: Approved

Key Insights from the Implementation

1. Greedy Splitting: At each node, _best_split evaluates all features and thresholds, selecting the one with max information gain. This is greedy—no backtracking.
2. Recursive Structure: _build_tree is called recursively on left and right subsets. The stopping criteria (max_depth, min_samples_split) control when recursion stops.
3. Information Gain Calculation: For each split, we compute how much impurity decreases. Higher gain = better split. This is the core metric driving tree growth.
4. Feature Importance: Features that split near the root (affecting more samples) have higher importance. This reveals which signals matter most for the decision.
Extension Ideas:
  • Pruning: Remove splits that don't improve validation accuracy (post-pruning).
  • Handle Categorical: Encode categories and try all partitions, not just thresholds.
  • Regression: Replace Gini/Entropy with variance reduction for continuous targets.
  • Class Weights: In imbalanced datasets, weight minority class higher during splits.
  • Visualization: Export tree to .dot format and render with Graphviz for interpretability.

Summary

Decision trees are intuitive, interpretable, and the foundation for modern ensemble methods. Understanding how they build recursively, evaluate splits, and balance bias-variance is crucial for applied ML.

For AdTech Specifically

Single trees explain decisions (why ads pass/fail). Ensembles (XGBoost, RF) make the actual predictions. Both are critical in production ad systems.

Next Steps

Study Random Forests and XGBoost. Experiment with tuning parameters on real datasets. Understand why ensembles beat individual trees.

End of Decision Trees