Load Package
# Time series decomposition
library("feasts") # Using STL
library('tidyverse')
library('tsibble')
library('fable')
library('stR') # Using Seasonal Trend regression model
library('bsts')
# Date + forecast
library('lubridate') # date and time
library('prophet') # time series analysis
library('timetk') # time series analysis
library('timeDate') # time series analysis
# general data manipulation
library('dplyr') # data manipulation
library('readr') # input/output
library('vroom') # input/output
library('tibble') # data wrangling
library('tidyr') # data wrangling
# Prophet model
library('fable.prophet')
# Quadratic Solver
library('quadprog')
# Decomposition and forecast components
library("stsm") # Model BSTS
require("stsm.class")
library("KFKSDS") # Smoothing, forecasting with Kalman Filter
# Forecasting with Adam
library("smooth")
# General visualization
library('ggplot2') # visualisation
library('scales') # visualisation
library('patchwork') # visualisation
library('RColorBrewer') # visualisation
library('corrplot') # visualisation
# specific visualisation
library('alluvial') # visualisation
library('ggrepel') # visualisation
library('ggforce') # visualisation
library('ggridges') # visualisation
library('gganimate') # animations
library('GGally') # visualisation
library('ggthemes') # visualisation
library('wesanderson') # visualisation
library('kableExtra') # display
# install.packages("modeltime.ensemble")
# Install Python Dependencies
# modeltime.gluonts::install_gluonts()
#
# library(tidymodels)
# library(modeltime)
# library(modeltime.resample)
# library(modeltime.gluonts)
# library(modeltime.ensemble)
Load data
rm(list = ls())
# Preventing clash from Dplyr and MASS package for select function
select <- dplyr::select
train_df <- vroom("D:\\Dataset\\sales_train_validation.csv", delim = ",", col_types = cols())
price_df <- vroom("D:\\Dataset\\sell_prices.csv", delim = ",",
col_types = cols())
calendar_df <- vroom("D:\\Dataset\\calendar.csv", col_types = cols())
test_df <- vroom("D:\\Dataset\\sales_train_evaluation.csv", delim = ",",
col_types = cols())
# Retrieving special holidays
holidays <- c(USThanksgivingDay(2011:2016), USChristmasDay(2011:2016),
USGoodFriday(2011:2016), USNewYearsDay(2011:2016),
USIndependenceDay(2011:2016), USElectionDay(2011:2016),
USLaborDay(2011:2016))@Data
holidays <- as.Date(holidays)
holidays_df <- holidays %>%
as.data.frame() %>% bind_cols(as.data.frame(rep(c("ThanksGiving", "Christmas",
"GoodFriday","Newyear",
"Independay","Elecday","Laborday"), each = 6)))
names(holidays_df) <- c("date", "holiday")
holidays_df <- holidays_df %>%
as_tsibble(index = date)
Prepare data
start_date = date("2011-01-29")
temp <- train_df %>%
group_by(cat_id, state_id) %>%
summarise_at(vars(starts_with("d_")), sum) %>%
ungroup() %>%
select(ends_with("id"), starts_with("d_")) %>%
pivot_longer(starts_with("d_"), names_to = "dates", values_to = "sales") %>%
mutate(dates = as.integer(str_remove(dates, "d_"))) %>%
mutate(dates = start_date + dates - 1) %>%
mutate(dates = ymd(dates)) %>%
mutate(dummy = if_else(dates %in% holidays, 1, 0)) %>%
mutate(dow = wday(dates, label = TRUE))
# filter(!str_detect(as.character(dates), '..-12-25'))
# replace_na(list(sales = 0))
temp_ts <- temp %>%
as_tsibble(key = c(cat_id, state_id), index = dates)
food.CA <- temp_ts %>%
filter(cat_id == "FOODS" & state_id == "CA") %>%
select(dates, sales, dummy, dow)
id_examples <- str_c(c("FOODS_2_092_CA_1", "HOUSEHOLD_2_071_TX_2",
"HOBBIES_1_348_WI_3"), "_validation")
sales <- train_df %>%
filter(id %in% id_examples) %>%
wide_to_long() %>%
mutate(state_id = str_sub(id, -4, -3))
# Price dataframe
prices <- price_df %>%
unite("id", item_id, store_id, sep = "_") %>%
filter(id %in% str_remove(id_examples, "_validation")) %>%
select(-wm_yr_wk)
example_df <- sales %>%
mutate(holiday = if_else(dates %in% holidays, 1, 0))%>%
mutate(dow = wday(dates, label = TRUE))
# start and end times for holiday intervals
holiday_intervals <- example_df %>%
group_by(state_id) %>%
mutate(foo = lead(holiday, 1),
bar = lag(holiday, 1)) %>%
mutate(holiday_start = if_else(holiday == 1 & bar == 0, dates, date(NA_character_)),
holiday_end = if_else(holiday == 1 & foo == 0, dates, date(NA_character_))) %>%
ungroup()
holiday_intervals <- holiday_intervals %>%
select(state_id, holiday_start) %>%
filter(!is.na(holiday_start)) %>%
bind_cols(holiday_intervals %>%
select(holiday_end) %>%
filter(!is.na(holiday_end)))
Visualizing
example_df %>%
filter(between(dates, date("2015-04-01"), date("2015-10-01"))) %>%
left_join(holiday_intervals, by = c("state_id", "dates" = "holiday_start")) %>%
mutate(holiday_start = if_else(is.na(holiday_end), date(NA_character_), dates))-> example_df_plot
ggplot(example_df_plot, aes(dates, sales, group = id)) +
geom_rect(data = example_df_plot[example_df_plot$id == unique(example_df$id)[1] ,], aes(xmin = holiday_start, xmax = holiday_end + ddays(1), ymin = -Inf, ymax = Inf), fill = "grey90", na.rm = TRUE, alpha = 0.2) +
geom_rect(data = example_df_plot[example_df_plot$id == unique(example_df$id)[2],], aes(xmin = holiday_start, xmax = holiday_end + ddays(1), ymin = -Inf, ymax = Inf), fill = "grey90", na.rm = TRUE, alpha = 0.2) +
geom_rect(data = example_df_plot[example_df_plot$id == unique(example_df$id)[3],], aes(xmin = holiday_start, xmax = holiday_end + ddays(1), ymin = -Inf, ymax = Inf), fill = "grey90", na.rm = TRUE, alpha = 0.7) +
geom_line(aes(col = id), na.rm = TRUE) +
facet_wrap(~id, nrow = 3, scales = "free") +
theme_hc() +
theme(legend.position = "none") +
labs(x = "", y = "Sales", title = "Sales for 3 random items for 6 months in 2015 with typical US events",
subtitle = "Grey background = holiday dates.")

