Exercise 8.1

Consider the the number of pigs slaughtered in Victoria, available in the aus_livestock dataset.

  1. Use the ETS() smoothing. Find the optimal values of function to estimate the equivalent model for simple exponential smoothing. Find the optimal values of \(\alpha\) and \(l_0\), and generate forecasts for the next four months.

Answer: As shown below, the optimal values are \(\alpha = 0.32\) and \(l_0 = 1.01 \times 10^5\).

Explanation:

First, I visualized the time series.

# Select the data of interest
victoria_pigs <- aus_livestock %>%
  filter(Animal == 'Pigs', State == 'Victoria')

# Plot the time series
victoria_pigs %>%
  autoplot(Count) +
    ggtitle('Pigs slaughtered in Victoria Australia, 1972-2018') + 
    guides(
      x = guide_axis(minor.ticks = TRUE)
    ) +
    theme_classic() +
    theme(
      axis.title = element_text(face = 'bold'),
      plot.title = element_text(face = 'bold')
    )

Then I applied simple exponential smoothing and determined \(\alpha\) and \(l_0\)

# Fit the model
slaughter_fit <- victoria_pigs %>%
  model(ETS(Count ~ error('A') + trend('N') + season('N')))
  
# Convert results to dataframe
slaughter_fit %>% tidy()
## # A tibble: 2 × 5
##   Animal State    .model                                          term  estimate
##   <fct>  <fct>    <chr>                                           <chr>    <dbl>
## 1 Pigs   Victoria "ETS(Count ~ error(\"A\") + trend(\"N\") + sea… alpha  3.22e-1
## 2 Pigs   Victoria "ETS(Count ~ error(\"A\") + trend(\"N\") + sea… l[0]   1.01e+5

The forecast is shown below

# Generate forecast for 4 months in future
slaughter_forecast <- slaughter_fit %>%
  forecast(h = 4)

# Plot forecast with historical data
slaughter_forecast %>%
  autoplot(victoria_pigs) +
  autolayer(
    .vars = Count,
    filter_index(victoria_pigs, '2019 Jan' ~ .)
  ) +
  # Only plot subset of data so the forecast is visible
  coord_cartesian(xlim = c(as.Date('2010-01-01'), as.Date('2020-01-01')),
                  ylim = c(40000, 120000)) +
  # Aesthetics
  labs(
    title = 'Pigs slaughtered in Victoria Australia, 2010-2018 + 4-Month Forecast',
    subtitle = 'Simple exponential smoothing',
  ) +
  theme_classic() +
  theme(
    axis.title = element_text(face = 'bold'),
    plot.title = element_text(face = 'bold')
  )   

b. Compute a 95% prediction interval for the first forecast using \(\hat{y} \pm 1.96s\) where \(s\) is the standard deviation of the residuals. Compare your interval with the interval produced by R.

Answer: The prediction intervals are very close. However, the prediction interval produced by R is more accurate because it accounts for changes in the forecast variance over time.

Explanation:

Prediction interval for first forecast using standard deviation of residuals

# Calculate standard deviation of residuals
slaughter_forecast_e <- augment(slaughter_fit)
slaughter_forecast_sd <- sd(slaughter_forecast_e$.resid)

# Calculate the 95% prediction interval for the first forecast
y_hat <- slaughter_forecast$.mean[1]
lower_bound95 <- y_hat - 1.96 * slaughter_forecast_sd
upper_bound95 <- y_hat + 1.96 * slaughter_forecast_sd

# Round bounds to nearest whole animal
sprintf('The calculated 95%% prediction interval is (%d, %d)', 
        floor(lower_bound95), ceiling(upper_bound95))
## [1] "The calculated 95% prediction interval is (76871, 113503)"

The 95% prediction interval produced by R is similar.

Note: Hyndman section 8.7 presents formulas for forecast variance, which are used to calculate prediction intervals; however, the text does not show the R function that is used to calculate the prediction interval. After searching online, I found that the hilo() function in the distributional package reports a 95% prediction interval.

hilo(slaughter_forecast) %>%
  as.data.frame() %>%
  select(`95%`) %>%
  head(n = 1) %>%
  unlist()
## 95%.lower 95%.upper 95%.level 
##  76854.79 113518.33     95.00

