Search code examples
javascriptarraysobjecttime-serieslodash

Average time-series arrays of different length given the same timestamp in Javascript


I would like to average several time-series arrays of different length using Javascript in an efficient manner.

  • The reason for having different length is because some data is missing in some of the arrays.
  • Time should be used as the common variable in all arrays for the average, i.e. you average the available values for a given timestamp.

For example:

array1 = [[1, 100], [2, 200], [3, 300], [4, 400]];
array2 = [[1, 150], [3, 350], [4, 450]];
array3 = [[1, 250], [2, 350], [4, 450]];

allArrays = [array1, array2, array3];

// This is the result I would like to get, i.e. average of same timestamp 1, 2 .... n
average = [[1, 166.7], [2, 275], [3, 325], [4, 433]];

where each pair is [time, value]. Time is a number in Unix format.

My idea was to use Lodash to first convert the pairs to objects, then to group all equal timestamps and average the values. Then convert back to array.

// Convert arrays to objects
for (int i = 0; allArrays.length; i++){
var array = allArrays[i];
allArrays[i] = toObject(array, ['time', 'value']);
}

// Group based on similar timestamp and average
???

// Convert averaged object to array
???

// Functions
function toObject(array, names){
  var obj = _.chain(array).map(function(p) {
    return _.zipObject(names, p)
}).value()

I am not a Javascript programmer and I would really appreciate any help on this one.


Solution

  • Without using lodash you could use the following:

    const array1 = [
      [1, 100],
      [2, 200],
      [3, 300],
      [4, 400],
    ];
    const array2 = [
      [1, 150],
      [3, 350],
      [4, 450],
    ];
    const array3 = [
      [1, 250],
      [2, 350],
      [4, 450],
    ];
    
    const groupedByTime = [...array1, ...array2, ...array3].reduce(
      (acc, [time, value]) => ((acc[time] ??= []).push(value), acc),
      {}
    );
    console.log("Grouped by time:");
    console.log(JSON.stringify(groupedByTime, null, 4));
    const result = Object.entries(groupedByTime).map(([time, values]) => [Number(time), (values.reduce((total, value) => (total += value), 0) / values.length).toFixed(1)]);
    console.log("Result:");
    console.log(JSON.stringify(result, null, 4));
    /* Stackoverflow: show only console */
    .as-console-wrapper {
      max-height: 100% !important;
      top: 0;
    }

    General idea

    First you wanna group by time and then for each of the groupings calculate the average and bring it in the desired form. If required you can round the average using toFixed().

    More detailed explanation

    First you wanna create a flat array that contains all of the values by using the spread syntax [...array1, ...array2, ...array3]. Then you use reduce() to group all values together that belong to the same time. I use a JavaScript object here and then push() each value into the group as I encounter them.

    Now it's about mapping the keys and values (= time and the values for that time) of that created JavaScript object, which we obtain by using Object.entries(), to the expected result structure which is an array. But we also need to first compute the average. Again, we can use reduce() to sum up the group total and then divide by the number of elements in a group. If required we can use toFixed() to round the result. By using the Number constructor Number() I make sure that JavaScript treats the time as a number. This is also optional, but I think it makes sense since time especially in your case is a numeric value.

    See also: