BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

LINQ
.NET 3.5+

A LINQ Style Simple Moving Average Operator

Raw data series with extreme peaks and troughs can be difficult to interpret. One way to remove sudden spikes is by applying a simple moving average calculation. This article describes a simple moving average extension method with a LINQ style.

Implementing a Simple Moving Average

In the remainder of this article we will implement the simple moving average calculation. We'll create an extension method that operates in much the same way as the standard query operators of Language-Integrated Query (LINQ). This means that the method will accept data in any collection that implements IEnumerable<T> and will return data in another IEnumerable<T>. It will validate its arguments immediately but the calculations will be performed using deferred execution.

For the purposes of this article we'll only use double-precision floating point numbers for the data set and the results. It is a relatively simple task to create overloaded versions of the method that process other data types.

To begin, create a new console application project. Add a new class named, "SimpleMovingAverageExtensions" to the project. This will hold the extension method so needs to be a static class.

public static class SimpleMovingAverageExtensions
{
}

We can now add the public method that will be our extension method. This simply checks that the source data and the sample size are valid. If they are, the SimpleMovingAverageImpl method is called. This is the iterator that provides the deferred execution.

public static IEnumerable<double> SimpleMovingAverage(
    this IEnumerable<double> source, int sampleLength)
{
    if (source == null) throw new ArgumentNullException("source");
    if (sampleLength <= 0) throw new ArgumentException("Invalid sample length");

    return SimpleMovingAverageImpl(source, sampleLength);
}

There are a number of ways that you could write the iterator. In this case we'll aim for readability. You might optimise the method for greater speed, should performance be a problem.

The method holds the sample in a generic Queue object. Initially the queue is empty. Each time a new value is read from the source sequence, it is added to the queue. The average of all of the items in the queue is calculated and returned. Once the queue has reached the desired sample size, an item is removed before a new one is added. This allows the sample to grow over the first few items but remain constant once the correct sample size is available.

private static IEnumerable<double> SimpleMovingAverageImpl(
    IEnumerable<double> source, int sampleLength)
{
    Queue<double> sample = new Queue<double>(sampleLength);

    foreach (double d in source)
    {
        if (sample.Count == sampleLength)
        {
            sample.Dequeue();
        }
        sample.Enqueue(d);
        yield return sample.Average();
    }
}

Calling the Method

We can now try out the moving average calculation. Let's use the values from the year of sales data for our fictitious company. The code below initialises a sequence with the twelve values before calling the SimpleMovingAverage method. The sample size is five in this case. The results are zipped with the original values before being outputted to the console. You can see the results in the comments below. Note that the averages are formatted with two decimal places to make it easier to read the results.

IEnumerable<double> values = new double[] { 4, 5, 4, 20, 5, 5, 0, 10, 5, 5, 4, 7 };
var averages = values.SimpleMovingAverage(5);
var results = values.Zip(averages, (v, a) => new { Value = v, Average = a.ToString("F2") });

foreach (var result in results)
{
    Console.WriteLine(result);
}

/* OUTPUT

{ Value = 4, Average = 4.00 }
{ Value = 5, Average = 4.50 }
{ Value = 4, Average = 4.33 }
{ Value = 20, Average = 8.25 }
{ Value = 5, Average = 7.60 }
{ Value = 5, Average = 7.80 }
{ Value = 0, Average = 6.80 }
{ Value = 10, Average = 8.00 }
{ Value = 5, Average = 5.00 }
{ Value = 5, Average = 5.00 }
{ Value = 4, Average = 4.80 }
{ Value = 7, Average = 6.20 }

*/
29 November 2013