Croston optimization function
crost.opt <- function(data,type=c("croston","sba","sbj"),cost=c("mar","msr","mae","mse","mape"),
w=NULL,nop=c(2,1),init,init.opt=c(TRUE,FALSE)){
# Optimisation function for Croston and variants
type <- type[1]
cost <- cost[1]
nop <- nop[1]
init.opt <- init.opt[1]
# Croston decomposition
nzd <- which(data != 0) # Find location on non-zero demand
k <- length(nzd)
x <- c(nzd[1],nzd[2:k]-nzd[1:(k-1)]) # Intervals
if (is.null(w) == TRUE && init.opt == FALSE){
# Optimise only w
p0 <- c(rep(0.05,nop))
lbound <- c(rep(0,nop))
ubound <- c(rep(1,nop))
if (nop != 1){
wopt <- optim(par=p0,crost.cost,method="Nelder-Mead",data=data,cost=cost,
type=type,w=w,nop=nop,w.opt=is.null(w),init=init,init.opt=init.opt,
lbound=lbound,ubound=ubound,control=list(maxit=2000))$par
} else {
# Use Brent
wopt <- optim(par=p0,crost.cost,method="Brent",data=data,cost=cost,
type=type,w=w,nop=nop,w.opt=is.null(w),init=init,init.opt=init.opt,
lbound=lbound,ubound=ubound,lower=lbound,upper=ubound,
control=list(maxit=2000))$par
}
wopt <- c(wopt,init)
} else if (is.null(w) == TRUE && init.opt == TRUE){
# Optimise w and init
p0 <- c(rep(0.05,nop),init[1],init[2])
lbound <- c(rep(0,nop),0,1)
ubound <- c(rep(1,nop),max(data),max(x))
wopt <- optim(par=p0,crost.cost,method="Nelder-Mead",data=data,cost=cost,
type=type,w=w,nop=nop,w.opt=is.null(w),init=init,init.opt=init.opt,
lbound=lbound,ubound=ubound,control=list(maxit=2000))$par
} else if (is.null(w) == FALSE && init.opt == TRUE){
# Optimise only init
nop <- length(w)
p0 <- c(init[1],init[2])
lbound <- c(0,1)
ubound <- c(max(data),max(x))
wopt <- optim(par=p0,crost.cost,method="Nelder-Mead",data=data,cost=cost,
type=type,w=w,nop=nop,w.opt=is.null(w),init=init,init.opt=init.opt,
lbound=lbound,ubound=ubound,control=list(maxit=2000))$par
wopt <- c(w,wopt)
}
return(list(w=wopt[1:nop],init=wopt[(nop+1):(nop+2)]))
}
Cost function for Croston
crost.cost <- function(p0,data,cost,type,w,nop,w.opt,init,init.opt,lbound,ubound){
if (w.opt == TRUE && init.opt == TRUE){
frc.in <- croston(data=data,w=p0[1:nop],h=0,init=p0[(nop+1):(nop+2)],
type=type,opt.on=TRUE)$frc.in
} else if (w.opt == TRUE && init.opt == FALSE){
frc.in <- croston(data=data,w=p0[1:nop],h=0,init=init,
type=type,opt.on=TRUE)$frc.in
} else if (w.opt == FALSE && init.opt == TRUE){
frc.in <- croston(data=data,w=w,h=0,init=p0,
type=type,opt.on=TRUE)$frc.in
}
if (cost == "mse"){
E <- data - frc.in
E <- E[!is.na(E)]
E <- mean(E^2)
}
else if (cost == "mape"){
E <- data - frc.in
E <- E[!is.na(E)]
E <- mean(abs(E)/data) * 100
}
else if(cost == "mae"){
E <- data - frc.in
E <- E[!is.na(E)]
E <- mean(abs(E))
} else if(cost == "mar"){
n <- length(data)
temp <- cumsum(data)/(1:n)
n <- ceiling(0.3*n)
temp[1:n] <- temp[n]
E <- abs(frc.in - temp)
E <- E[!is.na(E)]
E <- sum(E)
} else if(cost == "msr"){
n <- length(data)
temp <- cumsum(data)/(1:n)
n <- ceiling(0.3*n)
temp[1:n] <- temp[n]
E <- (frc.in - temp)^2
E <- E[!is.na(E)]
E <- sum(E)
}
# Constrains
for (i in 1:(nop*w.opt+2*init.opt)){
if (!(p0[i]>=lbound[i]) | !(p0[i]<=ubound[i])){
E <- 9*10^99
}
}
return(E)
}
Croston main function
croston <- function(data, h = 10, w= NULL, init = c("mean", "naive"), nop = c(2,1),
type = c("croston","sba","sbj"), cost = c("mar","msr","mae","mse","mape"),
init.opt = c(TRUE, FALSE),
plot=c(FALSE,TRUE),
opt.on=c(FALSE,TRUE),
na.rm=c(FALSE,TRUE)){
# Default settings
type <- tolower(type[1])
cost <- cost[1]
init.opt <- init.opt[1]
plot <- plot[1]
opt.on <- opt.on[1]
na.rm <- na.rm[1]
if(!is.numeric(init)){
init <- init[1]}
else {
if (length(init >=2)){
init <- init[1:2]
}
else {
init <- "mean"
}
}
# Check the lenght of nop is correctly specified
if (nop > 2 || nop < 1){
nop <- 2
warning("nop can be either 1 or 2. Overriden to 2.")
}
# Prepare data
if (class(data)=="data.frame"){
if (ncol(data) > 1){
warning("Data frame with more than one columns. Using only first one for univariate time series.")
}
data <- data[[1]]
}
if (na.rm == TRUE){
data <- data[!is.na(data)]
}
n <- length(data)
# Checking number of integer-valued observations. At least two !!!
if (sum(data != 0 ) < 2){
stop("Need at least two non-zero values to perform modeling")
}
# Croston decomposition
nzd <- which(data != 0) # Find location on non-zero demand
k <- length(nzd)
z <- data[nzd] # Demand
x <- c(nzd[1],nzd[2:k]-nzd[1:(k-1)]) # Intervals
# Initialise
if (!(is.numeric(init) && length(init)==2)){
if (init=="mean"){
init <- c(z[1],mean(x))
} else {
init <- c(z[1],x[1]) # Naive with the first interval
}
}
# Optimise parameters if requested
if (opt.on == FALSE){
if (is.null(w) || init.opt == TRUE){
wopt <- crost.opt(data,type,cost,w,nop,init,init.opt)
w <- wopt$w
init <- wopt$init
}
}
# Pre-allocate memory
zfit <- vector("numeric", k)
xfit <- vector("numeric", k)
# Assign initial values and parameters
if (opt.on == FALSE){
if (init[1]<0){
stop("Initial demand cannot be a negative number.")
}
if (init[2]<1){
stop("Initial interval cannot be less than 1.")
}
}
zfit[1] <- init[1]
xfit[1] <- init[2]
if (length(w)==1){
a.demand <- w[1]
a.interval <- w[1]
} else {
a.demand <- w[1]
a.interval <- w[2]
}
# Set coefficient
if(type == "sba"){
coeff <- 1-(a.interval/2)
} else if(type == "sbj"){
coeff <- (1-a.interval/(2-a.interval))
} else {
coeff <- 1
}
# Fit model
for (i in 2:k){
zfit[i] <- zfit[i-1] + a.demand * (z[i] - zfit[i-1]) # Demand
xfit[i] <- xfit[i-1] + a.interval * (x[i] - xfit[i-1]) # Interval
}
cc <- coeff * zfit/xfit
# Calculate in-sample demand rate
frc.in <- x.in <- z.in <- rep(NA,n)
tv <- c(nzd+1,n) # Time vector used to create frc.in forecasts
for (i in 1:k){
if (tv[i]<=n){
frc.in[tv[i]:min(c(tv[i+1],n))] <- cc[i]
x.in[tv[i]:min(c(tv[i+1],n))] <- xfit[i]
z.in[tv[i]:min(c(tv[i+1],n))] <- zfit[i]
}
}
# Forecast out-of-sample demand rate
if (h>0){
frc.out <- rep(cc[k],h)
x.out <- rep(xfit[k],h)
z.out <- rep(zfit[k],h)
} else {
frc.out <- x.out <- z.out <- NULL
}
# Ploting setting
if (plot==TRUE){
plot(1:n,data,type="l",xlim=c(1,(n+h)),xlab="Period",ylab="",
xaxs="i",yaxs="i",ylim=c(0,max(data)*1.1))
lines(which(data>0),data[data>0],type="p",pch=20)
lines(1:n,frc.in,col="green")
lines((n+1):(n+h),frc.out,col="red",lwd=2)
}
# Prepare demand and interval vectors for output
c.in <- array(cbind(z.in,x.in),c(n,2),dimnames=list(NULL,c("Demand","Interval")))
if (h>0){
c.out <- array(cbind(z.out,x.out),c(h,2),dimnames=list(NULL,c("Demand","Interval")))
} else {
c.out <- NULL
}
c.coeff <- coeff
comp <- list(c.in=c.in,c.out=c.out,coeff=coeff)
return(list(model=type,frc.in=frc.in,frc.out=frc.out,
weights=w,initial=c(zfit[1],xfit[1]),components=comp))
}
Test on real data
y <- example_df %>%
filter(id == unique(example_df$id)[1] & dates < "2016-04-18") %>%
pull(sales) %>%
ts(start = c(2011,01))
fc <- croston(y, h = 7, type = "sba", cost = "mae", init.opt = TRUE, plot = TRUE)

