~/blog
Decision Tree Regression
You have a house. You know its square footage. How much is it worth? You could draw a straight line through the data — that's linear regression, and it works well when the relationship is smooth. But what if the housing market has sharp breakpoints? Houses under 1000 sq ft all sell in a narrow range, then jump at 1200 sq ft because of a zoning change.
A regression tree handles this naturally — it splits the data into regions and predicts the average price within each region, creating a staircase instead of a line.
Anchor dataset: 6 houses with sq_ft and price — the same anchor from the linear regression section.
import numpy as np
X = np.array([650, 850, 1100, 1400, 1600, 1900]).reshape(-1, 1)
y = np.array([180, 220, 280, 340, 370, 430])
# True OLS: ŷ = 53.33 + 0.20×sq_ft (from linear regression section)What It Is
A regression tree is the same algorithm as a classification tree, but it predicts numbers instead of categories. Instead of measuring impurity with entropy or Gini, it measures how spread out the target values are using variance. At each leaf, instead of a majority vote, it predicts the average of the samples that reached it. This is not the same as linear regression — the tree doesn't assume the relationship is a straight line. It produces a piecewise constant prediction that can handle sharp jumps and nonlinear patterns.
The algorithm skeleton — threshold search, stopping conditions, pruning — is identical to classification trees.
| Aspect | Classification Tree | Regression Tree |
|---|---|---|
| Impurity measure | Entropy / Gini | Variance (MSE) |
| Leaf prediction | Majority class | Mean of leaf samples |
| Split criterion | Maximize IG or Gini gain | Maximize variance reduction |
| Output type | Discrete class | Continuous value |
The Plan — Three Levels to a Regression Tree
We'll walk through how a regression tree builds its staircase. First we compute variance at the root, then find the best split for sq_ft, then build two more levels, and finally compare against linear regression.
Level 1: Compute Root Variance
Here's the goal: measure how spread out house prices are at the root node. The variance is the squared error of predicting the mean for every sample:
At the root:
The root variance is 7422.3. If we predict $303.3k for every house, our average squared error is this number.
✓ Level 1 complete. Root variance = 7422.3.
Threshold Search for sq_ft
Split criterion for regression:
5 midpoint candidates: 750, 975, 1250, 1500, 1750.
| Left | Left | Left Var | Right | Right | Right Var | Weighted Var | ||
|---|---|---|---|---|---|---|---|---|
| 750 | [180] | 180 | 0 | [220,280,340,370,430] | 328 | 5424 | ||
| 975 | [180,220] | 200 | 400 | [280,340,370,430] | 355 | 3550 | ||
| 1250 | [180,220,280] | 226.7 | 1555 | [340,370,430] | 380 | 1133 | (3/6)(1555)+(3/6)(1133)=1344 | |
| 1500 | [180,220,280,340] | 255 | 3350 | [370,430] | 400 | 900 | ||
| 1750 | [180,220,280,340,370] | 278 | 4544 | [430] | 430 | 0 |
Best split: with . The split at sq_ft = 1250 reduces the total variance from 7422 to 1344 — an 82% reduction.
✓ Level 2 complete. Best threshold found: sq_ft ≤ 1250 with IG_reg = 6078.
Level 3: Split Left Child (sq_ft ≤ 1250, samples [650, 850, 1100])
Goal: the left child is still impure (Var=1555). Can we split further?
, , .
Test thresholds and :
- : Left=[180], Var=0; Right=[220,280], , Var=900. Weighted: . .
- : Left=[180,220], , Var=400; Right=[280], Var=0. Weighted: . .
Best: . Splits into:
- Left-Left (sq_ft ≤ 975): [650, 850] → — predict $200k
- Left-Right (975 < sq_ft ≤ 1250): [1100] → — predict $280k
✓ Left child complete. Split at t=975 with IG_reg=1288.
Level 2: Right Child (sq_ft > 1250, samples [1400, 1600, 1900])
, , .
Test thresholds and :
- : Left=[340], Var=0; Right=[370,430], , Var=900. Weighted: . .
- : Left=[340,370], , Var=225; Right=[430], Var=0. Weighted: . .
Best: . Splits into:
- Right-Left (1250 < sq_ft ≤ 1750): [1400, 1600] → — predict $355k
- Right-Right (sq_ft > 1750): [1900] → — predict $430k
✓ Right child complete. Split at t=1750 with IG_reg=983.
The 4-Leaf Staircase
| Leaf | Condition | Samples | |
|---|---|---|---|
| 1 | sq_ft ≤ 975 | 650, 850 | $200k |
| 2 | sq_ft | 1100 | $280k |
| 3 | sq_ft | 1400, 1600 | $355k |
| 4 | sq_ft | 1900 | $430k |
The orange staircase shows tree predictions: constant within each leaf region. The blue dashed line is the linear regression fit. For this near-linear dataset, the linear model tracks the data better; the tree's staircase has visible errors at the leaf boundaries.
Predictions vs Actual — Tree vs Linear
| sq_ft | Tree | Linear | Tree error | Linear error | |
|---|---|---|---|---|---|
| 650 | 180 | 200 | 183 | 20 | 3 |
| 850 | 220 | 200 | 223 | 20 | 3 |
| 1100 | 280 | 280 | 273 | 0 | 7 |
| 1400 | 340 | 355 | 333 | 15 | 7 |
| 1600 | 370 | 355 | 373 | 15 | 3 |
| 1900 | 430 | 430 | 433 | 0 | 3 |
Linear regression wins decisively on this near-linear dataset. The tree is limited by its piecewise-constant prediction: leaves 1 and 4 each contain 2 samples whose true values differ, forcing the average to miss both.
Trace Table: Regression Tree Construction
| Level | Node | Threshold | Variance Reduction | |
|---|---|---|---|---|
| 1 | Root | — | — | Root Var = 7422.3 |
| 2 | Root → split sq_ft | 6078 | 7422 → 1344 (82%) | |
| 3 | Left child (sq_ft ≤ 1250) | 1288 | 1555 → 267 | |
| 3 | Right child (sq_ft > 1250) | 983 | 1133 → 150 | |
| — | Leaf 1: sq_ft ≤ 975 | — | — | |
| — | Leaf 2: 975 < sq_ft ≤ 1250 | — | — | |
| — | Leaf 3: 1250 < sq_ft ≤ 1750 | — | — | |
| — | Leaf 4: sq_ft > 1750 | — | — |
✓ Tree complete. 4-leaf staircase with Tree MSE = 208.3 vs Linear MSE = 22.3.
sklearn Implementation
Let's verify with sklearn. We use DecisionTreeRegressor instead of DecisionTreeClassifier. We set criterion='squared_error' to use MSE as the split measure (this is the default, but we specify it explicitly). max_depth=2 matches our manual construction. random_state=42 ensures reproducible results.
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
dt_reg = DecisionTreeRegressor(criterion='squared_error', max_depth=2, random_state=42)
dt_reg.fit(X, y)
y_pred = dt_reg.predict(X)
print(f"Tree MSE: {mean_squared_error(y, y_pred):.2f}")
print(f"Tree R²: {dt_reg.score(X, y):.4f}")
print(f"Predictions: {y_pred}")
print(f"Unique leaf predictions: {np.unique(y_pred)}")Tree MSE: 208.33
Tree R²: 0.9720
Predictions: [200. 200. 280. 355. 355. 430.]
Unique leaf predictions: [200. 280. 355. 430.]The 4 unique prediction values are the mean of each leaf: [200, 280, 355, 430]. Despite high R²=0.972, the MSE of 208 is 9× worse than the linear model's MSE of 22.
max_depth Effect on Regression
print(f"{'depth':>8} {'MSE':>10} {'steps':>8} {'leaves':>8}")
for d in [1, 2, 3, None]:
dt = DecisionTreeRegressor(max_depth=d, random_state=42)
dt.fit(X, y)
y_p = dt.predict(X)
mse = mean_squared_error(y, y_p)
steps = len(np.unique(y_p))
print(f"{str(d):>8} {mse:>10.2f} {steps:>8} {dt.get_n_leaves():>8}")depth MSE steps leaves
1 3041.67 2 2
2 208.33 4 4
3 22.22 6 6 ← one leaf per sample
None 0.00 6 6 ← memorizes training setAt depth=3 with 6 samples and 6 leaves: each sample has its own leaf, MSE=22.22 (interpolation errors from single-sample leaves). At depth=None: MSE=0 (perfect memorization). Neither of these generalizes.
When Does a Regression Tree Beat Linear Regression?
The staircase is piecewise constant — it assumes the target is flat within each region. Linear regression assumes a global linear trend. Trees win when:
- The true relationship has sharp breakpoints (e.g., a salary cap at a specific experience level)
- The relationship is nonlinear with different slopes in different regions
- There are strong feature interactions (the effect of feature A depends on feature B)
Linear regression wins when the true relationship is approximately linear (as here).
Related Concepts
Regression trees extend the classification tree (posts 01–04) by replacing entropy with variance reduction — the algorithmic skeleton is identical. This is the direct foundation for gradient boosting: each boosting round fits a regression tree on the residuals from the previous round, and the leaf prediction at each step is the mean of those residuals. Understanding how a regression tree predicts the mean within a leaf region directly explains why the first gradient boosting iteration predicts the global mean of .
Honest Limitations
Here's the thing about regression trees that I've learned the hard way: they predict a constant within each leaf. That means approximating even a simple linear trend requires many splits — each staircase step is a leaf. On this 6-sample dataset, with max_depth=2, the tree already makes visible errors at leaf boundaries, while a two-parameter linear model fits perfectly. For smooth relationships, a regression tree needs leaves to match linear regression accuracy — and at that point you're basically memorizing.
I've also been caught by the extrapolation problem. At max_depth=None, the tree achieves MSE=0 by assigning every sample its own leaf. But for a query point outside the training range, it just predicts the value of the nearest training point — there's no extrapolation. Linear regression at least extends the fitted line. Neither is correct by default; I now choose based on whether I expect the relationship to continue beyond the training range.
Test Your Understanding
-
At the root, the best threshold was with . The threshold produced . The left node of is pure (1 sample, Var=0), yet its IG is lower. Why does a single-sample pure left node not maximize variance reduction?
-
Leaf 1 (sq_ft ≤ 975) predicts 180k) and 850 sq_ft (true \pm$20k for both. What would the prediction be if you used the median instead of the mean? Would this reduce or increase MSE on this leaf?
-
At depth=3, MSE=22.22 with 6 leaves for 6 samples. Is this the irreducible error of the model, or could a depth=4 tree (if allowed) reduce it further? What would the depth=4 tree's MSE be?
-
The tree with
max_depth=Noneachieves MSE=0 on training data. If you added a 7th house with sq_ft=1100 and price=280k), what would the depth=None tree predict for sq_ft=1100? How does the presence of two training samples at the same sq_ft affect the tree? -
DecisionTreeRegressoruses MSE by default (criterion='squared_error'). An alternative is MAE (criterion='absolute_error'). The MAE-optimal leaf prediction is the median instead of the mean. For leaf 1 with : the mean is 200 and the median is also 200 (average of two). For a leaf with (one outlier): what is the mean vs median, and which leaf prediction minimizes MAE?