HardClassic ML

Decision Tree Best Split

Classic ML

Hard

Problem

A decision tree grows by repeatedly splitting the data on the feature and threshold that best separates the classes. The quality of a split is measured by the information gain: how much the Gini impurity decreases after splitting.

Given a feature matrix X and class labels y, find the single best split (feature index and threshold) that maximizes information gain using Gini impurity.

Algorithm

  1. Compute the Gini impurity of the parent node:

\text{Gini}(S) = 1 - \sum_{k} p_k^2

Where p_k is the fraction of samples belonging to class k.

  1. For each feature and each midpoint between consecutive sorted unique values, split the data into left (feature <= threshold) and right (feature > threshold)

  2. Compute the weighted Gini impurity after the split:

\text{Gini}_{\text{split}} = \frac{|S_L|}{|S|} \cdot \text{Gini}(S_L) + \frac{|S_R|}{|S|} \cdot \text{Gini}(S_R)

  1. The information gain is the parent Gini minus the weighted split Gini. Return the feature and threshold with the highest gain. Break ties by smallest feature index, then smallest threshold.

Return the selected feature index and threshold as a two-item list.

Theory

A decision tree split divides a node's samples into two or more groups based on a feature value. The goal is to create child nodes that are purer than the parent, meaning samples in each child are more homogeneous in their class labels.

At each internal node, the tree asks a question like "Is feature X greater than threshold T?" and routes samples left or right based on the answer.


The Splitting Criterion

To find the best split, we need a measure of node impurity. Common measures:

Gini impurity:

\text{Gini}(S) = 1 - \sum_{i=1}^{C} p_i^2

Entropy:

H(S) = -\sum_{i=1}^{C} p_i \log_2(p_i)

where p_i is the proportion of samples in class i and C is the number of classes.

The best split maximizes the reduction in impurity.


Information Gain

Information gain measures how much a split reduces impurity:

\text{IG}(S, A) = \text{Impurity}(S) - \sum_{v \in \text{values}(A)} \frac{|S_v|}{|S|} \text{Impurity}(S_v)

where:

We choose the split that maximizes information gain.


Types of Splits

Binary split (most common):

For numeric features: "Is X <= threshold?"

For categorical features: "Is X in subset S?"

Multi-way split:

For categorical features with k values, create k children, one for each value.


Finding the Best Split for Numeric Features

Step 1: Sort samples by the feature value

Step 2: Consider split points between consecutive distinct values

Step 3: For each candidate threshold, compute information gain

Step 4: Choose the threshold with maximum information gain

Example:

Feature values (sorted): [1, 2, 2, 4, 5, 7]

Candidate thresholds: 1.5, 3, 4.5, 6 (midpoints between distinct values)

For each threshold, split the data and compute impurity reduction.


Worked Example: Finding the Best Split

Data: 10 samples with feature X and binary label (+ or -)

Parent node: 4 positive, 6 negative

Parent Gini: p_+ = 0.4, p_- = 0.6

\text{Gini} = 1 - (0.4^2 + 0.6^2) = 1 - (0.16 + 0.36) = 0.48


Consider split at X = 5.5:

Left child (X <= 5.5): samples 1, 2, 3, 4

Right child (X > 5.5): samples 5, 6, 7, 8, 9, 10

Weighted average Gini: \text{Gini}_{\text{children}} = \frac{4}{10}(0.375) + \frac{6}{10}(0.278) = 0.15 + 0.167 = 0.317

Gini gain: 0.48 - 0.317 = 0.163


Consider split at X = 3.5:

Left child (X <= 3.5): samples 1, 2

Right child (X > 3.5): samples 3, 4, 5, 6, 7, 8, 9, 10

Weighted average Gini: \text{Gini}_{\text{children}} = \frac{2}{10}(0) + \frac{8}{10}(0.375) = 0 + 0.3 = 0.3

Gini gain: 0.48 - 0.3 = 0.18

Better split! Splitting at X = 3.5 gives higher Gini gain.


Splitting Categorical Features

Approach 1: One-vs-rest

For each category value, create a binary split: "Is X = value?"

Approach 2: Subset search

Find the best subset of values for the left child. For k categories, there are 2^{k-1} - 1 possible subsets to consider.

Approach 3: Binary encoding

Convert categorical feature to multiple binary features, then split on each.


Stopping Criteria

The tree stops splitting when:

Maximum depth reached: Tree has grown to specified depth limit

Minimum samples: Node has fewer than minimum required samples to split

Pure node: All samples have the same class (impurity = 0)

No improvement: No split improves impurity beyond a threshold

Minimum samples per leaf: Split would create a leaf with too few samples


Greedy Splitting

Decision trees use a greedy approach:

  1. At each node, find the locally optimal split
  2. Do not reconsider previous splits
  3. Do not look ahead to future splits

This is computationally efficient but may not find the globally optimal tree.


Split Quality Metrics Comparison

Gini impurity:

Entropy:

Misclassification error:

In practice, Gini and entropy produce similar trees.


Handling Missing Values

During training:

During prediction:


Numerical Stability

When computing impurity:


Computational Complexity

For each node:

For the whole tree:

Optimizations:


Regression Tree Splits

For regression, use variance reduction instead of Gini/entropy:

Parent variance:

\text{Var}(S) = \frac{1}{|S|} \sum_{i \in S} (y_i - \bar{y})^2

Split criterion: Minimize weighted child variance

\text{Var reduction} = \text{Var}(S) - \frac{|S_L|}{|S|}\text{Var}(S_L) - \frac{|S_R|}{|S|}\text{Var}(S_R)

The best split maximizes variance reduction.

Examples

Example 1

Input
X = [[1, 5], [2, 5], [3, 5], [4, 5]], y = [0, 0, 1, 1]
Output
[0, 2.5]
Explanation
Feature 0 at 2.5 separates the two classes perfectly.

Example 2

Input
X = [[1, 1], [2, 1], [1, 10], [2, 10]], y = [0, 0, 1, 1]
Output
[1, 5.5]

Hints

  1. Generate candidate thresholds from midpoints between sorted unique feature values.
  2. Compare parent impurity with the size-weighted impurities of both children.

Requirements

Constraints

Starter Code

def decision_tree_split(X: list, y: list) -> list:
    """
    Returns the best feature index and threshold.
    """
    # Write code here
    pass

Test Cases

CaseMatches
Clean split on feature 0public
Split on feature 1public