DEV Community

Cover image for Calculating a Moving Average on Streaming Data
Nested Software
Nested Software

Posted on • Updated on • Originally published at nestedsoftware.com

Calculating a Moving Average on Streaming Data

Recently I needed to calculate some statistics (the average and the standard deviation) on a stream of incoming data. I did some research about it, and this article is the result. I'm going to split it into several parts. This first part is about how to calculate the average incrementally. The second part will be about how do the same thing with the standard deviation. A third part will be about the exponential moving average, also known as a low pass filter.

What people often call the 'average' is more technically referred to in statistics as the 'arithmetic mean'. For this article, the terms 'average' and 'mean' are interchangeable.

The usual way to calculate the average for a set of data that we all learn in school is to add up all of the values (the total), and then divide by the number of values (the count):

mean = total/count

Here's the math notation that describes what I just wrote above:

arithmetic mean formula

Below is a simple javascript function that uses this naive approach to obtain the mean:

const simpleMean = values => {
    validate(values)

    const sum = values.reduce((a,b)=>a+b, 0)
    const mean = sum/values.length
    return mean
}

const validate = values =>  {
    if (!values || values.length == 0) {
        throw new Error('Mean is undefined')
    }
}
Enter fullscreen mode Exit fullscreen mode

While this logic is fine as far as it goes, it does have a couple of limitations in practice:

  • We accumulate a potentially large sum, which can cause precision and overflow problems when using floating point types.
  • We need to have all of the data available before we can do the calculation.

Both of these problems can be solved with an incremental approach where we adjust the average for each new value that comes along. I'll show how to derive this formula with some math first, and then I'll show a JavaScript implementation.

There are two symbols in math that are often used to denote the mean. σ, the lowercase greek letter sigma, refers to the population mean. This is the average for an entire population - all of the possible values. The average of all the grades on a particular test is an example.

, pronounced x-bar, refers to the sample mean. This is the average of a sample of values from the total population. You might take a random sample of people across the country to find the average height of the population, but it's impractical to measure the height of every single person in an entire country. Of course when using a sample, it's desirable to try to get as close as possible to the mean of the population the sample represents.

I decided to use the sample notation for this article to indicate that the average we're calculating could be based on a sample.

Okay, let's start with the formula for the average that we saw earlier:

arithmetic mean formula

Let's split up the sum so that we add up the first n-1 values first, and then we add the last value xn.

arithmetic mean split

We know that the average = total / count:

average of first n-1 values

Let's rearrange this a bit:

sum to n-1 = mean of n-1 values * n-1

Here's the result of applying the above substitution to the total of the first n-1 values:

result of substitution

Let's expand this:

expansion

Rearranging a bit, we get:

rearrange expression

We can cancel out the n's in the first fraction to obtain our final result:

cancel out n's

What does this all really mean? We now have a recurrence relation that defines our mean for the nth value as follows: Add a differential to whatever the mean was for the previous n-1 values. Each time we add a new value, all we have to do is to calculate this differential and add it to the previous average. This now becomes the new average.

Below is a simple implementation of this idea:

class MovingAverageCalculator {
    constructor() {
        this.count = 0
        this._mean = 0
    }

    update(newValue) {
        this.count++

        const differential = (newValue - this._mean) / this.count

        const newMean = this._mean + differential

        this._mean = newMean
    }

    get mean() {
        this.validate()
        return this._mean
    }

