How to dynamically calculate the array of object values in Javascript?

I have input format as below,

var boxplotInput = [{Day: "01-07-2021", "Thomas": 95, "Diana": 94, "Claura": 93, "Chandler": 93},
    {Day: "02-07-2021", "Thomas": 95, "Diana": 94, "Claura": 94, "Chandler": 94},        
    ...
    ...
    {Day: "31-07-2021", "Thomas": 92, "Diana": 94, "Claura": 93, "Chandler": 91}];

I am quite new to javascript objects handling. I have written the code as below to calculate Q1, Q3, and median and it is working fine mathematically the way I am expecting.

//Getting the list of students (excluding date)
var keys;
for(var i = 0; i <boxplotInput.length; i++ ){
  keys = Object.keys(boxplotInput[i]).slice(1);      
}

////Here, I am hard-coding keys[0]. and getting "Thomas" data only. I am not getting how to avoid for one students only and achieve it for all students.
var studentDataSample = [];
for(var i = 0; i <boxplotInput.length; i++ ){  
  student1 = boxplotInput[i][keys[0]];
  studentDataSample.push(student1);
}

studentDataSample.sort(function(a, b) {return a - b;});

var length = studentDataSample.length;//31
var midIndex = middleIndex(studentDataSample, 0, length);//16
var medianValue = studentDataSample[midIndex];

var Q1 = studentDataSample[middleIndex(studentDataSample, 0, midIndex)];
var Q3 = studentDataSample[middleIndex(studentDataSample, midIndex + 1, length)];

console.log(Q1+", "+medianValue+", "+Q3);// here, the values are fine.

function middleIndex(data, initial, length){
  var n = length - initial + 1;
    n = parseInt((n + 1) / 2);
    return parseInt(n + initial);
}

Something, I understand that it could be achievable through the loop again.. but, not getting how to achieve it for all the students. Kindly, provide the suggestion or idea on this.

Thanks in advance.


Solution 1:

if I understand you correctly all need following JS methods:

  • Array.reduce
  • Array.filter
  • Object.keys

The main thing you need here is create useful collection of students with their grades. After this you can calculate all the things you want. In this example I show how to calculate mean.

var boxplotInput = [
  {Day: "01-07-2021", "Thomas": 95, "Diana": 94, "Claura": 93, "Chandler": 93},
  {Day: "02-07-2021", "Thomas": 95, "Diana": 94, "Claura": 94, "Chandler": 94},        
  {Day: "31-07-2021", "Thomas": 92, "Diana": 94, "Claura": 93, "Chandler": 91}
];

/*
  Get collection of students like:
  {
    Thomas: [ 95, 95, 92 ],
    Diana: [ 94, 94, 94 ],
    Claura: [ 93, 94, 93 ],
    Chandler: [ 93, 94, 91 ]
  }
*/

const students = boxplotInput.reduce((accumulator, currentDay) => {
  const students = Object
    .keys(currentDay)
    .filter(el => el !== 'Day');
    
  students.forEach(student => {
    if (!accumulator[student]) {
      accumulator[student] = [];
    }
    
    accumulator[student].push(currentDay[student]);
  });

  return accumulator;
}, {});

console.log('Student grades:', students);

// Then we can do anything with it
const studentNames = Object.keys(students);

// Example: finding mean
const studentMeans = studentNames.reduce((acc, student) => {
  const grades = students[student];
  const sumOfGrades = grades.reduce((acc, cur) => cur + acc, 0);
  
  acc[student] = sumOfGrades / grades.length;

  return acc;
}, {});

console.log('Means:', studentMeans);

/*
{
  Thomas: 94,
  Diana: 94,
  Claura: 93.33333333333333,
  Chandler: 92.66666666666667
}
*/

Solution 2:

I will show you a very clean way to do this using Underscore. Let's inspect all the tools that Underscore and JavaScript provide for this purpose and build our solution one step at a time.

A nice function from Underscore is chain, which lets us massage data in a different shape step by step, while keeping the code very easy to read. For example, you can probably guess what the following chain will do:

var sortedLast = _.chain([2, 3, 1])
.sort()
.last();

console.log(sortedLast);
<script src="https://underscorejs.org/underscore-umd-min.js"></script>

chain creates a special wrapper around the input data, which has all Underscore functions as methods. Each method returns a new wrapper, so you can continue to apply more Underscore functions. At the end, you can unwrap the result by calling .value(). In some cases, like in the example above, this happens automatically. last returns the last element of an array.

A nice end shape, which we might want to work towards, could be the following:

{
    Thomas: {min: 92, Q1: 93.5, median: 95, Q3: 95, max: 95},
    Diana: {min: 94, Q1: 94, median: 94, Q3: 94, max: 94},
    Claura: {min: 93, Q1: 93, median: 93, Q3: 93.5, max: 94},
    Chandler: {min: 91, Q1: 92, median: 93, Q3: 93.5, max: 94},
}

