7

I struggled finding the solution I was looking for in other stackoverflow posts, even though I strongly feel as if it must exist. If it does, please do forward me in the right direction.

I am trying to do a pretty standard group by in javascript using sports data. I have the following array of objects:

 const myData = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ]

Each row in my data corresponds to the results of a specific basketball game. Simply put, I would like to group by the data, and apply a mean/average function to the grouped data. The results I would expect are:

const groupedData = [
    {team: "GSW", pts: 118.3, ast: 27.0, reb: 25.3},
    {team: "HOU", pts: 101, ast: 15.5, reb: 37.5},
    {team: "SAS", pts: 134.7, ast: 21.3, reb: 29.7} 
] 

I would prefer to use vanilla javascript with reduce() here... given what I know about reduce, it seems like the best way. I am currently working on this and will post if i can get it to work before someone else posts the answer.

EDIT: My actual data has ~30 keys. I am hoping to find a solution that simply asks me to either (a) specify only the team column to be grouped by, and assume it groups the rest, or (b) pass an array of stat columns (pts, asts, etc.) rather than creating a line for each stat.

Thanks!

adiga
  • 34,372
  • 9
  • 61
  • 83
Canovice
  • 9,012
  • 22
  • 93
  • 211
  • 3
    You need to do it in steps because you need to have all data in order to be able to average the points and the rest of the data – A. Llorente Jun 26 '18 at 10:31
  • 1
    what do you mean when you say all data needs to be "in order" – Canovice Jun 26 '18 at 10:35
  • 2
    in order as "in order to boil water you need to warm it up first". So first you need to group all data and then you need to go through it to calculate the average. – A. Llorente Jun 26 '18 at 10:37
  • 1
    from the first couple of answers posted, i understand what you mean now – Canovice Jun 26 '18 at 10:42
  • Possible duplicate of [What is the most efficient method to groupby on a JavaScript array of objects?](https://stackoverflow.com/questions/14446511/what-is-the-most-efficient-method-to-groupby-on-a-javascript-array-of-objects) – Vignesh Raja Jun 26 '18 at 11:08
  • does the object have always the same keys? – Nina Scholz Jun 26 '18 at 11:11
  • (a) do you mean if you specify "GSW", only the team "GSW" need to be grouped and averaged? (b) do you mean the columns to be averaged is sent as an array ? or the stats columns in the data are clubbed into an array ? – Vignesh Raja Jun 26 '18 at 13:28

7 Answers7

12

One way to do this is to use reduce and map in conjunction.

const myData = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ]
 
 // Calculate the sums and group data (while tracking count)
 const reduced = myData.reduce(function(m, d){
    if(!m[d.team]){
      m[d.team] = {...d, count: 1};
      return m;
    }
    m[d.team].pts += d.pts;
    m[d.team].ast += d.ast;
    m[d.team].reb += d.reb;
    m[d.team].count += 1;
    return m;
 },{});
 
 // Create new array from grouped data and compute the average
 const result = Object.keys(reduced).map(function(k){
     const item  = reduced[k];
     return {
         team: item.team,
         ast: item.ast/item.count,
         pts: item.pts/item.count,
         reb: item.reb/item.count
     }
 })
 
 console.log(JSON.stringify(result,null,4));

EDIT: Just saw your update to the question. You can do away with each line for each key if you can either whitelist (provide an array of keys to compute) or blacklist (provide an array of keys to ignore) keys to do that programmatically.

const myData = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ]
 
/**
 * Function which accepts a data array and a list of whitelisted
 * keys to find the average of each key after grouping
 */
function getGroupedData(data, whitelist) {
  // Calculate the sums and group data (while tracking count)
  const reduced = data.reduce(function(m, d) {
    if (!m[d.team]) {
      m[d.team] = { ...d,
        count: 1
      };
      return m;
    }
    whitelist.forEach(function(key) {
      m[d.team][key] += d[key];
    });
    m[d.team].count += 1;
    return m;
  }, {});

  // Create new array from grouped data and compute the average
  return Object.keys(reduced).map(function(k) {
    const item = reduced[k];
    const itemAverage = whitelist.reduce(function(m, key) {
      m[key] = item[key] / item.count;
      return m;
    }, {})
    return {
      ...item, // Preserve any non white-listed keys
      ...itemAverage // Add computed averege for whitelisted keys
    }
  })
}


