Module 4 Discussion

Author

Ryan Bean

I. What is Box Cox?

Box Cox is a type of transformation used to stabilize variance in a time series, especially when variance increases with level. It follows this series of equations:

\[ y_t^{(\lambda)} =\begin{cases}\dfrac{y_t^\lambda - 1}{\lambda}, & \lambda \neq 0, \\\log(y_t), & \lambda = 0.\end{cases} \]

Where lambda defines the degree of curvature of the transformation. The main advantages of the Box Cox transformation are variance stabilization and improved residuals. Additionally, it often improves model selection criteria such as AIC and makes seasonal patterns easier to interpret and model. However, the transformation requires positive data and forecasts must be transformed back to the original scale. This can introduce bias. It also does not address non-stationarity in the mean, so differencing may still be required.

II. Simple Linear Regression

remove(list = ls())

# Set up
library(fpp3)
library(fredr)
library(tidyverse)
library(patchwork)
library(knitr)
library(kableExtra)
library(writexl)
library(urca)
# Dataset
fredr_set_key("523a2b98a1ce120186357fd0c916cc26")

op <- fredr(series_id = "OILPRICE",
              observation_start = as.Date("1980-01-01"),
              observation_end   = as.Date("2024-12-01")
              ) |>
  transmute(Month = yearmonth(date), value) |>
  as_tsibble(index = Month)

After loading data, it has to be split into train and test, done below.

n <- nrow(op)
train_size <- floor(0.8 * n)

train <- op |> slice(1:train_size)
test  <- op |> slice((train_size + 1):n)
fit_basic <- train |>
  model(
    MEAN  = MEAN(value),
    DRIFT = RW(value ~ drift()),
    SNAIVE = SNAIVE(value)
  )
h <- nrow(test)

fc_basic <- fit_basic |>
  forecast(h = h)
acc_basic <- fc_basic |>
  accuracy(test)

acc_basic
# A tibble: 3 × 10
  .model .type    ME  RMSE   MAE   MPE  MAPE  MASE RMSSE  ACF1
  <chr>  <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 DRIFT  Test   21.9  28.9  24.3  21.6  27.0   NaN   NaN 0.912
2 MEAN   Test   57.0  60.2  57.0  65.6  65.6   NaN   NaN 0.912
3 SNAIVE Test   18.4  26.6  22.3  17.1  25.1   NaN   NaN 0.909

As the metrics show, snaive performs best overall, with the lowest RMSE (26.55), MAE (22.30), and MAPE (25.15). The mean model performs worst, with large bias (ME = 57.01) and error magnitudes, suggesting it systematically overpredicts.

After applying Box Cox, bias metrics such as ME and MPE should decrease and error spread measures like RMSE and MAE should improve slightly, particularly if variance increases with the level of oil prices.

III. Transformations

lambda_hat <- train |> features(value, guerrero) |> pull(lambda_guerrero)

train_t <- train |>
  mutate(
    y_none = value,
    y_log  = log(value),
    y_sqrt = sqrt(value),
    y_bc   = box_cox(value, lambda_hat)
  )

test_t <- test |>
  mutate(
    y_none = value,
    y_log  = log(value),
    y_sqrt = sqrt(value),
    y_bc   = box_cox(value, lambda_hat)
  )

fit_none <- train_t |>
  model(
    MEAN   = MEAN(y_none),
    DRIFT  = RW(y_none ~ drift()),
    SNAIVE = SNAIVE(y_none)
  )

fit_log <- train_t |>
  model(
    MEAN   = MEAN(y_log),
    DRIFT  = RW(y_log ~ drift()),
    SNAIVE = SNAIVE(y_log)
  )

fit_sqrt <- train_t |>
  model(
    MEAN   = MEAN(y_sqrt),
    DRIFT  = RW(y_sqrt ~ drift()),
    SNAIVE = SNAIVE(y_sqrt)
  )

fit_bc <- train_t |>
  model(
    MEAN   = MEAN(y_bc),
    DRIFT  = RW(y_bc ~ drift()),
    SNAIVE = SNAIVE(y_bc)
  )

