Splitting Criteria
Comparison of Splitting Criteria: A Unified Perspective
Read aloud is unavailable in this browser.
Three Measures, One Goal
We've now studied three impurity measures in depth: Gini impurity, entropy, and misclassification error. Each has its own mathematical foundation, computational properties, and intuitive interpretation. But in practice, a natural question arises:
Which one should I actually use?
This page answers that question definitively. We'll compare the criteria across multiple dimensions, examine empirical evidence, and provide clear, actionable guidance. You'll understand not just what to choose, but why—and critically, when the choice matters.
By the end of this page, you will be able to systematically compare splitting criteria, understand when they produce different splits, know which to use in various scenarios, and appreciate why the differences are often smaller than expected in practice.
A Unified Mathematical Framework
All three impurity measures fit into a common framework. Let $\phi: [0,1] \to \mathbb{R}$ be a concave function with $\phi(0) = \phi(1) = 0$. Then the impurity of a distribution $p = (p_1, ..., p_K)$ is:
$$I_\phi(p) = \sum_{k=1}^K \phi(p_k)$$
The Three Measures:
| Measure | $\phi(t)$ | Concavity |
|---|---|---|
| Entropy | $-t \log t$ | Strictly concave |
| Gini | $t(1-t)$ | Strictly concave |
| Misclass | $\min(t, 1-t)$ | Concave (not strict) |
Note: For binary case, misclass $= \min(p, 1-p) = p_1(1 - \mathbb{1}[p_1 > 0.5]) + (1-p_1)(1 - \mathbb{1}[p_1 \leq 0.5])$.
This framework reveals that:
- All measures are symmetric in class labels
- All are minimized at purity (vertices of simplex)
- All are maximized at uniformity (center of simplex)
- The key difference is strict vs non-strict concavity
Gini and entropy are both Bregman divergences from the uniform distribution. Gini corresponds to squared Euclidean distance; entropy corresponds to KL divergence. This deep connection to divergence theory explains many of their shared properties.
Sensitivity to Class Distribution
A critical difference between Gini and entropy is their sensitivity to small probability classes.
Derivative Analysis (Binary Case):
For binary classification with proportion $p$:
| Measure | $f(p)$ | $f'(p)$ | $f'(0^+)$ | $f'(0.5)$ |
|---|---|---|---|---|
| Gini | $2p(1-p)$ | $2(1-2p)$ | $+2$ | $0$ |
| Entropy | $H(p)$ | $\log_2\frac{1-p}{p}$ | $+\infty$ | $0$ |
| Misclass | $\min(p, 1-p)$ | $\pm 1$ | $+1$ | undefined |
Key Insight: At $p = 0^+$ (very rare minority class):
- Entropy has infinite slope — extremely sensitive
- Gini has slope 2 — moderately sensitive
- Misclass has slope 1 — least sensitive
This means entropy 'rewards' separating rare classes more than Gini does.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
import numpy as npimport matplotlib.pyplot as plt def analyze_sensitivity(): """ Analyze and visualize sensitivity of impurity measures to small p. """ # Very small probabilities p_tiny = np.array([0.001, 0.005, 0.01, 0.02, 0.05, 0.1]) print("Sensitivity Analysis: Small Minority Class") print("=" * 65) print(f"{'p':>8} | {'Gini':>10} | {'Entropy':>10} | {'Misclass':>10} | {'H/G ratio':>10}") print("-" * 65) for p in p_tiny: g = 2 * p * (1 - p) h = -p * np.log2(p) - (1-p) * np.log2(1-p) if 0 < p < 1 else 0 m = min(p, 1-p) ratio = h / g if g > 0 else float('inf') print(f"{p:>8.3f} | {g:>10.6f} | {h:>10.6f} | {m:>10.6f} | {ratio:>10.2f}") print("" + "=" * 65) print("Observation: For very small p, entropy >> gini >> misclass") print("Entropy is MUCH more sensitive to separating rare classes.") # Plot derivatives fig, axes = plt.subplots(1, 2, figsize=(14, 5)) p = np.linspace(0.001, 0.999, 1000) # Gini derivative: 2(1-2p) gini_deriv = 2 * (1 - 2*p) # Entropy derivative: log((1-p)/p) / ln(2) entropy_deriv = np.log2((1-p)/p) # Misclass derivative: +1 for p<0.5, -1 for p>0.5 misclass_deriv = np.where(p < 0.5, 1, -1) ax1 = axes[0] ax1.plot(p, gini_deriv, 'b-', linewidth=2.5, label="Gini: 2(1-2p)") ax1.plot(p, entropy_deriv, 'r-', linewidth=2.5, label="Entropy: log₂((1-p)/p)") ax1.plot(p, misclass_deriv, 'g--', linewidth=2.5, label="Misclass: ±1") ax1.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax1.axvline(x=0.5, color='black', linestyle='--', alpha=0.3) ax1.set_xlabel('Proportion of Class 1 (p)', fontsize=12) ax1.set_ylabel('Derivative dI/dp', fontsize=12) ax1.set_title('First Derivative: Rate of Change', fontsize=14) ax1.legend(loc='upper right') ax1.grid(True, alpha=0.3) ax1.set_xlim(0, 1) ax1.set_ylim(-10, 10) # Zoom on small p region ax2 = axes[1] p_zoom = np.linspace(0.001, 0.2, 500) ax2.plot(p_zoom, 2 * (1 - 2*p_zoom), 'b-', linewidth=2.5, label="Gini") ax2.plot(p_zoom, np.log2((1-p_zoom)/p_zoom), 'r-', linewidth=2.5, label="Entropy") ax2.plot(p_zoom, np.ones_like(p_zoom), 'g--', linewidth=2.5, label="Misclass") ax2.set_xlabel('p (zoom on small values)', fontsize=12) ax2.set_ylabel('Derivative dI/dp', fontsize=12) ax2.set_title('Zoom: Sensitivity Near p=0', fontsize=14) ax2.legend(loc='upper right') ax2.grid(True, alpha=0.3) ax2.annotate('Entropy → ∞as p → 0', xy=(0.02, 5), fontsize=11, color='red') plt.tight_layout() plt.savefig('sensitivity_derivatives.png', dpi=150) plt.show() if __name__ == "__main__": analyze_sensitivity()In highly imbalanced datasets (e.g., fraud detection with 0.1% positive), entropy may be preferred. It gives more 'credit' for correctly separating the rare positive class, potentially leading to trees that better detect minority cases.
When Do Gini and Entropy Produce Different Splits?
Despite their different formulas, Gini and entropy are remarkably similar in practice. Empirical studies consistently find that they produce identical tree structures in 95-98% of cases.
When DO they differ?
Case 1: Ties or Near-Ties
When two features have very similar impurity reduction, Gini and entropy may 'break the tie' differently due to their different curvatures.
Case 2: Extreme Class Imbalance
As we saw, entropy is more sensitive to small probabilities. For highly imbalanced data, entropy may prefer splits that isolate the minority class, while Gini spreads the reduction more evenly.
Case 3: Multi-class Problems
With many classes, the functional forms diverge more. Entropy's $\log K$ maximum vs Gini's $1 - 1/K$ maximum means different 'scales' of impurity.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
import numpy as npfrom typing import List, Tuple def gini(counts): n = counts.sum() if n == 0: return 0 p = counts / n return 1 - np.sum(p ** 2) def entropy(counts): n = counts.sum() if n == 0: return 0 p = counts / n p = p[p > 0] return -np.sum(p * np.log2(p)) def compute_gain(parent, children, impurity_fn): """Compute impurity reduction for a split.""" parent_imp = impurity_fn(parent) n = parent.sum() child_imp = sum( (c.sum() / n) * impurity_fn(c) for c in children ) return parent_imp - child_imp def find_differing_example(): """ Find a case where Gini and Entropy prefer different splits. """ print("Searching for Gini/Entropy Disagreement...") print("=" * 65) # Construct a scenario with two competing splits # Parent: 100 samples with significant imbalance parent = np.array([90, 10]) # 90% class 0, 10% class 1 print(f"Parent: {parent}") print(f" Gini = {gini(parent):.4f}") print(f" Entropy = {entropy(parent):.4f}") # Split A: Balanced split, modest separation split_A = [ np.array([45, 5]), # 50 samples, 90% class 0 np.array([45, 5]) # 50 samples, 90% class 0 ] # Split B: Unbalanced split, one child isolates minority split_B = [ np.array([88, 2]), # 90 samples, 97.8% class 0 np.array([2, 8]) # 10 samples, 80% class 1 ] print(f"Split A (balanced, modest separation):") print(f" Child 1: {split_A[0]} (n=50)") print(f" Child 2: {split_A[1]} (n=50)") gini_gain_A = compute_gain(parent, split_A, gini) entropy_gain_A = compute_gain(parent, split_A, entropy) print(f" Gini Gain: {gini_gain_A:.6f}") print(f" Entropy Gain: {entropy_gain_A:.6f}") print(f"Split B (unbalanced, isolates minority):") print(f" Child 1: {split_B[0]} (n=90)") print(f" Child 2: {split_B[1]} (n=10)") gini_gain_B = compute_gain(parent, split_B, gini) entropy_gain_B = compute_gain(parent, split_B, entropy) print(f" Gini Gain: {gini_gain_B:.6f}") print(f" Entropy Gain: {entropy_gain_B:.6f}") print(f"" + "=" * 65) print("Preference:") print(f" Gini prefers: Split {'A' if gini_gain_A > gini_gain_B else 'B'}") print(f" Entropy prefers: Split {'A' if entropy_gain_A > entropy_gain_B else 'B'}") # More extreme example print(f" " + "=" * 65) print("More Extreme Example (severe imbalance):") print("=" * 65) parent_extreme = np.array([990, 10]) # 99% vs 1% print(f"Parent: {parent_extreme}") # Split C: Spreads minority split_C = [ np.array([500, 5]), np.array([490, 5]) ] # Split D: Concentrates minority split_D = [ np.array([985, 1]), np.array([5, 9]) ] print(f"Split C (minority spread evenly):") gini_C = compute_gain(parent_extreme, split_C, gini) entropy_C = compute_gain(parent_extreme, split_C, entropy) print(f" Gini Gain: {gini_C:.6f}") print(f" Entropy Gain: {entropy_C:.6f}") print(f"Split D (minority concentrated in small leaf):") gini_D = compute_gain(parent_extreme, split_D, gini) entropy_D = compute_gain(parent_extreme, split_D, entropy) print(f" Gini Gain: {gini_D:.6f}") print(f" Entropy Gain: {entropy_D:.6f}") print(f"Preference:") print(f" Gini prefers: Split {'C' if gini_C > gini_D else 'D'}") print(f" Entropy prefers: Split {'C' if entropy_C > entropy_D else 'D'}") if __name__ == "__main__": find_differing_example()Empirical Evidence: What Research Shows
Researchers have extensively compared splitting criteria across hundreds of datasets. Here's what the evidence shows:
Key Studies:
-
Breiman et al., 1984 (CART book): 'In our experience, [Gini and entropy] give very similar results. The choice between them is primarily a matter of computational convenience.'
-
Mingers, 1989: Across 8 datasets, Gini and entropy produced identical tree accuracy in most cases; differences were within noise.
-
Raileanu & Stoffel, 2004: 'The theoretical differences between Gini and entropy do not translate into significant practical differences in most situations.'
-
Modern ensemble methods: XGBoost, LightGBM, and CatBoost all offer both criteria. Benchmarks show <1% accuracy difference on most datasets.
| Finding | Implication |
|---|---|
| Tree structures identical ~95-98% of time | Choice rarely matters for final tree |
| Accuracy differences typically <1% | Both criteria work well in practice |
| Entropy slightly better for imbalanced data | Consider entropy for rare-class detection |
| Gini 10-50% faster computationally | Prefer Gini for large datasets |
| Multi-class: entropy more consistent | Consider entropy for K > 5 classes |
For most problems, the choice between Gini and entropy doesn't matter. Use Gini (faster) as default; switch to entropy only for severe class imbalance or when consistency with cross-entropy loss is desired. Always validate with cross-validation.
Practical Decision Guide
Let's synthesize everything into actionable guidance:
When to Use Gini Impurity
- •Default choice for most classification problems
- •Large datasets where computational speed matters
- •Binary classification with balanced or moderately imbalanced classes
- •Following CART conventions or scikit-learn defaults
- •Ensemble methods (Random Forest, Gradient Boosting) where many trees are built
- •Real-time/embedded systems requiring minimal compute
When in doubt, use Gini. It's faster and empirically equivalent to entropy in most cases. This is why it's the default in scikit-learn's DecisionTreeClassifier.
Implementation Checklist
When implementing or configuring decision trees, here's your checklist:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
from sklearn.tree import DecisionTreeClassifierfrom sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifierimport numpy as np def criterion_selection_guide(X, y, scenario='default'): """ Guide for selecting splitting criterion based on scenario. Returns configured classifier with appropriate criterion. """ n_samples, n_features = X.shape n_classes = len(np.unique(y)) class_counts = np.bincount(y) imbalance_ratio = class_counts.max() / class_counts.min() print(f"Dataset Analysis:") print(f" Samples: {n_samples:,}") print(f" Features: {n_features}") print(f" Classes: {n_classes}") print(f" Imbalance ratio: {imbalance_ratio:.1f}:1") print() # Decision logic if scenario == 'default': # Standard case: use Gini criterion = 'gini' reason = "Default choice - Gini is fast and empirically equivalent" elif scenario == 'imbalanced' or imbalance_ratio > 10: # Severe imbalance: consider entropy criterion = 'entropy' reason = f"Severe imbalance ({imbalance_ratio:.0f}:1) - entropy more sensitive to minority" elif scenario == 'multiclass' or n_classes > 5: # Many classes: entropy often preferred criterion = 'entropy' reason = f"Multi-class ({n_classes} classes) - entropy handles well" elif scenario == 'speed' or n_samples > 100000: # Large dataset: prioritize speed criterion = 'gini' reason = f"Large dataset ({n_samples:,} samples) - Gini is faster" elif scenario == 'cross_entropy_consistency': # Matching loss elsewhere criterion = 'entropy' reason = "Consistency with cross-entropy loss in pipeline" else: criterion = 'gini' reason = "Default fallback" print(f"Recommendation: criterion='{criterion}'") print(f"Reason: {reason}") print() return DecisionTreeClassifier(criterion=criterion, random_state=42) # Examplesprint("=" * 60)print("SCENARIO 1: Standard Binary Classification")print("=" * 60)X1 = np.random.randn(1000, 10)y1 = (X1[:, 0] + X1[:, 1] > 0).astype(int)clf1 = criterion_selection_guide(X1, y1) print("=" * 60)print("SCENARIO 2: Fraud Detection (Severe Imbalance)")print("=" * 60)X2 = np.random.randn(10000, 20)y2 = np.zeros(10000, dtype=int)y2[:100] = 1 # Only 1% fraudclf2 = criterion_selection_guide(X2, y2, scenario='imbalanced') print("=" * 60)print("SCENARIO 3: Multi-class (10 classes)")print("=" * 60)X3 = np.random.randn(5000, 15)y3 = np.random.randint(0, 10, 5000)clf3 = criterion_selection_guide(X3, y3, scenario='multiclass') print("=" * 60)print("SCENARIO 4: Large Dataset (1M samples)")print("=" * 60)print("(simulated)")X4 = np.random.randn(100, 10) # placeholdery4 = np.random.randint(0, 2, 100)# Pretend it's 1M samplesprint(f"Dataset Analysis:")print(f" Samples: 1,000,000")print(f" Features: 50")print(f" Classes: 2")print(f" Imbalance ratio: 1.5:1")print()print("Recommendation: criterion='gini'")print("Reason: Large dataset - Gini is 2-4x faster") print("" + "=" * 60)print("LIBRARIES & DEFAULTS")print("=" * 60)print("""Library Default Criterion Options----------------- ------------------ ----------------------scikit-learn gini gini, entropy, log_lossXGBoost N/A (uses loss) N/ALightGBM N/A (uses loss) N/AC4.5/C5.0 entropy entropy (gain ratio)CART (original) gini gini, entropy""")Beyond Traditional Criteria
Modern machine learning has developed additional splitting criteria for specialized scenarios:
1. Log Loss (Cross-Entropy Loss)
Used in gradient boosting, this directly optimizes the log-likelihood: $$L = -\sum_i \left[ y_i \log \hat{p}_i + (1-y_i) \log(1 - \hat{p}_i) \right]$$
Advantage: Directly comparable to neural network losses; enables end-to-end optimization.
2. Twoing Rule
CART's alternative criterion that considers binary supergroupings: $$\text{Twoing} = \frac{n_L n_R}{n^2} \left( \sum_k |p_{kL} - p_{kR}| \right)^2$$
Advantage: Can find good multi-class splits when classes group naturally.
3. Distance-Based Criteria
Used in some specialized trees:
- Hellinger distance (robust to imbalance)
- Kolmogorov-Smirnov statistic
- Chi-squared divergence
4. Structural Criteria
For ordinal/structured outputs:
- Ranking-based impurity for ordinal regression
- Cluster-based impurity for structured prediction
XGBoost and LightGBM don't use traditional impurity measures. They directly optimize a loss function (log loss, squared error) using gradients and Hessians. This is more flexible and enables custom objectives.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.