Data Analysis for Engineers/Module 5

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

  1. Try a seasonal model (statsmodels.tsa.statespace.sarimax.SARIMAX with seasonal_order=(1,1,1,12)) and compare its forecast shape to plain ARIMA(2,1,1).
  2. 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).
  3. 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

  1. What is a key limitation of a simple moving-average forecast?
  2. What does ARIMA's "I" (Integrated) component account for?
  3. Why does a forecast's confidence interval typically widen further into the future?
Answer key
  1. It can't anticipate trend or seasonality - it just projects recent history flat forward
  2. Differencing the series to make it stationary (removing trend) before the AR and MA parts
  3. Uncertainty compounds the further ahead you forecast

53 / 63 sections · Course home · Join the coaching cohort