//
//  OpenForecast - open source, general-purpose forecasting package.
//  Copyright (C) 2002-2011  Steven R. Gould
//
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//

package net.sourceforge.openforecast.models;


import java.util.Iterator;
import net.sourceforge.openforecast.DataPoint;
import net.sourceforge.openforecast.DataSet;
import net.sourceforge.openforecast.Observation;


/**
 * Double exponential smoothing - also known as Holt exponential smoothing
 * - is a refinement of the popular simple exponential smoothing model but
 * adds another component which takes into account any trend in the data.
 * Simple exponential smoothing models work best with data where there are no
 * trend or seasonality components to the data. When the data exhibits either
 * an increasing or decreasing trend over time, simple exponential smoothing
 * forecasts tend to lag behind observations. Double exponential smoothing is
 * designed to address this type of data series by taking into account any
 * trend in the data.
 *
 * 

Note that double exponential smoothing still does not address * seasonality. For better exponentially smoothed forecasts using data where * there is expected or known to be seasonal variation in the data, use triple * exponential smoothing. * *

As with simple exponential smoothing, in double exponential smoothing * models past observations are given exponentially smaller weights as the * observations get older. In other words, recent observations are given * relatively more weight in forecasting than the older observations. * *

There are two equations associated with Double Exponential Smoothing. * *

* *

where: *

* *

To initialize the double exponential smoothing model, * f1 is set to Y1, and the * initial slope b1 is set to the difference between * the first two observations; i.e. Y2-Y1. * Although there are other ways to initialize the model, as of the time of * writing, these alternatives are not available in this implementation. * Future implementations of this model may offer these options. * *

Choosing values for the smoothing constants

*

The smoothing constants must be a values in the range 0.0-1.0. But, what * are the "best" values to use for the smoothing constants? This depends on * the data series being modeled. * *

In general, the speed at which the older responses are dampened * (smoothed) is a function of the value of the smoothing constant. When this * smoothing constant is close to 1.0, dampening is quick - more weight is * given to recent observations - and when it is close to 0.0, dampening is * slow - and relatively less weight is given to recent observations. * *

The best value for the smoothing constant is the one that results in the * smallest mean of the squared errors (or other similar accuracy indicator). * The {@link net.sourceforge.openforecast.Forecaster} class can help with * selection of the best values for the smoothing constants. * @author Steven R. Gould * @since 0.4 * @see Engineering Statistics Handbook, 6.4.3.3 Double Exponential Smoothing */ public class DoubleExponentialSmoothingModel extends AbstractTimeBasedModel { /** * The default value of the tolerance permitted in the estimates of the * smoothing constants in the {@link #getBestFitModel} methods. */ private static double DEFAULT_SMOOTHING_CONSTANT_TOLERANCE = 0.001; /** * The smoothing constant used in this exponential smoothing model. */ private double alpha; /** * The second smoothing constant (gamma) used in this exponential * smoothing model. This is used to smooth the trend. */ private double gamma; /** * Provides a cache of calculated slopeValues. Since these values are * used very frequently when calculating forecast values, it is more * efficient to cache the previously calculated slope values for future * use. */ private DataSet slopeValues; /** * Factory method that returns a "best fit" double exponential smoothing * model for the given data set. This, like the overloaded * {@link #getBestFitModel(DataSet,double,double)}, attempts to derive * "good" - hopefully near optimal - values for the alpha and gamma * smoothing constants. * @param dataSet the observations for which a "best fit" double * exponential smoothing model is required. * @return a best fit double exponential smoothing model for the given * data set. * @see #getBestFitModel(DataSet,double,double) */ public static DoubleExponentialSmoothingModel getBestFitModel( DataSet dataSet ) { return getBestFitModel( dataSet, DEFAULT_SMOOTHING_CONSTANT_TOLERANCE, DEFAULT_SMOOTHING_CONSTANT_TOLERANCE ); } /** * Factory method that returns a best fit double exponential smoothing * model for the given data set. This, like the overloaded * {@link #getBestFitModel(DataSet)}, attempts to derive "good" - * hopefully near optimal - values for the alpha and gamma smoothing * constants. * *

To determine which model is "best", this method currently uses only * the Mean Squared Error (MSE). Future versions may use other measures in * addition to the MSE. However, the resulting "best fit" model - and the * associated values of alpha and gamma - is expected to be very similar * either way. * *

Note that the approach used to calculate the best smoothing * constants - alpha and gamma - may end up choosing values near * a local optimum. In other words, there may be other values for * alpha and gamma that result in an even better model. * @param dataSet the observations for which a "best fit" double * exponential smoothing model is required. * @param alphaTolerance the required precision/accuracy - or tolerance * of error - required in the estimate of the alpha smoothing constant. * @param gammaTolerance the required precision/accuracy - or tolerance * of error - required in the estimate of the gamma smoothing constant. * @return a best fit double exponential smoothing model for the given * data set. */ public static DoubleExponentialSmoothingModel getBestFitModel( DataSet dataSet, double alphaTolerance, double gammaTolerance ) { DoubleExponentialSmoothingModel model1 = findBestGamma( dataSet, 0.0, 0.0, 1.0, gammaTolerance ); DoubleExponentialSmoothingModel model2 = findBestGamma( dataSet, 0.5, 0.0, 1.0, gammaTolerance ); DoubleExponentialSmoothingModel model3 = findBestGamma( dataSet, 1.0, 0.0, 1.0, gammaTolerance ); // First rough estimate of alpha and gamma to the nearest 0.1 DoubleExponentialSmoothingModel bestModel = findBest( dataSet, model1, model2, model3, alphaTolerance, gammaTolerance ); return bestModel; } /** * Performs a non-linear - yet somewhat intelligent - search for the best * values for the smoothing coefficients alpha and gamma for the given * data set. * *

For the given data set, and models with a small, medium and large * value of the alpha smoothing constant, returns the best fit model where * the value of the alpha and gamma (trend) smoothing constants are within * the given tolerances. * *

Note that the descriptions of the parameters below include a * discussion of valid values. However, since this is a private method and * to help improve performance, we don't provide any validation of these * parameters. Using invalid values may lead to unexpected results. * @param dataSet the data set for which a best fit model is required. * @param modelMin the pre-initialized best fit model with the smallest * value of the alpha smoothing constant found so far. * @param modelMid the pre-initialized best fit model with the value of * the alpha smoothing constant between that of modelMin and modelMax. * @param modelMax the pre-initialized best fit model with the largest * value of the alpha smoothing constant found so far. * @param alphaTolerance the tolerance within which the alpha value is * required. Must be considerably less than 1.0. However, note that the * smaller this value the longer it will take to diverge on a best fit * model. * @param gammaTolerance the tolerance within which the gamma value is * required. Must be considerably less than 1.0. However, note that the * smaller this value the longer it will take to diverge on a best fit * model. This value can be the same as, greater than or less than the * value of the alphaTolerance parameter. It makes no difference - at * least to this code. */ private static DoubleExponentialSmoothingModel findBest( DataSet dataSet, DoubleExponentialSmoothingModel modelMin, DoubleExponentialSmoothingModel modelMid, DoubleExponentialSmoothingModel modelMax, double alphaTolerance, double gammaTolerance ) { double alphaMin = modelMin.getAlpha(); double alphaMid = modelMid.getAlpha(); double alphaMax = modelMax.getAlpha(); // If we're not making much ground, then we're done if (Math.abs(alphaMid-alphaMin)Note that the descriptions of the parameters below include a * discussion of valid values. However, since this is a private method and * to help improve performance, we don't provide any validation of these * parameters. Using invalid values may lead to unexpected results. * @param dataSet the data set for which a best fit model is required. * @param alpha the (fixed) value of the alpha smoothing constant to use * for the best fit model. * @param gammaMin the minimum value of the gamma (trend) smoothing * constant accepted in the resulting best fit model. Must be greater than * (or equal to) 0.0 and less than gammaMax. * @param gammaMax the maximum value of the gamma (trend) smoothing * constant accepted in the resulting best fit model. Must be greater than * gammaMin and less than (or equal to) 1.0. * @param gammaTolerance the tolerance within which the gamma value is * required. Must be considerably less than 1.0. However, note that the * smaller this value the longer it will take to diverge on a best fit * model. */ private static DoubleExponentialSmoothingModel findBestGamma( DataSet dataSet, double alpha, double gammaMin, double gammaMax, double gammaTolerance ) { int stepsPerIteration = 10; if ( gammaMin < 0.0 ) gammaMin = 0.0; if ( gammaMax > 1.0 ) gammaMax = 1.0; DoubleExponentialSmoothingModel bestModel = new DoubleExponentialSmoothingModel( alpha, gammaMin ); bestModel.init(dataSet); double initialMSE = bestModel.getMSE(); boolean gammaImproving = true; double gammaStep = (gammaMax-gammaMin)/stepsPerIteration; double gamma = gammaMin + gammaStep; for ( ; gamma<=gammaMax || gammaImproving; ) { DoubleExponentialSmoothingModel model = new DoubleExponentialSmoothingModel( alpha, gamma ); model.init( dataSet ); if ( model.getMSE() < bestModel.getMSE() ) bestModel = model; else gammaImproving = false; gamma += gammaStep; if ( gamma > 1.0 ) gammaImproving = false; } // If we're making progress, then try to refine the gamma estimate if ( bestModel.getMSE() < initialMSE && gammaStep > gammaTolerance ) { // Can this be further refined - improving efficiency - by // only searching in the range gamma-gammaStep/2 to // gamma+gammaStep/2 ? return findBestGamma( dataSet, bestModel.getAlpha(), bestModel.getGamma()-gammaStep, bestModel.getGamma()+gammaStep, gammaTolerance ); } return bestModel; } /** * Constructs a new double exponential smoothing forecasting model, using * the given smoothing constants - alpha and gamma. For a valid model to * be constructed, you should call init and pass in a data set containing * a series of data points with the time variable initialized to identify * the independent variable. * @param alpha the smoothing constant to use for this exponential * smoothing model. Must be a value in the range 0.0-1.0. * @param gamma the second smoothing constant, gamma to use in this model * to smooth the trend. Must be a value in the range 0.0-1.0. * @throws IllegalArgumentException if the value of either smoothing * constant is invalid - outside the range 0.0-1.0. */ public DoubleExponentialSmoothingModel( double alpha, double gamma ) { if ( alpha < 0.0 || alpha > 1.0 ) throw new IllegalArgumentException("DoubleExponentialSmoothingModel: Invalid smoothing constant, " + alpha + " - must be in the range 0.0-1.0."); if ( gamma < 0.0 || gamma > 1.0 ) throw new IllegalArgumentException("DoubleExponentialSmoothingModel: Invalid smoothing constant, gamma=" + gamma + " - must be in the range 0.0-1.0."); slopeValues = new DataSet(); this.alpha = alpha; this.gamma = gamma; } /** * Returns the forecast value of the dependent variable for the given * value of the (independent) time variable using a single exponential * smoothing model. See the class documentation for details on the * formulation used. * @param t the value of the time variable for which a forecast * value is required. * @return the forecast value of the dependent variable at time, t. * @throws IllegalArgumentException if there is insufficient historical * data - observations passed to init - to generate a forecast for the * given time value. */ protected double forecast( double t ) throws IllegalArgumentException { double previousTime = t - getTimeInterval(); // As a starting point, we set the first forecast value to be // the same as the observed value if ( previousTime < getMinimumTimeValue()+TOLERANCE ) return getObservedValue( t ); try { double b = getSlope( previousTime ); double forecast = alpha*getObservedValue(t) + (1.0-alpha)*(getForecastValue(previousTime)+b); return forecast; } catch ( IllegalArgumentException iaex ) { double maxTimeValue = getMaximumTimeValue(); double b = getSlope( maxTimeValue-getTimeInterval() ); double forecast = getForecastValue(maxTimeValue) + (t-maxTimeValue)*b; return forecast; } } /** * Calculates and returns the slope for the given time period. Except * for the initial periods - where forecasts are not available - the * slope is calculated using forecast values, and not observed values. * See the class documentation for details on the formulation used. * @param time the time value for which the slope is required. * @return the slope of the data at the given period of time. * @param IllegalArgumentException if the slope cannot be determined for * the given time period. */ private double getSlope( double time ) throws IllegalArgumentException { // TODO: Optimize this search by having data set sorted by time // Search for previously calculated - and saved - slope value String timeVariable = getTimeVariable(); Iterator it = slopeValues.iterator(); while ( it.hasNext() ) { DataPoint dp = it.next(); double dpTimeValue = dp.getIndependentValue( timeVariable ); if ( Math.abs(time-dpTimeValue) < TOLERANCE ) return dp.getDependentValue(); } // Saved slope not found, so calculate it // (and save it for future reference) double previousTime = time - getTimeInterval(); double slope = 0.0; // Initial condition for first periods if ( previousTime < getMinimumTimeValue()+TOLERANCE ) slope = getObservedValue(time)-getObservedValue(previousTime); else slope = gamma*(forecast(time)-forecast(previousTime)) + (1-gamma)*getSlope(previousTime); DataPoint dp = new Observation( slope ); dp.setIndependentValue( timeVariable, time ); slopeValues.add( dp ); return slope; } /** * Returns the current number of periods used in this model. This is also * the minimum number of periods required in order to produce a valid * forecast. Strictly speaking, for double exponential smoothing only two * previous periods are needed - though such a model would be of relatively * little use. At least ten to fifteen prior observations would be * preferred. * @return the minimum number of periods used in this model. */ protected int getNumberOfPeriods() { return 2; } /** * Returns the number of predictors used by the underlying model. * @return the number of predictors used by the underlying model. * @since 0.5 */ public int getNumberOfPredictors() { return 1; } /** * Since this version of double exponential smoothing uses the current * observation to calculate a smoothed value, we must override the * calculation of the accuracy indicators. * @param dataSet the DataSet to use to evaluate this model, and to * calculate the accuracy indicators against. */ protected void calculateAccuracyIndicators( DataSet dataSet ) { // Note that the model has been initialized initialized = true; // Reset various helper summations double sumErr = 0.0; double sumAbsErr = 0.0; double sumAbsPercentErr = 0.0; double sumErrSquared = 0.0; String timeVariable = getTimeVariable(); double timeDiff = getTimeInterval(); // Calculate the Sum of the Absolute Errors Iterator it = dataSet.iterator(); while ( it.hasNext() ) { // Get next data point DataPoint dp = it.next(); double x = dp.getDependentValue(); double time = dp.getIndependentValue( timeVariable ); double previousTime = time - timeDiff; // Get next forecast value, using one-period-ahead forecast double forecastValue = getForecastValue( previousTime ) + getSlope( previousTime ); // Calculate error in forecast, and update sums appropriately double error = forecastValue - x; sumErr += error; sumAbsErr += Math.abs( error ); sumAbsPercentErr += Math.abs( error / x ); sumErrSquared += error*error; } // Initialize the accuracy indicators int n = dataSet.size(); accuracyIndicators.setBias( sumErr / n ); accuracyIndicators.setMAD( sumAbsErr / n ); accuracyIndicators.setMAPE( sumAbsPercentErr / n ); accuracyIndicators.setMSE( sumErrSquared / n ); accuracyIndicators.setSAE( sumAbsErr ); } /** * Returns the value of the smoothing constant, alpha, used in this model. * @return the value of the smoothing constant, alpha. * @see #getGamma */ public double getAlpha() { return alpha; } /** * Returns the value of the trend smoothing constant, gamma, used in this * model. * @return the value of the trend smoothing constant, gamma. * @see #getAlpha */ public double getGamma() { return gamma; } /** * Returns a one or two word name of this type of forecasting model. Keep * this short. A longer description should be implemented in the toString * method. * @return a string representation of the type of forecasting model * implemented. */ public String getForecastType() { return "double exponential smoothing"; } /** * This should be overridden to provide a textual description of the * current forecasting model including, where possible, any derived * parameters used. * @return a string representation of the current forecast model, and its * parameters. */ public String toString() { return "Double exponential smoothing model, with smoothing constants of alpha=" + alpha + ", gamma=" + gamma + ", and using an independent variable of " + getIndependentVariable(); } } // Local Variables: // tab-width: 4 // End:

Ads help maintain this website.