A moving average takes a noisy time series and replaces each value with the average value of a neighborhood about the given value. This neighborhood may consist of purely historical data, or it may be centered about the given value. Furthermore, the values in the neighborhood may be weighted using different sets of weights. Here is an example of an equally weighted three point moving average, using historical data,
Here, represents the smoothed signal, and represents the noisy time series. In contrast to simple moving averages, an exponentially weighted moving average (EWMA) adjusts a value according to an exponentially weighted sum of all previous values. This is the basic idea,
This is nice because you don’t have to worry about having a three point window, versus a five point window, or worry about the appropriateness of your weighting scheme. With the EWMA, previous perturbations “remembered,” and “slowly forgotten,” by the term in the last equation, whereas with a window or neighborhood with discrete boundaries, a perturbation is forgotten as soon as it passes out of the window.
Averaging the EWMA to Accommodate Trends
After reading about EWMAs in a data analysis book, I had gone along happily using this tool on every single smoothing application that I came across. It was not until later that I learned that the EWMA function is really only appropriate for stationary data, i.e., data without trends or seasonality. In particular, the EWMA function resists trends away from the current mean that it’s already “seen”. So, if you have a noisy hat function that goes from 0, to 1, and then back to 0, then the EWMA function will return low values on the up-hill side, and high values on the down-hill side. One way to circumvent this is to smooth the signal in both directions, marching forward, and then marching backward, and then average the two. Here, we will use the EWMA function provided by the pandas module.
import pandas, numpy as np ewma = pandas.stats.moments.ewma # make a hat function, and add noise x = np.linspace(0,1,100) x = np.hstack((x,x[::-1])) x += np.random.normal( loc=0, scale=0.1, size=200 ) plot( x, alpha=0.4, label='Raw' ) # take EWMA in both directions with a smaller span term fwd = ewma( x, span=15 ) # take EWMA in fwd direction bwd = ewma( x[::-1], span=15 ) # take EWMA in bwd direction c = np.vstack(( fwd, bwd[::-1] )) # lump fwd and bwd together c = np.mean( c, axis=0 ) # average # regular EWMA, with bias against trend plot( ewma( x, span=20 ), 'b', label='EWMA, span=20' ) # "corrected" (?) EWMA plot( c, 'r', label='Reversed-Recombined' ) legend(loc=8) savefig( 'ewma_correction.png', fmt='png', dpi=100 )
Holt-Winters Second Order EWMA
The Holt-Winters second order method attempts to incorporate the estimated trend into the smoothed data, using a term that keeps track of the slope of the original signal. The smoothed signal is written to the term.
And here is some Python code implementing the Holt-Winters second order method on another noisy hat function, as before.
import numpy as np def holt_winters_second_order_ewma( x, span, beta ): N = x.size alpha = 2.0 / ( 1 + span ) s = np.zeros(( N, )) b = np.zeros(( N, )) s = x for i in range( 1, N ): s[i] = alpha * x[i] + ( 1 - alpha )*( s[i-1] + b[i-1] ) b[i] = beta * ( s[i] - s[i-1] ) + ( 1 - beta ) * b[i-1] return s # make a hat function, and add noise x = np.linspace(0,1,100) x = np.hstack((x,x[::-1])) x += np.random.normal( loc=0, scale=0.1, size=200 ) + 3.0 plot( x, alpha=0.4, label='Raw' ) # holt winters second order ewma plot( holt_winters_second_order_ewma( x, 10, 0.3 ), 'b', label='Holt-Winters' ) title('Holt-Winters' ) legend( loc=8 ) savefig( 'holt_winters.png', fmt='png', dpi=100 )