console.log(JSON.stringify(getGroupedData(myData, ['pts', 'ast', 'reb']), null, 4));
Chirag Ravindra
  • 4,760
  • 1
  • 24
  • 35
1

const myData = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ]

const groubElement = myData.reduce((obj, val) => {
    if (obj[val.team]) {
        obj[val.team].pts = obj[val.team].pts + val.pts;
        obj[val.team].ast = obj[val.team].pts + val.ast;
        obj[val.team].reb = obj[val.team].pts + val.reb;
        obj[val.team].counter = obj[val.team].counter + 1;
    } else {
        obj[val.team] = val;
        obj[val.team].counter = 1;
    }
    return obj;

}, {});



const groupElementWithMean = Object.values(groubElement).map(({
    counter,
    ...element
}) => {
    element.pts = (element.pts / counter).toFixed(1);
    element.ast = (element.ast / counter).toFixed(1);
    element.reb = (element.reb / counter).toFixed(1);
    return element;
});

console.log(groupElementWithMean);
Nishant Dixit
  • 5,388
  • 5
  • 17
  • 29
1

You can do this by using reduce with Object.keys and Array.prototype.map as follows:-

const myData = [
    { team: "GSW", pts: 120, ast: 18, reb: 11 },
    { team: "GSW", pts: 125, ast: 28, reb: 18 },
    { team: "GSW", pts: 110, ast: 35, reb: 47 },
    { team: "HOU", pts: 100, ast: 17, reb: 43 },
    { team: "HOU", pts: 102, ast: 14, reb: 32 },
    { team: "SAS", pts: 127, ast: 21, reb: 25 },
    { team: "SAS", pts: 135, ast: 25, reb: 37 },
    { team: "SAS", pts: 142, ast: 18, reb: 27 }
]

let grpData = myData.reduce((acc, cv) => {
    if (!acc[cv.team]) {
        acc[cv.team] = {};
        acc[cv.team].team = cv.team;
        acc[cv.team].count = acc[cv.team].pts = acc[cv.team].ast = acc[cv.team].reb = 0
    }
    acc[cv.team].count++;
    acc[cv.team].pts += cv.pts;
    acc[cv.team].ast += cv.ast;
    acc[cv.team].reb += cv.reb;
    return acc;
}, {});
grpData = Object.keys(grpData).map(key => {
    let { team, reb, ast, pts, count } = grpData[key];
    return {
        team, reb: reb / count, ast: ast / count, pts: pts / count
    };
})
console.log(grpData);
vibhor1997a
  • 2,336
  • 2
  • 17
  • 37
1

Using array of statsFields and loop over those to create totals and later to get averages

const myData = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ]
 
 const statsFields = ['pts','ast','reb'];
 
 const teamsObject = myData.reduce((a,{team,...stats})=>{
   a[team] = a[team] || {team, games:0};
   a[team].games++
   statsFields.forEach(k=> a[team][k] = (a[team][k] || 0) + stats[k]);
   return a;
 },{});
 
 const res = Object.values(teamsObject).map(({games,...team})=>{
    // average for each field total/games
    statsFields.forEach(k=> team[k] = team[k]/games);    
    return team;
 })
 
 console.log(JSON.stringify(res))
charlietfl
  • 170,828
  • 13
  • 121
  • 150
1

It can be simply done as follows.

Note: Used JSON.parse and stringify to deep shallow copy the data. Or else the original array gets modified. Its not needed if the original array can be modified.

const data = [
    {team: "GSW", pts: 120, ast: 18, reb: 11},
    {team: "GSW", pts: 125, ast: 28, reb: 18},
    {team: "GSW", pts: 110, ast: 35, reb: 47},
    {team: "HOU", pts: 100, ast: 17, reb: 43},
    {team: "HOU", pts: 102, ast: 14, reb: 32},
    {team: "SAS", pts: 127, ast: 21, reb: 25},
    {team: "SAS", pts: 135, ast: 25, reb: 37},
    {team: "SAS", pts: 142, ast: 18, reb: 27}
 ];

function groupData(mydata,keys)
{
    var accresult = mydata.reduce(function(acc, value){
      var arr = acc.filter(function(obj){return obj.team==value.team});
      arr.length ? (item=arr[0] , keys.forEach(function(key){ item[key]+=value[key]; })) : acc.push(value);
      return acc;
  },[]);

  var result = accresult.map(function(val){
      var l = mydata.filter(function(obj){return obj.team==val.team}).length;
      keys.forEach(function(key){ val[key]=(val[key]/l).toFixed(2); })
      return val;
  });
  return result;
}

