Assignment 1

Author

Sam Kuhn

Read data

Gather URLs, create temp file directory in data folder, then download files and bind together single data frame

library(tidyverse)
library(fpp3)
url <- c(
  "https://www.eia.gov/electricity/wholesale/xls/archive/ice_electric-2021final.xlsx",
  "https://www.eia.gov/electricity/wholesale/xls/archive/ice_electric-2020final.xlsx",
  "https://www.eia.gov/electricity/wholesale/xls/archive/ice_electric-2019final.xlsx",
  "https://www.eia.gov/electricity/wholesale/xls/archive/ice_electric-2018final.xlsx"
)
# Download the files to the directory
file_paths <- map(url, ~ {
  file_name <- basename(.x)
  file_path <- file.path(here::here("data", file_name))
  download.file(.x, destfile = file_path, mode = "wb")
  file_path
})

data_frames <- map(file_paths, ~ readxl::read_xlsx(.x))

# Combine the data frames into one
combined_data <- bind_rows(data_frames) |> 
  janitor::clean_names()

Create tsibble

Since data observations are not unique, calculate weekly average

energy_prices <- combined_data |> 
  group_by(week = lubridate::floor_date(trade_date, "week")) |> 
  mutate(week = yearweek(week)) |> 
  summarise(mean_weekly_avg = mean(wtd_avg_price_m_wh)) |> 
  as_tsibble(index = week)

Plot

combined_data |> 
  group_by(price_hub, week = lubridate::floor_date(trade_date, "week")) |> 
  summarise(mean_price = mean(wtd_avg_price_m_wh)) |> 
  filter(price_hub != "ERCOT North 345KV Peak") |> 
  ggplot(aes(week, mean_price, color = "value")) +
  geom_line(linewidth = 1.25, show.legend = FALSE) +
  facet_wrap(~price_hub) +
  ggthemes::theme_fivethirtyeight() +
  labs(
    title = "Weekly average energy prices by hub",
    subtitle = "2018-2022"
  )

Create models

Create training set

train <- energy_prices |> 
  filter_index("2018-01-03" ~ "2020-12-31")

NAIVE, Drift, SNAIVE, ETS

energy_fit <- train |> 
  model(
    ets = ETS(mean_weekly_avg ~ error("M") + trend("M")),
    naive = NAIVE(mean_weekly_avg),
    snaive = SNAIVE(mean_weekly_avg),
    drift = RW(mean_weekly_avg ~ drift())
  )

1-year ahead forecast

energy_fc <- energy_fit |> 
  forecast(h = 52)

Performance metrics:

Surprisingly, naive had both the lowest RMSE, as well as MAE and MAPE.

energy_fc |>
  accuracy(energy_prices) |> 
  arrange(RMSE)
# A tibble: 4 × 10
  .model .type        ME  RMSE      MAE       MPE     MAPE     MASE  RMSSE
  <chr>  <chr>     <dbl> <dbl>    <dbl>     <dbl>    <dbl>    <dbl>  <dbl>
1 naive  Test   2.63e  1  37.9 2.67e  1  3.58e  1 3.74e  1 1.70e  0   1.29
2 drift  Test   2.79e  1  39.2 2.82e  1  3.87e  1 3.98e  1 1.79e  0   1.34
3 snaive Test   2.32e  1  45.0 3.13e  1  3.39e  1 5.10e  1 1.99e  0   1.53
4 ets    Test  -1.83e183 Inf   1.83e183 -2.67e183 2.67e183 1.16e182 Inf   
# ℹ 1 more variable: ACF1 <dbl>
energy_fc |> 
  autoplot(
    energy_prices |> 
      filter(as.Date(week) < "2021-01-01"),
    level = c(90, 95)
  ) +
  scale_y_continuous(
    breaks = scales::breaks_width(50),
    labels = scales::label_dollar(),
    limits = c(0, 300)
  ) +
  labs(
    title = "Forecasted weekly average energy prices",
    subtitle = "Shaded area is 90th and 95th percent confidence interval"
  ) +
  ggthemes::theme_fivethirtyeight()

Compare results with cross-validation

Start with 4 weeks, move 1 ahead

energy_prices_cv <- train |> 
  stretch_tsibble(.init = 4, .step = 1)

By performing cross-validation, we drastically improved the forecasts overall, and snaive had the best RMSE and MAPE.

energy_fit_cv <- energy_prices_cv |> 
  model(
    ets = ETS(mean_weekly_avg),
    naive = NAIVE(mean_weekly_avg),
    snaive = SNAIVE(mean_weekly_avg),
    drift = RW(mean_weekly_avg ~ drift())
  ) |> 
  forecast(h = 52)

energy_fit_cv |> 
  accuracy(train) |> 
  arrange(RMSE)
# A tibble: 4 × 10
  .model .type     ME  RMSE   MAE   MPE  MAPE  MASE RMSSE  ACF1
  <chr>  <chr>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 snaive Test  -5.10   28.8  15.3 -30.4  42.9  1.00  1.00 0.532
2 naive  Test  -0.631  30.8  17.4 -19.6  47.7  1.14  1.07 0.721
3 ets    Test  -1.18   42.2  19.3 -21.3  53.4  1.26  1.47 0.824
4 drift  Test   1.45   43.7  24.9 -15.0  68.4  1.63  1.52 0.854

Residual checks

Since snaive performed the best in the cross-validation model, lets check the residuals. There’s one significant lag.

energy_fit_cv |> 
  filter(.model == "snaive", .id == 154) |> 
  gg_tsdisplay()