HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Calculate average of array of objects per key value using reduce

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
perobjectsarrayaveragevaluereducecalculateusingkey

Problem

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?

Solution

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.

Context

StackExchange Code Review Q#141530, answer score: 5

Revisions (0)

No revisions yet.