iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Decision Trees

A decision tree splits data on feature thresholds to maximise a purity metric (Gini / entropy). Interpretable; handles mixed feature types; prone to overfitting — the building block of Random Forest and GBDT.

Train, visualise, prune, interpret

EXAMPLE
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, load_breast_cancer, fetch_california_housing
from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, plot_tree, export_text
from sklearn.metrics import classification_report, mean_absolute_error

# === Classification ===
data = load_iris()
X_train, X_test, y_train, y_test = train_test_split(data.data, data.target, stratify=data.target, random_state=42)

clf = DecisionTreeClassifier(
    criterion='gini',          # 'gini' or 'entropy' or 'log_loss'
    max_depth=4,                # PRUNE — single biggest knob
    min_samples_split=20,
    min_samples_leaf=10,
    random_state=42,
)
clf.fit(X_train, y_train)
print(classification_report(y_test, clf.predict(X_test), target_names=data.target_names))

# Probabilities
clf.predict_proba(X_test[:5])

# === Regression ===
hx, hy = fetch_california_housing(return_X_y=True)
rx_tr, rx_te, ry_tr, ry_te = train_test_split(hx, hy, test_size=0.2, random_state=42)

reg = DecisionTreeRegressor(
    max_depth=8,
    min_samples_leaf=20,
    random_state=42,
).fit(rx_tr, ry_tr)
print('MAE:', mean_absolute_error(ry_te, reg.predict(rx_te)))

# === Visualise ===
fig, ax = plt.subplots(figsize=(20, 12))
plot_tree(
    clf,
    feature_names=data.feature_names,
    class_names=data.target_names,
    filled=True,
    rounded=True,
    ax=ax,
)
plt.savefig('tree.png', dpi=100)

# Text export
print(export_text(clf, feature_names=data.feature_names))
# --- example output ---
# |--- petal length (cm) <= 2.45
# |   |--- class: setosa
# |--- petal length (cm) >  2.45
# |   |--- petal length (cm) <= 4.85
# ...

# === Feature importance ===
import pandas as pd
importances = pd.Series(clf.feature_importances_, index=data.feature_names).sort_values()
print(importances)
importances.plot.barh()

# Permutation importance — more robust
from sklearn.inspection import permutation_importance
pi = permutation_importance(clf, X_test, y_test, n_repeats=10, random_state=42)

# === Hyperparameters ===
# criterion           : split quality measure ('gini'|'entropy'|'log_loss')
# max_depth           : maximum depth of the tree (None = unlimited)
# min_samples_split   : min samples needed to split (default 2; raise to 10-20 for noisy data)
# min_samples_leaf    : min samples in a leaf (raise to smooth predictions)
# max_features        : how many features to consider per split (None = all)
# class_weight        : 'balanced' for imbalanced classification
# ccp_alpha           : cost-complexity pruning (post-pruning)
# max_leaf_nodes      : alternative to max_depth — limit total leaves

# === Pruning ===

# Pre-pruning (during fit) — limit max_depth, min_samples_leaf, etc.

# Post-pruning — cost complexity
path = clf.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas = path.ccp_alphas

# Train trees at each alpha
trees = [DecisionTreeClassifier(random_state=42, ccp_alpha=alpha).fit(X_train, y_train) for alpha in ccp_alphas]

# Pick alpha by validation score
best_alpha = ccp_alphas[np.argmax([t.score(X_test, y_test) for t in trees])]
final = DecisionTreeClassifier(random_state=42, ccp_alpha=best_alpha).fit(X_train, y_train)

# === Grid search ===
param_grid = {
    'max_depth':         [3, 5, 7, 10, None],
    'min_samples_leaf':  [1, 5, 10, 20],
    'criterion':         ['gini', 'entropy'],
}
grid = GridSearchCV(DecisionTreeClassifier(random_state=42), param_grid, cv=5, n_jobs=-1)
grid.fit(X_train, y_train)
print(grid.best_params_, grid.best_score_)

# === Imbalanced classes ===
clf_balanced = DecisionTreeClassifier(class_weight='balanced', max_depth=6, random_state=42)

# Or class_weight=dict — manual weights
clf_balanced2 = DecisionTreeClassifier(class_weight={0: 1, 1: 10})

# === Decision path — explain a single prediction ===
sample = X_test[0:1]
path = clf.decision_path(sample)
feature = clf.tree_.feature
threshold = clf.tree_.threshold

# Walk the tree manually for explanation
node_indicator = path.toarray()[0]
for node_id in range(len(node_indicator)):
    if node_indicator[node_id]:
        if feature[node_id] >= 0:
            print(f'Node {node_id}: feature[{data.feature_names[feature[node_id]]}] '
                  f'{"<=" if sample[0, feature[node_id]] <= threshold[node_id] else ">"} {threshold[node_id]:.2f}')

# === Why trees can overfit ===
# A tree can keep splitting until every leaf has 1 sample → perfect train accuracy, bad test.
# Pruning + ensemble methods (RF, GBDT) address this.

# === Pros + cons ===
# Pros:
#   • Interpretable (visualisable, rule-extractable)
#   • No scaling required
#   • Handles missing values (via surrogate splits — sklearn doesn't support directly)
#   • Mixed types (numeric + categorical via encoding)
#   • Captures non-linear boundaries + feature interactions
#
# Cons:
#   • High variance (small data change → very different tree)
#   • Greedy splits → not globally optimal
#   • Prone to overfit without pruning
#   • Doesn't extrapolate well for regression

# === When to use vs alternatives ===
# Decision tree single  : interpretable baseline, audit-friendly, regulated industries
# Random Forest         : strong out-of-the-box accuracy, less interpretable
# Gradient Boosting      : highest accuracy, more tuning
# Linear models          : when relationships are roughly linear + interpretable coefficients matter

# === sklearn API patterns ===
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

numeric_cols = ['age', 'salary']
categorical_cols = ['city', 'role']

preprocessor = ColumnTransformer([
    ('num', 'passthrough', numeric_cols),                            # trees don't need scaling
    ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols),
])

pipe = Pipeline([
    ('prep', preprocessor),
    ('clf',  DecisionTreeClassifier(max_depth=6, random_state=42)),
])
pipe.fit(X_train, y_train)

# === Save / load ===
import joblib
joblib.dump(clf, 'tree.pkl')
loaded = joblib.load('tree.pkl')

# === Tips ===
#   • Always start with max_depth=5-10; deeper = noisier
#   • Use class_weight='balanced' for imbalanced data
#   • For regression, set min_samples_leaf to prevent overfitting to outliers
#   • Plot the tree for small max_depth (<=4) for review with stakeholders
#   • For production accuracy, switch to Random Forest / XGBoost / LightGBM

Why it matters

A single decision tree is the most interpretable model in classical ML — great for explaining a prediction, weak on accuracy. For production, ensembles (Random Forest, GBDT) handle the variance problem and usually win.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(max_depth=5, min_samples_leaf=20).fit(Xtr, ytr)
Try it Yourself »

Discussion

Loading…