y_true <- example_df %>%
filter(id == unique(example_df$id)[1]) %>%
slice(tail(row_number(), 7)) %>%
pull(sales)
# Testing the overdispersion test
library(AER)
## Loading required package: car
## Loading required package: carData
##
## Attaching package: 'car'
## The following object is masked from 'package:dplyr':
##
## recode
## The following object is masked from 'package:purrr':
##
## some
## Loading required package: lmtest
## Loading required package: sandwich
## Loading required package: survival
dispersiontest(glm(y~1,family="poisson"))
##
## Overdispersion test
##
## data: glm(y ~ 1, family = "poisson")
## z = 11.429, p-value < 2.2e-16
## alternative hypothesis: true dispersion is greater than 1
## sample estimates:
## dispersion
## 2.039269
# Confirm that the expectation is equal to the variance => Expected point forecast
example_df %>%
filter(id == unique(example_df$id)[1] & dates < "2016-04-18") %>%
select(dates, sales) %>%
as_tsibble(index = dates) %>%
fabletools::model(CROSTON(sales)) %>%
fabletools::forecast(h = 7) -> fc_package
compare <- tibble(method1 = fc$frc.out,
method2 = fc_package$.mean,
actual = y_true)
compare %>%
kable(caption = "Table: Compare Point Forecast of 2 Ways
(My Croston function and Fable Package)", escape = TRUE) %>%
kable_classic(full_width = FALSE, html_font = "Cambria")
Table: Compare Point Forecast of 2 Ways (My Croston function and Fable Package)
|
method1
|
method2
|
actual
|
|
0.4231962
|
0.5196051
|
0
|
|
0.4231962
|
0.5196051
|
0
|
|
0.4231962
|
0.5196051
|
1
|
|
0.4231962
|
0.5196051
|
0
|
|
0.4231962
|
0.5196051
|
0
|
|
0.4231962
|
0.5196051
|
0
|
|
0.4231962
|
0.5196051
|
1
|
Modified Croston Method
y <- example_df %>%
filter(id == unique(example_df$id)[1]) %>%
pull(sales) %>%
ts(start = c(2011,01))
plot(y, col = "black", lwd = 2, main = "A Example of Intermittent Demand")

adamModelsiETS <- vector("list",4)
adamModelsiETS[[1]] <- adam(y, "FFF", h=10, holdout=TRUE,
occurrence="odds-ratio")
adamModelsiETS[[2]] <- adam(y, "FFF", h=10, holdout=TRUE,
occurrence="inverse-odds-ratio")
adamModelsiETS[[3]] <- adam(y, "FFF", h=10, holdout=TRUE,
occurrence="direct")
adamModelsiETS[[4]] <- adam(y, "FFF", h=10, holdout=TRUE,
occurrence="general")
adamModelsiETSAICcs <-
setNames(sapply(adamModelsiETS,AICc),
c("odds-ratio", "inverse-odds-ratio",
"direct", "general"))
adamModelsiETSAICcs
## odds-ratio inverse-odds-ratio direct general
## 6473.214 6473.214 6520.805 6448.940
adamModelsiETS[[4]]$occurrence
## Occurrence state space model estimated: General
## Underlying ETS model: oETS[G](MNN)
##
## Error standard deviation: NaN
## Sample size: 1903
## Number of estimated parameters: 4
## Number of degrees of freedom: 1899
## Information criteria:
## AIC AICc BIC BICc
## 2021.368 2021.389 2043.573 2043.652
oETSModel <- oes(y, "MNN", h=10, holdout=TRUE,
occurrence=names(adamModelsiETSAICcs)[4])
adamETSARIMAModel <- adam(y, model="PPP",
occurrence=oETSModel,
orders=list(ar=c(3,3),i=c(2,1),ma=c(3,3),select=TRUE),
h=10, holdout=TRUE, initial="back",
distribution="dlnorm",
bounds = "admissible")
adamETSARIMAModel
## Time elapsed: 1.68 seconds
## Model estimated using auto.adam() function: iETS(ANN)[G]
## Occurrence model type: General
## Distribution assumed in the model: Mixture of Bernoulli and Log-Normal
## Loss function type: likelihood; Loss function value: 1647.615
## Persistence vector g:
## alpha
## 0.0561
##
## Sample size: 1903
## Number of estimated parameters: 2
## Number of degrees of freedom: 1901
## Number of provided parameters: 4
## Information criteria:
## AIC AICc BIC BICc
## 5312.598 5312.604 5323.700 5323.724
##
## Forecast errors:
## Asymmetry: 3.78%; sMSE: 5.779%; rRMSE: 0.886; sPIS: -311.444%; sCE: 79.141%
plot(forecast(adamETSARIMAModel, h = 365, interval = "parametric"))

plot(reapply(adamETSARIMAModel))
