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
<- aus_livestock %>%
pig_data 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_data %>%
pig_fit model(ETS(Count ~ error("A") + trend("N") + season("N")))
# save model parameters
<- tidy(pig_fit)
pig_fit_params %>%
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.
<- pig_fit %>%
forecast_pig_slag 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
<- pig_fit %>% augment() %>% pull(.resid)
residuals_pig
<- sd(residuals_pig, na.rm = TRUE)
s_pig
s_pig
[1] 9344.666
Now we can extract the first value from the forecast, and compute the manual prediction interval
<- forecast_pig_slag$.mean[1]
y_hat_1
<- y_hat_1 - (1.96 * s_pig)
lower_bound <- y_hat_1 + (1.96 * s_pig)
upper_bound
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:
<- forecast_pig_slag %>%
intervals 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
<- as.numeric(gsub("\\[", "", intervals$Lower[1])) # Remove "[" from lower bound
r_lower <- as.numeric(gsub("\\]", "", intervals$Upper[1])) # Remove "]" from upper bound
r_upper
# Print values
r_lower
[1] 76854.79
r_upper
[1] 113518.3
<- tibble(
comparison 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.
<- global_economy %>%
colombia_exports 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_exports %>%
colombia_ets 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_ets %>%
colombia_forecast 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.
<- colombia_ets %>%
accuracy_results accuracy()
<- accuracy_results %>%
rmse_value 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_exports %>%
colombia_ets_2 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_ets %>%
colombia_forecast 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_ets_2 %>%
colombia_forecast_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
<- bind_rows(colombia_forecast, colombia_forecast_2) %>%
combined_forecasts 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:
<- colombia_ets_2 %>%
accuracy_2 accuracy()
<- accuracy_2 %>%
rmse_2 pull(RMSE)
<- tibble(
comparison_rmse 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.
<- colombia_forecast$.mean[1] # first forecast from ETS(A,N,N)
col_y_hat_1_ets <- colombia_forecast_2$.mean[1] # first forecast from ETS(A,A,N) col_y_hat_1_ets_2
Since we already had RMSE values computed we can plug them to get upper and lower bounds for each model.
<- col_y_hat_1_ets - (1.96 * rmse_value)
col_lower_ets <- col_y_hat_1_ets + (1.96 * rmse_value)
col_upper_ets
<- col_y_hat_1_ets_2 - (1.96 * rmse_2)
col_lower_ets_2 <- col_y_hat_1_ets_2 + (1.96 * rmse_2)
col_upper_ets_2
<- tibble(
manual_intervals 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_ets %>%
colombia_forecast forecast(h = "10 years", level = 95)
<- colombia_forecast %>%
intervals 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
<- as.numeric(gsub("\\[", "", intervals$Lower[1]))
col_r_lower_ets <- as.numeric(gsub("\\]", "", intervals$Upper[1]))
col_r_upper_ets
# Extract 95% prediction intervals for ETS(A,N,N)
<- colombia_ets_2 %>%
colombia_forecast_2 forecast(h = "10 years", level = 95)
<- colombia_forecast_2 %>%
intervals_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
<- as.numeric(gsub("\\[", "", intervals_2$Lower[1]))
col_r_lower_ets_2 <- as.numeric(gsub("\\]", "", intervals_2$Upper[1])) col_r_upper_ets_2
Putting R values in a table:
# Create comparison table
<- tibble(
col_r_intervals 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
<- global_economy %>%
china_gdp 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_gdp %>% model(ETS(GDP ~ error("A") + trend("N") + season("N")))
china_ets_ANN <- china_gdp %>% model(ETS(GDP ~ error("A") + trend("A") + season("N")))
china_ets_AAN <- china_gdp %>% model(ETS(GDP ~ error("A") + trend("Ad") + season("N"))) # Damped trend
china_ets_AAdN <- china_gdp %>% model(ETS(box_cox(GDP, 0.3) ~ error("A") + trend("A") + season("N"))) # Box-Cox transformation china_ets_boxcox
Generate some forecasts for each for the next 30 years:
# Forecasting GDP for 30 years
<- china_ets_ANN %>% forecast(h = "30 years")
china_forecast_ANN <- china_ets_AAN %>% forecast(h = "30 years")
china_forecast_AAN <- china_ets_AAdN %>% forecast(h = "30 years")
china_forecast_AAdN <- china_ets_boxcox %>% forecast(h = "30 years") china_forecast_boxcox
And plotting:
<- china_forecast_ANN %>% mutate(Model = "ETS(A,N,N)")
china_forecast_ANN <- china_forecast_AAN %>% mutate(Model = "ETS(A,A,N)")
china_forecast_AAN <- china_forecast_AAdN %>% mutate(Model = "ETS(A,Ad,N)")
china_forecast_AAdN <- china_forecast_boxcox %>% mutate(Model = "Box-Cox ETS(A,A,N)")
china_forecast_boxcox
<- bind_rows(
china_forecast_combined
china_forecast_ANN, china_forecast_AAN, china_forecast_AAdN, china_forecast_boxcox
)
<- as_tsibble(china_forecast_combined, index = Year, key = Model)
china_forecast_combined
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:
<- aus_production %>%
gas_data 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_data %>% model(ETS(Gas ~ error("A") + trend("A") + season("M"))) # Multiplicative
gas_ets_AANM <- gas_data %>% model(ETS(Gas ~ error("A") + trend("Ad") + season("M"))) # Multiplicative damped
gas_ets_AAdM
<- gas_ets_AANM %>% forecast(h = "8 years")
gas_forecast_AANM <- gas_ets_AAdM %>% forecast(h = "8 years") gas_forecast_AAdM
And then we plot
<- gas_forecast_AANM %>% mutate(Model = "ETS(A,A,M)")
gas_forecast_AANM <- gas_forecast_AAdM %>% mutate(Model = "ETS(A,Ad,M)")
gas_forecast_AAdM
<- bind_rows(gas_forecast_AANM, gas_forecast_AAdM)
gas_forecast_combined
<- as_tsibble(gas_forecast_combined, index = Quarter, key = Model)
gas_forecast_combined
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
<- aus_retail %>%
myseries 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:
<- myseries %>%
retail_ets_AAM model(ETS(Turnover ~ error("A") + trend("A") + season("M")))
<- myseries %>%
retail_ets_AAdM model(ETS(Turnover ~ error("A") + trend("Ad") + season("M"))) # Damped trend
Forecasting:
<- retail_ets_AAM %>% forecast(h = "5 years")
retail_forecast_AAM <- retail_ets_AAdM %>% forecast(h = "5 years") retail_forecast_AAdM
And plotting:
<- retail_forecast_AAM %>% mutate(Model = "ETS(A,A,M)")
retail_forecast_AAM <- retail_forecast_AAdM %>% mutate(Model = "ETS(A,Ad,M)")
retail_forecast_AAdM
<- bind_rows(retail_forecast_AAM, retail_forecast_AAdM)
retail_forecast_combined
<- as_tsibble(retail_forecast_combined, index = Month, key = Model)
retail_forecast_combined
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(retail_ets_AAM)
accuracy_AAM <- accuracy(retail_ets_AAdM)
accuracy_AAdM
<- accuracy_AAM %>% filter(.model == "ETS(Turnover ~ error(\"A\") + trend(\"A\") + season(\"M\"))") %>% pull(RMSE)
rmse_AAM <- accuracy_AAdM %>% filter(.model == "ETS(Turnover ~ error(\"A\") + trend(\"Ad\") + season(\"M\"))") %>% pull(RMSE)
rmse_AAdM
<- tibble(
rmse_comparison 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.
<- augment(retail_ets_AAdM) %>%
residuals_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
<- residuals_AAdM %>%
ljung_box_test 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
<- myseries %>%
train_data filter(Month <= yearmonth("2010 Dec"))
# Test set
<- myseries %>%
test_data filter(Month > yearmonth("2010 Dec"))
# Train the model
<- train_data %>%
retail_ets_train model(ETS(Turnover ~ error("A") + trend("Ad") + season("M")))
Then forecast for ETS (A,Ad,M) model.
<- retail_ets_train %>%
ets_forecast forecast(h = nrow(test_data)) # Forecast same length as test set
We 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
<- train_data %>%
seasonal_naive_model model(SNAIVE(Turnover ~ lag("year")))
# seasonal naive forecasts
<- seasonal_naive_model %>%
snaive_forecast forecast(h = nrow(test_data))
Finally we compute the RMSE for both models
# accuracy for ETS model
<- accuracy(ets_forecast, test_data)
accuracy_ets
# accuracy for SNAIVE model
<- accuracy(snaive_forecast, test_data)
accuracy_snaive
# Extract RMSE values
<- accuracy_ets %>% pull(RMSE)
rmse_ets <- accuracy_snaive %>% pull(RMSE)
rmse_snaive
<- tibble(
rmse_comparison 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
<- myseries %>%
lambda_retail features(Turnover, features = guerrero) %>%
pull(lambda_guerrero)
# STL decomposition with Box-Cox transformation
<- myseries %>%
Decomp 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.
$Turnover_sa <- Decomp$season_adjust myseries
Splitting the training and test data.
<- myseries %>% filter(year(Month) < 2011)
train_data <- myseries %>% filter(year(Month) >= 2011) test_data
At the same time we fit the ETS (A,Ad,M) model on seasonally adjusted data
<- train_data %>%
fit_stl_ets model(ETS(Turnover_sa ~ error("A") + trend("Ad") + season("M")))
# Check residuals to confirm model validity
%>% gg_tsresiduals() +
fit_stl_ets ggtitle("Residuals of ETS(A,Ad,M) Model on Seasonally Adjusted Data")
Then we forecast test set
<- myseries %>%
myseries_test filter(year(Month) >= 2011) %>%
select(State, Industry, Month, Turnover_sa)
<- fit_stl_ets %>%
fc_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
<- train_data %>%
retail_ets_train model(ETS(Turnover ~ error("A") + trend("Ad") + season("M")))
# Forecast the test set with the previous ETS(A,Ad,M) model
<- retail_ets_train %>%
ets_forecast 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
<- ets_forecast %>%
rmse_ets_test accuracy(test_data) %>%
filter(.type == "Test") %>%
pull(RMSE)
# Compute RMSE for STL + ETS(A,Ad,M) on test set
<- fc_stl_ets %>%
rmse_stl_ets_test accuracy(test_data) %>%
filter(.type == "Test") %>%
pull(RMSE)
# Compute RMSE for ETS(A,Ad,M) on training set
<- fit_stl_ets %>%
rmse_stl_ets accuracy() %>%
filter(.type == "Training") %>%
pull(RMSE)
# Compare RMSE
<- tibble(
rmse_comparison 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.