Moving Average in Lodash
Let's write a Moving Average function in JavaScript with Lodash.
But first, we'll review the regular average function. How does that look in JavaScript without Lodash?
function average(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) sum += numbers[i];
return sum / (numbers.length || 1);
}
The only tricky thing is that we can't divide by zero, so we are using
the fact that 0
is falsey to default numbers.length
to 1
. In that
case, we'll just return 0. Not technically correct but a decent
compromise.
But let's turn this into a functional style. We're iterating through a
list and aggregating a result. That's a good job for reduce
.
function average(numbers) {
return _.reduce(numbers, (a, b) => a + b, 0) / (numbers.length || 1);
}
Very nice! Calling reduce
with +
and 0
could be called sum
.
Let's refactor:
function sum(numbers) {
return _.reduce(numbers, (a, b) => a + b, 0);
}
function average(numbers) {
return sum(numbers) / (numbers.length || 1);
}
Now that's declarative! It almost reads like the English-language definition for average.
Now let's tackle the Moving Average. Given a list of numbers, you take a "window" around each number and calculate the average of the numbers in that window.
function moving_average(numbers) {
return _.chain(numbers).map(window).map(average).value();
}
We can read it like this: for each number
in numbers
, create a
window, average the window, and return the list of averages. We have the
function average
above. But what about window
?
function window(_number, index, array) {
return _.slice(
array,
Math.max(0, index - 3),
Math.min(array.length, index + 3)
);
}
Maybe that one-liner is hard to read. We'll give the parts some names:
function window(_number, index, array) {
const start = Math.max(0, index - 3);
const end = Math.min(array.length, index + 3);
return _.slice(array, start, end);
}
Let's see it all together:
function sum(numbers) {
return _.reduce(numbers, (a, b) => a + b, 0);
}
function average(numbers) {
return sum(numbers) / (numbers.length || 1);
}
function window(_number, index, array) {
const start = Math.max(0, index - 3);
const end = Math.min(array.length, index + 3);
return _.slice(array, start, end);
}
function moving_average(numbers) {
return _.chain(numbers).map(window).map(average).value();
}
Okay! That works. But now I want to be able to change the window. Right now, the window is 6 elements wide. Our boss is asking if we can change it. And if we need to change it once, we probably will need to do so again in the future. Let's anticipate that and make window more flexible:
function make_window(before, after) {
return function (_number, index, array) {
const start = Math.max(0, index - before);
const end = Math.min(array.length, index + after + 1);
return _.slice(array, start, end);
};
}
I changed it slightly: notice that I add index+ after + 1
, where
before there was no + 1
. It just seems to make more sense with the
names before
and after
.
Now how does moving_average
look?
function moving_average(numbers) {
return _.chain(numbers).map(make_window(3, 2)).map(average).value();
}
Or we can pass the arguments in:
function moving_average(before, after, numbers) {
return _.chain(numbers).map(make_window(before, after)).map(average).value();
}