learn-timeseries-forecasting-with-phoebe / Builder session 4 of 10
Learn Timeseries Forecasting with Phoebe · Builder track · Session 4 of 10

ARIMA & SARIMA: the Box-Jenkins method

ARIMA is the classical workhorse of forecasting: model a series by its own past values and past errors, once you have made it stationary. This session builds the Box-Jenkins method in real Python on the Lumen series - test for stationarity, difference to get it, read the orders off the ACF and PACF, then fit a seasonal SARIMA for the weekly pattern and let auto_arima search. ARIMA shines on stable, structured series, and it asks for more care than exponential smoothing - so we treat it with the discipline it needs.

🟡 Builder track Practitioners: analysts · data scientists · engineers Python 3.10+ · a notebook ~45 min
0-3 · Welcome 3-18 · Stationarity + orders 18-42 · Build-along: ADF + SARIMA 42-45 · Q&A
Part 0

Model the series by its own past

Exponential smoothing (b3) modelled the series through weighted averages of recent values. ARIMA takes a different route from the same idea: forecast each point from a linear mix of its own recent values (the AR part) and its own recent errors (the MA part), after differencing the series into something stable. Add a seasonal copy of the same machinery and you have SARIMA - the method that carries the weekly Lumen rhythm. It is powerful and it is fiddly, so Box and Jenkins gave us a disciplined recipe: make it stationary, read the orders, fit, then check the residuals.

Live - presented in session Self-study - read after class ★ Run it now (Python) Sources covered (cited)
★ What you walk out with today A stationarity check with the ADF test, a differenced Lumen series, ACF and PACF plots you can read for orders, and a fitted SARIMA(p,d,q)(P,D,Q,7) that forecasts H days ahead with a confidence interval - scored against the seasonal-naive baseline from b1 so you know whether it earned its complexity.
Part 1 · covers Box-Jenkins & FPP3 (stationarity)

Stationarity, and why differencing gets you there 7 min live

ARIMA assumes the series is stationary - its mean, variance, and autocorrelation do not drift over time. A trending or seasonal series is not stationary, so we difference it: replace each value with the change from the previous one. First differencing removes a trend; seasonal differencing (subtract the value 7 days ago) removes the weekly pattern. The "I" in ARIMA is that integration order, d.

Not stationary - the level drifts mean climbs with the trend + weekly wiggle Stationary - after differencing flat mean changes wobble around zero - stable to model The ADF test checks it numerically: a small p-value ( < 0.05 ) says stationary. Difference until it passes. First difference removes a trend (d); seasonal difference at lag 7 removes the weekly pattern (D).
🔍 Click to zoom - differencing turns a drifting series into a stationary one
LiveAR, I, and MA in plain terms3 min▶

ARIMA(p, d, q) is three ideas bolted together:

  • AR(p) - autoregression. Predict today from the last p values. "Busy days tend to follow busy days" is an AR effect.
  • I(d) - integration. The number of times you difference to reach stationarity. Usually 0 or 1; 2 at most.
  • MA(q) - moving average. Predict today from the last q forecast errors. It lets the model correct after a shock.
Do not confuse MA(q) with a rolling average The "moving average" in ARIMA is a regression on past errors, not a smoothed line over past values. Same words, different idea - a classic source of confusion when you first meet Box-Jenkins.
Self-studyReading ACF and PACF for orders2 min read▶

Two correlograms suggest the orders on the differenced series:

  • ACF (autocorrelation) - correlation of the series with its own lags. A sharp cut-off after lag q hints at an MA(q).
  • PACF (partial autocorrelation) - the same, with intermediate lags removed. A sharp cut-off after lag p hints at an AR(p).
  • A spike at lag 7 (and multiples) in either plot flags the seasonal orders P and Q.

These are guides, not gospel. In practice you shortlist a few orders from the plots and let an information criterion (AIC) or auto_arima pick between them.

Part 2 · covers Box-Jenkins seasonal models

SARIMA - a seasonal copy of the same machinery 6 min live

