library(fpp3) # Forecasting package
library(ggplot2) # For visualizations
library(kableExtra) # Nicely formatted tables
library(feasts) LFMG Lab 5
1 Exponential Smoothing
2 Exercises
2.1 8.1
Consider the number of pigs slaughtered in Victoria, available in the aus_livestock dataset.
a. Use the ETS() function to estimate the equivalent model for simple exponential smoothing. Find the optimal values of α and ℓ0, and generate forecasts for the next four months.
First, we load required libraries.
Load and prepare data
pig_data <- aus_livestock %>%
filter(State == "Victoria", Animal == "Pigs") %>%
select(Month, Count) %>%
as_tsibble(index = Month)
autoplot(pig_data, Count) +
ggtitle("Pigs Slaughtered in Victoria") +
ylab("Count") +
xlab("Year")You can add options to executable code like this
pig_fit <- pig_data %>%
model(ETS(Count ~ error("A") + trend("N") + season("N")))
# save model parameters
pig_fit_params <- tidy(pig_fit)
pig_fit_params %>%
kable(caption = "ETS Parameters for Simple Exponential Smoothing") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| .model | term | estimate |
|---|---|---|
| ETS(Count ~ error("A") + trend("N") + season("N")) | alpha | 3.221247e-01 |
| ETS(Count ~ error("A") + trend("N") + season("N")) | l[0] | 1.006466e+05 |
We see that the alpha is around 0.322, meaning 32.2% of the new observation is used in updating forecasts, while 67.8% is based on past values. The ℓ0 was around 100,647, so that will be the starting point for the smoothing process.
Now that we have optimal parameters we can forecast the next 4 months.
forecast_pig_slag <- pig_fit %>%
forecast(h = "4 months", level = c(95))
autoplot(pig_data, Count) +
autolayer(forecast_pig_slag, level = 95, alpha = 0.5) +
ggtitle("Forecast: Pig Slaughtering in Victoria - 4 months") +
ylab("Count") +
xlab("Year")b. Compute a 95% prediction interval for the first forecast using \(y±1.96s\) where \(s\) is the standard deviation of the residuals. Compare your interval with the interval produced by R.
First we need to calculate the standard deviation of the residuals from the fitted model
residuals_pig <- pig_fit %>% augment() %>% pull(.resid)
s_pig <- sd(residuals_pig, na.rm = TRUE)
s_pig[1] 9344.666
Now we can extract the first value from the forecast, and compute the manual prediction interval
y_hat_1 <- forecast_pig_slag$.mean[1]
lower_bound <- y_hat_1 - (1.96 * s_pig)
upper_bound <- y_hat_1 + (1.96 * s_pig)
c("Lower Bound" = lower_bound, "Forecast" = y_hat_1, "Upper Bound" = upper_bound)Lower Bound Forecast Upper Bound
76871.01 95186.56 113502.10
Let’s compare that now:
intervals <- forecast_pig_slag %>%
hilo(95) %>%
as_tibble() # Convert to tibble for easier handling
# Separate lower and upper bounds
intervals <- intervals %>%
separate(`95%`, into = c("Lower", "Upper"), sep = ", ", convert = TRUE)
# Remove brackets and convert to numeric
r_lower <- as.numeric(gsub("\\[", "", intervals$Lower[1])) # Remove "[" from lower bound
r_upper <- as.numeric(gsub("\\]", "", intervals$Upper[1])) # Remove "]" from upper bound
# Print values
r_lower[1] 76854.79
r_upper[1] 113518.3
comparison <- tibble(
Method = c("Manual Calculation", "R Forecast"),
Lower = c(lower_bound, r_lower),
Upper = c(upper_bound, r_upper)
)
# Display as a table
library(kableExtra)
comparison %>%
kable(caption = "Comparison of Manual vs. R's Prediction Interval") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Method | Lower | Upper |
|---|---|---|
| Manual Calculation | 76871.01 | 113502.1 |
| R Forecast | 76854.79 | 113518.3 |
We see that we got almost identical results, meaning that we calculated the standard deviation correctly, and we can assume that the residuals are normally distributed. Differences may be due to rounding differences in standar deviation calculations.
2.2 8.5
Data set global_economy contains the annual Exports from many countries. Select one country to analyse.
a. Plot the Exports series and discuss the main features of the data.
For this exercise I want to focus on exports from Colombia. We’ll start by loading the data, plotting it and getting main descriptive statistics.
colombia_exports <- global_economy %>%
filter(Country == "Colombia") %>%
select(Year, Exports) %>%
as_tsibble(index = Year)
autoplot(colombia_exports, Exports) +
ggtitle("Annual Exports of Colombia") +
ylab("Exports (USD Billions)") +
xlab("Year") +
theme_minimal()We can see that the series does not show a clear upward or downward trend over the entire period, and instead exports fluctuate significantly over time. The exports increase and decrease in cycles, with sharp changes in certain periods, and the most significant drops occur around the early 1980s and mid-2010s.
On the other hand, the peaks around 1990 and early 2000s suggest strong export performance in those years, possibly due to high oil prices or trade agreements.
Finally, since the data is annual, there are no visible seasonal patterns.
b. Use an ETS(A,N,N) model to forecast the series, and plot the forecasts.
First we will output the estimated smoothing parameter α and the initial level \(ℓ0\)
colombia_ets <- colombia_exports %>%
model(ETS(Exports ~ error("A") + trend("N") + season("N")))
report(colombia_ets)Series: Exports
Model: ETS(A,N,N)
Smoothing parameters:
alpha = 0.9707053
Initial states:
l[0]
15.61444
sigma^2: 2.479
AIC AICc BIC
292.1271 292.5716 298.3084
With those values we can get the forecast and plot it
colombia_forecast <- colombia_ets %>%
forecast(h = "10 years", level = 95)
autoplot(colombia_forecast, colombia_exports) +
ggtitle("ETS Forecast: Colombia's Exports") +
ylab("Exports (USD Billions)") +
xlab("Year") +
theme_minimal()c. Compute the RMSE values for the training data.
accuracy_results <- colombia_ets %>%
accuracy()
rmse_value <- accuracy_results %>%
filter(.model == "ETS(Exports ~ error(\"A\") + trend(\"N\") + season(\"N\"))") %>%
pull(RMSE)
rmse_value[1] 1.547114
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.
First, we fit an ETS (AAN) model
colombia_ets_2 <- colombia_exports %>%
model(ETS(Exports ~ error("A") + trend("A") + season("N")))
report(colombia_ets_2)Series: Exports
Model: ETS(A,A,N)
Smoothing parameters:
alpha = 0.9283752
beta = 0.0001000128
Initial states:
l[0] b[0]
14.36393 0.01625495
sigma^2: 2.5994
AIC AICc BIC
296.7668 297.9207 307.0690
Generate forecast and plot,
colombia_forecast <- colombia_ets %>%
forecast(h = "10 years", level = 95) %>%
as_tibble() %>% # Convert to tibble first
select(Year, Exports, .mean) %>% # Ensure Year exists
mutate(Model = "ETS(A,N,N)") # Add model label
colombia_forecast_2 <- colombia_ets_2 %>%
forecast(h = "10 years", level = 95) %>%
as_tibble() %>% # Convert to tibble first
select(Year, Exports, .mean) %>% # Ensure Year exists
mutate(Model = "ETS(A,A,N)") # Add model label
combined_forecasts <- bind_rows(colombia_forecast, colombia_forecast_2) %>%
as_tsibble(index = Year, key = Model) # Convert back to tsibble
autoplot(colombia_exports, Exports) +
geom_line(data = combined_forecasts, aes(x = Year, y = .mean, color = Model), linewidth = 1) +
ggtitle("ETS Forecast Comparison: With & Without Trend") +
ylab("Exports (USD Billions)") +
xlab("Year") +
theme_minimal()And get RMS so we can later compare both models:
accuracy_2 <- colombia_ets_2 %>%
accuracy()
rmse_2 <- accuracy_2 %>%
pull(RMSE)
comparison_rmse <- tibble(
Model = c("ETS(A,N,N)", "ETS(A,A,N)"),
RMSE = c(rmse_value, rmse_2)
)
comparison_rmse %>%
kable(caption = "RMSE Comparison: ETS(A,N,N) vs ETS(A,A,N)") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | RMSE |
|---|---|
| ETS(A,N,N) | 1.547114 |
| ETS(A,A,N) | 1.555670 |
ETS(A,N,N) has a slightly lower RMSE than ETS(A,A,N), meaning it fits the training data marginally better. Adding a trend (ETS(A,A,N)) did not significantly improve accuracy. Since ETS(A,A,N) uses one more parameter, this extra complexity does not justify the small RMSE difference.
Given that export levels fluctuate but do not show a strong, consistent trend, ETS(A,N,N) (Simple Exponential Smoothing) is the preferred model.
e. Compare the forecasts from both methods. Which do you think is best?
The forecast for ETS (ANN) is flat (constant) because this model assumes no trend. The prediction remains at a constant level with increasing uncertainty over time. At the same time, the ETS (AAN) forecast shows a very slight upward trend as it may have detected a weak trend in the data, however, since the trend is minimal, it may not significantly improve accuracy over time.
Since the exports data for Colombia does not have a strong, consistent trend, the ETS (ANN) seems like a better choice for this dataset.
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.
Similar to 8.1 we start by extracting first forecasted value for each model.
col_y_hat_1_ets <- colombia_forecast$.mean[1] # first forecast from ETS(A,N,N)
col_y_hat_1_ets_2 <- colombia_forecast_2$.mean[1] # first forecast from ETS(A,A,N)Since we already had RMSE values computed we can plug them to get upper and lower bounds for each model.
col_lower_ets <- col_y_hat_1_ets - (1.96 * rmse_value)
col_upper_ets <- col_y_hat_1_ets + (1.96 * rmse_value)
col_lower_ets_2 <- col_y_hat_1_ets_2 - (1.96 * rmse_2)
col_upper_ets_2 <- col_y_hat_1_ets_2 + (1.96 * rmse_2)
manual_intervals <- tibble(
Model = c("ETS(A,N,N)", "ETS(A,A,N)"),
Lower = c(col_lower_ets, col_lower_ets_2),
Forecast = c(col_y_hat_1_ets, col_y_hat_1_ets_2),
Upper = c(col_upper_ets, col_upper_ets_2)
)
manual_intervals %>%
kable(caption = "Manual 95% Prediction Intervals") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | Lower | Forecast | Upper |
|---|---|---|---|
| ETS(A,N,N) | 11.52101 | 14.55335 | 17.58570 |
| ETS(A,A,N) | 11.52638 | 14.57549 | 17.62461 |
Now, we get the values straight from R to later compare.
# Extract 95% prediction intervals for ETS(A,N,N)
colombia_forecast <- colombia_ets %>%
forecast(h = "10 years", level = 95)
intervals <- colombia_forecast %>%
hilo(95) %>%
as_tibble() # Convert only AFTER extracting intervals
# Separate lower and upper bounds
intervals <- intervals %>%
separate(`95%`, into = c("Lower", "Upper"), sep = ", ", convert = TRUE)
# Remove brackets and convert to numeric
col_r_lower_ets <- as.numeric(gsub("\\[", "", intervals$Lower[1]))
col_r_upper_ets <- as.numeric(gsub("\\]", "", intervals$Upper[1]))
# Extract 95% prediction intervals for ETS(A,N,N)
colombia_forecast_2 <- colombia_ets_2 %>%
forecast(h = "10 years", level = 95)
intervals_2 <- colombia_forecast_2 %>%
hilo(95) %>%
as_tibble() # Convert only AFTER extracting intervals
# Separate lower and upper bounds
intervals_2 <- intervals_2 %>%
separate(`95%`, into = c("Lower", "Upper"), sep = ", ", convert = TRUE)
# Remove brackets and convert to numeric
col_r_lower_ets_2 <- as.numeric(gsub("\\[", "", intervals_2$Lower[1]))
col_r_upper_ets_2 <- as.numeric(gsub("\\]", "", intervals_2$Upper[1])) Putting R values in a table:
# Create comparison table
col_r_intervals <- tibble(
Model = c("ETS(A,N,N)", "ETS(A,A,N)"),
Lower = c(col_r_lower_ets, col_r_lower_ets_2),
Forecast = c(col_y_hat_1_ets, col_y_hat_1_ets_2),
Upper = c(col_r_upper_ets, col_r_upper_ets_2)
)
# Display table
col_r_intervals %>%
kable(caption = "R Forecast 95% Prediction Intervals") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | Lower | Forecast | Upper |
|---|---|---|---|
| ETS(A,N,N) | 11.46739 | 14.55335 | 17.63931 |
| ETS(A,A,N) | 11.41553 | 14.57549 | 17.73546 |
So we see in the final comparison:
The forecasted values (
.mean) are identical since both methods use the same ETS models.The lower and upper bounds are slightly different, likely due to small rounding differences and how R calculates variance in forecast distributions.
2.3 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.
[Hint: use a relatively large value of h when forecasting, so you can clearly see the differences between the various options when plotting the forecasts.]
Let’s load and prepare the data
china_gdp <- global_economy %>%
filter(Country == "China") %>%
select(Year, GDP) %>%
as_tsibble(index = Year)
autoplot(china_gdp, GDP) +
ggtitle("Chinese GDP Over Time") +
ylab("GDP (USD Billions)") +
xlab("Year") +
theme_minimal()Now let’s start fitting different models:
china_ets_ANN <- china_gdp %>% model(ETS(GDP ~ error("A") + trend("N") + season("N")))
china_ets_AAN <- china_gdp %>% model(ETS(GDP ~ error("A") + trend("A") + season("N")))
china_ets_AAdN <- china_gdp %>% model(ETS(GDP ~ error("A") + trend("Ad") + season("N"))) # Damped trend
china_ets_boxcox <- china_gdp %>% model(ETS(box_cox(GDP, 0.3) ~ error("A") + trend("A") + season("N"))) # Box-Cox transformationGenerate some forecasts for each for the next 30 years:
# Forecasting GDP for 30 years
china_forecast_ANN <- china_ets_ANN %>% forecast(h = "30 years")
china_forecast_AAN <- china_ets_AAN %>% forecast(h = "30 years")
china_forecast_AAdN <- china_ets_AAdN %>% forecast(h = "30 years")
china_forecast_boxcox <- china_ets_boxcox %>% forecast(h = "30 years")And plotting:
china_forecast_ANN <- china_forecast_ANN %>% mutate(Model = "ETS(A,N,N)")
china_forecast_AAN <- china_forecast_AAN %>% mutate(Model = "ETS(A,A,N)")
china_forecast_AAdN <- china_forecast_AAdN %>% mutate(Model = "ETS(A,Ad,N)")
china_forecast_boxcox <- china_forecast_boxcox %>% mutate(Model = "Box-Cox ETS(A,A,N)")
china_forecast_combined <- bind_rows(
china_forecast_ANN, china_forecast_AAN, china_forecast_AAdN, china_forecast_boxcox
)
china_forecast_combined <- as_tsibble(china_forecast_combined, index = Year, key = Model)
autoplot(china_gdp, GDP) +
geom_line(data = china_forecast_combined, aes(x = Year, y = .mean, color = Model), linewidth = 1)+
ggtitle("Comparison of ETS Model Forecasts for Chinese GDP") +
ylab("GDP (USD Billions)") +
xlab("Year") +
theme_minimal()It seems models with “A” (Additive Trend) predict continued GDP growth, as ANN is the only one showing a flat line (no growth), which isn’t realistic.
Also, damped trends (Ad) seems to assume growth slows over time, as opposed to the Box-Cox which stabilizes variance but also can introduce strong curvature in forecast.
Personally, I would say that for this dataset ETS(A,Ad,N) (Damped trend) is the most realistic for forecasting long-term GDP.
2.4 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?
Let’s load and prepare this dataset:
gas_data <- aus_production %>%
select(Quarter, Gas) %>%
as_tsibble(index = Quarter)
autoplot(gas_data, Gas) +
ggtitle("Australian Quarterly Gas Production") +
ylab("Gas Production (Petajoules)") +
xlab("Year") +
theme_minimal()So far we can see from the plot that the data has a strong seasonal pattern, and the magnitude of seasonal fluctuations increases over time, suggesting multiplicative seasonality.
This can guide us in choosing a model, since seasonality is not only clearly present but also the variation increases over time, we need to try ETS models with multiplicative seasonality.
If we used additive seasonality it would assume seasonal variations remain constant over time and would not capture the increasing magnitude of fluctuations seen in later years.
So the next step is fitting the models and forecasting.
gas_ets_AANM <- gas_data %>% model(ETS(Gas ~ error("A") + trend("A") + season("M"))) # Multiplicative
gas_ets_AAdM <- gas_data %>% model(ETS(Gas ~ error("A") + trend("Ad") + season("M"))) # Multiplicative damped
gas_forecast_AANM <- gas_ets_AANM %>% forecast(h = "8 years")
gas_forecast_AAdM <- gas_ets_AAdM %>% forecast(h = "8 years")And then we plot
gas_forecast_AANM <- gas_forecast_AANM %>% mutate(Model = "ETS(A,A,M)")
gas_forecast_AAdM <- gas_forecast_AAdM %>% mutate(Model = "ETS(A,Ad,M)")
gas_forecast_combined <- bind_rows(gas_forecast_AANM, gas_forecast_AAdM)
gas_forecast_combined <- as_tsibble(gas_forecast_combined, index = Quarter, key = Model)
autoplot(gas_data, Gas) +
geom_line(data = gas_forecast_combined, aes(x = Quarter, y = .mean, color = Model), linewidth = 1) +
ggtitle("Comparison of ETS Models for Australian Gas Production") +
ylab("Gas Production (Petajoules)") +
xlab("Year") +
theme_minimal()From the plot we can see that when using a damped trend (blue), growth slows over time and future gas production does not rise exponentially. This makes sense because physical and economic limits can restrict unlimited gas production growth, making it more realistic for long-term forecasting.
2.5 8.8
Recall your retail time series data (from Exercise 7 in Section 2.10)
Once again this retail time series making a comeback.
So let’s load and prepare the data used back then (random seed and everything).
# seed
set.seed(86863)
# Use same random time series from aus_retail
myseries <- aus_retail %>%
filter(`Series ID` == sample(aus_retail$`Series ID`, 1))
glimpse(myseries)Rows: 441
Columns: 5
Key: State, Industry [1]
$ State <chr> "New South Wales", "New South Wales", "New South Wales", "…
$ Industry <chr> "Takeaway food services", "Takeaway food services", "Takea…
$ `Series ID` <chr> "A3349792X", "A3349792X", "A3349792X", "A3349792X", "A3349…
$ Month <mth> 1982 Apr, 1982 May, 1982 Jun, 1982 Jul, 1982 Aug, 1982 Sep…
$ Turnover <dbl> 85.4, 84.8, 80.7, 82.4, 80.7, 82.1, 87.3, 87.4, 97.2, 93.0…
autoplot(myseries, Turnover) +
labs(title = "Monthly Retail Turnover",
x = "Year", y = "Turnover") +
theme_minimal()a) Why is multiplicative seasonality necessary for this series?
For one, seasonal variations increase over time. If we used additive seasonality, we would assume the seasonal variations remain constant over time, which clearly isn’t the case here.
But also retail sales exhibit proportional growth, and multiplicative seasonality allows seasonal patterns to scale appropriately with the increasing trend.
b) Apply Holt-Winters’ multiplicative method to the data. Experiment with making the trend damped.
Fitting the models:
retail_ets_AAM <- myseries %>%
model(ETS(Turnover ~ error("A") + trend("A") + season("M")))
retail_ets_AAdM <- myseries %>%
model(ETS(Turnover ~ error("A") + trend("Ad") + season("M"))) # Damped trendForecasting:
retail_forecast_AAM <- retail_ets_AAM %>% forecast(h = "5 years")
retail_forecast_AAdM <- retail_ets_AAdM %>% forecast(h = "5 years")And plotting:
retail_forecast_AAM <- retail_forecast_AAM %>% mutate(Model = "ETS(A,A,M)")
retail_forecast_AAdM <- retail_forecast_AAdM %>% mutate(Model = "ETS(A,Ad,M)")
retail_forecast_combined <- bind_rows(retail_forecast_AAM, retail_forecast_AAdM)
retail_forecast_combined <- as_tsibble(retail_forecast_combined, index = Month, key = Model)
autoplot(myseries, Turnover) +
geom_line(data = retail_forecast_combined, aes(x = Month, y = .mean, color = Model), linewidth = 1) +
ggtitle("Comparison of Holt-Winters Multiplicative Models for Retail Turnover") +
ylab("Turnover") +
xlab("Year") +
theme_minimal()c) Compare the RMSE of the one-step forecasts from the two methods. Which do you prefer?
accuracy_AAM <- accuracy(retail_ets_AAM)
accuracy_AAdM <- accuracy(retail_ets_AAdM)
rmse_AAM <- accuracy_AAM %>% filter(.model == "ETS(Turnover ~ error(\"A\") + trend(\"A\") + season(\"M\"))") %>% pull(RMSE)
rmse_AAdM <- accuracy_AAdM %>% filter(.model == "ETS(Turnover ~ error(\"A\") + trend(\"Ad\") + season(\"M\"))") %>% pull(RMSE)
rmse_comparison <- tibble(
Model = c("ETS(A,A,M)", "ETS(A,Ad,M)"),
RMSE = c(rmse_AAM, rmse_AAdM)
)
rmse_comparison %>%
kable(caption = "RMSE Comparison: Multiplicative vs. Damped") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | RMSE |
|---|---|
| ETS(A,A,M) | 9.719140 |
| ETS(A,Ad,M) | 9.509444 |
For this data-set the damped trend model produces more accurate forecasts for one-step-ahead predictions as it accounts for the possibility of slowing growth in retail trends.
d) Check that the residuals from the best method look like white noise.
In this case it means checking the residuals of the ETS (A,Ad,M) method.
residuals_AAdM <- augment(retail_ets_AAdM) %>%
select(Month, .resid)Plotting residuals:
# Residual time series plot
autoplot(residuals_AAdM, .resid) +
ggtitle("Residuals from ETS(A,Ad,M) Model") +
ylab("Residuals") +
xlab("Year") +
theme_minimal()# Histogram of residuals
ggplot(residuals_AAdM, aes(x = .resid)) +
geom_histogram(bins = 30, fill = "lightblue", color = "black") +
ggtitle("Histogram of Residuals") +
xlab("Residuals") +
theme_minimal()# ACF plot to check for autocorrelation
residuals_AAdM %>%
ACF(.resid) %>%
autoplot() +
ggtitle("Autocorrelation of Residuals") +
theme_minimal()# Perform Ljung-Box test
ljung_box_test <- residuals_AAdM %>%
features(.resid, ljung_box, lag = 10)
ljung_box_test# A tibble: 1 × 2
lb_stat lb_pvalue
<dbl> <dbl>
1 17.2 0.0709
Residuals over time fluctuate around zero, with no clear trend suggesting the model is capturing most of the structure in the data.
The histogram shows residuals as approximately normally distributed.
ACF plot shows most bars falling within the blue significance limits, meaning no strong autocorrelation.
Finally, the p-value is bigger than 0.05, which fails to reject null hypothesis, confirming that the residuals behave like white noise.
e) 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?
To find the RMSE we have to train the model up to 2010 first.
# Training set
train_data <- myseries %>%
filter(Month <= yearmonth("2010 Dec"))
# Test set
test_data <- myseries %>%
filter(Month > yearmonth("2010 Dec"))
# Train the model
retail_ets_train <- train_data %>%
model(ETS(Turnover ~ error("A") + trend("Ad") + season("M")))Then forecast for ETS (A,Ad,M) model.
ets_forecast <- retail_ets_train %>%
forecast(h = nrow(test_data)) # Forecast same length as test setWe will also compute the SNAIVE, assuming that the forecast is equal to the value from the same season in the previous year.
# seasonal naive model
seasonal_naive_model <- train_data %>%
model(SNAIVE(Turnover ~ lag("year")))
# seasonal naive forecasts
snaive_forecast <- seasonal_naive_model %>%
forecast(h = nrow(test_data))Finally we compute the RMSE for both models
# accuracy for ETS model
accuracy_ets <- accuracy(ets_forecast, test_data)
# accuracy for SNAIVE model
accuracy_snaive <- accuracy(snaive_forecast, test_data)
# Extract RMSE values
rmse_ets <- accuracy_ets %>% pull(RMSE)
rmse_snaive <- accuracy_snaive %>% pull(RMSE)
rmse_comparison <- tibble(
Model = c("ETS(A,Ad,M)", "Seasonal Naïve"),
RMSE = c(rmse_ets, rmse_snaive)
)
rmse_comparison %>%
kable(caption = "Test Set RMSE: ETS(A,Ad,M) vs. Seasonal Naïve") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | RMSE |
|---|---|
| ETS(A,Ad,M) | 78.32102 |
| Seasonal Naïve | 96.80193 |
The ETS(A,Ad,M) has a lower RMSE (78.32 vs. 96.80) than the SNAIVE model. This means the ETS model forecasts retail turnover more accurately than the SNAIVE approach, probably because it captures both trend and seasonality, improving predictive power.
2.6 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?
Let’s start by applying STL decomposition with Box-Cox transformation.
# Find optimal Box-Cox lambda for variance stabilization
lambda_retail <- myseries %>%
features(Turnover, features = guerrero) %>%
pull(lambda_guerrero)
# STL decomposition with Box-Cox transformation
Decomp <- myseries %>%
model(STL(box_cox(Turnover, lambda_retail) ~ season(window = "periodic"), robust = TRUE)) %>%
components()
# Plot STL decomposition
autoplot(Decomp) +
ggtitle("STL with Box-Cox Transformation")Now extracting the seasonally adjusted data.
myseries$Turnover_sa <- Decomp$season_adjustSplitting the training and test data.
train_data <- myseries %>% filter(year(Month) < 2011)
test_data <- myseries %>% filter(year(Month) >= 2011)At the same time we fit the ETS (A,Ad,M) model on seasonally adjusted data
fit_stl_ets <- train_data %>%
model(ETS(Turnover_sa ~ error("A") + trend("Ad") + season("M")))
# Check residuals to confirm model validity
fit_stl_ets %>% gg_tsresiduals() +
ggtitle("Residuals of ETS(A,Ad,M) Model on Seasonally Adjusted Data")Then we forecast test set
myseries_test <- myseries %>%
filter(year(Month) >= 2011) %>%
select(State, Industry, Month, Turnover_sa)
fc_stl_ets <- fit_stl_ets %>%
forecast(new_data = myseries_test)Now, train the previous ETS(A,Ad,M) model on raw data and forecast
# Train the previous ETS(A,Ad,M) model on raw data
retail_ets_train <- train_data %>%
model(ETS(Turnover ~ error("A") + trend("Ad") + season("M")))
# Forecast the test set with the previous ETS(A,Ad,M) model
ets_forecast <- retail_ets_train %>%
forecast(h = nrow(test_data))Finally we compute RMSE and compare to the previous model
# Compute RMSE for previous ETS(A,Ad,M) model on test set
rmse_ets_test <- ets_forecast %>%
accuracy(test_data) %>%
filter(.type == "Test") %>%
pull(RMSE)
# Compute RMSE for STL + ETS(A,Ad,M) on test set
rmse_stl_ets_test <- fc_stl_ets %>%
accuracy(test_data) %>%
filter(.type == "Test") %>%
pull(RMSE)
# Compute RMSE for ETS(A,Ad,M) on training set
rmse_stl_ets <- fit_stl_ets %>%
accuracy() %>%
filter(.type == "Training") %>%
pull(RMSE)
# Compare RMSE
rmse_comparison <- tibble(
Model = c("ETS(A,Ad,M) on Raw Data", "STL + ETS(A,Ad,M)"),
RMSE_Training = c(rmse_ets, rmse_stl_ets),
RMSE_Test = c(rmse_ets_test, rmse_stl_ets_test)
)
# Display RMSE comparison
rmse_comparison %>%
kable(caption = "RMSE Comparison: ETS(A,Ad,M) vs. STL + ETS(A,Ad,M)") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Model | RMSE_Training | RMSE_Test |
|---|---|---|
| ETS(A,Ad,M) on Raw Data | 78.3210168 | 79.2024235 |
| STL + ETS(A,Ad,M) | 0.0435141 | 0.1558501 |
The results indicate a huge improvement in RMSE when using STL + ETS(A,Ad,M) compared to the raw ETS(A,Ad,M) model, meaning the new model generalizes better to unseen data.