Skip to content
Machine Learning
Time Series 10 min read

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.

Download notebook Open Google ColabIn Colab: File → Upload notebook → pick the downloaded file.

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.

Python — runs in your browser

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).

Python — runs in your browser

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.

Python — runs in your browser

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

5 questions · free
  1. Q1.Why does a shuffled train/test split inflate a forecasting model's test score?

  2. Q2.What is the seasonal-naive forecast for July 2024 on monthly data?

  3. Q3.In the lag-feature regression, why were the test-set results 'one-step-ahead' rather than a true 24-month forecast?

  4. Q4.Which component does Holt-Winters add on top of Holt's double exponential smoothing?

  5. 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.