SARIMA adds a second set of orders that operate at the seasonal lag: (P, D, Q) with period m. For Lumen, m=7. So a SARIMA(p,d,q)(P,D,Q,7) has a non-seasonal part that handles day-to-day structure and a seasonal part that handles the week-to-week pattern - one model, two rhythms.

Non-seasonal part (p, d, q) AR(p)past values I(d)differencing MA(q)past errors Seasonal part (P, D, Q) at period m = 7 AR(P)weekly values I(D)seasonal diff MA(Q)weekly errors SARIMA (p, d, q)(P, D, Q, 7) one model, two rhythms auto_arima searches the orders by AIC so you do not hand-pick every value from the plots. ARIMA shines on stable, structured series. It needs more care than ETS - always check the residuals afterwards.
🔍 Click to zoom - non-seasonal (p,d,q) and seasonal (P,D,Q,7) orders feed one SARIMA
LiveLet auto_arima search, but keep judgement3 min▶

auto_arima (pmdarima) walks the order space and picks the combination with the best AIC, so you are not guessing every value by eye. Use it well:

  • Fix what you know. Pass m=7 for the weekly period and let it search seasonal orders around it.
  • Bound the differencing. Set sensible d and D (0 or 1) from the ADF test, rather than letting the search wander.
  • Always check residuals. A good fit leaves white-noise residuals. If the ACF of residuals still has spikes, the orders are wrong - AIC alone will not tell you that.
Real world

ARIMA vs ETS is a judgement call. On stable, structured series with clear autocorrelation, ARIMA can edge out exponential smoothing - but it asks for stationarity, order selection, and residual checks that ETS does not. FPP3's guidance: try both, backtest them, and keep whichever wins on your real horizon. Neither gets to skip the seasonal-naive bar.

Demo 1 of 2

Test stationarity, difference, read ACF/PACF ★ 12 min · everyone builds

Run the ADF test on the level series, difference it, confirm stationarity, then plot the correlograms to shortlist orders.

Rebuild the Lumen daily series from b1 so this notebook runs standalone.

Run adfuller on the level and on the first difference; read the p-values.

Plot plot_acf and plot_pacf on the differenced series and note where they cut off.

★ Run it now (Python) - ADF test + differencing + ACF/PACFimport pandas as pd, numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # rebuild the Lumen daily sessions series (from b1) idx = pd.date_range("2026-01-01", periods=112, freq="D") season = np.array([40,55,60,50,30,-120,-140]) i = np.arange(112) y = 1000 + 2.5*i + season[i % 7] + 14*np.sin(i*1.7) + 9*np.cos(i*0.6) s = pd.Series(np.round(y), index=idx, name="sessions") def adf(x, label): stat, p = adfuller(x.dropna())[:2] verdict = "stationary" if p < 0.05 else "NOT stationary" print(f"{label:12s} ADF p = {p:.3f} -> {verdict}") adf(s, "level") # trending + seasonal -> not stationary d1 = s.diff() # first difference removes the trend adf(d1, "differenced") # should now pass fig, ax = plt.subplots(1, 2, figsize=(11, 3)) plot_acf(d1.dropna(), lags=21, ax=ax[0]) # MA(q): where ACF cuts off plot_pacf(d1.dropna(), lags=21, ax=ax[1]) # AR(p): where PACF cuts off plt.tight_layout(); plt.show() # spikes at lag 7 = seasonal orders
Difference just enough, no more Over-differencing injects artificial negative autocorrelation and inflates the forecast variance. Difference until the ADF test passes and the ACF decays, then stop. Usually d is 0 or 1, and one seasonal difference (D=1) handles the weekly pattern.
Demo 2 of 2

Fit SARIMA, forecast with an interval, beat the baseline ★ 10 min · build your own

Let auto_arima search the seasonal orders, forecast H days with a confidence interval, and score MAPE against the seasonal-naive baseline from b1. A forecast is a probability, not a promise - so we always carry the interval.

