library(readxl)
library(ggplot2)
library(fpp3)
library(dplyr)
library(fst)
library(tsibbledata)
library(tsibble)
library(feasts)
library(lubridate)
library(seasonal)
library(zoo)
library(fable)
library(readxl)
library(ggplot2)
library(forecast)
library(tidyverse)
library(kableExtra)
library(Amelia)
data <- read_excel("data/DRO Difference 2021 -- formatted.xlsx")
# converting to time-series
data$Date = as.Date(data$Date, format = "%Y/%U")
data <- data %>%
select(Date, Day, Actual) %>%
group_by(Date) %>%
as_tsibble(index = Date)
data %>% gg_tsdisplay()
# missing values ---
#
colSums(is.na(data))
Date Day Actual
0 0 3
Replace NA
# finding locations
loc = which(is.na(data), arr.ind=TRUE)
# function calculates average volume for given day of week
averageDay <- function(data, day){
value = round(sum(data[which(data$Day == day), 3], na.rm = T) / sum(data$Day == day))
return(value)
}
# replaces missing values with average value for given day
for (i in 1:3){
data[loc[i],3] = averageDay(data, data$Day[loc[i]])
}
colSums(is.na(data))
Date Day Actual
0 0 0
bp <- boxplot(data$Actual, id=list(method="y"))
# There is one outlier.
# Replacing outlier with its daily average
data[data$Actual == bp$out, 3] = averageDay(data, unlist(data[data$Actual == bp$out, 2]))
boxplot(data$Actual)
# need to convert from class back to tsibble
data <- data %>% as_tsibble()
data %>% gg_tsdisplay()
# much better
Testing Stationarity
# testing stationarity
ndiffs(data$Actual)
[1] 0
data %>% unitroot_nsdiffs()
nsdiffs
0
data %>%
features(Actual, unitroot_kpss)
# A tibble: 1 x 2
kpss_stat kpss_pvalue
<dbl> <dbl>
1 0.330 0.1
# the data is stationary
Training Data
# 90/10 split
tr.lim <- toString(as.Date(unlist(data[round(0.9 * nrow(data)),1])))
train = data %>% filter_index(. ~tr.lim)
# 8.10
# Evaluating Various ETS
myfits <- train %>%
model(
`ANN` = ETS(Actual~ error("A") + trend("N") + season("N")),
`AAN` = ETS(Actual~ error("A") + trend("A") + season("N")),
`Auto ETS` = ETS(Actual),
`Damped` = ETS(Actual ~ error("A") + trend("Ad") + season("N")),
`Damped Holt's method` = ETS(Actual~ error("A") +
trend("Ad", phi=0.9) + season("N")),
`Damped Holt's method 2` = ETS(Actual~ error("A") +
trend("Ad", phi=0.8) + season("N"))
)
bind_rows(
myfits %>% accuracy(),
myfits %>% forecast(h = 20) %>% accuracy(data)
)%>%
select(-ME, -MPE, -ACF1)
# A tibble: 12 x 7
.model .type RMSE MAE MAPE MASE RMSSE
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 ANN Training 581. 497. 23.9 2.12 1.96
2 AAN Training 592. 508. 24.5 2.16 2.00
3 Auto ETS Training 220. 174. 7.92 0.739 0.744
4 Damped Training 597. 511. 24.0 2.18 2.02
5 Damped Holt's method Training 581. 498. 24.1 2.12 1.96
6 Damped Holt's method 2 Training 581. 498. 24.1 2.12 1.96
7 AAN Test 605. 518. 28.6 2.21 2.04
8 ANN Test 623. 505. 30.2 2.15 2.10
9 Auto ETS Test 212. 171. 9.39 0.727 0.717
10 Damped Test 634. 540. 27.5 2.30 2.14
11 Damped Holt's method Test 616. 505. 29.8 2.15 2.08
12 Damped Holt's method 2 Test 618. 505. 29.9 2.15 2.09
# Auto ETS performs best
# Auto ETS
myets = train %>%
model(
ETS(Actual)
)
# seasonal NAIVE
mysnaive <- train %>%
model(
SNAIVE(Actual)
)
# STL dcmp applied to regular data
fit.dcmp <- train %>%
model(stlf = decomposition_model(
STL(Actual ~ trend(window = 7), robust = TRUE),
NAIVE(season_adjust)
))
# STL dcmp applied to log transform
fit.dcmp.log <- train %>%
model(stlf = decomposition_model(
STL(log(Actual) ~ trend(window = 7), robust = TRUE),
NAIVE(season_adjust)
))
# Comparing
bind_rows(
myets %>% accuracy(),
mysnaive %>% accuracy(),
fit.dcmp %>% accuracy(),
fit.dcmp.log %>% accuracy(),
myets %>% forecast(h = 20) %>% accuracy(data),
mysnaive %>% forecast(h = 20) %>% accuracy(data),
fit.dcmp %>% forecast(h=20) %>% accuracy(data),
fit.dcmp.log %>% forecast(h=20) %>% accuracy(data),
) %>%
select(-.model, -ME, -MPE, -ACF1)
# A tibble: 8 x 6
.type RMSE MAE MAPE MASE RMSSE
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Training 220. 174. 7.92 0.739 0.744
2 Training 296. 235. 10.6 1 1
3 Training 298. 222. 10.0 0.947 1.01
4 Training 316. 234. 10.5 0.997 1.07
5 Test 212. 171. 9.39 0.727 0.717
6 Test 245. 192. 9.22 0.817 0.828
7 Test 392. 341. 17.2 1.45 1.32
8 Test 742. 704. 33.6 3.00 2.50
#
#
####
# The auto ETS outperforms all models in both the train and test set.
Auto ARIMA
# Auto ARIMA
myarimaC = train %>%
model(
`Auto` = ARIMA(Actual),
)
bind_rows(
myarimaC %>% accuracy(),
myarimaC %>% forecast(h = 20) %>% accuracy(data)
)
# A tibble: 2 x 10
.model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Auto Training -32.7 223. 168. -2.51 7.74 0.717 0.752 -0.00871
2 Auto Test -126. 219. 180. -7.23 9.36 0.764 0.740 -0.293
Finding best pdq combination
# This function produces accuracy measures for each combination
arimaIterate <- function(train, p, d, q){
myarimaC = train %>%
model(
`Auto` = ARIMA(Actual~ pdq(p,d,q) )
)
measures = bind_rows(
myarimaC %>% accuracy(),
myarimaC %>% forecast(h = 20) %>% accuracy(data)
)
measures['combo'] = toString(unlist(rbind(p,d,q)))
return(measures)
}
# this loop sends pdq combinations to above function
totals = list()
for (p in 0:4){
for(d in 0:1){
for(q in 0:4){
vals = arimaIterate(train, p,d,q)
totals = bind_rows(totals, vals)
}
}
}
totals = na.omit(totals)
head(totals,20)
# A tibble: 20 x 11
.model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1 combo
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
1 Auto Training -44.6 225. 169. -3.05 7.85 0.721 0.761 -0.0538 0, 0, 0
2 Auto Test -129. 226. 184. -7.28 9.49 0.783 0.764 -0.319 0, 0, 0
3 Auto Training -44.9 225. 169. -3.06 7.85 0.721 0.761 -0.0466 0, 0, 1
4 Auto Test -128. 226. 184. -7.27 9.47 0.782 0.764 -0.319 0, 0, 1
5 Auto Training -41.0 224. 170. -2.90 7.85 0.722 0.757 -0.0492 0, 0, 2
6 Auto Test -129. 225. 184. -7.32 9.52 0.785 0.760 -0.312 0, 0, 2
7 Auto Training -44.1 223. 168. -3.00 7.78 0.716 0.753 -0.0464 0, 0, 3
8 Auto Test -132. 226. 186. -7.46 9.63 0.791 0.763 -0.306 0, 0, 3
9 Auto Training -44.2 224. 169. -3.02 7.81 0.718 0.756 -0.0427 0, 0, 4
10 Auto Test -142. 230. 188. -7.94 9.79 0.802 0.776 -0.287 0, 0, 4
11 Auto Training -1.32 311. 246. -1.06 11.1 1.05 1.05 -0.526 0, 1, 0
12 Auto Test -262. 320. 273. -14.1 14.5 1.16 1.08 -0.329 0, 1, 0
13 Auto Training 27.8 225. 173. 0.151 7.79 0.736 0.759 -0.0867 0, 1, 1
14 Auto Test -68.2 198. 162. -4.43 8.30 0.690 0.670 -0.316 0, 1, 1
15 Auto Training 29.0 226. 175. 0.183 7.91 0.744 0.763 -0.0270 0, 1, 2
16 Auto Test -77.7 199. 163. -4.81 8.35 0.693 0.671 -0.288 0, 1, 2
17 Auto Training 29.1 226. 175. 0.190 7.89 0.743 0.762 -0.0298 0, 1, 3
18 Auto Test -76.0 198. 162. -4.73 8.32 0.691 0.668 -0.284 0, 1, 3
19 Auto Training 26.9 222. 170. 0.143 7.70 0.726 0.751 -0.0155 0, 1, 4
20 Auto Test -90.8 201. 163. -5.61 8.50 0.693 0.678 -0.283 0, 1, 4
# Finding best p,d,q combinations for training data
measure_train = totals %>%
filter(.type == "Training")
#
best_train = head(sort(measure_train$RMSE),20)
best_train = totals %>% filter(totals$RMSE %in% best_train)
# Finding best p,d,q combinations for test data
measure_test = totals %>%
filter(.type == "Test")
#
best_test = head(sort(measure_test$RMSE),20)
best_test = totals %>% filter(totals$RMSE %in% best_test)
# Finding models that perform best on training and test data
best_both = best_test %>% filter(best_test$combo %in% best_train$combo)
#
# Top 5 models -- determined by smallest RMSE value
best5 = best_both %>% filter(best_both$RMSE %in% head(sort(best_both$RMSE),5))
head(best5)
# A tibble: 5 x 11
.model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1 combo
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
1 Auto Test -68.2 198. 162. -4.43 8.30 0.690 0.670 -0.316 0, 1, 1
2 Auto Test -76.7 196. 162. -4.73 8.29 0.692 0.661 -0.262 1, 1, 2
3 Auto Test -75.2 195. 162. -4.67 8.26 0.689 0.659 -0.259 1, 1, 3
4 Auto Test -76.1 198. 161. -4.89 8.38 0.687 0.667 -0.265 2, 1, 2
5 Auto Test -84.5 198. 162. -5.26 8.37 0.689 0.670 -0.272 3, 1, 1
Best ARIMA
bestArima = train %>%
model(
ARIMA(Actual~ pdq(0,1,1)) # 0,1,1 has smallest AIC
)
report(bestArima)
Series: Actual
Model: ARIMA(0,1,1)(2,1,1)[7]
Coefficients:
ma1 sar1 sar2 sma1
-0.9349 -0.0071 -0.1088 -0.8575
s.e. 0.0434 0.1051 0.0971 0.0779
sigma^2 estimated as 54336: log likelihood=-1159.36
AIC=2328.72 AICc=2329.1 BIC=2344.34
bind_rows(
myarimaC %>% accuracy(),
bestArima %>% accuracy(),
myarimaC %>% forecast(h = 20) %>% accuracy(data),
bestArima %>% forecast(h = 20) %>% accuracy(data)
)
# A tibble: 4 x 10
.model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Auto Trai~ -32.7 223. 168. -2.51 7.74 0.717 0.752 -0.00871
2 ARIMA(Actual ~ pdq~ Trai~ 27.8 225. 173. 0.151 7.79 0.736 0.759 -0.0867
3 Auto Test -126. 219. 180. -7.23 9.36 0.764 0.740 -0.293
4 ARIMA(Actual ~ pdq~ Test -68.2 198. 162. -4.43 8.30 0.690 0.670 -0.316
# The auto-ARIMA performs slightly better on the training data, but the model
# with the optimal pdq values performs significantly better on the test data.
Auto ETS
# auto ETS
myets = train %>%
model(
ETS(Actual)
)
report(myets)
Series: Actual
Model: ETS(A,N,A)
Smoothing parameters:
alpha = 0.07030721
gamma = 0.000100008
Initial states:
l[0] s[0] s[-1] s[-2] s[-3] s[-4] s[-5] s[-6]
2588.34 -905.1391 -239.9334 454.6483 501.2507 634.4895 57.10333 -502.4194
sigma^2: 51210.91
AIC AICc BIC
2829.260 2830.593 2860.964
myets %>% forecast(h=20) %>% autoplot(data) + ggtitle("ETS")
myets %>% gg_tsresiduals()
Best ARIMA
# best ARIMA
bestArima = train %>%
model(
ARIMA(Actual~ pdq(0,1,1))
)
report(bestArima)
Series: Actual
Model: ARIMA(0,1,1)(2,1,1)[7]
Coefficients:
ma1 sar1 sar2 sma1
-0.9349 -0.0071 -0.1088 -0.8575
s.e. 0.0434 0.1051 0.0971 0.0779
sigma^2 estimated as 54336: log likelihood=-1159.36
AIC=2328.72 AICc=2329.1 BIC=2344.34
bestArima %>% forecast(h=20) %>% autoplot(data) + ggtitle("ARIMA")
bestArima %>% gg_tsresiduals()
bind_rows(
myets %>% accuracy(),
bestArima %>% accuracy(),
myets %>% forecast(h = 20) %>% accuracy(data),
bestArima %>% forecast(h = 20) %>% accuracy(data)
)
# A tibble: 4 x 10
.model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 ETS(Actual) Trai~ -20.1 220. 174. -1.93 7.92 0.739 0.744 -0.0540
2 ARIMA(Actual ~ pdq(~ Trai~ 27.8 225. 173. 0.151 7.79 0.736 0.759 -0.0867
3 ETS(Actual) Test -100. 212. 171. -6.65 9.39 0.727 0.717 -0.266
4 ARIMA(Actual ~ pdq(~ Test -68.2 198. 162. -4.43 8.30 0.690 0.670 -0.316
# Wow. The ARIMA outperforms the ETS on the test set!
# ETS wins on training set but test set and ARIMA take the win.
ets.fc = myets %>% forecast(h = 20)
arima.fc = bestArima %>% forecast(h = 20)
actual = tail(data, 20)
final.compare = bind_cols(actual$Date, actual$Actual, round(ets.fc$.mean), round(arima.fc$.mean))
names(final.compare) = c("Date", "Actual","ETS", "ARIMA")
ggplot(data = final.compare, mapping = aes(x = Date)) +
geom_line(aes(y = Actual), color = "blue") +
geom_point(aes(y = Actual), color = "blue") +
geom_line(aes(y = ETS), color = "red") +
geom_point(aes(y = ETS), color = "red") +
geom_line(aes(y = ARIMA), color = "darkgreen") +
geom_point(aes(y = ARIMA), color = "darkgreen") +
ggtitle("Actual (blue) v ETS (red) v ARIMA (green)")
ets.difference = sum(abs(final.compare$ETS - final.compare$Actual))
ets.difference
[1] 3414
arima.difference = sum(abs(final.compare$ARIMA - final.compare$Actual))
arima.difference
[1] 3242
It’s not necessarily apparent in the above graph but if the numbers are correct, then the ARIMA model achieves better performance than the ETS. The added sum of differences between both models predictions with the actual values, along with the residual histograms seen above show that less error is present in the ARIMA model making it the more accurate model. Due to this, the ARIMA model is the chosen model.
For future work, the same process used to find the best combination of pdq could be applied to finding the best PDQ combination. Since this technique found a model better than AutoARIMA, it may follow that further improvements to model accuracy would be observed.