Chapter 22 — Time Series Analysis and Forecasting
A time series is data ordered in time. This chapter covers date indexing, rolling statistics, decomposition into trend/seasonality/residual, and forecasting with ARIMA using the classic monthly airline passengers dataset.
Learning Objectives
- Index data by dates with pandas.
- Compute rolling statistics and differencing.
- Decompose a series into trend, seasonality, and residual.
- Read autocorrelation (ACF) plots.
- Fit an ARIMA model and forecast.
- Evaluate a forecast with train/test split.
Prerequisites / Imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.graphics.tsaplots import plot_acf
import warnings
warnings.filterwarnings('ignore')
1 Load and Index the Series
The seaborn flights dataset records monthly airline passengers from 1949–1960.
flights = sns.load_dataset('flights')
flights['date'] = pd.to_datetime(flights['year'].astype(str) + '-' + flights['month'].astype(str) + '-01')
ts = flights.set_index('date')['passengers'].astype(float)
print('start:', ts.index.min(), 'end:', ts.index.max(), 'n:', len(ts))
ts.head()
start: 1949-01-01 00:00:00 end: 1960-12-01 00:00:00 n: 144
date 1949-01-01 112.0 1949-02-01 118.0 1949-03-01 132.0 1949-04-01 129.0 1949-05-01 121.0 Name: passengers, dtype: float64
2 Visualize the Series
Airline travel shows clear trend and yearly seasonality.
plt.figure(figsize=(10,4))
ts.plot()
plt.title('Monthly airline passengers'); plt.ylabel('passengers (thousands)')
plt.show()
3 Rolling Statistics
A 12-month moving average smooths noise and reveals the trend.
plt.figure(figsize=(10,4))
ts.plot(label='original', alpha=0.5)
ts.rolling(12).mean().plot(label='12-month MA')
plt.title('Rolling mean'); plt.legend()
plt.show()
4 Decomposition
seasonal_decompose splits the series into trend, seasonal, and residual components.
decomp = seasonal_decompose(ts, model='multiplicative', period=12)
fig = decomp.plot()
fig.set_size_inches(10, 7)
plt.show()
5 Stationarity and Differencing
A stationary series has constant mean/variance. Differencing ($y_t - y_{t-1}$) helps stabilize trend.
diff = ts.diff().dropna()
fig, ax = plt.subplots(1, 2, figsize=(12,4))
ts.plot(ax=ax[0], title='Original')
diff.plot(ax=ax[1], title='First difference')
plt.tight_layout(); plt.show()
6 Autocorrelation
ACF shows how a series correlates with its own lags — useful for choosing ARIMA orders.
fig, ax = plt.subplots(figsize=(8,4))
plot_acf(ts, lags=24, ax=ax)
plt.title('Autocorrelation (ACF) of passengers')
plt.show()
7 Forecasting with ARIMA
We fit ARIMA(1,1,1) on the training portion and forecast the held-out months.
train = ts.iloc[:-24]
test = ts.iloc[-24:]
model = ARIMA(train, order=(1, 1, 1)).fit()
forecast = model.get_forecast(steps=len(test))
pred_mean = forecast.predicted_mean
ci = forecast.conf_int()
plt.figure(figsize=(10,4))
train.plot(label='train')
test.plot(label='actual')
pred_mean.plot(label='forecast')
plt.fill_between(ci.index, ci.iloc[:,0], ci.iloc[:,1], color='pink', alpha=0.3)
plt.title('ARIMA forecast vs actual'); plt.legend()
plt.show()
8 Evaluate the Forecast
Mean Absolute Error and Mean Absolute Percentage Error summarize forecast accuracy.
mae = np.mean(np.abs(pred_mean.values - test.values))
mape = np.mean(np.abs((pred_mean.values - test.values) / test.values)) * 100
print('MAE :', round(float(mae), 2))
print('MAPE:', round(float(mape), 2), '%')
MAE : 93.9 MAPE: 18.84 %
Case Study: Forecasting Web Traffic
We generate a synthetic daily series with trend and weekly seasonality, then forecast the next 30 days.
rng = np.random.default_rng(0)
dates = pd.date_range('2026-01-01', periods=180, freq='D')
trend = np.linspace(100, 200, 180)
season = 15 * np.sin(2 * np.pi * np.arange(180) / 7)
noise = rng.normal(0, 5, 180)
traffic = pd.Series(trend + season + noise, index=dates)
fit = ARIMA(traffic, order=(2, 1, 1)).fit()
fc = fit.get_forecast(steps=30).predicted_mean
plt.figure(figsize=(10,4))
traffic.plot(label='actual')
fc.index = pd.date_range(traffic.index[-1] + pd.Timedelta(days=1), periods=30, freq='D')
fc.plot(label='30-day forecast')
plt.title('Synthetic web traffic forecast'); plt.legend()
plt.show()
Exercises
- Load the flights data and set a DatetimeIndex.
- Plot the series and its 6-month rolling mean.
- Decompose the series and interpret the seasonal component.
- Plot the first difference and comment on stationarity.
- Produce an ACF plot up to 30 lags.
- Fit an ARIMA(1,1,1) model and forecast 12 steps.
- Compute MAPE for your forecast.
- Try ARIMA(2,1,2) and compare MAPE to ARIMA(1,1,1).
- Explain what the
dparameter does in ARIMA. - Generate a synthetic series with trend and monthly seasonality and forecast it.
Python Data Science: From Foundations to Applications — Chapter 22