console.log(groupData(JSON.parse(JSON.stringify(data.slice(0))),['pts','ast']));
console.log(groupData(JSON.parse(JSON.stringify(data.slice(0))),['pts','ast','reb']));
console.log(groupData(JSON.parse(JSON.stringify(data.slice(0))),['pts']));
Vignesh Raja
  • 7,927
  • 1
  • 33
  • 42
0

You could take a dynamic approach by using a Map and generate all items after collecting the unknown keys.

function groupBy(array, key) {
    return Array.from(
        array.reduce((m, o) => {
            var temp = m.get(o[key]);
            if (!temp) {
                m.set(o[key], temp = {});
            }
            Object.entries(o).forEach(([k, v]) => {
                if (k === key) {
                    return;
                }
                temp[k] = temp[k]  || { sum: 0, count: 0 };
                temp[k].sum += v;
                temp[k].count++;
            });
            return m;
        }, new Map),
        ([k, v]) => Object.assign({ [key]: k }, ...Object.entries(v).map(([l, { sum, count }]) => ({ [l]: +(sum / count).toFixed(1) })))
    );
}

const myData = [{ team: "GSW", pts: 120, ast: 18, reb: 11 }, { team: "GSW", pts: 125, ast: 28, reb: 18 }, { team: "GSW", pts: 110, ast: 35, reb: 47 }, { team: "HOU", pts: 100, ast: 17, reb: 43 }, { team: "HOU", pts: 102, ast: 14, reb: 32 }, { team: "SAS", pts: 127, ast: 21, reb: 25 }, { team: "SAS", pts: 135, ast: 25, reb: 37 }, { team: "SAS", pts: 142, ast: 18, reb: 27 }];

console.log(groupBy(myData, 'team'));
.as-console-wrapper { max-height: 100% !important; top: 0; }

With rest properties (babel: true).

function groupBy(array, key) {
    return Array.from(
        array.reduce((m, { [key]: k, ...rest}) => {
            var temp = m.get(k);
            if (!temp) {
                m.set(k, temp = {});
            }
            Object.entries(rest).forEach(([l, v]) => {
                temp[l] = temp[l]  || { sum: 0, count: 0 };
                temp[l].sum += v;
                temp[l].count++;
            });
            return m;
        }, new Map),
        ([k, v]) => Object.assign({ [key]: k }, ...Object.entries(v).map(([l, { sum, count }]) => ({ [l]: +(sum / count).toFixed(1) })))
    );
}

const myData = [{ team: "GSW", pts: 120, ast: 18, reb: 11 }, { team: "GSW", pts: 125, ast: 28, reb: 18 }, { team: "GSW", pts: 110, ast: 35, reb: 47 }, { team: "HOU", pts: 100, ast: 17, reb: 43 }, { team: "HOU", pts: 102, ast: 14, reb: 32 }, { team: "SAS", pts: 127, ast: 21, reb: 25 }, { team: "SAS", pts: 135, ast: 25, reb: 37 }, { team: "SAS", pts: 142, ast: 18, reb: 27 }];

console.log(groupBy(myData, 'team'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
0

const myData = [
  { team: "GSW", pts: 120 },
  { team: "HOU", pts: 100 },
  { team: "GSW", pts: 110 },
  { team: "SAS", pts: 135 },
  { team: "HOU", pts: 102 },
  { team: "SAS", pts: 127 },
  { team: "SAS", pts: 142 },
  { team: "GSW", pts: 125 }
];

var result = myData.reduce(function (a, b) {
  var exist = -1;
  //some breaks the loop once it gets the true
  a.some((x, y) => {
    if (x.team == b.team) {
      //assigning index of existing object in array
      exist = y;
      return true;
    } else {
      return false;
    }
  });
  if (exist == -1) {
    a.push({ team: b.team, pts: b.pts, count: 1 });
  } else {
    a[exist].count += 1;
    a[exist].pts += b.pts;
  }
  return a;
}, []).map(t => {return {team: t.team, avg: t.pts/t.count, count:t.count}});


console.log(result);
  • 2
    Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes – Boken Dec 09 '20 at 09:14