Exercise 8.5

Data set global_economy contains the annual Exports from many countries. Select one country to analyse.

  1. Plot the Exports series and discuss the main features of the data.

The plot below shows that exports in the United States (as percentage of GDP) trended upward from 1960 to 2017. There is no seasonality. There is some evidence of cyclical behavior every 15 years or so (eg, 1970-1985).

# Select the data of interest
us_economy <- global_economy %>%
  filter(Country == 'United States') %>%
  drop_na(Exports)

# Plot the time series
us_economy %>%
  autoplot(Exports) +
    xlim(1960, 2020) +
    labs(
      y = '% of GDP',
      title = 'US Exports, 1960-2017',
      caption = 'GDP, gross domestic product.'
    ) +
    guides(
      x = guide_axis(minor.ticks = TRUE)
    ) +
    theme_classic() +
    theme(
      axis.title = element_text(face = 'bold'),
      plot.title = element_text(face = 'bold'),
      plot.caption = element_text(color = 'darkgray', hjust = 0)
    )

b. Use an ETS(A,N,N) model to forecast the series, and plot the forecasts.

# Fit the model
us_exports_fit <- us_economy %>%
  model(ETS(Exports ~ error('A') + trend('N') + season('N')))
# Generate forecast for 3 years in future
us_exports_forecast <- us_exports_fit %>%
  forecast(h = 3)

# Plot forecast with historical data
us_exports_forecast %>%
  autoplot(us_economy) +
  autolayer(
    .vars = Exports,
    filter_index(us_economy, '2018' ~ .)
  ) +
  # Only plot subset of data so the forecast is visible
  coord_cartesian(xlim = c(1990, 2020)) +  
  labs(
    y = '% of GDP',    
    title = 'US Exports, 1990-2017 + 3-Year Forecast',
    subtitle = 'Simple exponential smoothing',
    caption = 'GDP, gross domestic product.'    
  ) +
  theme_classic() +
  theme(
    axis.title = element_text(face = 'bold'),
    plot.title = element_text(face = 'bold'),
    plot.caption = element_text(color = 'darkgray', hjust = 0)    
  )   

c. Compute the RMSE values for the training data.

Answer: The RMSE is 0.627.

accuracy(us_exports_fit)$RMSE
## [1] 0.6270672

d. Compare the results to those from an ETS(A,A,N) model. (Remember that the trended model is using one more parameter than the simpler model.) Discuss the merits of the two forecasting methods for this data set.

e. Compare the forecasts from both methods. Which do you think is best?

Answer: The ETS(A,A,N) model includes an additive trend component whereas the ETS(A,N,N) model does not have a trend component. As a result, the forecast from the ETS(A,A,N) model follows the trend of the time series data (ie, upward), whereas the forecast from the ETS(A,N,N) model is flat. Based on the forecast plot, I would say that the ETS(A,A,N) model is better because it follows the overall trend of the historical data. This conclusion is supported by the lower RMSE of the ETS(A,A,N) model compared with the ETS(A,N,N) model (0.61 vs 0.63).

Explanation:

# Fit the models
us_exports_fit2 <- us_economy %>%
  model(ANN = ETS(Exports ~ error('A') + trend('N') + season('N')),
        AAN = ETS(Exports ~ error('A') + trend('A') + season('N')))
# Generate forecasts for 3 years in future
h <- 3
us_exports_forecast2 <- us_exports_fit2 %>%
  forecast(h = h)

# Plot forecast with historical data
us_exports_forecast2 %>%
  autoplot(us_economy, level = NULL) +
  autolayer(
    .vars = Exports,
    filter_index(us_economy, '2018' ~ .)
  ) +
  # Only plot subset of data so the forecast is visible
  coord_cartesian(xlim = c(1990, 2020)) +    
  labs(
    y = '% of GDP',    
    title = 'US Exports, 1990-2017 + 3-Year Forecast',
    subtitle = 'Simple exponential smoothing',
    color = 'model',
    caption = 'GDP, gross domestic product.'    
  ) +
  theme_classic() +
  theme(
    axis.title = element_text(face = 'bold'),
    plot.title = element_text(face = 'bold'),
    plot.caption = element_text(color = 'darkgray', hjust = 0)    
  )  