    validate() {
        if (this.count == 0) {
            throw new Error('Mean is undefined')
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, each time we call update with a new value, we increment the count and calculate our differential. newMean is the previous average added to this differential. That now becomes the average that will be used the next time we call update.

Below is a simple comparison of the two methods:

console.log('simple mean = ' + simpleMean([1,2,3]))

const calc = new MovingAverageCalculator()
calc.update(1)
calc.update(2)
calc.update(3)
console.log('moving average mean = ' + calc.mean)
Enter fullscreen mode Exit fullscreen mode

The result is as expected:

C:\dev\>node RunningMean.js
simple mean = 2
moving average mean = 2
Enter fullscreen mode Exit fullscreen mode

There are of course many other kinds of moving averages that are possible, but if you simply want a cumulative moving average, this logic works well: It's simple, you can apply it to a streaming data set, and it sidesteps problems with precision and overflow that can happen with the naive approach.

Before concluding, I'd like to derive one more identity using our last result. We don't need it right now, but we'll use in the next article.

We'll start with our recurrence relation for the mean:

recurrence relation for mean

Let's subtract the first term on the right from both sides, giving us the value of just our differential:

subtract

Now let's multiply by n:

multiply by n

Let's multiply both sides by -1:

multiply by -1

And finally let's mutiply the -1 through both sides:

multiply -1 through

We'll just hold on to this identity for now, but it will be useful in part 2 where we derive the formula for incrementally calculating the variance and standard deviation.

Related:

Top comments (12)

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I'm not sure if this corrects the floating point precision problem or swaps it out for a new one.

When N gets large, the original equation suffers from having a large accumulated value, thus running the risk of added samples not preserving their full precision -- in the worst case they become 0.

But, in the online approach, you're dividing each sample by N and adding to the current average. This has the same problem that as N grows the value of individual samples decreases, running the risk of not being significant compared to the current running average.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

That's an excellent point! It does seem as though the average will basically stop changing once n gets high enough. Off the top of my head, it seems that one could calculate the average over a window of the most recent m values rather than the total n of all the values received so far. Maybe there's a better solution than that one though. What do you think?

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Yes, a windowed solution will work. It depends on the needs of the software. I've use the solution you've provided many times without problem, where accuracy wasn't vitally important.

Creating higher precision solutions can be challenging. An alternative, if speed isn't a big issue (which it usually isn't), is to use high precision number library, like libGmp, and use something like 1024bit floating point numbers. It's what I use for constants in the Leaf compiler.

Collapse
 
sam_ferree profile image
Sam Ferree

I might be missing something, but why not save the sum, and run the update function like so?

update(newValue) {
    this.count++;
    this.sum += newValue;
    this._mean = this.sum/this.count;
}

If you have a problem where a large accumulated value, and a desire for full precision are both in scope, then you simply use a datatype better suited to handle this than float.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

Yes, if you are okay accumulating the sum like this, it’s fine. With floating point values, this approach can have issues, but if it meets your needs (e.g. using a different data type as you mentioned) then it’s okay too. Everything depends on one’s particular use case. As @edA-qa mentioned, the approach I present here can have some problems too when n gets big, so it’s not perfect either. I believe it can in turn be corrected with, for example, something like an exponential moving average, but that’s a whole other story. There are many ways to skin a cat!

Collapse
 
slidenerd profile image
slidenerd • Edited

A much faster way to do that JS version is to get rid of reduce and use the plain old for loop, I could setup a JSPERF test to prove that the plain old for loop kicks ass when it comes to performance compared to map filter reduce and the other crappy ES6 methods, mentioning this since this post says "real time"

Also a better approach is
sum = sum - oldestValue
sum = sum + newestValue

where oldestValue is just beyond the moving average s period

Collapse
 
melroy89 profile image
Melroy van den Berg

Wow impressive, thanks!! I never learned so much from a single article.

Collapse
 
nestedsoftware profile image
Nested Software

Thank you so much!

Collapse
 
mikerg profile image
mikerg • Edited

this is awesome - do you know if this technique could be applied to computing a percentile such as 90% in web log analysis?

Collapse
 
nestedsoftware profile image
Nested Software • Edited

In the next article, I will show how to calculate variance and standard deviation incrementally. Assuming your distribution is applicable, that could work I think.

Collapse
 
noblebe4st profile image
Jeff Hall

Very cool, thanks for sharing. Mind you, I’ll have to read this a couple more times, and then I’ll be waiting anxiously for anything resembling a nail to use this hammer on.

Collapse
 
nestedsoftware profile image
Nested Software

Thank you!