ECON 8310: Business Forecasting — Lecture 5
University of Nebraska at Omaha
Averaging many imperfect models can outperform any single model.
A single expert can be wrong. Average many independent estimates and the errors tend to cancel.
Classic Example: Guessing the Ox’s Weight
At a 1906 county fair, 800 people guessed the weight of an ox. No individual was very accurate — but the average of all 800 guesses came within one pound of the true weight.
Models work the same way. One tree is unstable, Lecture 4’s closing complaint. Average many trees fitted to different versions of the data and the noise largely cancels while the signal survives.
Ensembles work because independent errors cancel. The more independent the trees, the more averaging removes: the difference between bagging and random forests turns on that word.
Train many trees on bootstrap samples; average their predictions.
Problem from Lecture 4: one tree has high variance. Bagging (Breiman 1996) averages it away:
Why it works, and where it stops. With \(\sigma^2\) one tree’s variance and \(\rho\) the correlation between two trees’ predictions,
\[\mathrm{Var}(\bar{f}) = \rho\sigma^2 + \tfrac{1-\rho}{B}\sigma^2 .\]
More trees shrink the second term; the first never moves.
Divide by \(\sigma^2\) and each \(\rho\) draws its own curve, flattening onto a floor at \(\rho\):
Growing more trees only slides you along a curve. The floor is set by how alike the trees are, not by how many you grow: to get below it, make them less alike.
A bootstrap sample holds about 63% of the distinct rows. Score each row with only the trees that never saw it (oob_score=True): no refitting. But look at who scores it:
Weekly CA_1 FOODS forest. Gray: in that tree’s bootstrap sample; white: out of bag.
On a time series it is not a forecast test. The trees that score a week still trained on about 63% of the weeks after it: OOB fills gaps. Use walk-forward CV.
Restrict each split to a random subset of features so trees are less alike.
If one feature dominates (dow daily, the four-week lag weekly), nearly every bagged tree splits on it first and \(\rho\) stays high. The random forest fix (Breiman 2001): at each split, consider only a random \(m < p\) of the features.
Lab 5 (daily CA_1 FOODS), 300 trees each. The fix spreads the roots, and its trees average better (417 to 254, against 398 to 248), but they start worse: with one dominant feature among ten, sqrt loses.
| Parameter | sklearn name | Default | Effect |
|---|---|---|---|
| Number of trees | n_estimators |
100 | More = lower variance; plateaus ~500 |
| Max features/split | max_features |
1.0 (all) |
Lower = more decorrelated |
| Max depth | max_depth |
None |
Limit only to reduce memory |
| Min samples/leaf | min_samples_leaf |
1 | Higher = smoother predictions |
| OOB score | oob_score |
False |
True gives a free CV estimate |
The default is plain bagging. scikit-learn’s regressor considers every feature at every split; 'sqrt' is the classifier’s default. So set max_features yourself: it is the dial for \(\rho\), and the only one that really needs tuning. Start with n_estimators=500, min_samples_leaf=5.
Set random_state=42: a forest is random twice over, in the bootstrap samples and in the feature subsets.
Which predictors matter most? RF gives a data-driven answer.
Mean Decrease in Impurity (MDI)
For each feature \(j\), sum the impurity reduction over every split on \(j\), weighted by the fraction of observations reaching that node. Average over trees; normalize to sum to 1.
| Feature (CA_1 FOODS, weekly) | Importance |
|---|---|
units_lag4 |
0.28 |
units_lag1 |
0.21 |
snap_days |
0.17 |
units_lag52 |
0.15 |
units_lag2 |
0.09 |
units_lag3 |
0.06 |
From the next slide’s forest; Lecture 4’s single tree gave the four-week lag 0.62. A screening device only: MDI is biased toward features with many split points. permutation_importance (shuffle a column, measure the damage) is the honest alternative.
sklearn Code Template
| Model (CA_1 FOODS, 52-week test) | Test RMSE |
|---|---|
| Seasonal naive | 1,660 |
| Single deep tree | 1,456 |
| Random Forest | 1,115 |
The forest cuts RMSE by 23% against a single deep tree. Both beat seasonal naive, but they forecast only one week ahead; Lectures 1–2 forecast a whole year.
One week ahead, SES and ARIMA beat 1,660 too, and a forest on the series’ lags alone scores 1,231: most of the gain is last week’s actual sales, not price or SNAP.
Instead of averaging independent trees, fit each new tree to the errors of all previous trees.
Bagging (Random Forest):
Boosting:
The intuition. Forecast 1 says $50,000 against an actual $60,000; forecast 2 predicts the error, +$10,000, and the sum is right. Averaging never fixes a bias like that.
Gradient Boosting (squared-error loss)
Initialize \(F_0(\mathbf{x}) = \bar{y}\). For \(b = 1, \ldots, B\):
Final prediction: \(\hat{y} = F_B(\mathbf{x})\)
Trees are shallow (depth 3 to 6): each captures a little of what remains.
The learning rate \(\eta\) is the main dial: 0.01–0.1 is slow and robust, above 0.3 risks overshooting. Use a small \(\eta\), many trees and early stopping, or boosting fits your noise.
The algorithm on one feature, so the fit can be drawn: weekday electricity load against day of the year (Lecture 3’s data). Depth-3 trees, \(\eta = 0.3\), starting from the mean (dashed):
Each tree fits what the curve still misses, so the year’s W emerges a piece at a time. By 50 trees the training RMSE is 2.89 GW, against 2.86 for the best any day-of-year curve can do: what remains is noise this feature cannot explain.
Three improvements over vanilla boosting: second-order optimization, explicit regularization, and column subsampling.
reg_lambda is Ridge and reg_alpha is LASSO, on leaf weights (next week). gamma is a price per leaf, like Lecture 4’s ccp_alpha.
XGBoost dominated Kaggle tabular competitions in 2014–2018.
| Parameter | XGB name | Typical | Effect |
|---|---|---|---|
| Number of trees | n_estimators |
500–2000 | More + small \(\eta\) = better (use early stopping) |
| Learning rate | learning_rate |
0.01–0.1 | Smaller = more robust; needs more trees |
| Tree depth | max_depth |
3–6 | Shallow preferred; depth >6 risks overfit |
| Row subsample | subsample |
0.7–0.9 | Fraction of training rows per tree |
| Col. subsample | colsample_bytree |
0.7–0.9 | Feature fraction per tree (like RF) |
| L2 leaf penalty | reg_lambda |
1 | Ridge on leaf weights; stabilizes |
Practical starting point: learning_rate=0.05, max_depth=4, n_estimators=1000 with early stopping on validation RMSE. Tune max_depth and subsample via TimeSeriesSplit.
Code Template
eval_set is what makes early_stopping_rounds work: XGBoost watches validation RMSE and halts when it stops improving, which is how you get away with setting n_estimators far higher than you need.
The validation set must respect time. Build it with TimeSeriesSplit and hold out the final stretch as a true test set.
On macOS xgboost also needs OpenMP: brew install libomp, or the import fails.
Bagging reduces variance; boosting reduces bias. Together they cover most of what tabular forecasting needs.
| Method | Training | Interpretable | Tuning | Accuracy |
|---|---|---|---|---|
| Decision Tree | Single tree | High | Low | Baseline |
| Random Forest | Parallel (\(B\) trees) | Medium | Low | Good |
| XGBoost | Sequential (\(B\) trees) | Low | Higher | Best |
Random Forest is the better default: robust, almost no tuning, OOB error for free. Move to XGBoost when you can afford to tune and need the last few points — and budget for that tuning, because an untuned XGBoost often loses to a default forest.
A forest’s prediction is a weighted average of training targets, so it can never leave [training min, training max]. A forest trained on US retail sales through 2019, then given each later month’s true lags:
It never forecasts above $450bn; by mid-2026 sales are $665bn. Boosted trees go flat beyond their last split too. On a trending series, keep a model that can extrapolate.
TimeSeriesSplit; pair boosting with early stopping.Next: Regularization & Model Selection.
ECON 8310 | University of Nebraska at Omaha