# Compare RMSE values
accuracy(us_exports_fit2) %>%
  select(`.model`, RMSE)
## # A tibble: 2 × 2
##   .model  RMSE
##   <chr>  <dbl>
## 1 ANN    0.627
## 2 AAN    0.615

f. Calculate a 95% prediction interval for the first forecast for each model, using the RMSE values and assuming normal errors. Compare your intervals with those produced using R.

Answer: Similar to Exercise 8.1 part (b), the 95% prediction intervals calculated from residuals (calc_lower95 and calc_upper95) versus those produced by R (hilo_lower95 and hilo_upper95) are very close for both models.

Explanation:

# Calculate standard deviation of residuals
us_exports_forecast_e <- augment(us_exports_fit2)
us_exports_forecast_sd <- sd(us_exports_forecast_e$.resid)

# 95% prediction intervals for the first forecast for each model
us_exports_forecast2 %>%
  mutate(
    # Bounds of interval calculated from residuals
    calc_lower95 = .mean - 1.96 * us_exports_forecast_sd,
    calc_upper95 = .mean + 1.96 * us_exports_forecast_sd,
    # Bounds of interval produced by hilo() function
    hilo_interval95 = hilo(Exports, 95),
    hilo_lower95 = hilo_interval95$lower,
    hilo_upper95 = hilo_interval95$upper
  ) %>%
  as.data.frame() %>%
  # h is length of the forecast (defined in part (e)), so 
  # h+1 is the row number for the first forecast of the second model
  slice(1, h+1) %>%  
  select(model = `.model`, calc_lower95, calc_upper95, hilo_lower95, hilo_upper95)
##   model calc_lower95 calc_upper95 hilo_lower95 hilo_upper95
## 1   ANN     10.67539     13.10597     10.63951     13.14186
## 2   AAN     10.79132     13.22190     10.75667     13.25656

Exercise 8.6

Forecast the Chinese GDP from the global_economy data set using an ETS model. Experiment with the various options in the ETS() function to see how much the forecasts change with damped trend, or with a Box-Cox transformation. Try to develop an intuition of what each is doing to the forecasts.

Answer: Simple exponential smoothing (SES) produces a flat forecast, while the Holt method produces a linear forecast with a positive slope. Box-Cox transformation produces an exponentially increasing forecast, which is consistent with the log transformation and subsequent back-transformation. As the term “dampening” suggests, the dampened Holt and Box-Cox methods produce forecasts that are less extreme (relative to the un-dampened versions), particular for long-range forecasts. The dampening effect is more pronounced for the Box-Cox forecast than the Holt forecast.

Comparison of the RMSEs for these forecasts indicates that the Holt, dampened Holt, and dampened Box-Cox models have a better fit with the historical data than the SES or un-dampened Box-Cox models.

Explanation:

The time series plot shows that the GDP of mainland China trended upward (exponentially) from 1960 to 2017.

# Select the data of interest
# The global_economy dataset for China includes data for Hong Kong and Macao
# Below, I filter for 'China', which is only mainland China
china_gdp <- global_economy %>%
  filter(Country == 'China') %>%
  mutate(
    GDP = GDP / 1e12  # Express GDP in trillions
  )

# Plot the time series
china_gdp %>%
  autoplot(GDP) +
    xlim(1960, 2020) +
    labs(
      y = 'GDP (USD, trillion)',
      title = 'GDP of Mainland China, 1960-2017',
      caption = 'GDP, gross domestic product; USD, US dollar.'
    ) +
    guides(
      x = guide_axis(minor.ticks = TRUE)
    ) +
    theme_classic() +
    theme(
      axis.title = element_text(face = 'bold'),
      plot.title = element_text(face = 'bold'),
      plot.caption = element_text(color = 'darkgray', hjust = 0)
    )

The optimal lambda value \(\lambda = -0.03\) for Box-Cox transformation is near zero, which suggests a log transformation.

# Calculate optimal value of lambda for Box-Cox transformation using Guerrero method
china_GDP_lambda <- china_gdp %>%
  features(GDP, features = guerrero) %>%
  pull(lambda_guerrero)

