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 transformpython
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)
| Method | Strength | Weakness |
|---|---|---|
| Seasonal-naive | Free baseline, hard to beat | No trend adaptation |
| SARIMA | Principled, interpretable | Manual tuning, one series |
| Prophet | Holidays, robust, easy | Can over-smooth |
| LightGBM + lags | Features, many series, accuracy | Needs 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
| Metric | Use when | Avoid when |
|---|---|---|
| MAE | Robust, same units | Big errors must dominate |
| RMSE | Penalize large misses | Many legit spikes |
| MAPE | % error, cross-series compare | Values near/at zero |
| sMAPE / WAPE | Fixes 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
- Random K-fold on time series (leaks the future)
- Rolling features that include the current row
- Skipping the naive baseline and over-engineering
- Using MAPE on series with zeros
- Ignoring stationarity before ARIMA
Quick cheatsheet
seasonal_decompose() -> trend/seasonal/residadfuller() -> stationarity testTimeSeriesSplit() -> forward validation.shift(1).rolling(n) -> leak-safe rollingProphet() -> quick robust forecast