Afi Afi - 3 months ago 5
Javascript Question

Calculate average of array of objects per key value using reduce

I want to find average of an array of objects based on their key values using the new functional programming style. I found my way around array

reduce
and solved my problem, but not sure if this is the best way to do it.

Please take a look at my code and see if this is the way to use reduce for my purpose.

Let's say I have an array of objects as follows:

private data = [
{tv: 1, radio:5,fridge:4},
{tv: 2, radio:2,fridge:null},
{tv: 3, radio:6,fridge:5}
];


I want to create another array containing the averages of each of the items in my data array. What I have, and is working, is below:

function summary(){
var keys= Object.keys(data[0]);
var sums = {};
var averages = Object.keys(this.data.reduce((previous, element) => {
keys.forEach(el => {
if(element[el] !== null){
if (previous.hasOwnProperty(el)) {
previous[el].value += element[el];
previous[el].count += 1;
} else {
previous[el] = {
value: element[el],
count: 1
};
}
}
});
return previous;
}, sums)).map(name => {
return {
name: name,
average: sums[name].value / sums[name].count
};
});
console.log(averages);
}


Running the code will give me my expected results:

average = [
{ "name": "tv", "average": 2 },
{ "name": "radio", "average": 4.333333333333333 },
{ "name": "fridge", "average": 4.5 }
]


But is this the best way to solve my problem using new
reduce
functions?

Answer

Here is possibly an even more functional programming style solution, which makes use of a temporary ES6 Map object. This has the advantage over a plain object: you can turn it into an array of pairs, and chain on that to get the final result:

var data = [
    {tv: 1, radio:5, fridge:4},
    {tv: 2, radio:2, fridge:null},
    {tv: 3, radio:6, fridge:5}
];

var avg = Array.from(data.reduce(
        (acc, obj) => Object.keys(obj).reduce( 
            (acc, key) => typeof obj[key] == "number"
                ? acc.set(key, (acc.get(key) || []).concat(obj[key]))
                : acc,
        acc),
    new Map()), 
        ([name, values]) =>
            ({ name, average: values.reduce( (a,b) => a+b ) / values.length })
    );

console.log(avg);

Instead of immediately summing up the values, this code first collects the different values into an array per property, in a Map, then it calculates the averages from those arrays, turning it into the desired target structure.

Alternative output structure

Personally I find it more logical to produce output that has the same structure as the input objects, so I provide this very similar alternative. Only the final map is replaced by a reduce:

var data = [
    {tv: 1, radio:5, fridge:4},
    {tv: 2, radio:2, fridge:null},
    {tv: 3, radio:6, fridge:5}
];

var avg = Array.from(data.reduce(
        (acc, obj) => Object.keys(obj).reduce( 
            (acc, key) => typeof obj[key] == "number"
                ? acc.set(key, (acc.get(key) || []).concat(obj[key]))
                : acc,
        acc),
    new Map())).reduce( 
        (acc, [name, values]) =>
            Object.assign(acc, { [name]: values.reduce( (a,b) => a+b ) / values.length }),
        {}
    );

console.log(avg);

Performance improvement

As you asked in comments about performance, I tried to improve on it, without giving up on functional programming.

I took my first code version (which will be more performant than the second), and changed the first half of the algorithm: the numbers are now summed up immediately, keeping a count next to it. For this I introduced an immediately invoked (arrow) function:

var data = [
    {tv: 1, radio:5, fridge:4},
    {tv: 2, radio:2, fridge:null},
    {tv: 3, radio:6, fridge:5}
];

var avg = Array.from(data.reduce(
        (acc, obj) => Object.keys(obj).reduce( 
            (acc, key) => typeof obj[key] == "number"
                ? acc.set(key, ( // immediately invoked function:
                        ([sum, count]) => [sum+obj[key], count+1] 
                    )(acc.get(key) || [0, 0])) // pass previous value
                : acc,
        acc),
    new Map()), 
        ([name, [sum, count]]) => ({ name, average: sum/count })
    );

console.log(avg);

This stays within the functional programming rules, but I expect better performance than the first two versions I posted.

Comments