0

I have user ratings for multiple courses for each user. I need to get the average user rating for each courseCode.

What would be the most efficient way to get these averages? The data can be found in either of the following two formats (though preferably the second one is better).

I was thinking of sorting the array and then aggregating values till I find a different courseCode, but I am not very good with array functions and I was wondering if there was a faster way to do this using hashmaps, .map, sets, reduce. Please help me find an efficient solution as there are many users and I would want this to be as fast as possible so that the website loads quicker.

[
    [
        {courseCode: "SYD393", rating: 3},
        {rating: 3, courseCode: "STA244"},
        {courseCode: "STA255", rating: 5},
        {rating: 5, courseCode: "CSE201"},
        {courseCode: "CSE255", rating: 4},
        {rating: 2, courseCode: "CSE202"},
        {courseCode: "ASD323", rating: 5},
    ],
    [
        {courseCode: "ASD323", rating: 5},
        {rating: 5, courseCode: "STA244"},
        {courseCode: "STA255", rating: 5},
        {courseCode: "SYD393", rating: 1},
    ],
    //...more arrays for each user
];
[
    [
        {SYD393: 3},
        {STA244: 4},
        {STA255: 5},
        {CSE255: 4},
        {ASD323: 5},
    ],
    [
        {ASD323: 5},
        {STA255: 5},
        {SYD393: 1},
    ],
    //...more arrays for each user
];
Siddharth Agrawal
  • 354
  • 2
  • 3
  • 11
  • Yes. Would you be able to suggest/guide me on which one of the solutions provided on that page *should* be the fastest? In your opinion/intuition? – Siddharth Agrawal Jul 24 '21 at 22:24

4 Answers4

2

Looks like this is similar to the answer you posted yourself, except I separated out the averaging of the Map into its own step.

const data = [[{ SYD393: 3 }, { STA244: 4 }, { STA255: 5 }, { CSE255: 4 }, { ASD323: 5 },], [{ ASD323: 5 }, { STA255: 5 }, { SYD393: 1 },],];
const avg = a => a.reduce((a, b) => a + b, 0) / a.length,

  sums = data.flat().reduce((a, o) => {
    const [[c, r]] = Object.entries(o);
    a.set(c, (a.get(c) ?? []).concat(r));
    return a;
  }, new Map),

  avgs = new Map([...sums.entries()].map(([c, rs]) => [c, avg(rs)])),

  res = data.map(us => us.map(o => {
    const [k] = Object.keys(o); return { [k]: avgs.get(k) }
  }));

console.log(res);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Though it is more straightforward using your first data shape as there is no need to wrangle the Object.entries/keys.

const data = [[{ courseCode: "SYD393", rating: 3 }, { rating: 3, courseCode: "STA244" }, { courseCode: "STA255", rating: 5 }, { rating: 5, courseCode: "CSE201" }, { courseCode: "CSE255", rating: 4 }, { rating: 2, courseCode: "CSE202" }, { courseCode: "ASD323", rating: 5 },], [{ courseCode: "ASD323", rating: 5 }, { rating: 5, courseCode: "STA244" }, { courseCode: "STA255", rating: 5 }, { courseCode: "SYD393", rating: 1 },],];
const avg = a => a.reduce((a, b) => a + b, 0) / a.length,

  sums = data.flat().reduce((a, { courseCode: c, rating: r }) =>
    (a.set(c, (a.get(c) ?? []).concat(r)), a), new Map),

  avgs = new Map([...sums.entries()].map(([c, rs]) => [c, avg(rs)])),

  res = data.map(us => us.map(({ courseCode: c }) => ({ courseCode: c, rating: avgs.get(c) })));

console.log(res);
.as-console-wrapper { max-height: 100% !important; top: 0; }
pilchard
  • 12,414
  • 5
  • 11
  • 23
0

This should get you to the end result. It maps each set of arrays into a reduce that accumulates the ratings, then through another map/reduce combo to get the final output.