fc_none <- fit_none |> forecast(h = h)
fc_log  <- fit_log  |> forecast(h = h)
fc_sqrt <- fit_sqrt |> forecast(h = h)
fc_bc   <- fit_bc   |> forecast(h = h)

acc_transformed <- bind_rows(
  accuracy(fc_none, test_t) |> mutate(transformation = "None (level)"),
  accuracy(fc_log,  test_t) |> mutate(transformation = "Log"),
  accuracy(fc_sqrt, test_t) |> mutate(transformation = "Sqrt"),
  accuracy(fc_bc,   test_t) |> mutate(transformation = paste0("Box-Cox (lambda=", round(lambda_hat, 3), ")"))
) |>
  select(transformation, .model, ME, RMSE, MAE, MPE, MAPE, ACF1) |>
  arrange(transformation, RMSE)

fc_log_bt  <- fc_log  |> mutate(.mean = exp(.mean))
fc_sqrt_bt <- fc_sqrt |> mutate(.mean = (.mean)^2)
fc_bc_bt   <- fc_bc   |> mutate(.mean = inv_box_cox(.mean, lambda_hat))

acc_level <- bind_rows(
  accuracy(fc_none, test_t) |> mutate(transformation = "None (level)"),
  accuracy(fc_log_bt, test_t) |> mutate(transformation = "Log (back-transformed)"),
  accuracy(fc_sqrt_bt, test_t) |> mutate(transformation = "Sqrt (back-transformed)"),
  accuracy(fc_bc_bt,   test_t) |> mutate(transformation = paste0("Box-Cox (back-transformed, lambda=", round(lambda_hat, 3), ")"))
) |>
  select(transformation, .model, ME, RMSE, MAE, MPE, MAPE, ACF1) |>
  arrange(transformation, RMSE)

acc_level
# A tibble: 12 × 8
   transformation                  .model     ME   RMSE    MAE   MPE  MAPE  ACF1
   <chr>                           <chr>   <dbl>  <dbl>  <dbl> <dbl> <dbl> <dbl>
 1 Box-Cox (back-transformed, lam… SNAIVE  2.54   3.73   3.18  11.4  16.1  0.907
 2 Box-Cox (back-transformed, lam… DRIFT   3.00   3.99   3.42  13.9  17.2  0.913
 3 Box-Cox (back-transformed, lam… MEAN    9.84  10.2    9.84  50.6  50.6  0.914
 4 Log (back-transformed)          SNAIVE  0.221  0.334  0.289  4.70  6.48 0.903
 5 Log (back-transformed)          DRIFT   0.252  0.346  0.301  5.43  6.71 0.911
 6 Log (back-transformed)          MEAN    1.18   1.21   1.18  26.6  26.6  0.914
 7 None (level)                    SNAIVE 18.4   26.6   22.3   17.1  25.1  0.909
 8 None (level)                    DRIFT  21.9   28.9   24.3   21.6  27.0  0.912
 9 None (level)                    MEAN   57.0   60.2   57.0   65.6  65.6  0.912
10 Sqrt (back-transformed)         SNAIVE  1.01   1.48   1.26   9.71 13.4  0.907
11 Sqrt (back-transformed)         DRIFT   1.19   1.58   1.35  11.7  14.3  0.913
12 Sqrt (back-transformed)         MEAN    4.00   4.15   4.00  43.1  43.1  0.914

On the original level, seasonal naive had an RMSE of 26.55 and MAPE of 25.15, whereas after log back-transformation, it’s RMSE drops dramatically to 0.33 and MAPE to 6.48. The Box Cox transformation (λ = 0.553) and square root transformation also reduce error from the level model, but not as much as the log. Across all specifications, seasonal naive consistently performs best among the benchmark models, while mean performs worst. This indicates that stabilizing variance materially improves forecast performance for oil prices, which likely exhibit level dependent volatility.

Once we apply a transformation such as the log or Box Cox, the model is no longer forecasting oil prices directly. It now forecasts the transformed series. After back-transformation, forecasts return to dollar units, but the underlying structure is multiplicative instead of additive. Transformations change both the scale of estimation and the economic interpretation of model components.