STAT 416 — Statistical Analysis of Time Series

Time Series Forecasting of Residential Energy Demand

Decomposing seasonal patterns, fitting SARIMA and STL+ETS models, and producing 12-month-ahead forecasts of monthly electricity consumption with prediction intervals.

R forecast · tseries · ggplot2 Time Series Analysis

Overview

Accurate energy demand forecasting is critical for utility companies to manage capacity planning, negotiate fuel contracts, and ensure grid reliability. This project analyzes 8 years of monthly residential electricity consumption data (2015–2022), builds competing time series models, and produces 12-month-ahead forecasts with uncertainty quantification.

The analysis follows the full Box-Jenkins methodology: visual inspection and decomposition, stationarity testing with the Augmented Dickey-Fuller test, model identification via ACF/PACF analysis, parameter estimation, diagnostic checking, and finally forecasting. I compare a manually specified SARIMA model against an automated STL decomposition + ETS approach and evaluate them on a held-out test set.

SARIMA STL Decomposition ETS Box-Jenkins Method ADF Test forecast package

Data Exploration

The dataset contains 96 monthly observations of residential electricity consumption (in GWh) for a mid-size metropolitan area. Clear seasonality is evident — demand peaks in summer (air conditioning) and winter (heating), with troughs in spring and fall.

library(forecast)
library(tseries)
library(ggplot2)

# Load and create time series object
energy <- ts(energy_data$consumption_gwh,
              start = c(2015, 1), frequency = 12)

# Train/test split: hold out last 12 months
train <- window(energy, end = c(2021, 12))
test  <- window(energy, start = c(2022, 1))

# STL decomposition
decomp <- stl(train, s.window = "periodic")
autoplot(decomp) +
  ggtitle("STL Decomposition of Monthly Energy Demand") +
  theme_minimal()
Monthly Residential Energy Consumption (2015–2022) GWh 200 300 400 500 600 2016 2017 2018 2019 2020 2021 2022 Train Test Trend

Strong seasonal pattern with summer and winter peaks. A slight upward trend is visible over the 8-year period.

Stationarity and Differencing

The Augmented Dickey-Fuller test on the raw series yielded a p-value of 0.09 — borderline non-stationary. After applying one seasonal difference (lag 12), the ADF test strongly rejects the null of a unit root (p < 0.01), confirming seasonal differencing alone is sufficient.

# Stationarity testing
adf.test(train)                           # p = 0.09
adf.test(diff(train, lag = 12))           # p < 0.01

# Check if additional first differencing is needed
nsdiffs(train)  # Returns 1 → need D=1
ndiffs(diff(train, lag = 12))  # Returns 0 → d=0 sufficient

# ACF/PACF of seasonally differenced series
sdiff <- diff(train, lag = 12)
ggtsdisplay(sdiff, main = "Seasonally Differenced Series")

Model Fitting

Model 1: SARIMA(1,0,1)(0,1,1)[12]

Based on the ACF/PACF patterns — exponential decay in ACF at seasonal lags and a single significant spike at lag 12 in the PACF — I identified a SARIMA(1,0,1)(0,1,1)[12] model. The non-seasonal AR(1) and MA(1) terms capture the short-term autocorrelation, while the seasonal MA(1) with seasonal differencing handles the yearly cycle.

# Fit SARIMA model
sarima_fit <- Arima(train,
                     order = c(1, 0, 1),
                     seasonal = list(order = c(0, 1, 1), period = 12))
summary(sarima_fit)

# Diagnostic checks
checkresiduals(sarima_fit)
# Ljung-Box test: p = 0.42 → no significant autocorrelation
# Residuals appear normally distributed

Model 2: STL + ETS

As an alternative, I used STL decomposition to separate the seasonal component, then fit an ETS(A,Ad,N) — additive error, damped additive trend, no additional seasonality — to the seasonally adjusted series. The seasonal component is then reinjected into the forecasts.

# STL + ETS approach
stlf_fit <- stlf(train, method = "ets", h = 12)
summary(stlf_fit)

Model Comparison

SARIMA(1,0,1)(0,1,1)[12] ★

AICc742.3
RMSE (test)18.4 GWh
MAPE (test)4.7%
Coverage (95% PI)91.7%

STL + ETS(A,Ad,N)

AICc758.1
RMSE (test)22.1 GWh
MAPE (test)5.6%
Coverage (95% PI)100%
# Generate forecasts from both models
fc_sarima <- forecast(sarima_fit, h = 12)
fc_stlf   <- stlf_fit

# Test set accuracy
accuracy(fc_sarima, test)
accuracy(fc_stlf, test)

# Plot forecasts with actuals
autoplot(train) +
  autolayer(fc_sarima, series = "SARIMA", PI = TRUE) +
  autolayer(test, series = "Actual") +
  ggtitle("12-Month Ahead Energy Demand Forecast") +
  xlab("Year") + ylab("Consumption (GWh)") +
  theme_minimal()
SARIMA Forecast vs. Actual (2022) Forecast Actual 95% PI

The SARIMA model tracks the actual 2022 values closely, with all observations falling within the 95% prediction interval.

4.7% Test MAPE
18.4 Test RMSE (GWh)
91.7% 95% PI Coverage

Key Takeaways

The SARIMA(1,0,1)(0,1,1)[12] model achieved a test MAPE of 4.7%, well within the utility industry benchmark of sub-5% for monthly forecasts. The Box-Jenkins methodology produced a parsimonious 3-parameter model that outperformed the automated STL+ETS approach on all accuracy metrics. The prediction intervals were well-calibrated at 91.7% empirical coverage for 95% nominal level. This forecast accuracy would support reliable capacity planning and procurement decisions up to a year ahead.

The project highlights the enduring power of classical time series methods when properly applied. The systematic Box-Jenkins workflow — identification, estimation, diagnostics — produced a model that was both interpretable and accurate. The SARIMA framework naturally handles the dual seasonality and autocorrelation structure in energy demand data, and its prediction intervals provide the uncertainty quantification that utility planners need for risk management.