china_GDP_lambda
## [1] -0.03446284
# Fit various models
china_gdp_fit <- china_gdp %>%
  model(
    # simple exponential smoothing
    SES = ETS(GDP ~ error('A') + trend('N') + season('N')), 
    # Holt's linear
    Holt = ETS(GDP ~ error('A') + trend('A') + season('N')),
    # Additive damped trend
    Holt_damped = ETS(GDP ~ error('A') + trend('Ad') + season('N')),
    # Box-Cox transformation
    BoxCox = ETS(box_cox(GDP, china_GDP_lambda)),
    # Box-Cox + dampening
    BoxCox_damped = ETS(box_cox(GDP, china_GDP_lambda) ~ error('A') + trend('Ad') + season('N'))
  )
# Generate forecast for 10 years in future
china_gdp_forecast <- china_gdp_fit %>%
  forecast(h = 10)

# Plot forecasts with historical data
china_gdp_forecast %>%
  autoplot(china_gdp, level = NULL) +
  autolayer(
    .vars = GDP,
    filter_index(china_gdp, '2018' ~ .)
  ) +
  # Only plot subset of data so the forecast is visible
  coord_cartesian(xlim = c(2000, 2030)) +
  labs(
    y = 'GDP (USD, trillion)',
    title = 'GDP of Mainland China, 2000-2017 + 10-Year Forecast',
    color = 'model',
    caption = 'GDP, gross domestic product; USD, US dollar.'  
  ) +
  guides(
    x = guide_axis(minor.ticks = TRUE)
  ) +  
  theme_classic() +
  theme(
    axis.title = element_text(face = 'bold'),
    plot.title = element_text(face = 'bold'),
    plot.caption = element_text(color = 'darkgray', hjust = 0)    
  ) 

accuracy(china_gdp_fit) %>%
  select(`.model`, RMSE)
## # A tibble: 5 × 2
##   .model         RMSE
##   <chr>         <dbl>
## 1 SES           0.416
## 2 Holt          0.190
## 3 Holt_damped   0.190
## 4 BoxCox        0.292
## 5 BoxCox_damped 0.206

Exercise 8.7

Find an ETS model for the Gas data from aus_production and forecast the next few years. Why is multiplicative seasonality necessary here? Experiment with making the trend damped. Does it improve the forecasts?

Answer: Multiplicative seasonality is needed because the seasonal variations change proportional to the level of the series. Comparison of the plots below shows that dampening does not have any effect on the forecast. The similarity is consistent with the estimated dampening parameter \(\phi = 0.98\) , which means that there is little dampening (because it is close to 1).

Explanation:

The time series plot below shows that gas production in Australia has trended upward since 1970 and that seasonal fluctuations in production vary with the level of the series.

# Plot the time series
aus_production %>%
  autoplot(Gas) +
    labs(
      y = 'Production, petajoules',
      title = 'Gas Production in Australia, 1956-2010'
    ) +
    guides(
      x = guide_axis(minor.ticks = TRUE)
    ) +
    theme_classic() +
    theme(
      axis.title = element_text(face = 'bold'),
      plot.title = element_text(face = 'bold')
    )

# Fit models
aus_gas_fit <- aus_production %>%
  model(
    # multiplicative
    Holt = ETS(Gas ~ error('M') + trend('A') + season('M')),
    # multiplicative + dampening
    Holt_damped = ETS(Gas ~ error('M') + trend('Ad') + season('M'))
  )  
# Generate forecast for 12 quarters (3 years) in future
aus_gas_forecast <- aus_gas_fit %>%
  forecast(h = 12)

# Plot forecasts with historical data
aus_gas_forecast %>%
  autoplot(aus_production, level = NULL) +
  autolayer(
    .vars = Gas,
    filter_index(aus_production, '2010 Q3' ~ .)
  ) +
  facet_grid(.model ~ .) +
  labs(
    y = 'Production, petajoules',
    title = 'Gas Production in Australia, 1956-2010 + 3-Year Forecast',
    color = 'model'
  ) +
  guides(
    x = guide_axis(minor.ticks = TRUE)
  ) +
  theme_classic() +
  theme(
    axis.title = element_text(face = 'bold'),
    plot.title = element_text(face = 'bold'),
    legend.position = 'none'    
  ) 

