Classification & Decision Trees — Why Ensembles Beat a Single Tree

Logistic · Decision Tree · Random Forest · Gradient Boosting on a synthetic XOR-shape dataset
DATASET: 1,500 samples · 4 features (2 informative + 2 noise)
Synthetic XOR blobs · deterministic (seed 42)
75 / 25 train-test split · stratified
0.981
Best Accuracy
Random Forest (200 trees)
0.461
Logistic Accuracy
Below chance — XOR is unsolvable linearly

Model scorecard

ModelAccuracyF1ROC-AUC
Logistic Regression0.4610.4680.452
Decision Tree depth 60.8690.8550.966
Random Forest 200 trees0.9810.9810.997
Gradient Boosting 200 stages0.9790.9790.995
● navy = best in column   ● orange = notably worst (Logistic — below chance on XOR)   ● gray = neutral  ·  values from results/metrics.json
Headline finding: Logistic regression is worse than chance — that's not bad luck, it's a mathematical guarantee. XOR is the canonical example of a problem no linear classifier can solve, and a synthetic dataset lets us demonstrate that cleanly. The ensembles' 98% accuracy is the gap a single decision tree leaves on the table.

Visual diagnostics

1 · Test-set metrics across models

The bar chart maps the model-complexity ladder almost perfectly: a linear model floors at ~50% (random), a single tree climbs to ~87%, and either ensemble of trees lands at ~98%.
metrics

2 · Decision boundaries — the visual proof

Logistic can only draw one straight line (and fails). The Decision Tree draws jagged axis-aligned rectangles. Random Forest and Gradient Boosting arrive at smooth, near-circular boundaries that almost perfectly separate the four XOR blobs.
decision boundaries

3 · ROC curves on the test set

Logistic's ROC sits on the diagonal — its score function carries no information. Random Forest (AUC 0.997) and Gradient Boosting (0.995) nearly touch the top-left corner.
ROC curves

4 · Confusion matrices

Logistic's matrix is balanced-but-wrong. The other three drain the off-diagonals to nearly zero — the signature of a working classifier.
confusion matrices

5 · Feature importance — the noise rejection check

Synthetic data ships with two pure-noise features (x3_noise, x4_noise). All three tree-based models assign them near-zero importance, confirming they learn to ignore irrelevant features.
feature importance

Metric definitions

All metrics computed on the 375-row held-out test set

375 test rows the models never saw during fitting; class balance is 50/50 (stratified split), so random chance is ~0.50.
MetricDefinitionHow to read it for this problem
Accuracycorrect predictions / nFraction of test points classified correctly. With 50/50 balance, random chance is ~0.50. Logistic at 0.461 is below chance — mathematical impossibility, not noise.
F1 (binary)2·precision·recall / (precision+recall)Harmonic mean of precision and recall. With balanced classes, F1 closely tracks accuracy; diverges when a model systematically over-predicts one class.
ROC-AUCarea under the ROC curveScore of 0.5 means the model's probability score carries no information. Logistic at 0.452 confirms its scores are useless. AUC near 1.0 for ensembles means their predict_proba almost perfectly ranks class 1 above class 0.
Confusion matrixTN/FP/FN/TP cell countsVisualized as figure 4. A working classifier drains off-diagonals toward zero. Logistic shows near-uniform off-diagonals.

Full results

ModelTest AccuracyTest F1Test ROC-AUC
Logistic Reg.0.46130.46840.4524
Decision Tree (depth 6)0.86930.85460.9661
Random Forest (200 trees)0.98130.98130.9968
Gradient Boost (200 stages)0.97870.97870.9952
Determinism: a single seed = 42 drives blob sampling, noise draw, shuffling, and the train/test split. All four model constructors also receive random_state=42. The qualitative ranking (Logistic fails → single tree ~87% → ensembles ~98%) is robust across seeds; third-decimal values are seed-dependent.

Run it yourself

# 1. environment
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# 2. generate the synthetic XOR-shape dataset (deterministic)
python generate_data.py

# 3. fit, evaluate, render dashboard figures
python train.py
Produces the five PNGs in assets/ and the metric summary in results/metrics.json.

Tweak the difficulty

DataConfig(
    n_samples=1500,
    cluster_std=0.55,         # blob spread — higher = more class overlap
    n_noise_features=2,       # how many distractor features to add
    seed=42,
)
Try cluster_std=0.9 to see all models degrade gracefully, or n_noise_features=10 to watch the single decision tree start to choke even at depth 6.