0

I have a JSON array of objects (a collection) like:

[{
  "x": {
        "x1": 1
  },
  "y": {
    "yt": 0,
    "zt": 4,
    "qa": 3,
    "ft": 0,
    ...
  }
},
{
  "x": {
        "x1": 5
  },
  "y": {
    "yt": 10,
    "zt": 2,
    "qa": 0,
    "ft": 0,
    ...
  }
}]

I'd like to calculate average for each field. The result structure should be same. Like:

    {
      "x": {
            "x1": 3
      },
      "y": {
        "yt": 5,
        "zt": 3,
        "qa": 1.5,
        "ft": 0,
        ...
      }
    }

Thanks

Eric Kigathi
  • 1,815
  • 21
  • 23
serkan
  • 6,885
  • 4
  • 41
  • 49

4 Answers4

1

You could first collect and sum all values in the same data structure and then calculkate the average by a division with the length of the given array.

function getParts(array, result) {
    function iter(o, r) {
        Object.keys(o).forEach(function (k) {
            if (o[k] && typeof o[k] === 'object') {
                return iter(o[k], r[k] = r[k] || {});
            }
            r[k] = (r[k] || 0) + o[k];
        });
    }

    function avr(o) {
        Object.keys(o).forEach(function (k) {
            if (o[k] && typeof o[k] === 'object') {
                return avr(o[k]);
            }
            o[k] = o[k] /data.length;
        });
    }

    data.forEach(function (a) {
        iter(a, result);
    });
    avr(result);
}

var data = [{ x: { x1: 1 }, y: { yt: 0, zt: 4, qa: 3, ft: 0, } }, { x: { x1: 5 }, y: { yt: 10, zt: 2, qa: 0, ft: 0, } }],
    result = {};

getParts(data, result);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thank you @Nina. It has been over 3 years and this is still the most elegant answer hereabouts. – mukund Feb 10 '21 at 02:15
1

You can merge the objects using the spread syntax and lodash's _.mergeWith(). When merging, if the 2nd parameter (b) is a number divide it by the number of items in the original array to get it's respective contribution to the total average. If the 1st parameter (a) is a number, just add it without dividing (to avoid dividing the sum multiple times), or add 0 if it's undefined.

I've added examples of 2 objects array, and 3 objects array.

const getAvg = (data) => _.mergeWith({}, ...data, (a, b) => {
  if(_.isNumber(b)) {
    return ((b || 0) / data.length) + (_.isNumber(a) ? (a || 0) : 0);
  }
});

const data1 = [
{"x":{"x1":1},"y":{"yt":0,"zt":4,"qa":3,"ft":0}},
{"x":{"x1":5},"y":{"yt":10,"zt":2,"qa":0,"ft":0}}
];

const data2 = [
{"x":{"x1":1},"y":{"yt":0,"zt":4,"qa":3,"ft":0}},
{"x":{"x1":5},"y":{"yt":10,"zt":2,"qa":0,"ft":0}},
{"x":{"x1":3},"y":{"yt":2,"zt":6,"qa":3,"ft":0}}
];

const result1 = getAvg(data1);

console.log('2 objects in the array: ', result1);

const result2 = getAvg(data2);

console.log('3 objects in the array: ', result2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • This is a great way! I use data.map((arr) => arr.ratings ); to filter. Because there are other NaN fields as well. Can I cain that also to above function? Lodash map wont return same data as builtin map funtion. – serkan May 11 '17 at 20:55
  • Are the NaN function are actual `NaN` or just other things like objects, strings, etc...? If so, they'll be merge regularly. If their actual `NaN`, will have to deal with them, as `NaN` is considered to be a number in javascript. Anyway, `_.mergeWith()` doesn't filter properties. – Ori Drori May 11 '17 at 20:59
  • It is a text. It might be an actual NaN as well though. – serkan May 11 '17 at 21:03
0

let objectArray = [{
  "x": {
        "x1": 1
  },
  "y": {
    "yt": 0,
    "zt": 4,
    "qa": 3,
    "ft": 0,
  }
},
{
  "x": {
        "x1": 5
  },
  "y": {
    "yt": 10,
    "zt": 2,
    "qa": 0,
    "ft": 0,
  }
}];

function findAverage(array) {
  
  let counter = {},
    result = {},
    i,
    obj,
    key,
    subKey;
  
  // Iterate through array
  for (i = 0; i < array.length; i++) {
    obj = array[i];
    // Copy each key in array element to counter object
    for (key in obj) {
      counter[key] = counter[key] || {};
      // Increment and keep count of key-values of counter based on values in array element
      for (subKey in obj[key]) {
        counter[key][subKey] = counter[key][subKey] || {total: 0, numElements: 0};
        counter[key][subKey].total += obj[key][subKey];
        counter[key][subKey].numElements += 1;
      }
    }
  }
  // Go back through counter to find average of all existing subkeys (based on incremented total and the number of elements recorded) and throw it into result object
  for (key in counter) {
    result[key] = result[key] || {};
    
    for (subKey in counter[key]) {
      result[key][subKey] = counter[key][subKey].total / counter[key][subKey].numElements;
    }
  }
  
  return result;
}

console.log(findAverage(objectArray));

Not designed to be absolutely optimal, and copying objects can be done recursively without knowing in advance their structure, but I wanted to keep the steps as clear as possible.

Edited to allow testing as snippet. Had no idea you could even do that on SO!

Robert Taussig
  • 581
  • 2
  • 11
0

var array = [{
  "x": {
        "x1": 1
  },
  "y": {
    "yt": 0,
    "zt": 4,
    "qa": 3,
    "ft": 0
  }
},
{
  "x": {
        "x1": 5
  },
  "y": {
    "yt": 10,
    "zt": 2,
    "qa": 0,
    "ft": 0
  }
}];
function aintob(){
  var o = {};
  var first = array[0],
      second = array[1];
  var result = {x:{},y:{}};
  var each = function(letter, oa, ob){
    var i,
    letter = {};
    for(i in oa){
      letter[i] = (oa[i]+ob[i])/2;
    }
    return letter;
  }
  o.x = each("x", first.x, second.x);
  o.y = each("y", first.y, second.y);
  return o;
}
console.log(aintob());
alessandrio
  • 4,282
  • 2
  • 29
  • 40