diff --git a/geomagio/algorithm/SQDistAlgorithm.py b/geomagio/algorithm/SQDistAlgorithm.py deleted file mode 100644 index 2905055b5f2a5745d754be6287745d74262cdfe9..0000000000000000000000000000000000000000 --- a/geomagio/algorithm/SQDistAlgorithm.py +++ /dev/null @@ -1,393 +0,0 @@ -"""Algorithm that produces Solar Quiet (SQ), Secular Variation (SV) and - Magnetic Disturbance (DIST). - - Algorithm for producing SQ, SV and DIST. - This module implements Holt-Winters exponential smoothing. It predicts - an offset-from-zero plus a "seasonal" correction given observations - up to time t-1. Each observation's influence on the current prediction - decreases exponentially with time according to user-supplied runtime - configuration parameters. - - Use of fmin_l_bfgs_b to estimate parameters inspired by Andre Queiroz: - https://gist.github.com/andrequeiroz/5888967 -""" - -from Algorithm import Algorithm -from AlgorithmException import AlgorithmException -import numpy as np -from scipy.optimize import fmin_l_bfgs_b - - -class SqDistAlgorithm(Algorithm): - """Solar Quiet, Secular Variation, and Disturbance algorithm""" - - def __init__(self, alpha=None, beta=None, gamma=None, phi=1, - yhat0=None, s0=None, l0=None, b0=None, sigma0=None, - zthresh=6, fc=0, hstep=0): - self.alpha = alpha - self.beta = beta - self.gamma = gamma - self.phi = phi - self.yhat0 = yhat0 - self.s0 = s0 - self.l0 = l0 - self.b0 = b0 - self.sigma0 = sigma0 - self.zthresh = zthresh - self.fc = fc - self.hstep = hstep - - def process(self, yobs, m): - pass - - @classmethod - def additive(yobs, m, alpha, beta, gamma, phi=1, - yhat0=None, s0=None, l0=None, b0=None, sigma0=None, - zthresh=6, fc=0, hstep=0): - """Primary function for Holt-Winters smoothing/forecasting with - damped linear trend and additive seasonal component. - - The adaptive standard deviation (sigma), multiplied by zthresh to - determine which observations should be smoothed or ignored, is always - updated using the latest error if a valid observation is available. - This way, if what seemed a spike in real-time was actually a more - permanent baseline shift, the algorithm will adjust to the new baseline - once sigma grows enough to accommodate the errors. - - The standard deviation also updates when no obserations are present, - but does so according to Hyndman et al (2005) prediction intervals. - The result is a sigma that grows over gaps, and for forecasts beyond - yobs[-1]. - - Parameters - ---------- - yobs : array_like - input series to be smoothed/forecast - m : int - number of "seasons" - alpha : float - the level smoothing parameter (0<=alpha<=1). - beta : float - the slope smoothing parameter (0<=beta<=1). - gamma : float - the seasonal adjustment smoothing parameter (0<=gamma<=1). - phi : float - the dampening factor for slope (0<=phi<=1) - (if None, phi will be estimated; default is 1) - yhat0 : array_like - initial yhats for hstep>0 (len(yhat0) == hstep) - (if None, yhat0 will be set to NaNs) - s0 : array_like - initial set of seasonal adjustments - (if None, default is [yobs[i] - a[0] for i in range(m)]) - l0 : float - initial level (i.e., l(t-hstep)) - (if None, default is mean(yobs[0:m])) - b0 : float - initial slope (i.e., b(t-hstep)) - (if None, default is (mean(yobs[m:2*m]) - mean(yobs[0:m]))/m ) - sigma0 : float - initial standard-deviation estimate (len(sigma0) == hstep+1) - (if None, default is [sqrt(var(yobs))] * (hstep+1) ) - zthresh : int - z-score threshold to determine whether yhat is updated by - smoothing observations, or by simulation alone; if exceeded, - only sigma is updated to reflect latest observation - fc : int - the number of steps beyond the end of yobs (the available - observations) to forecast - hstep : int - the number of steps ahead to predict yhat[i] - which forces an hstep prediction at each time step - - Returns - ------- - yhat : array_like - series of smoothed/forecast values (aligned with yobs(t)) - shat : array_like - series of seasonal adjustments (aligned with yobs(t)) - sigmahat : array_like - series of time-varying standard deviations (aligned with yobs(t)) - yhat0next : array_like - use as yhat0 when function called again with new observations - s0next : float - use as s0 when function called again with new observations - l0next : float - use as l0 when function called again with new observations - b0next : float - use as b0 when function called again with new observations - sigma0next : float - use as sigma0 when function called again with new observations - alpha : float - optimized alpha (if input alpha is None) - beta : float - optimized beta (if input beta is None) - gamma : float - optimized gamma (if input gamma is None) - phi : float - optimized phi (if input phi is None) - """ - - if alpha is None: - raise AlgorithmException('alpha is required') - if beta is None: - raise AlgorithmException('beta is required') - if gamma is None: - raise AlgorithmException('gamma is required') - if phi is None: - raise AlgorithmException('phi is required') - - # set some default values - if l0 is None: - l = np.nanmean(yobs[0:int(m)]) - else: - l = l0 - if not np.isscalar(l0): - raise AlgorithmException("l0 must be a scalar") - - if b0 is None: - b = (np.nanmean(yobs[m:2 * m]) - np.nanmean(yobs[0:m])) / m - b = 0 if np.isnan(b) else b # replace NaN with 0 - else: - b = b0 - if not np.isscalar(b0): - raise AlgorithmException("b0 must be a scalar") - - if yhat0 is None: - yhat = [np.nan for i in range(hstep)] - else: - yhat = list(yhat0) - if len(yhat) != hstep: - raise AlgorithmException("yhat0 must have length %d" % hstep) - - if s0 is None: - s = [yobs[i] - l for i in range(m)] - s = [i if ~np.isnan(i) else 0 for i in s] # replace NaNs with 0s - else: - s = list(s0) - if len(s) != m: - raise AlgorithmException("s0 must have length %d " % m) - - if sigma0 is None: - sigma = [np.sqrt(np.nanvar(yobs))] * (hstep + 1) - else: - sigma = list(sigma0) - if len(sigma) != (hstep + 1): - raise AlgorithmException( - "sigma0 must have length %d" % (hstep + 1)) - - # - # Now begin the actual Holt-Winters algorithm - # - - # ensure mean of seasonal adjustments is zero by setting first element - # of r equal to mean(s) - r = [np.nanmean(s)] - - # determine sum(c^2) and phi_(j-1) for hstep "prediction interval" - # outside of loop; initialize variables for jstep (beyond hstep) - # prediction intervals - sumc2_H = 1 - phiHminus1 = 0 - for h in range(1, hstep): - phiHminus1 = phiHminus1 + phi ** (h - 1) - sumc2_H = sumc2_H + (alpha * (1 + phiHminus1 * beta) + - gamma * (1 if (h % m == 0) else 0)) ** 2 - phiJminus1 = phiHminus1 - sumc2 = sumc2_H - jstep = hstep - - # convert to, and pre-allocate numpy arrays - yobs = np.array(yobs) - sigma = np.concatenate((sigma, np.zeros(yobs.size + fc))) - yhat = np.concatenate((yhat, np.zeros(yobs.size + fc))) - r = np.concatenate((r, np.zeros(yobs.size + fc))) - s = np.concatenate((s, np.zeros(yobs.size + fc))) - - # smooth/simulate/forecast yobs - for i in range(len(yobs) + fc): - # Update/append sigma for h steps ahead of i following - # Hyndman-et-al-2005. This will be over-written if valid - # observations exist at step i - if jstep == hstep: - sigma2 = sigma[i] * sigma[i] - sigma[i + hstep + 1] = np.sqrt(sigma2 * sumc2) - - # predict h steps ahead - yhat[i + hstep] = l + phiHminus1 * b + s[i + hstep % m] - - # discrepancy between observation and prediction at step i - if i < len(yobs): - et = yobs[i] - yhat[i] - else: - et = np.nan - - if (np.isnan(et) or np.abs(et) > zthresh * sigma[i]): - # forecast (i.e., update l, b, and s assuming et==0) - - # no change in seasonal adjustments - r[i + 1] = 0 - s[i + m] = s[i] - - # update l before b - l = l + phi * b - b = phi * b - - if np.isnan(et): - # when forecasting, grow sigma=sqrt(var) like a prediction - # interval; sumc2 and jstep will be reset with the next - # valid observation - phiJminus1 = phiJminus1 + phi ** jstep - sumc2 = sumc2 + (alpha * (1 + phiJminus1 * beta) + - gamma * (1 if (jstep % m == 0) else 0)) ** 2 - jstep = jstep + 1 - - else: - # still update sigma using et when et > zthresh * sigma - # (and is not NaN) - sigma[i + 1] = alpha * np.abs(et) + (1 - alpha) * sigma[i] - else: - # smooth (i.e., update l, b, and s by filtering et) - - # renormalization could occur inside loop, but we choose to - # integrate r, and adjust a and s outside the loop to improve - # performance. - r[i + 1] = gamma * (1 - alpha) * et / m - - # update and append to s using equation-error formulation - s[i + m] = s[i] + gamma * (1 - alpha) * et - - # update l and b using equation-error formulation - l = l + phi * b + alpha * et - b = phi * b + alpha * beta * et - - # update sigma with et, then reset prediction interval - sigma[i + 1] = alpha * np.abs(et) + (1 - alpha) * sigma[i] - sumc2 = sumc2_H - phiJminus1 = phiHminus1 - jstep = hstep - # endif (np.isnan(et) or np.abs(et) > zthresh * sigma[i]) - - # endfor i in range(len(yobs) + fc - hstep) - - r = np.cumsum(r) - l = l + r[-1] - s = list(np.array(s) - np.hstack((r, np.tile(r[-1], m - 1)))) - - return (yhat[:len(yobs) + fc], - s[:len(yobs) + fc], - sigma[1:len(yobs) + fc + 1], - yhat[len(yobs) + fc:], - s[len(yobs) + fc:], - l, - b, - sigma[len(yobs) + fc:], - alpha, - beta, - gamma) - - @classmethod - def estimate_parameters(yobs, m, alpha=None, beta=None, gamma=None, phi=1, - yhat0=None, s0=None, l0=None, b0=None, sigma0=None, - zthresh=6, fc=0, hstep=0, - alpha0=0.3, beta0=0.1, gamma0=0.1): - """Estimate alpha, beta, and gamma parameters based on observed data. - - Parameters - ---------- - yobs : array_like - input series to be smoothed/forecast - m : int - number of "seasons" - alpha : float - the level smoothing parameter (0<=alpha<=1). - beta : float - the slope smoothing parameter (0<=beta<=1). - gamma : float - the seasonal adjustment smoothing parameter (0<=gamma<=1). - phi : float - the dampening factor for slope (0<=phi<=1) - (if None, phi will be estimated; default is 1) - yhat0 : array_like - initial yhats for hstep>0 (len(yhat0) == hstep) - (if None, yhat0 will be set to NaNs) - s0 : array_like - initial set of seasonal adjustments - (if None, default is [yobs[i] - a[0] for i in range(m)]) - l0 : float - initial level (i.e., l(t-hstep)) - (if None, default is mean(yobs[0:m])) - b0 : float - initial slope (i.e., b(t-hstep)) - (if None, default is (mean(yobs[m:2*m]) - mean(yobs[0:m]))/m ) - sigma0 : float - initial standard-deviation estimate (len(sigma0) == hstep+1) - (if None, default is [sqrt(var(yobs))] * (hstep+1) ) - zthresh : int - z-score threshold to determine whether yhat is updated by - smoothing observations, or by simulation alone; if exceeded, - only sigma is updated to reflect latest observation - fc : int - the number of steps beyond the end of yobs (the available - observations) to forecast - hstep : int - the number of steps ahead to predict yhat[i] - which forces an hstep prediction at each time step - alpha0 : float - initial value for alpha. - used only when alpha is None. - beta0 : float - initial value for beta. - used only when beta is None. - gamma0 : float - initial value for gamma. - used only when gamma is None. - - Returns - ------- - alpha : float - optimized parameter alpha (if alpha was None). - beta : float - optimized parameter beta (if beta was None). - gamma : float - optimized gamma, if gamma was None. - rmse : float - root-mean-squared-error for data using optimized parameters. - """ - # if alpha/beta/gamma is specified, restrict bounds to "fix" parameter. - boundaries = [ - (alpha, alpha) if alpha is not None else (0, 1), - (beta, beta) if beta is not None else (0, 1), - (gamma, gamma) if gamma is not None else (0, 1) - ] - initial_values = np.array([ - alpha if alpha is not None else alpha0, - beta if beta is not None else beta0, - gamma if gamma is not None else gamma0 - ]) - - def func(params, *args): - """Function that computes root-mean-squared-error based on current - alpha, beta, and gamma parameters; as provided by fmin_l_bfgs_b. - - Parameters - ---------- - params: list-like - list containing alpha, beta, and gamma parameters to test - """ - # extract parameters to fit - alpha, beta, gamma = params - # call Holt-Winters with additive seasonality - yhat, _, _, _, _, _, _, _, _, _, _ = SqDistAlgorithm.additive( - yobs, m, - alpha=alpha, beta=beta, gamma=gamma, l0=l0, b0=b0, s0=s0, - zthresh=zthresh, hstep=hstep) - # compute root-mean-squared-error of predictions - error = np.sqrt(np.nanmean(np.square(np.subtract(yobs, yhat)))) - return error - - parameters = fmin_l_bfgs_b(func, x0=initial_values, args=(), - bounds=boundaries, approx_grad=True) - alpha, beta, gamma = parameters[0] - rmse = parameters[1] - return (alpha, beta, gamma, rmse)