Module 5 · Section 10 of 12
Lesson 5.9 - Time Series Forecasting
Target: ~10 min read - 20 min hands-on
Overview
Forecasting predicts future values from historical patterns. A moving average is the simplest baseline - useful, but it can't anticipate trend or seasonality. ARIMA models trend and autocorrelation explicitly, and SARIMA adds seasonality. We'll forecast the monthly energy-demand series from earlier in this module.
Why This Matters (Engineering Context)
Utility and facilities planners routinely produce 12-month-ahead demand forecasts to inform generation, procurement, and capacity decisions - the exact deliverable this lesson and Mini-Project 5 rehearse.
Code-Along
from statsmodels.tsa.arima.model import ARIMA
# a single time-indexed series to forecast
demand_series = demand_df.set_index("date")["demand_mw"]
# --- Baseline: a flat forecast at the last 6-month rolling average ---
window = 6
moving_avg = demand_series.rolling(window=window).mean()
naive_forecast = pd.Series(
[moving_avg.iloc[-1]] * 12, # repeat the last value 12x
index=pd.date_range(demand_series.index[-1] + pd.DateOffset(months=1), periods=12, freq="MS"),
)
# --- ARIMA(p, d, q): d=1 differences the series to remove trend before fitting ---
model = ARIMA(demand_series, order=(2, 1, 1))
fitted = model.fit()
fc = fitted.get_forecast(steps=12) # forecast object
arima_forecast = fc.predicted_mean # the point forecast
conf_int = fc.conf_int() # a DataFrame of [lower, upper] bounds
print("ARIMA forecast (next 12 months):")
print(arima_forecast.round(1))
fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(demand_series.index, demand_series.values, color="steelblue", label="Historical demand")
ax.plot(naive_forecast.index, naive_forecast.values, color="gray", linestyle=":", label=f"{window}-month moving average")
ax.plot(arima_forecast.index, arima_forecast.values, color="firebrick", label="ARIMA(2,1,1)")
# shade the confidence band between the two conf_int columns
ax.fill_between(arima_forecast.index, conf_int.iloc[:, 0], conf_int.iloc[:, 1], color="firebrick", alpha=0.15, label="95% CI")
ax.set_title("Energy Demand Forecast: Moving Average vs ARIMA"); ax.set_ylabel("Demand (MW)"); ax.legend(fontsize=8)
plt.tight_layout(); plt.show()
Run it: the naive moving-average forecast is a flat line at the recent 6-month average - it can't project the upward activity-driven trend. The ARIMA(2,1,1) forecast also flattens after a month or two, because plain ARIMA(2,1,1) has no explicit trend or seasonal term. Its confidence interval still widens further out, correctly reflecting growing uncertainty. The practice exercises try a seasonal model.
Practice Exercises
- Try a seasonal model (
statsmodels.tsa.statespace.sarimax.SARIMAXwithseasonal_order=(1,1,1,12)) and compare its forecast shape to plain ARIMA(2,1,1). - Compute the RMSE of the moving-average vs ARIMA forecasts against the last 12 months of actual data (refit on the first 48 months, forecast the held-out 12).
- Change the forecast horizon from 12 to 24 months - what happens to the confidence interval width far into the future, and why does that make sense?
# Try the practice exercises here
Knowledge Check
- What is a key limitation of a simple moving-average forecast?
- What does ARIMA's "I" (Integrated) component account for?
- Why does a forecast's confidence interval typically widen further into the future?
Answer key
- It can't anticipate trend or seasonality - it just projects recent history flat forward
- Differencing the series to make it stationary (removing trend) before the AR and MA parts
- Uncertainty compounds the further ahead you forecast
53 / 63 sections · Course home · Join the coaching cohort