"""
croston model for intermittent time series
"""
import numpy
import pandas
from scipy.optimize import minimize
[docs]def fit_croston(
input_endog,
input_length,
forecast_length,
):
"""
:param input_endog:
:param input_length:
:param forecast_length:
:return:
"""
epsilon = 1e-7
input_series = numpy.asarray(input_endog.values)
nzd = numpy.where(input_series != 0)[0]
if list(nzd) != [0]:
try:
w_opt = _croston_opt(
input_series = numpy.asarray(input_endog.values),
input_series_length = input_length,
epsilon = epsilon,
w = None,
nop = 1,
croston_variant = 'original'
)
croston_training_result = _croston(
input_series = numpy.asarray(input_endog.values),
input_series_length = input_length,
w = w_opt,
h = forecast_length,
epsilon = epsilon,
croston_variant = 'original'
)
croston_model = croston_training_result['model']
croston_fittedvalues = croston_training_result['in_sample_forecast']
croston_forecast = croston_training_result['out_of_sample_forecast']
except Exception as e:
croston_model = None
croston_fittedvalues = None
croston_forecast = None
print(str(e))
else:
croston_model = None
croston_fittedvalues = None
croston_forecast = None
return croston_model, croston_fittedvalues, croston_forecast
def _croston(
input_series,
input_series_length,
w,
h,
epsilon,
croston_variant = 'original'
):
"""
:param input_series:
:param input_series_length:
:param w:
:param h:
:param epsilon:
:param croston_variant:
:return:
"""
# Croston decomposition
nzd = numpy.where(input_series != 0)[0] # find location of non-zero demand
k = len(nzd)
z = input_series[nzd] # demand
x = numpy.concatenate([[nzd[0]], numpy.diff(nzd)]) # intervals
# initialize
init = [z[0], numpy.mean(x)]
zfit = numpy.array([None] * k)
xfit = numpy.array([None] * k)
# assign initial values and prameters
zfit[0] = init[0]
xfit[0] = init[1]
if len(w) == 1:
a_demand = w[0]
a_interval = w[0]
else:
a_demand = w[0]
a_interval = w[1]
# compute croston variant correction factors
# sba: syntetos-boylan approximation
# sbj: shale-boylan-johnston
# tsb: teunter-syntetos-babai
if croston_variant == 'sba':
correction_factor = 1 - (a_interval / 2)
elif croston_variant == 'sbj':
correction_factor = (1 - a_interval / (2 - a_interval + epsilon))
else:
correction_factor = 1
# fit model
for i in range(1,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 = correction_factor * zfit / (xfit + epsilon)
croston_model = {
'a_demand': a_demand,
'a_interval': a_interval,
'demand_series': pandas.Series(zfit),
'interval_series': pandas.Series(xfit),
'demand_process': pandas.Series(cc),
'correction_factor': correction_factor
}
# calculate in-sample demand rate
frc_in = numpy.zeros(input_series_length)
tv = numpy.concatenate([nzd, [input_series_length]]) # Time vector used to create frc_in forecasts
for i in range(k):
frc_in[tv[i]:min(tv[i+1], input_series_length)] = cc[i]
# forecast out_of_sample demand rate
if h > 0:
frc_out = numpy.array([cc[k-1]] * h)
else:
frc_out = None
return_dictionary = {
'model': croston_model,
'in_sample_forecast': pandas.Series(frc_in),
'out_of_sample_forecast': pandas.Series(frc_out)
}
return return_dictionary
def _croston_opt(
input_series,
input_series_length,
epsilon,
w = None,
nop = 1,
croston_variant = 'original'
):
"""
:param input_series:
:param input_series_length:
:param epsilon:
:param w:
:param nop:
:param croston_variant:
:return:
"""
p0 = numpy.array([0.1] * nop)
wopt = minimize(
fun = _croston_cost,
x0 = p0,
method='Nelder-Mead',
args=(input_series, input_series_length, epsilon, croston_variant)
)
constrained_wopt = numpy.minimum([1], numpy.maximum([0], wopt.x))
return constrained_wopt
def _croston_cost(
p0,
input_series,
input_series_length,
epsilon,
croston_variant
):
"""
:param p0:
:param input_series:
:param input_series_length:
:param epsilon:
:param croston_variant:
:return:
"""
# cost function for croston and variants
frc_in = _croston(input_series = input_series, input_series_length = input_series_length, w=p0, h=0, epsilon = epsilon, croston_variant = croston_variant)['in_sample_forecast']
E = input_series - frc_in
E = E[E != numpy.array(None)]
E = numpy.mean(E ** 2)
return E