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.
A moving average is essentially the average of the last n frames.
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.
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 sumconst oldestFrame = this.frames.pop();this.sumOfFrames += value - oldestFrame;this.frames.unshift(value);// Calculate new averagethis.movingAverage = this.sumOfFrames / this.nFrames;// Calculate standard deviationlet sumOfSquares = 0;for (let i = 0; i < this.nFrames; i++) {sumOfSquares += (this.frames[i] - this.movingAverage) ** 2;}this.standardDeviation = Math.sqrt(sumOfSquares / this.nFrames);}}
const movingWindow = new MovingWindow({ nFrames: 100 });
movingWindow.push(newFrameValue);
const currentMovingAverage = movingWindow.movingAverage;const currentStandardDeviation = movingWindow.standardDeviation;
The math contained in
is fairly straightforward, though there are a couple of things to highlight.MovingWindow
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
is not your favoritereduce
method:Array
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:
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
loop, even though I prefer the clarity offor
, becausereduce
loops tend to outperformfor
(and evenArray.reduce
) in extreme conditions.Array.forEach
If we wanted to keep
for clarity, we could rewrite that part as:reduce
const sumOfSquares = this.frames.reduce((sumOfSquares, frame) => (frame - this.movingAverage) ** 2,0);this.standardDeviation = Math.sqrt(sumOfSquares / this.nFrames);
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 sumconst oldestFrame = frames.pop();sumOfFrames += value - oldestFrame;frames.unshift(value);// Calculate new averagemovingAverage = sumOfFrames / nFrames;// Calculate standard deviationlet 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();