Reuse the time split from b1: last H=14 days as test, the rest as train.

Fit auto_arima(train, seasonal=True, m=7) and read the chosen orders.

Forecast H with a 95% interval, then compare MAPE to seasonal-naive - did it earn its complexity?

★ Run it now (Python) - auto_arima SARIMA + interval + MAPE vs baselinefrom pmdarima import auto_arima # statsmodels alternative: # from statsmodels.tsa.statespace.sarimax import SARIMAX # fit = SARIMAX(train, order=(1,1,1), seasonal_order=(1,1,1,7)).fit(disp=False) H, m = 14, 7 train, test = s.iloc[:-H], s.iloc[-H:] model = auto_arima(train, seasonal=True, m=m, d=1, D=1, stepwise=True, suppress_warnings=True) print(model.order, model.seasonal_order) # chosen (p,d,q)(P,D,Q,7) fc, ci = model.predict(n_periods=H, return_conf_int=True, alpha=0.05) fc = pd.Series(fc, index=test.index) def mape(actual, f): return (np.abs(actual.values - f.values) / actual.values).mean() * 100 # b1 seasonal-naive baseline on the same split snaive = pd.Series([train.iloc[-m + (k % m)] for k in range(H)], index=test.index) print(f"SARIMA MAPE {mape(test, fc):.2f}%") print(f"seasonal-naive {mape(test, snaive):.2f}% (the bar to beat)") print(f"95% interval width, day {H}: {ci[-1, 1] - ci[-1, 0]:.0f} sessions")
Real world

Watch the interval widen with the horizon. Print the confidence band for day 1 and day 14 - the day-14 band is far wider, because uncertainty compounds the further out you forecast. That is honest, not a flaw. And if SARIMA does not beat seasonal-naive on this backtest, it has not earned the extra machinery here - report that plainly and keep the simpler model, exactly as the M-competitions would predict.

Homework

Try it yourself - this week ◐ 30-45 min total

Source material

Sources covered (cited)

Taught from the forecasting canon and its founding text. This session covers:

✓Box & Jenkins (1970), Time Series Analysis: Forecasting and ControlParts 1-2 · stationarity, differencing, ACF/PACF, AR/I/MA orders, seasonal models
✓Forecasting: Principles and Practice, 3rd ed (Hyndman & Athanasopoulos, FPP3)Ch 9 (ARIMA) · differencing, unit-root tests, SARIMA, auto_arima
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why must a series be stationary before fitting ARIMA?

ARIMA models the series by its own past values and errors, which only works if the statistical properties are stable over time. Differencing removes trend and seasonality to get there; the ADF test confirms it.

2 · What do the ACF and PACF plots help you choose?

A cut-off in the PACF suggests the AR order p; a cut-off in the ACF suggests the MA order q. Spikes at lag 7 flag the seasonal orders. They are guides you confirm with AIC or auto_arima.

3 · What does the seasonal part (P, D, Q, m) of SARIMA add?

SARIMA handles two rhythms: the non-seasonal (p,d,q) for day-to-day structure and the seasonal (P,D,Q) at period m for the week-to-week pattern. It still has to beat seasonal-naive on a backtest.

Builder session 4 cheat sheet · pin this

Stationaritystable mean, variance, autocorrelation. ARIMA needs it. Check with adfuller (p < 0.05 = stationary).
Differencing (d)replace value with its change. First diff removes trend; seasonal diff (lag 7) removes the week. Difference just enough.
AR(p)regress on the last p values. PACF cut-off suggests p.
MA(q)regress on the last q errors (not a rolling average). ACF cut-off suggests q.
SARIMA(p,d,q)(P,D,Q,m) - a seasonal copy of AR/I/MA at period m=7 for the weekly pattern.
auto_arimasearches orders by AIC with m=7. Fix d, D from the ADF test; always check residuals.
Always an intervalforecast with a 95% band - it widens with the horizon. A point alone is false confidence.
Next sessionb5 - Prophet: decomposable trend + seasonality + holidays, analyst-friendly.