library(fpp3)
library(magrittr)
library(tidyverse)
library(patchwork)
library(fable) #forecast
library(flextable)
Consider the the number of pigs slaughtered in Victoria, available in the aus_livestock dataset.
#unique(aus_livestock$Animal)
<-aus_livestock%>%
pigfilter(State == 'Victoria', Animal == 'Pigs')
%>%
(pigautoplot(color='steelblue')+
labs(title='Figure 1. Number of Pigs Slaughtered in Victoria, AUS')+
theme_classic())
Use the ETS() function to estimate the equivalent model for simple exponential smoothing. Find the optimal values of
α and \(l_0\) (fitted value at time 1), and generate forecasts for the next four months.
# build model
<-pig%>%
pig_fitmodel(ETS(Count ~ error("A") + trend("N") + season("N")))
report(pig_fit) # model stats
## Series: Count
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.3221247
##
## Initial states:
## l[0]
## 100646.6
##
## sigma^2: 87480760
##
## AIC AICc BIC
## 13737.10 13737.14 13750.07
# forecast predictions
<-pig_fit%>%
for_pigforecast(h=4)
# plot predictions
%>%
for_pig autoplot(pig)+
labs(y = 'Mean Count', title='Figure 2. 4Yr Forecast: Pigs')+
theme_classic()
Compute a 95% prediction interval for the first forecast using \(\hat{y}\) ±1.96 s where s is the standard deviation of the residuals. Compare your interval with the interval produced by R.
\(\hat{y} + 1.96σ_1\) = 113502.1
\(\hat{y} - 1.96σ_1\) = 76871.01
Base R prediction intervals
\(\hat{y} + 1.96σ_1\) = 113518.33
\(\hat{y} - 1.96σ_1\) = 76854.79
The one-step prediction interval estimated by R is slightly wider than that calculated by hand.
# collect first forecast
<-head(for_pig$.mean, 1)
yhat
# calculate standard deviation
<-augment(pig_fit)
res
<-sd(res$.resid)
stdev
# calculate prediction interval manually
<-yhat+1.96*stdev # 113502.1
plus_int<-yhat-1.96*stdev # 76871.01 minus_int
# calculate prediction interval automatically -- ref: https://fabletools.tidyverts.org/reference/forecast.html
<-for_pig%>%
rpredhilo()%>%
as.data.frame()%>%
select('95%')%>%
head(1)%>%
unlist()
Data set global_economy contains the annual Exports from many countries. Select one country to analyse.
Plot the Exports series and discuss the main features of the data.
The Exports series for Mexico displays an upward trend beginning during the 1970s. There does not appear to be an indication of a seasonal signal. However, the series may include an irregular multi-year cyclic signal.
# select Mexico for analysis
<-global_economy%>%
Mexfilter(Country %in% 'Mexico')%>%
select(Exports)
%>%
Mexautoplot(Exports, color = 'steelblue')+
labs(title='Figure 3. Exports from Mexico')+
theme_classic()
Use an ETS(A,N,N) model to forecast the series, and plot the forecasts.
# build model
<- Mex %>%
Mex_mod model(ETS(Exports ~ error("A") + trend("N") + season("N")))
# forecast 10 years
<- Mex_mod %>%
Mex_fc forecast(h = 10)
#plot with forecast
%>%
Mex_fc autoplot(Mex, color='steelblue') +
labs(title="Figure 4. Mexico, ETS(A,N,N)") +
theme_classic()
Compute the RMSE values for the training data.
The RMSE for our training set is 2.154. The RMSE is the square root of the mean of the squared differences between actual and predicted data. Or, in other terms, the variance of the residuals
# compute forecast accuracy measures
<-Mex_mod %>% accuracy() RMSE1
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.
# build model
<- Mex %>%
Mex_AAN_mod model(ETS(Exports ~ error("A") + trend("A") + season("N")))
# forecast 10 years
<- Mex_mod %>%
Mex_AAN_fc forecast(h = 10)
#plot with forecast
%>%
Mex_AAN_fc autoplot(Mex, color='steelblue') +
labs(title="Figure 4. Mexico, ETS(A,N,N)") +
theme_classic()
# compute forecast accuracy measures
<-Mex_AAN_mod %>% accuracy() # RMSE = 2.093) RMSE2
Compare the forecasts from both methods. Which do you think is best?
We can use the RMSE measures to compare the accuracy of each model relative to the training set. The ANN model has a higher RMSE (2.154) than the AAN model (2.093), which suggests that the latter provides a better fit to the data.
# compare RMSE between models
cbind(RMSE1$RMSE, RMSE2$RMSE)%>%
as.data.frame()%>%
rename(RMSE_ANN = 'V1', RMSE_AAN = 'V2')%>%flextable()
RMSE_ANN | RMSE_AAN |
2.154425 | 2.093999 |
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.
Assuming that the RMSE substitutes for the standard deviation of model residuals, the calculated 95% prediction intervals for each forecast model are nearly identical. That said, the span of the ANN model is slightly greater than the AAN model (diff ~.02).
# collect first forecast Mex_fc
<-head(Mex_fc$.mean, 1)
m1_yhat
# calculate prediction interval manually, sub RMSE for stdev
<-m1_yhat+1.96*2.154
p1_int<-m1_yhat-1.96*2.154
p2_int
<-p1_int-p2_int
span1
# collect first forecast Mex_AAN_fc
<-head(Mex_AAN_fc$.mean, 1)
m2_yhat
# calculate standard deviation
#m2_res<-augment(Mex_AAN_mod)
#m2_stdev<-sd(m2_res$.resid)
# calculate prediction interval manually, sub in RMSE for stdev
<-m2_yhat+1.96*2.093
p3_int<-m2_yhat-1.96*2.093
p4_int
<- p3_int-p4_int
span2
# compare prediction intervals
<- cbind(p2_int, p1_int, span1)
pred1<- cbind(p4_int, p3_int, span2)
pred2
<-rbind(pred1, pred2)%>%
dfas.data.frame()%>%
rename('Lower 95%' = p1_int, 'Upper 95%' = p2_int, 'Span' = span1)
row.names(df) <- c('ANN', 'AAN')
df
The forecast prediction intervals predicted by R (hilo()) are identical compared between ANN and AAN models. However, the span (8.59) is slightly greater than the calculated prediction intervals.
<-Mex_fc%>%
fc1hilo()%>%
as.data.frame()%>%
select('95%')%>%
head(1)%>%
unlist()
<-Mex_AAN_fc%>%
fc2hilo()%>%
as.data.frame()%>%
select('95%')%>%
head(1)%>%
unlist()
paste('The span for hilo prediction intervals is', 42.16368-33.56900)
## [1] "The span for hilo prediction intervals is 8.59468"
Forecast the Chinese GDP from the global_economy data set using an ETS model.
# subset data
<-global_economy%>%
gdpselect(Country, GDP)%>%
filter(Country %in% 'China')
#Evaluate raw data
%>%autoplot(color='steelblue')+
gdplabs(title='Figure 5. China GDP')+
theme_classic()
<- gdp %>%
gdp_mod model(ETS(GDP ~ error("A") + trend("A") + season("N")))
# forecast 5 years
<- gdp_mod %>%
gdp_fc forecast(h = 5)
#plot with forecast
%>%
gdp_fc autoplot(gdp, color='steelblue') +
labs(title="Figure 6. China GDP - 5YR Forecast, ETS(A,A,N)") +
theme_classic()
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.
The following graph provides a 10 year forecast using the Holts Damped Trend method at two levels of
\(\phi\) (0.8, 0.9). Given that season variation and/or cyclic signals are absent, a Box-Cox transformation is not warranted.
# forecast with damped trend
%>%
gdpmodel('Holts'=ETS(GDP ~ error("A") + trend("A") + season("N")),
'Damped Holts (Phi= 0.9)'= ETS(GDP ~ error("A") + trend("Ad", phi = 0.9) + season("N")),
'Damped Holts (Phi= 0.8)'= ETS(GDP ~ error("A") + trend("Ad", phi = 0.8) + season("N")))%>%
forecast(h=10)%>%
autoplot(gdp, color='steelblue', level=NULL) + # level NULL removes confidence intervals
labs(title="Figure 8. China GDP - 10YR Forecast with Damped (phi=0.8) Model") +
#guides(color = guide_legend(title = "Forecast"))+
theme_classic()
Find an ETS model for the Gas data from aus_production and forecast the next few years.
#subset data
<-aus_production%>%
gasselect(Gas)
#Evaluate raw data
%>%autoplot(color='steelblue')+
gaslabs(title='Figure 9. Australian Gas Production')+
theme_classic()
#plot forecast with seasonality
%>%
gasmodel(
'Holts'=ETS(Gas ~ error("M") + trend("A") + season("M")),
'Damped Holts (Phi= 0.8)'= ETS(Gas ~ error("M") + trend("Ad", phi = 0.8) + season("M")))%>%
forecast(h=20)%>%
autoplot(gas, color='steelblue', level=NULL) + # level NULL removes confidence intervals
labs(title="Figure 9. Australian Gas Production', subtitle = '5YR Multiplicative Forecast with Damped Model (phi=0.8), ETS(M,A,M)") +
#guides(color = guide_legend(title = "Forecast"))+
theme_classic()
Why is multiplicative seasonality necessary here? Experiment with making the trend damped.
A multiplicative seasonal model is necessary due to the fact that there is an obvious seasonal signal that is also increasing over time(annual).
Does it improve the forecasts?
The multiplicative model generates a forecast that captures the increase in seasonality over time. It also has a lower RMSE (4.59) that the additive model (4.76).
<-gas%>%
gas_modmodel(
Gas_AAA=ETS(Gas ~ error("A") + trend("A") + season("A")),
Gas_MAM = ETS(Gas ~ error("M") + trend("A") + season("M")))
<-gas_mod%>%
gas_fcforecast(h=20)
%>%
gas_fcautoplot(gas, level=NULL) + # level NULL removes confidence intervals
labs(title='Figure 10. Australian Gas Production',
subtitle = '5YR Multiplicative Forecast: Comparing AAA vs. MAM') +
guides(color = guide_legend(title = "Forecast"))+
theme_classic()
# assess rmse
%>% accuracy()%>%
gas_mod select(.model, RMSE)%>%
rename(Model = '.model')%>%flextable()
Model | RMSE |
Gas_AAA | 4.763979 |
Gas_MAM | 4.595113 |
Recall your retail time series data (from Exercise 8 in Section 2.10).
Why is multiplicative seasonality necessary for this series?
The data refers to inter-annual turnover in the Australian “pharmaceutical, cosmetic and toiletry goods retailing”. There is both a strong trend and distinct seasonality that increases over time. As a result a multiplicative model is appropriate.
Apply Holt-Winters’ multiplicative method to the data. Experiment with making the trend damped.
set.seed(124566791)
# load data
<- tsibbledata::aus_retail %>%
series filter(`Series ID` == sample(aus_retail$`Series ID`,1)) #1 specifies number of Series ID groups
# plot raw data
%>%
seriesautoplot(Turnover)+
labs(title = 'Figure 11. Inter-Annual Turnover in Pharmaceutical, Cosmetic and Toiletry Retail Industry',
subtitle = '1982-2020')+
theme(axis.text.x = element_text(angle = 90))+
theme_classic()
# build model
<-series%>%
retail_modmodel(
Turnover_MAM = ETS(Turnover ~ error("M") + trend("A") + season("M")),
Turnover_MAM_Damped_phi_0.8 = ETS(Turnover ~ error("M") + trend("Ad", phi=.8) + season("M")))
# forecast
<-retail_mod%>%
retail_fcforecast(h=80)
#plot
%>%
retail_fcautoplot(series, level=NULL) + # level NULL removes confidence intervals
labs(title='Figure 12. Turnover in Australian Pharmaceutical, Cosmetic and Toiletry Goods Retailing',
subtitle = '10 YR Multiplicative Forecast: Comparing AAA vs. MAM') +
guides(color = guide_legend(title = "Forecast"))+
theme_classic()
Compare the RMSE of the one-step forecasts from the two methods. Which do you prefer?
The model results are quite similar comparing just the RMSE. However, the multiplicative model without damping captures the trend and seasonality to a much greater degree.
%>% accuracy()%>%
retail_mod select(.model, RMSE)%>%
rename(Model = '.model')%>%flextable()
Model | RMSE |
Turnover_MAM | 2.925942 |
Turnover_MAM_Damped_phi_0.8 | 2.940494 |
Check that the residuals from the best method look like white noise.
The innovation residuals appear to be randomly distributed about a mean of 0 and there does not appear to be autocorrelation in the lag features (ACF) which I would expect for ‘white noise’. Similarly, Although several outliers are indicated in the ACF plot. The histogram of residuals is nearly normal.
%>%
retail_mod select(Turnover_MAM) %>%
gg_tsresiduals()
Now find the test set RMSE, while training the model to the end of 2010.
# create train and test sets
<- series %>%
train filter(year(Month) < 2011)
<- series %>%
testfilter(year(Month) > 2010)
# build model on train
<-train%>%
train.modmodel(train_MAM = ETS(Turnover ~ error("M") + trend("A") + season("M")))
# create forecast
<-train.mod%>%
train.fcforecast(h=96)
#plot
%>%
train.fcautoplot(series, color='steelblue')+
labs(title='Figure 13. Training Set: Seasonal Decomposed ETS Forecast', subtitle='Compare to observed data')+
theme_classic()
# create function to calculate test RMSE
<- nrow(test)
nrow <- train.fc[6]
pred <- test$Turnover
obs
<- function(obs, pred, nrow){
rmse <- sqrt(1/nrow*sum((pred-obs)^2))
rmse return (rmse)
}
<-rmse(obs, pred, nrow)
test_rmse
str_glue('The RMSE of the Test set is', {test_rmse})
## The RMSE of the Test set is11.5450491482437
Can you beat the seasonal naïve approach from Exercise 7 in Section 5.11?
Yes, the RMSE for the seasonal naive approach is 22.68. This is almost double that of the Holts Winter’s Multiplicative Model.
#fit model
<- train %>%
snaive model(SNAIVE(Turnover))
#forecast
<- snaive %>%
snaive_fc forecast(new_data = anti_join(series, train))
#plot
%>%
snaive_fcautoplot(series, color='steelblue')+
labs(title='Figure 14. Turnover in Australian Pharmaceutical, Cosmetic and Toiletry Goods Retailing',
subtitle = '10 YR Multiplicative Forecast: Seasonal Naive Model')+
theme_classic()
# assess Naive RMSE
%>% accuracy(series)%>%
snaive_fc select(.model, RMSE)%>%
rename(Model = '.model')%>%flextable()%>%
set_caption('Turnover in Australian Pharmaceutical, Cosmetic and Toiletry Goods Retailing: RMSE for Seasonal Naive Model')
Model | RMSE |
SNAIVE(Turnover) | 22.68981 |
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?
#generate box-cox lambda
<- train %>%
lambda features(Turnover, features = guerrero) %>%
pull(lambda_guerrero)
# apply lambda to turnover
<-train%>%
box_trainmutate(trans_turnover = box_cox(Turnover, lambda))
# plot transformed turnover
%>%
box_train autoplot(trans_turnover, color = 'steelblue') +
labs(title = 'Figure 15. Box-Cox Transformed Turnover', subtitle='Based on Exercise 8.8 Data')+
theme_classic()
Seasonally adjust the data.
After some experimentation, I selected a trend window = 21, which is the default for STL. I also selected a seasonal window = ‘periodic’, which forces the seasonal component to be identical across years. In this instance, it produced good results.
<-box_train %>%
decomp_trainmodel(
STL(trans_turnover ~ trend(window = 21) + # 21 is STL default for monthly data
season(window='periodic'),
robust = TRUE)) %>%
components()
%>%
decomp_trainautoplot()+
labs(title='Figure 16. Seasonal Decomposition on Box-Cox Transformed Turnover Data')
Apply ETS to seasonally adjusted data
I have applied the decomposition_model() from fabletools to combine decomposition and model fitting. The models are fitted after decomposition. The ETS model includes seasonal damping (phi=.98).
see: https://rdrr.io/cran/fabletools/man/decomposition_model.html
The RMSE for STL/ETS training model is 2.33 and RMSE the STL/ETS test model is 12.15. Compare this to a test (without transformation or decomposition) RMSE of 11.545. The latter model performed better. However, both outperformed the SNAIVE model.
# build model on train
<-train%>%
train.stl.etsmodel(decomposition_model(
STL(box_cox(Turnover, lambda)),
ETS(season_adjust ~ error("M") + trend("Ad", phi=.98))))
#ETS(Turnover ~ error("M") + trend("Ad", phi=.8)
# create forecast
<-train.stl.ets%>%
train.stl.ets.fcforecast(h=96)
#plot
%>%
train.stl.ets.fcautoplot(series, color='steelblue')+
labs(title='Figure 17. Training Set: Seasonally Adjusted $ Decomposed ETS Forecast', subtitle='Compare to observed data')+
theme_classic()
# calculate train and test RMSE using STL/ETS model
<-train.stl.ets%>% accuracy()
train.rmse
<- nrow(test)
nrow <- train.stl.ets.fc[6]
pred <- test$Turnover
obs
<- function(obs, pred, nrow){
ets.stl.rmse <- sqrt(1/nrow*sum((pred-obs)^2))
rmse return (rmse)
}
<-ets.stl.rmse(obs, pred, nrow)
test.rmse
str_glue('The RMSE for STL/ETS training model is {train.rmse$RMSE}')
## The RMSE for STL/ETS training model is 2.36401973614103
str_glue('The RMSE of the STL/ETS test set is ', {test.rmse})
## The RMSE of the STL/ETS test set is 12.1548069255932