In the dampened model, the estimated dampening parameter \(\phi = 0.98\) is close to 1, which means that there is very little dampening.

aus_gas_fit %>%
  tidy() %>%
  filter(term == 'phi')
## # A tibble: 1 × 3
##   .model      term  estimate
##   <chr>       <chr>    <dbl>
## 1 Holt_damped phi      0.980

Exercise 8.8

Recall your retail time series data (from Exercise 7 in Section 2.10).

# Generate the same series used previously
set.seed(144)

retail_ts <- aus_retail %>%
  filter(`Series ID` == sample(aus_retail$`Series ID` , 1))
  1. Why is multiplicative seasonality necessary for this series?

    Answer: Multiplicative seasonality is needed because the seasonal variations in retail turnover change proportional to the level of the series.

    # Plot the time series
    retail_ts %>%
      autoplot(Turnover) +
        labs(
          y = 'Turnover (AUD, million)',
          title = 'Retail turnover in Australia, 1982-2018'
        ) +
        guides(
          x = guide_axis(minor.ticks = TRUE)
        ) +
        theme_classic() +
        theme(
          axis.title = element_text(face = 'bold'),
          plot.title = element_text(face = 'bold')
        )

  2. Apply Holt -Winters’ multiplicative method to the data. Experiment with making the trend damped.

    Answer: The plots below shows that dampening does not affect the forecast. The similarity is consistent with the estimated dampening parameter \(\phi = 0.98\), which means that there is little dampening.

    # Fit models
    aus_retail_fit <- retail_ts %>%
      model(
        # multiplicative
        Holt = ETS(Turnover ~ error('M') + trend('A') + season('M')),
        # Multiplicative damped
        Holt_damped = ETS(Turnover ~ error('M') + trend('Ad') + season('M'))
      )  
    # Generate forecast for 36 months (3 years) in future
    aus_retail_forecast <- aus_retail_fit %>%
      forecast(h = 36)
    
    # Plot forecasts with historical data
    aus_retail_forecast %>%
      autoplot(retail_ts, level = NULL) +
      autolayer(
        .vars = Turnover,
        filter_index(retail_ts, '2019 Jan' ~ .)
      ) +
      facet_grid(.model ~ .) +
      labs(
        y = 'Turnover (AUD, million)',
        title = 'Retail turnover in Australia, 1982-2018 + 3-Year Forecast',
        color = 'model'
      ) +
      guides(
        x = guide_axis(minor.ticks = TRUE)
      ) +
      theme_classic() +
      theme(
        axis.title = element_text(face = 'bold'),
        plot.title = element_text(face = 'bold'),
        legend.position = 'none'    
      )

    aus_retail_fit %>%
      tidy() %>%
      filter(term == 'phi') %>%
      select(`.model`, term, estimate)
    ## # A tibble: 1 × 3
    ##   .model      term  estimate
    ##   <chr>       <chr>    <dbl>
    ## 1 Holt_damped phi      0.980
  3. Compare the RMSE of the one-step forecasts from the two methods. Which do you prefer?

    Answer: On the basis of lower RMSE, the dampened forecast is slightly better; however, given that its dampening parameter \(\phi\) is close to 1, the additional complexity of the model may not be justified. So, if I had to choose one model, I’d choose the un-dampened Holt model.

    accuracy(aus_retail_fit) %>%
      select(`.model`, RMSE)
    ## # A tibble: 2 × 2
    ##   .model       RMSE
    ##   <chr>       <dbl>
    ## 1 Holt         2.95
    ## 2 Holt_damped  2.93
  4. Check that the residuals from the best method look like white noise.

    Answer: The residuals from the un-dampened Holt model do not appear to be white noise. This means that the forecasting method could be improved.

    Explanation: The time series plot shows some dependence on the level of the series, although magnitudes are small. In addition, the autocorrelation plot was not characteristic of white noise, which would be expected to have a single significant spike at lag=0. However, the data distribution (histogram) appears to be approximately normal with mean close to 0. The failure of two of the three residual diagnostics was also consistent with significant P values from Box-Pierce and Ljung-Box tests. Collectively, these results suggest that the residuals are not white noise.

    # Residual plots for Holt model (un-dampened)
    retail_ts %>%
      model(Holt = ETS(Turnover ~ error('M') + trend('A') + season('M'))) %>%  
      gg_tsresiduals()

    For Box-Pierce and Ljung-Box tests, the time series is seasonal with a monthly period, so \(l = 2m = 2 \times 12 = 24\).

    Both tests had significant P-values (P<0.05), so the null hypothesis that the residuals are from a white noise series is rejected.

    holt_model_residuals <- retail_ts %>%
      model(Holt = ETS(Turnover ~ error('M') + trend('A') + season('M'))) %>% 
      residuals()
    
    Box.test(holt_model_residuals$.resid, lag = 24, type = 'Box-Pierce')
    ## 
    ##  Box-Pierce test
    ## 
    ## data:  holt_model_residuals$.resid
    ## X-squared = 377.75, df = 24, p-value < 2.2e-16
    Box.test(holt_model_residuals$.resid, lag = 24, type = 'Ljung-Box')
    ## 
    ##  Box-Ljung test
    ## 
    ## data:  holt_model_residuals$.resid
    ## X-squared = 391.27, df = 24, p-value < 2.2e-16
  5. Now find the test set RMSE, while training the model to the end of 2010. Can you beat the seasonal naïve approach from Exercise 7 in Section 5.11?

    Answer: Yes. On the basis of lower RMSE, the ETS model forecast is better than the seasonal naive model forecast (14.7 vs 29.0).

    # Create training and test datasets with chronological split
    aus_retail_train <- retail_ts %>%
      filter(year(Month) < 2011)
    
    aus_retail_test <- retail_ts %>%
      filter(year(Month) >= 2011)
    # ETS model
    # Fit training dataset
    aus_retail_ets_fit <- aus_retail_train %>%
      model(ETS = ETS(Turnover ~ error('M') + trend('A') + season('M')))
    
    # Generate forecasts using test dataset
    aus_retail_ets_forecast <- forecast(aus_retail_ets_fit, 
                                        new_data = aus_retail_test)
    
    # Assess performance
    accuracy(aus_retail_ets_forecast, aus_retail_test) %>%
      select(model = `.model`, RMSE)
    ## # A tibble: 1 × 2
    ##   model  RMSE
    ##   <chr> <dbl>
    ## 1 ETS    14.7
    # Seasonal naive model
    # Fit training dataset
    aus_retail_snaive_fit <- aus_retail_train %>%
      model(seasonal_naive = SNAIVE(Turnover))
    
    # Generate forecasts using test dataset
    aus_retail_snaive_forecast <- forecast(aus_retail_snaive_fit, 
                                           new_data = aus_retail_test)
    
    # Assess performance
    accuracy(aus_retail_snaive_forecast, aus_retail_test) %>%
      select(model = `.model`, RMSE)
    ## # A tibble: 1 × 2
    ##   model           RMSE
    ##   <chr>          <dbl>
    ## 1 seasonal_naive  29.0