let data = [
    [
        {courseCode: "SYD393", rating: 3},
        {rating: 3, courseCode: "STA244"},
        {courseCode: "STA244", rating: 4},
        {courseCode: "STA255", rating: 5},
        {rating: 1, courseCode: "SYD393"},
        {rating: 5, courseCode: "STA244"},
        {rating: 5, courseCode: "CSE201"},
        {courseCode: "CSE255", rating: 4},
        {rating: 2, courseCode: "CSE202"},
        {courseCode: "ASD323", rating: 5},
    ],
    [
        {rating: 3, courseCode: "SYD393"},
        {courseCode: "ASD323", rating: 5},
        {rating: 5, courseCode: "STA244"},
        {courseCode: "STA255", rating: 5},
        {courseCode: "SYD393", rating: 1},
    ],
];

let newdata = data.map(e => e.reduce((b, a) => {
  let i = b.findIndex(e => e.courseCode === a.courseCode);
  if (i > 0) b[i].ratings.push(a.rating);
  else b.push({ ...a, ratings: [a.rating] });
  return b;
}, []).map(e => ({ [e.courseCode]: e.ratings.reduce((b, a) => b + a, 0) / e.ratings.length
})))

console.log(newdata)
Kinglish
  • 23,358
  • 3
  • 22
  • 43
0

Update: A friend of mine came up with the following:

function averageRatings(data)
{
   let step1 = data.flat()
   let step2 = step1.reduce((c,i) => { if (!c.hasOwnProperty(i.courseCode)) {c[i.courseCode] = []} c[i.courseCode].push(i.rating); return c}, {})
   let step3 = Object.entries(step2).map(([k,i]) => { return { courseCode: k, avg: i.reduce((c,v) => c+v) / i.length }})
}

It should be able to create what I am looking for in O(n) time as it uses hashtables, but I am not sure as it does do many divisions (costly operations) and computers i.length multiple times.

More concise version:

function averageRatings(data)
{
    return Object.entries(data.flat().reduce((c,i) => { if (!c.hasOwnProperty(i.courseCode)) {c[i.courseCode] = []} c[i.courseCode].push(i.rating); return c}, {})).map(([k,i]) => { return { courseCode: k, avg: i.reduce((c,v) => c+v) / i.length }})
}

Fastest version I could come up with the help of my friend:

  function averageArray(array) {
    let sum = 0;
    let i = 0;
    for (; array[i]; i++) {
      sum += array[i];
    }
    return sum / i;
  }
function getAverageRatings(data){
  return Object.entries(
    data
      .flat()
      .reduce((c, i) => {
        if (!c.hasOwnProperty(i.courseCode)) {
          c[i.courseCode] = [];
        }
        c[i.courseCode].push(i.rating);
        return c;
      }, {})
  ).map(([k, i]) => {
    return { courseCode: k, avg: averageArray(i) };
  });
}

Further optimises the code by removing .length() and .reduce() operations/function callbacks

Siddharth Agrawal
  • 354
  • 2
  • 3
  • 11
-1

This probably won't impact your website as much as you think, but here is my suggestion: If you are only worried about the average rating, the perfect method would be using an Array#reduce inside and Array#map:

const myUsers = [
    [
        {courseCode: "SYD393", rating: 3},
        {rating: 3, courseCode: "STA244"},
        {courseCode: "STA244", rating: 4},
        {courseCode: "STA255", rating: 5},
        {rating: 1, courseCode: "SYD393"},
        {rating: 5, courseCode: "STA244"},
        {rating: 5, courseCode: "CSE201"},
        {courseCode: "CSE255", rating: 4},
        {rating: 2, courseCode: "CSE202"},
        {courseCode: "ASD323", rating: 5},
    ],
    [
        {rating: 3, courseCode: "SYD393"},
        {courseCode: "ASD323", rating: 5},
        {rating: 5, courseCode: "STA244"},
        {courseCode: "STA255", rating: 5},
        {courseCode: "SYD393", rating: 1},
    ],
];


// The map function iterates throught myUsers array
// The reduce function will summ all the evaluation ratings
// the user.length divisor will transform the sum into an avarage
const usersAverageEval = myUsers.map(user => (
    (user.reduce((accumulator, line) => (accumulator + line.rating), 0))/user.length
))

This results in a more readable code, but not the most performative way. If you only care about performance, I'd go with the basic for loops you are already using since they are three to four times faster then this Array method. You can verify what I'm saying and learn much more here: https://leanylabs.com/blog/js-forEach-map-reduce-vs-for-for_of/

Pedro Henrique
  • 181
  • 3
  • 11