A decision tree is a model that predicts a target by recursively splitting the feature space into axis-aligned regions, choosing at each node the feature and threshold that best separate the data according to an impurity criterion.
Recursive Partitioning
Starting from the root with all training examples, the tree greedily picks the split that most reduces impurity, sends examples left or right based on a test like , and recurses on each child. Prediction for a new point follows the tests down to a leaf, whose stored value (majority class or mean target) is the output.
Splitting Criteria
For classification, two common impurity measures on a node with class proportions are:
A split with subsets is scored by the information gain (impurity decrease):
Gini vs entropy
Both are minimized at a pure node and maximized at a uniform mix. Gini is cheaper (no logarithm) and is the sklearn default; entropy penalizes impurity slightly more aggressively. In practice they usually produce very similar trees.
Stopping and Pruning
A fully grown tree can memorize the training set (one leaf per point), giving zero training error but poor generalization. Controls:
- Pre-pruning (early stopping): limit
max_depth, requiremin_samples_splitormin_samples_leaf, or demand a minimum impurity decrease. - Post-pruning: grow fully, then collapse nodes that do not help a validation metric (cost-complexity pruning trades tree size against error via a penalty ).
Overfitting
Deep unpruned trees have very high variance: small data changes can reshape the whole tree. This is exactly what motivates averaging many trees (see Random Forest) to trade variance for a little bias.
Regression Trees
For continuous targets, replace impurity with variance (or sum of squared error) within a node, and let each leaf predict the mean target of its examples:
Code: From-Scratch Gini Tree
import numpy as np
def gini(y):
_, counts = np.unique(y, return_counts=True)
p = counts / counts.sum()
return 1.0 - np.sum(p ** 2)
def best_split(X, y):
n, d = X.shape
best = {"gain": 0.0, "feat": None, "thr": None}
parent = gini(y)
for j in range(d):
for thr in np.unique(X[:, j]):
left = X[:, j] <= thr
if left.sum() == 0 or left.sum() == n:
continue
wl, wr = left.mean(), 1 - left.mean()
child = wl * gini(y[left]) + wr * gini(y[~left])
gain = parent - child
if gain > best["gain"]:
best = {"gain": gain, "feat": j, "thr": thr}
return best
def build_tree(X, y, depth=0, max_depth=5, min_samples=2):
# leaf conditions
if len(y) < min_samples or depth >= max_depth or len(np.unique(y)) == 1:
vals, counts = np.unique(y, return_counts=True)
return {"leaf": vals[np.argmax(counts)]}
s = best_split(X, y)
if s["feat"] is None:
vals, counts = np.unique(y, return_counts=True)
return {"leaf": vals[np.argmax(counts)]}
mask = X[:, s["feat"]] <= s["thr"]
return {
"feat": s["feat"], "thr": s["thr"],
"left": build_tree(X[mask], y[mask], depth + 1, max_depth, min_samples),
"right": build_tree(X[~mask], y[~mask], depth + 1, max_depth, min_samples),
}
def predict_one(node, x):
if "leaf" in node:
return node["leaf"]
branch = node["left"] if x[node["feat"]] <= node["thr"] else node["right"]
return predict_one(branch, x)In practice use sklearn
from sklearn.tree import DecisionTreeClassifier; clf = DecisionTreeClassifier(criterion="gini", max_depth=5).fit(X, y). It handles efficient split search, regression viaDecisionTreeRegressor, and cost-complexity pruning through theccp_alphaargument.
Related
Decision trees are the base learner behind Random Forest and other Ensemble Learning methods, which exist precisely to tame a single tree’s high-variance position on the Bias-Variance Tradeoff.