Exercise 8.9

For the same retail data, try an STL decomposition applied to the Box-Cox transformed series, followed by ETS on the seasonally adjusted data. How does that compare with your best previous forecasts on the test set?

Answer: STL decomposition of Box-Cox transformed data worsened the ETS model forecast using the test dataset, increasing RMSE from 14.7 in Exercise 8.8 part (e) to 19.2.

The reduced performance appears to be due to the data transformation; without transformation, the RMSE decreases to 9.8, which is 33% less than that from the ETS model from the previous exercise (14.7). This is consistent with the expectation that removal of seasonality enables the ETS model to better identify the long-term trend and make a more accurate forecast. Box-Cox transformation can be helpful, but it is not guaranteed to improve performance.

# Calculate lambda for Box-Cox transformation using training dataset
aus_retail_lambda <- aus_retail_train %>%
  features(Turnover, features = guerrero) %>%
  pull(lambda_guerrero)
# First apply STL decomposition to transformed training data,
# then use ETS model to fit seasonally adjust component ('season_adjust')
aus_retail_fit2 <- aus_retail_train %>% 
  model(
    STL = decomposition_model(
      STL(box_cox(Turnover, aus_retail_lambda)),
      # Seasonally adjusted data no longer has seasonality
      ETS(season_adjust ~ error('A') + trend('A') + season('N'))
    )
  )