This is an object which has the same keys as every element of boxplotInput, except for Day. Underscore has an omit function, which lets us do this cleanly, without having to rely on the keys appearing in a particular order:

_.chain(boxplotInput[0])
.omit('Day');
// {Thomas: 95, Diana: 94, Claura: 93, Chandler: 93}

Now we have an object with the correct keys, but wrong values.

mapObject lets us create a new object with the same keys but different values. Besides the input object, it takes a function which will be applied to every key-value pair of the input object in turn. That function takes the value as the first argument and the key as the second argument. Its return value becomes the value at the corresponding key in the new object.

As an intermediate step, let's create an object with a list of all scores per student:

{
    Thomas: [95, 95, 92],
    Diana: [94, 94, 94],
    Claura: [93, 94, 93],
    Chandler: [93, 94, 91],
}

In order to achieve this with mapObject, we need to write a function that, given the name of a student, returns an array with the student's scores. Its start will look like this:

function studentScores(firstScore, studentName) {
    // code here
}

Let's look at an elegant way to get those scores. In your original code, you wrote something like this (but with key[0] instead of studentName):

var studentDataSample = [];
for (var i = 0; i < boxplotInput.length; i++) {
    var student1 = boxplotInput[i][studentName];
    studentDataSample.push(student1);
}

Underscore lets you get the same result in a very short line using map:

var studentDataSample = _.map(boxplotInput, studentName);

JavaScript's arrays nowadays have a built-in map method which lets you do something similar. It is not as flexible and concise as Underscore's map, but I'll show how to use it for completeness:

var studentDataSample = boxplotInput.map(dayScores => dayScores[studentName]);

We now know how to write our studentScores:

function studentScores(firstScore, studentName) {
    return _.map(boxplotInput, studentName);
}

We don't need the firstScore, but we have to accept it as the first argument anyway, because we are going to pass this function to mapObject, which always passes the value first. Fortunately, we can just ignore it. We can write this function more concisely using the new arrow notation:

(fs, studentName) => _.map(boxplotInput, studentName)

Now we can include this function in our chain, in order to arrive at the intermediate result we previously discussed:

_.chain(boxplotInput[0])
.omit('Day')
.mapObject((fs, studentName) => _.map(boxplotInput, studentName));
// {
//     Thomas: [95, 95, 92],
//     Diana: [94, 94, 94],
//     Claura: [93, 94, 93],
//     Chandler: [93, 94, 91]
// }

Let's sort the scores as well, as a preparation for computing the quantiles:

_.chain(boxplotInput[0])
.omit('Day')
.mapObject((fs, studentName) => _.map(boxplotInput, studentName).sort());
// {
//     Thomas: [92, 95, 95],
//     Diana: [94, 94, 94],
//     Claura: [93, 93, 94],
//     Chandler: [91, 93, 94]
// }

We can add another mapObject to the chain in order to transform these arrays of sorted scores to the final {min, Q1, median, Q3, max} objects we were aiming for. Since this is not really what your question was about, I will just propose one possible way to do it in functional style:

// A function that returns a function (this is not a typo) that
// computes a particular quantile from a sorted array of numbers.
function quantile(fraction) {
    return function(numbers) {
        var middle = (numbers.length - 1) * fraction;
        return (numbers[Math.floor(middle)] + numbers[Math.ceil(middle)]) / 2;
    };
}

// A "blueprint" object with the keys we want to have, each having a
// function to compute the corresponding value from a sorted array of 
// scores.
var quantileComputations = {
    min: _.first,
    Q1: quantile(.25),
    median: quantile(.5),
    Q3: quantile(.75),
    max: _.last,
};

// A function that applies the above blueprint to a given array of 
// numbers.
function getQuantiles(numbers) {
    return _.mapObject(quantileComputations, f => f(numbers));
}

// Redefining the input data to make this snippet runnable.
var boxplotInput = [
    {Day: "01-07-2021", "Thomas": 95, "Diana": 94, "Claura": 93, "Chandler": 93},
    {Day: "02-07-2021", "Thomas": 95, "Diana": 94, "Claura": 94, "Chandler": 94},        
    {Day: "31-07-2021", "Thomas": 92, "Diana": 94, "Claura": 93, "Chandler": 91},
];

// Completing our chain using the above.
var statistics = _.chain(boxplotInput[0])
.omit('Day')
.mapObject((fs, studentName) => _.map(boxplotInput, studentName).sort())
.mapObject(getQuantiles)
.value();

console.log(statistics);
<script src="https://underscorejs.org/underscore-umd-min.js"></script>