import numpy as np LOWER_BOUND = np.sqrt(np.finfo(float).eps) class HoltWintersArgs: def __init__(self, xi, p, bounds, y, m, n, transform=False): self._xi = xi self._p = p self._bounds = bounds self._y = y self._lvl = np.empty(n) self._b = np.empty(n) self._s = np.empty(n + m - 1) self._m = m self._n = n self._transform = transform @property def xi(self): return self._xi @xi.setter def xi(self, value): self._xi = value @property def p(self): return self._p @property def bounds(self): return self._bounds @property def y(self): return self._y @property def lvl(self): return self._lvl @property def b(self): return self._b @property def s(self): return self._s @property def m(self): return self._m @property def n(self): return self._n @property def transform(self): return self._transform @transform.setter def transform(self, value): self._transform = value def to_restricted(p, sel, bounds): """ Transform parameters from the unrestricted [0,1] space to satisfy both the bounds and the 2 constraints beta <= alpha and gamma <= (1-alpha) Parameters ---------- p : ndarray The parameters to transform sel : ndarray Array indicating whether a parameter is being estimated. If not estimated, not transformed. bounds : ndarray 2-d array of bounds where bound for element i is in row i and stored as [lb, ub] Returns ------- """ a, b, g = p[:3] if sel[0]: lb = max(LOWER_BOUND, bounds[0, 0]) ub = min(1 - LOWER_BOUND, bounds[0, 1]) a = lb + a * (ub - lb) if sel[1]: lb = bounds[1, 0] ub = min(a, bounds[1, 1]) b = lb + b * (ub - lb) if sel[2]: lb = bounds[2, 0] ub = min(1.0 - a, bounds[2, 1]) g = lb + g * (ub - lb) return a, b, g def to_unrestricted(p, sel, bounds): """ Transform parameters to the unrestricted [0,1] space Parameters ---------- p : ndarray Parameters that strictly satisfy the constraints Returns ------- ndarray Parameters all in (0,1) """ # eps < a < 1 - eps # eps < b <= a # eps < g <= 1 - a a, b, g = p[:3] if sel[0]: lb = max(LOWER_BOUND, bounds[0, 0]) ub = min(1 - LOWER_BOUND, bounds[0, 1]) a = (a - lb) / (ub - lb) if sel[1]: lb = bounds[1, 0] ub = min(p[0], bounds[1, 1]) b = (b - lb) / (ub - lb) if sel[2]: lb = bounds[2, 0] ub = min(1.0 - p[0], bounds[2, 1]) g = (g - lb) / (ub - lb) return a, b, g def holt_init(x, hw_args: HoltWintersArgs): """ Initialization for the Holt Models """ # Map back to the full set of parameters hw_args.p[hw_args.xi.astype(bool)] = x # Ensure alpha and beta satisfy the requirements if hw_args.transform: alpha, beta, _ = to_restricted(hw_args.p, hw_args.xi, hw_args.bounds) else: alpha, beta = hw_args.p[:2] # Level, trend and dampening l0, b0, phi = hw_args.p[3:6] # Save repeated calculations alphac = 1 - alpha betac = 1 - beta # Setup alpha * y y_alpha = alpha * hw_args.y # In-place operations hw_args.lvl[0] = l0 hw_args.b[0] = b0 return alpha, beta, phi, alphac, betac, y_alpha def holt__(x, hw_args: HoltWintersArgs): """ Simple Exponential Smoothing Minimization Function (,) """ _, _, _, alphac, _, y_alpha = holt_init(x, hw_args) n = hw_args.n lvl = hw_args.lvl for i in range(1, n): lvl[i] = (y_alpha[i - 1]) + (alphac * (lvl[i - 1])) return hw_args.y - lvl def holt_mul_dam(x, hw_args: HoltWintersArgs): """ Multiplicative and Multiplicative Damped Minimization Function (M,) & (Md,) """ _, beta, phi, alphac, betac, y_alpha = holt_init(x, hw_args) lvl = hw_args.lvl b = hw_args.b for i in range(1, hw_args.n): lvl[i] = (y_alpha[i - 1]) + (alphac * (lvl[i - 1] * b[i - 1] ** phi)) b[i] = (beta * (lvl[i] / lvl[i - 1])) + (betac * b[i - 1] ** phi) return hw_args.y - lvl * b ** phi def holt_add_dam(x, hw_args: HoltWintersArgs): """ Additive and Additive Damped Minimization Function (A,) & (Ad,) """ _, beta, phi, alphac, betac, y_alpha = holt_init(x, hw_args) lvl = hw_args.lvl b = hw_args.b for i in range(1, hw_args.n): lvl[i] = (y_alpha[i - 1]) + (alphac * (lvl[i - 1] + phi * b[i - 1])) b[i] = (beta * (lvl[i] - lvl[i - 1])) + (betac * phi * b[i - 1]) return hw_args.y - (lvl + phi * b) def holt_win_init(x, hw_args: HoltWintersArgs): """Initialization for the Holt Winters Seasonal Models""" hw_args.p[hw_args.xi.astype(bool)] = x if hw_args.transform: alpha, beta, gamma = to_restricted( hw_args.p, hw_args.xi, hw_args.bounds ) else: alpha, beta, gamma = hw_args.p[:3] l0, b0, phi = hw_args.p[3:6] s0 = hw_args.p[6:] alphac = 1 - alpha betac = 1 - beta gammac = 1 - gamma y_alpha = alpha * hw_args.y y_gamma = gamma * hw_args.y hw_args.lvl[:] = 0 hw_args.b[:] = 0 hw_args.s[:] = 0 hw_args.lvl[0] = l0 hw_args.b[0] = b0 hw_args.s[: hw_args.m] = s0 return alpha, beta, gamma, phi, alphac, betac, gammac, y_alpha, y_gamma def holt_win__mul(x, hw_args: HoltWintersArgs): """ Multiplicative Seasonal Minimization Function (,M) """ (_, _, _, _, alphac, _, gammac, y_alpha, y_gamma) = holt_win_init( x, hw_args ) lvl = hw_args.lvl s = hw_args.s m = hw_args.m for i in range(1, hw_args.n): lvl[i] = (y_alpha[i - 1] / s[i - 1]) + (alphac * (lvl[i - 1])) s[i + m - 1] = (y_gamma[i - 1] / (lvl[i - 1])) + (gammac * s[i - 1]) return hw_args.y - lvl * s[: -(m - 1)] def holt_win__add(x, hw_args: HoltWintersArgs): """ Additive Seasonal Minimization Function (,A) """ (alpha, _, gamma, _, alphac, _, gammac, y_alpha, y_gamma) = holt_win_init( x, hw_args ) lvl = hw_args.lvl s = hw_args.s m = hw_args.m for i in range(1, hw_args.n): lvl[i] = ( (y_alpha[i - 1]) - (alpha * s[i - 1]) + (alphac * (lvl[i - 1])) ) s[i + m - 1] = ( y_gamma[i - 1] - (gamma * (lvl[i - 1])) + (gammac * s[i - 1]) ) return hw_args.y - lvl - s[: -(m - 1)] def holt_win_add_mul_dam(x, hw_args: HoltWintersArgs): """ Additive and Additive Damped with Multiplicative Seasonal Minimization Function (A,M) & (Ad,M) """ ( _, beta, _, phi, alphac, betac, gammac, y_alpha, y_gamma, ) = holt_win_init(x, hw_args) lvl = hw_args.lvl b = hw_args.b s = hw_args.s m = hw_args.m for i in range(1, hw_args.n): lvl[i] = (y_alpha[i - 1] / s[i - 1]) + ( alphac * (lvl[i - 1] + phi * b[i - 1]) ) b[i] = (beta * (lvl[i] - lvl[i - 1])) + (betac * phi * b[i - 1]) s[i + m - 1] = (y_gamma[i - 1] / (lvl[i - 1] + phi * b[i - 1])) + ( gammac * s[i - 1] ) return hw_args.y - (lvl + phi * b) * s[: -(m - 1)] def holt_win_mul_mul_dam(x, hw_args: HoltWintersArgs): """ Multiplicative and Multiplicative Damped with Multiplicative Seasonal Minimization Function (M,M) & (Md,M) """ ( _, beta, _, phi, alphac, betac, gammac, y_alpha, y_gamma, ) = holt_win_init(x, hw_args) lvl = hw_args.lvl s = hw_args.s b = hw_args.b m = hw_args.m for i in range(1, hw_args.n): lvl[i] = (y_alpha[i - 1] / s[i - 1]) + ( alphac * (lvl[i - 1] * b[i - 1] ** phi) ) b[i] = (beta * (lvl[i] / lvl[i - 1])) + (betac * b[i - 1] ** phi) s[i + m - 1] = (y_gamma[i - 1] / (lvl[i - 1] * b[i - 1] ** phi)) + ( gammac * s[i - 1] ) return hw_args.y - (lvl * b ** phi) * s[: -(m - 1)] def holt_win_add_add_dam(x, hw_args: HoltWintersArgs): """ Additive and Additive Damped with Additive Seasonal Minimization Function (A,A) & (Ad,A) """ ( alpha, beta, gamma, phi, alphac, betac, gammac, y_alpha, y_gamma, ) = holt_win_init(x, hw_args) lvl = hw_args.lvl s = hw_args.s b = hw_args.b m = hw_args.m for i in range(1, hw_args.n): lvl[i] = ( (y_alpha[i - 1]) - (alpha * s[i - 1]) + (alphac * (lvl[i - 1] + phi * b[i - 1])) ) b[i] = (beta * (lvl[i] - lvl[i - 1])) + (betac * phi * b[i - 1]) s[i + m - 1] = ( y_gamma[i - 1] - (gamma * (lvl[i - 1] + phi * b[i - 1])) + (gammac * s[i - 1]) ) return hw_args.y - ((lvl + phi * b) + s[: -(m - 1)]) def holt_win_mul_add_dam(x, hw_args: HoltWintersArgs): """ Multiplicative and Multiplicative Damped with Additive Seasonal Minimization Function (M,A) & (M,Ad) """ ( alpha, beta, gamma, phi, alphac, betac, gammac, y_alpha, y_gamma, ) = holt_win_init(x, hw_args) lvl = hw_args.lvl s = hw_args.s b = hw_args.b m = hw_args.m for i in range(1, hw_args.n): lvl[i] = ( (y_alpha[i - 1]) - (alpha * s[i - 1]) + (alphac * (lvl[i - 1] * b[i - 1] ** phi)) ) b[i] = (beta * (lvl[i] / lvl[i - 1])) + (betac * b[i - 1] ** phi) s[i + m - 1] = ( y_gamma[i - 1] - (gamma * (lvl[i - 1] * b[i - 1] ** phi)) + (gammac * s[i - 1]) ) return hw_args.y - ((lvl * phi * b) + s[: -(m - 1)])