blogdoodlesgamessounds
blogdoodlesgamessounds
  1. Home
  2. blog
  3. Moving Average

Moving Average

A class-based approach to tracking moving averages and standard deviations.

Feb 25, 2021
#JavaScript#moving average#standard deviation#statistics#class

Motivation

Moving averages are often used in dynamic statistics - for example, tracking the performance of a market stock.

I use moving averages to dynamically detect beats and other peaks in streaming music.

In that linked doodle, there are three columns that represent the frequency bands of the sonic input: low, mid, and high. For each column, the lower line shows the moving average of the amplitudes of the frequencies in that range. The upper (red) line shows one standard deviation above the moving average, which is then used to detect peaks. I define a "peak" in this method as a moment in time when the average amplitude of frequencies in a given range exceeds the current standard deviation.

Here's a tiny class that makes tracking moving averages and standard deviations a snap.

What is a moving average?

A moving average is essentially the average of the last n frames.

Standard deviation?

Standard deviation is a metric that can be used to measure the variance of a dataset. It can be calculated via the difference of data points in a set from the set average.

Code

class MovingWindow {
movingAverage = 0;
standardDeviation = 0;
sumOfFrames = 0;
constructor({ nFrames }) {
this.nFrames = nFrames;
this.frames = Array(nFrames).fill(0);
}
push(value) {
// Update frame history and calculate new sum
const oldestFrame = this.frames.pop();
this.sumOfFrames += value - oldestFrame;
this.frames.unshift(value);
// Calculate new average
this.movingAverage = this.sumOfFrames / this.nFrames;
// Calculate standard deviation
let sumOfSquares = 0;
for (let i = 0; i < this.nFrames; i++) {
sumOfSquares += (this.frames[i] - this.movingAverage) ** 2;
}
this.standardDeviation = Math.sqrt(sumOfSquares / this.nFrames);
}
}

Usage

Instantiation

const movingWindow = new MovingWindow({ nFrames: 100 });

Add a new frame

movingWindow.push(newFrameValue);

Access calculated properties at any time

const currentMovingAverage = movingWindow.movingAverage;
const currentStandardDeviation = movingWindow.standardDeviation;

How is this working?

The math contained in

MovingWindow
is fairly straightforward, though there are a couple of things to highlight.

In a world where we didn't care about performance, we might opt for the most intuitive (read: best developer experience) method of calculating the average.

For example:

const sumOfFrames = frames.reduce((sum, frame) => sum + frame, 0);
const average = frames / frames.length;

Or, if

reduce
is not your favorite
Array
method:

let sum = 0;
frames.forEach(frame => {
sum += frame;
});
const average = frames / frames.length;

Unfortunately, this requires us to loop through the entire array - O(n) time for those Big O fans out there. That might be fine if we are tracking, say, the daily performance of a stock index or something like that. However, if we are trying to analyze and react to streaming music, every millisecond counts!

In the above calculations of average, we can see that we take two steps to calculate the average:

  • First, calculate the sum of the last n frames.
  • Then, divide the sum by n.

How can we make this more efficient?

Well, we can eke out some performance gains if we sacrifice some memory. After all, caching strategies can be very useful for boosting performance of repetitive algorithms!

Each new frame that we calculate the average, we loop over frames that we have already seen, except the one new frame. In other words:

const newSumOfFrames = lastSumOfFrames + newFrame - frameJustExitedWindow;

This is a constant time equation! This sum formula will stay performant no matter how many frames we want to track in our window.

Unfortunately, we can't use the same trick in our standard deviation calculation, because essentially our formula changes every single frame because it is calculated using the latest moving average.

In this case I just opt for a simple

for
loop, even though I prefer the clarity of
reduce
, because
for
loops tend to outperform
Array.reduce
(and even
Array.forEach
) in extreme conditions.

If we wanted to keep

reduce
for clarity, we could rewrite that part as:

const sumOfSquares = this.frames.reduce(
(sumOfSquares, frame) => (frame - this.movingAverage) ** 2,
0
);
this.standardDeviation = Math.sqrt(sumOfSquares / this.nFrames);

Why a class?

This same logic could be wrapped up in a standard function that returns an object, so there's no requirement to use a class for this. In this case I like the interface that the class provides.

If your religion prohibits classes in JS, here's an alternative...

function createMovingWindow({ nFrames }) {
let movingAverage = 0;
let standardDeviation = 0;
let sumOfFrames = 0;
const frames = Array(nFrames).fill(0);
return {
push: (value) => {
// Update frame history and calculate new sum
const oldestFrame = frames.pop();
sumOfFrames += value - oldestFrame;
frames.unshift(value);
// Calculate new average
movingAverage = sumOfFrames / nFrames;
// Calculate standard deviation
let sumOfSquares = 0;
for (let i = 0; i < nFrames; i++) {
sumOfSquares += (frames[i] - movingAverage) ** 2;
}
standardDeviation = Math.sqrt(sumOfSquares / nFrames);
},
getMovingAverage: () => movingAverage,
getStandardDeviation: () => standardDeviation,
};
}
const movingWindow = createMovingWindow({ nFrames: 100 });
movingWindow.push(newFrameValue);
const currentMovingAverage = movingWindow.getMovingAverage();
const currentStandardDeviation = movingWindow.getStandardDeviation();