Forecasting Models
Split time series without cheating, beat naive baselines with lag features, and meet Holt-Winters and SARIMA — evaluated honestly with MAE and MAPE.
You can now take a series apart; this lesson is about predicting where it goes next. We'll start with the single most common mistake in forecasting (shuffled splits), establish baselines that are embarrassingly hard to beat, turn forecasting into a regression problem scikit-learn can solve, and then meet the classical specialists: exponential smoothing and ARIMA.
Never shuffle a time series
train_test_split shuffles by default, and on time series that's not a
small mistake — it's data leakage. A shuffled split trains on 1959 and
tests on 1955: the model has literally seen the future, autocorrelation
hands it the answers, and your test score becomes fiction. The honest split
is temporal: train on the past, test on the most recent stretch, because
that's exactly the situation the deployed model will face.
For cross-validation, scikit-learn's TimeSeriesSplit respects time: every
fold trains on an expanding window of the past and tests on the block right
after it.
Each fold answers the question a stakeholder actually asks: "if I had built this model a year ago, how would it have done?" Sliding this scheme forward one step at a time — refit, predict the next point, repeat — is called walk-forward validation, the gold standard when you can afford the compute.
Baselines first: naive and seasonal-naive
Before any model, establish the score to beat. Two forecasting baselines are so strong they're humbling:
- Naive — tomorrow equals today. The forecast is a flat line at the last observed value.
- Seasonal naive — this July equals last July. The forecast repeats the final observed seasonal cycle.
We'll judge them with two metrics: MAE (mean absolute error — average miss, in the series' own units) and MAPE (mean absolute percentage error — average miss as a percentage, comparable across series).
The flat naive line is hopeless on seasonal data, but the seasonal naive tracks the shape well — its only sin is missing the trend. Any model you build must beat that dashed line, or it isn't earning its complexity.
Forecasting as regression: lag features
Here's the trick that connects this module to everything you've learned:
turn the series into a supervised table. Each row's features are its own
past — lag_1 (last month), lag_12 (same month last year) — plus calendar
features like the month number. The target is the current value. Then any
regressor you already know can forecast.
The regression crushes both baselines — the lags carry the level and the
season, the month dummies mop up the rest. One honest caveat: these are
one-step-ahead predictions, because each test row's lags come from
actual observed values. To forecast 24 months into the unknown future
you'd predict one step, feed that prediction back in as lag_1, and repeat
— recursive forecasting — which lets errors compound as the horizon
grows. This lag-feature recipe is exactly how gradient-boosting models win
most forecasting competitions today; swap LinearRegression for
HistGradientBoostingRegressor and you have a genuinely modern pipeline.
Exponential smoothing and Holt-Winters
The classical specialists take a different route: instead of a feature table, they maintain running estimates of the components and update them with each observation. Simple exponential smoothing tracks the level as a weighted average that decays exponentially into the past — good for series with no trend or season. Holt's method adds a second equation tracking the trend, and Holt-Winters adds a third for seasonality — level, trend, and season, each smoothed with its own parameter. Fit it in statsmodels (notebook/Colab — not the browser):
import pandas as pd
from sklearn.metrics import mean_absolute_error
from statsmodels.tsa.holtwinters import ExponentialSmoothing
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
y = pd.read_csv(url, parse_dates=["Month"], index_col="Month")["Passengers"]
train, test = y[:-24], y[-24:]
hw = ExponentialSmoothing(
train, trend="add", seasonal="mul", seasonal_periods=12
).fit()
forecast = hw.forecast(24)
print(f"Holt-Winters MAE on the 24-month holdout: "
f"{mean_absolute_error(test, forecast):.1f}")
ax = y.plot(figsize=(10, 4), label="actual")
forecast.plot(ax=ax, style="--", label="Holt-Winters forecast")
ax.legend()Note seasonal="mul" — the airline series is multiplicative, as its
fanning peaks told us last lesson. Unlike the lag-regression above, this is
a true 24-step-ahead forecast made from training data alone, and
Holt-Winters remains a ferociously strong benchmark for seasonal business
data.
ARIMA and SARIMA, honestly
ARIMA(p, d, q) models a series as a linear function of its own past:
p autoregressive terms (past values), d rounds of differencing to reach
stationarity, and q moving-average terms (past forecast errors). Plain
ARIMA has no notion of seasonality, so in practice you use SARIMA, which
adds a seasonal quadruple (P, D, Q, s). The honest summary: ARIMA is
statistically elegant, gives principled confidence intervals, and rewards
expertise — analysts read ACF/PACF plots and compare AIC scores to choose
orders — but it's fiddly to tune, assumes linear dynamics, and on plenty of
real data a seasonal-naive baseline or a lag-feature gradient booster
matches it. Treat it as one candidate to evaluate, not a destination.
import pandas as pd
from sklearn.metrics import mean_absolute_error
from statsmodels.tsa.statespace.sarimax import SARIMAX
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
y = pd.read_csv(url, parse_dates=["Month"], index_col="Month")["Passengers"]
train, test = y[:-24], y[-24:]
sarima = SARIMAX(train, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12)).fit(disp=False)
forecast = sarima.forecast(24)
print(f"SARIMA MAE on the 24-month holdout: {mean_absolute_error(test, forecast):.1f}")
print(f"AIC: {sarima.aic:.1f}")Choosing between all of these
Evaluate every candidate — baselines, Holt-Winters, SARIMA, lag-feature regression — with the same temporal holdout and the same MAE/MAPE, and let the numbers decide. On short seasonal business series, Holt-Winters and SARIMA are hard to beat; with many related series or rich extra features (prices, promotions, weather), the ML route usually wins.
Check your understanding
Q1.Why does a shuffled train/test split inflate a forecasting model's test score?
Q2.What is the seasonal-naive forecast for July 2024 on monthly data?
Q3.In the lag-feature regression, why were the test-set results 'one-step-ahead' rather than a true 24-month forecast?
Q4.Which component does Holt-Winters add on top of Holt's double exponential smoothing?
Q5.In ARIMA(p, d, q), what does the d stand for?
Exercise: Beat the linear forecaster
Extend the lag-feature model from the lesson: add lag_3 and a 3-month
rolling-mean feature (built from shift(1) so it never touches the current
value — why does that matter?), and try Ridge alongside
LinearRegression. Keep the same last-24-months holdout and compare MAE
against the lesson's model. How much did the extra features buy you?
Next up: a new module — recommender systems, where the "past behavior" being modeled isn't your own history but everyone else's.