From 823c7819d6719a23637a0435a2c5fdf3fdeb5eeb Mon Sep 17 00:00:00 2001
From: Jeremy Fee <jmfee@usgs.gov>
Date: Mon, 25 Jan 2016 18:17:26 -0700
Subject: [PATCH] Refactor estimate_parameters method out of additive

---
 geomagio/algorithm/SQDistAlgorithm.py | 315 +++++++++++++-------------
 1 file changed, 157 insertions(+), 158 deletions(-)

diff --git a/geomagio/algorithm/SQDistAlgorithm.py b/geomagio/algorithm/SQDistAlgorithm.py
index abc1569ca..f3fa0d241 100644
--- a/geomagio/algorithm/SQDistAlgorithm.py
+++ b/geomagio/algorithm/SQDistAlgorithm.py
@@ -1,12 +1,15 @@
 """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.
+    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 AlgorithmException import AlgorithmException
@@ -14,7 +17,7 @@ import numpy as np
 from scipy.optimize import fmin_l_bfgs_b
 
 
-def additive(yobs, m, alpha=None, beta=None, gamma=None, phi=1,
+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
@@ -33,50 +36,44 @@ def additive(yobs, m, alpha=None, beta=None, gamma=None, phi=1,
 
     Parameters
     ----------
-    alpha:
-        the level smoothing parameter (0<=alpha<=1)
-        (if None, alpha will be estimated; default)
-    b0:
-        initial slope (i.e., b(t-hstep))
-        (if None, default is (mean(yobs[m:2*m]) - mean(yobs[0:m]))/m )
-    beta:
-        the slope smoothing parameter (0<=beta<=1)
-        (if None, beta will be estimated; default)
-    fc:
-        the number of steps beyond the end of yobs (the available
-        observations) to forecast
-        (default is 0)
-    gamma:
-        the seasonal adjustment smoothing parameter (0<=gamma<=1)
-        (if None, gamma will be estimated; default)
-    hstep:
-        the number of steps ahead to predict yhat[i]
-        which forces an hstep prediction at each time step
-        (default is 0)
-    l0:
-        initial level (i.e., l(t-hstep))
-        (if None, default is mean(yobs[0:m]))
-    m:
+    yobs : array_like
+        input series to be smoothed/forecast
+    m : int
         number of "seasons"
-    phi:
+    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)
-    s0:
+    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)])
-    sigma0:
+    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) )
-    yhat0:
-        initial yhats for hstep>0 (len(yhat0) == hstep)
-        (if None, yhat0 will be set to NaNs)
-    yobs:
-        input series to be smoothed/forecast
-    zthresh:
+    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
-        (default is 6)
+    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
     -------
@@ -93,11 +90,17 @@ def additive(yobs, m, alpha=None, beta=None, gamma=None, phi=1,
     beta      : optimized beta (if input beta is None)
     gamma     : optimized gamma (if input gamma is None)
     phi       : optimized phi (if input phi is None)
-    rmse      : root mean squared error metric from optimization
-             (only if alpha or beta or gamma were optimized)
-
     """
 
+    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)])
@@ -137,62 +140,6 @@ def additive(yobs, m, alpha=None, beta=None, gamma=None, phi=1,
             raise AlgorithmException(
                 "sigma0 must have length %d" % (hstep + 1))
 
-    #
-    # Optimal parameter estimation if requested
-    # FIXME: this should probably be extracted to a separate module function.
-    #
-
-    retParams = False
-    if (alpha is None or beta is None or gamma is None or phi is None):
-        # estimate parameters
-        retParams = True
-
-        if fc > 0:
-            print "WARNING: non-zero fc is not used in estimation mode"
-
-        if alpha is not None:
-            # allows us to fix alpha
-            boundaries = [(alpha, alpha)]
-            initial_values = [alpha]
-        else:
-            boundaries = [(0, 1)]
-            initial_values = [0.3]  # FIXME: should add alpha0 option
-
-        if beta is not None:
-            # allows us to fix beta
-            boundaries.append((beta, beta))
-            initial_values.append(beta)
-        else:
-            boundaries.append((0, 1))
-            initial_values.append(0.1)  # FIXME: should add beta0 option
-
-        if gamma is not None:
-            # allows us to fix gamma
-            boundaries.append((gamma, gamma))
-            initial_values.append(gamma)
-        else:
-            boundaries.append((0, 1))
-            initial_values.append(0.1)  # FIXME: should add gamma0 option
-
-        if phi is not None:
-            # allows us to fix phi
-            boundaries.append((phi, phi))
-            initial_values.append(phi)
-        else:
-            boundaries.append((0, 1))
-            initial_values.append(0.9)  # FIXME: should add phi0 option
-
-        initial_values = np.array(initial_values)
-        method = 'additive'
-
-        parameters = fmin_l_bfgs_b(RMSE, x0=initial_values,
-                             args=(yobs, method, m, s, l, b, hstep, zthresh),
-                             bounds=boundaries, approx_grad=True)
-        alpha, beta, gamma = parameters[0]
-        rmse = parameters[1]
-
-    # endif (alpha == None or beta == None or gamma == None)
-
     #
     # Now begin the actual Holt-Winters algorithm
     #
@@ -291,67 +238,119 @@ def additive(yobs, m, alpha=None, beta=None, gamma=None, phi=1,
     l = l + r[-1]
     s = list(np.array(s) - np.hstack((r, np.tile(r[-1], m - 1))))
 
-    # return different outputs depending on retParams
-    if retParams:
-        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,
-                rmse)
-    else:
-        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:])
-
-
-def RMSE(params, *args):
-    """Wrapper function passed to scipy.optimize.fmin_l_bfgs_b in
-      order to find optimal Holt-Winters prediction coefficients.
+    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)
+
+
+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.
     """
-    # extract parameters to fit
-    alpha, beta, gamma = params
-
-    # extract arguments
-    yobs = args[0]
-    method = args[1]
-    m = args[2]
-    s0 = args[3]
-    l0 = args[4]
-    b0 = args[5]
-    hstep = args[6]
-    zthresh = args[7]
-
-    if method == "additive":
+    # 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, _, _, _, _, _, _, _ = additive(yobs, m, alpha=alpha, beta=beta,
-            gamma=gamma, l0=l0, b0=b0, s0=s0, zthresh=zthresh, hstep=hstep)
-
-    else:
-        print 'Method must be additive or ...'
-        raise Exception
-
-    # calculate root-mean-squared-error of predictions
-    rmse = np.sqrt(np.nanmean([(x - y) ** 2 for x, y in zip(yobs, yhat)]))
-
-    return rmse
-
-if __name__ == '__main__':
-    pass
+        yhat, _, _, _, _, _, _, _, _, _, _ = 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)
-- 
GitLab