# Forecast using test data
# Note: I specify the forecast() function from the fabletools package rather than 
# using the version in R generics to ensure forecasts are back transformed 
# (see Hyndman section 5.2)
aus_retail_ets_forecast2 <- fabletools::forecast(aus_retail_fit2, new_data = aus_retail_test)

# Assess model performance
accuracy(aus_retail_ets_forecast2, aus_retail_test) %>%
  select(model = `.model`, RMSE)
## # A tibble: 1 × 2
##   model  RMSE
##   <chr> <dbl>
## 1 STL    14.9

In contrast, a model without Box-Cox transformation has lower RMSE.

# Apply STL decomposition to training data (untransformed), 
# then use ETS model to fit seasonally adjust component ('season_adjust')
aus_retail_fit3 <- aus_retail_train %>% 
  model(
    STL = decomposition_model(
      STL(Turnover),
      ETS(season_adjust ~ error('A') + trend('A') + season('N'))
    )
  )
# Forecast using test data
aus_retail_ets_forecast3 <- fabletools::forecast(aus_retail_fit3, new_data = aus_retail_test)

# Assess model performance
accuracy(aus_retail_ets_forecast3, aus_retail_test) %>%
  select(model = `.model`, RMSE)
## # A tibble: 1 × 2
##   model  RMSE
##   <chr> <dbl>
## 1 STL    9.82

Session Details

sessionInfo()
## R version 4.5.2 (2025-10-31)
## Platform: aarch64-apple-darwin20
## Running under: macOS Tahoe 26.2
## 
## Matrix products: default
## BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: America/New_York
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] distributional_0.6.0 fable_0.5.0          ggtime_0.1.0        
##  [4] forecast_9.0.0       feasts_0.4.2         fabletools_0.5.1    
##  [7] tsibbledata_0.4.1    tsibble_1.1.6        lubridate_1.9.4     
## [10] forcats_1.0.1        stringr_1.6.0        dplyr_1.1.4         
## [13] purrr_1.2.1          readr_2.1.6          tidyr_1.3.2         
## [16] tibble_3.3.1         ggplot2_4.0.1        tidyverse_2.0.0     
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6        anytime_0.3.12      xfun_0.56          
##  [4] bslib_0.10.0        lattice_0.22-7      numDeriv_2016.8-1.1
##  [7] tzdb_0.5.0          quadprog_1.5-8      vctrs_0.7.1        
## [10] tools_4.5.2         generics_0.1.4      curl_7.0.0         
## [13] parallel_4.5.2      xts_0.14.1          pkgconfig_2.0.3    
## [16] RColorBrewer_1.1-3  S7_0.2.1            lifecycle_1.0.5    
## [19] compiler_4.5.2      farver_2.1.2        htmltools_0.5.9    
## [22] sass_0.4.10         yaml_2.3.12         pillar_1.11.1      
## [25] jquerylib_0.1.4     ellipsis_0.3.2      cachem_1.1.0       
## [28] nlme_3.1-168        fracdiff_1.5-3      tidyselect_1.2.1   
## [31] digest_0.6.39       stringi_1.8.7       labeling_0.4.3     
## [34] tseries_0.10-60     fastmap_1.2.0       grid_4.5.2         
## [37] colorspace_2.1-2    cli_3.6.5           magrittr_2.0.4     
## [40] utf8_1.2.6          withr_3.0.2         scales_1.4.0       
## [43] rappdirs_0.3.4      timechange_0.3.0    TTR_0.24.4         
## [46] rmarkdown_2.30      quantmod_0.4.28     otel_0.2.0         
## [49] nnet_7.3-20         timeDate_4052.112   progressr_0.18.0   
## [52] zoo_1.8-15          hms_1.1.4           urca_1.3-4         
## [55] evaluate_1.0.5      knitr_1.51          ggdist_3.3.3       
## [58] lmtest_0.9-40       rlang_1.1.7         Rcpp_1.1.1         
## [61] glue_1.8.0          rstudioapi_0.18.0   jsonlite_2.0.0     
## [64] R6_2.6.1