Chapter 28 — Time Series

Time Series Analysis & Forecasting

Ordered data breaks the i.i.d. assumption. Stationarity, decomposition, ARIMA vs Prophet vs ML, time-aware validation, and forecasting metrics.

Time series data has memory and seasonality — the past predicts the future and rows are not independent. Standard train/test splitting and cross-validation will silently leak the future into the past.
28.1 The four components
decomposition
Observed series  =
   Trend        (long-term direction)
 + Seasonality  (fixed-period cycles: weekly, yearly)
 + Cyclic       (irregular multi-year swings)
 + Residual     (noise)
python
from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(series, model='additive', period=12)
result.plot()   # trend, seasonal, residual panels
28.2 Stationarity — most models require it

A stationary series has constant mean/variance over time. Trends and seasonality must be removed (differencing) before ARIMA-type models.

make it stationary
Is the series stationary? (ADF test p < 0.05?)
│
├── YES ──────────────────► model directly
└── NO
    ├── trend ───────────► difference: y[t] − y[t−1]
    ├── seasonality ─────► seasonal difference (lag = period)
    └── growing variance ► log transform
python
from statsmodels.tsa.stattools import adfuller
p = adfuller(series.dropna())[1]
print("stationary" if p < 0.05 else "needs differencing")
28.3 Choosing a forecasting approach
model selection
What fits your problem?
│
├── Simple baseline first ──────────► Naive / seasonal-naive
├── Clear trend + seasonality, 1 series ► SARIMA / ETS
├── Strong seasonality, holidays, gaps ──► Prophet
├── Many series + rich features ────────► LightGBM on lag features
└── Long horizon, lots of data ─────────► Deep models (N-BEATS, TFT)
MethodStrengthWeakness
Seasonal-naiveFree baseline, hard to beatNo trend adaptation
SARIMAPrincipled, interpretableManual tuning, one series
ProphetHolidays, robust, easyCan over-smooth
LightGBM + lagsFeatures, many series, accuracyNeeds careful feature design
28.4 Feature engineering for ML forecasts
python
# Lags, rolling stats, and calendar features — the bread and butter
df['lag_7']   = df['y'].shift(7)
df['roll_28'] = df['y'].shift(1).rolling(28).mean()
df['dow']     = df['date'].dt.dayofweek
df['month']   = df['date'].dt.month
Every lag/rolling feature must use only past values (note the .shift(1) before rolling). A rolling mean that includes the current row leaks the target.
28.5 Time-aware validation
Wrong
Random K-fold shuffles time — the model trains on the future to predict the past. Scores look great, production fails.
Correct
Expanding/rolling window: always train on past, validate on the next slice forward.
TimeSeriesSplit(n_splits=5)
rolling-origin validation
Fold 1: train[----]      test[--]
Fold 2: train[------]    test[--]
Fold 3: train[--------]  test[--]
        (always forward in time)
28.6 Forecasting metrics
MetricUse whenAvoid when
MAERobust, same unitsBig errors must dominate
RMSEPenalize large missesMany legit spikes
MAPE% error, cross-series compareValues near/at zero
sMAPE / WAPEFixes MAPE's zero problem

Professional recommendation

AlwaysBeat seasonal-naive first
ValidateRolling-origin (no shuffle)
Quick + robustProphet / ETS
Max accuracyLightGBM on lag features
Common mistakes to avoid
Quick cheatsheet
seasonal_decompose() -> trend/seasonal/resid
adfuller() -> stationarity test
TimeSeriesSplit() -> forward validation
.shift(1).rolling(n) -> leak-safe rolling
Prophet() -> quick robust forecast