Вычислить среднее значение данных массива

#javascript #arrays #algorithm #average

#javascript #массивы #алгоритм #среднее

Вопрос:

Я застрял на том, как реализовать эту относительно простую операцию в Javascript:

У меня есть список объектов, определенных таким образом:

 [
    {id: 1, region: "America", country:"USA", values:[1,2,3,4] },
    {id: 2, region: "America", country:"Canada", values:[3,4,5,6] },
    {id: 3, region: "Europe", country:"France", values:[1,2,3,4] },
    {id: 4, region: "Europe", country:"Italy", values:[1,2,3,4] },
    {id: 5, region: "Europe", country:"Spain", values:[5,9,1,7] },
    {id: 6, region: "Europe", country:"Germany", values:[1,6,2,8] },
    {id: 7, region: "Europe", country:"Ireland", values:[6,4,6,9]}
      ]
  

Я пытаюсь вычислить среднее значение чисел, содержащихся в поле значения, сгруппированных по региону.
Итак, к приведенному выше списку у меня будет еще два элемента, один для Америки и один для Европы, содержащие среднее значение значений:

 [
    {id: 1, region: "America", country:"USA", values:[1,2,3,4] },
    {id: 2, region: "America", country:"Canada", values:[3,4,5,6] },
    {id: 3, region: "Europe", country:"France", values:[1,2,3,4] },
    {id: 4, region: "Europe", country:"Italy", values:[1,2,3,4] },
    {id: 5, region: "Europe", country:"Spain", values:[5,9,1,7] },
    {id: 6, region: "Europe", country:"Germany", values:[1,6,2,8] },
    {id: 7, region: "Europe", country:"Ireland", values:[6,4,6,9]},
    {id: 8, region: "America", country:"avg", values:[2,3,4,5]},
    {id: 9, region: "Europe", country:"avg", values:[2.8,4.6,2.8,6.4]}
      ]
  

Есть идеи о том, как это сделать?
Пожалуйста, имейте в виду, что количество элементов для агрегирования может составлять около 10 ~ 15, а числа в полях значений могут составлять около 150 ~ 200
поле значения содержит одинаковое количество значений для всех элементов.
Некоторое значение может быть нулевым, поэтому в этом случае мне нужно вычислить средние значения соответственно, поскольку null не равен 0!

Я мог бы выполнить множество циклов, чтобы просканировать все и произвести вычисления, но мне интересно, есть ли что-то более простое и быстрое, обеспечивающее хорошую производительность.


Объяснение / Пример:
Первое среднее значение для Америки будет рассчитано следующим образом:

 (sum of first value of 'values' for each country with region 'America')
-----------------------------------------------------------------------
            (number of countries with region 'America')
  

Псевдокод:

 America.avg.values[0] = (USA.values[0]   Canada.values[0]) / 2 /*(1 3)/2 = 2*/;
America.avg.values[1] = (USA.values[1]   Canada.values[1]) / 2 /*(2 4)/2 = 3*/;
...
  

Комментарии:

1. @Andreas да, это правильно

Ответ №1:

Вы могли бы использовать объект для хранения суммы и количества элементов и присваивать среднее значение для каждого цикла значений сгруппированному объекту.

 var data = [{ id: 1, region: "America", country: "USA", values: [1, 2, 3, 4] }, { id: 2, region: "America", country: "Canada", values: [3, 4, 5, 6] }, { id: 3, region: "Europe", country: "France", values: [1, 2, 3, 4] }, { id: 4, region: "Europe", country: "Italy", values: [1, 2, 3, 4] }, { id: 5, region: "Europe", country: "Spain", values: [5, 9, 1, 7] }, { id: 6, region: "Europe", country: "Germany", values: [1, 6, 2, 8] }, { id: 7, region: "Europe", country: "Ireland", values: [6, 4, 6, 9] }];

data.forEach(function (a, i, aa) {
    if (!this[a.region]) {
        this[a.region] = { sum: [], count: [], values: [] };
        aa.push({ id: aa.length   1, region: a.region, country: 'avg', values: this[a.region].values });
    }
    a.values.forEach(function (b, i) {
        if (b !== null) {
            this[a.region].sum[i] = (this[a.region].sum[i] || 0)   b;
            this[a.region].count[i] = (this[a.region].count[i] || 0)   1;
            this[a.region].values[i] = this[a.region].sum[i] / this[a.region].count[i];
        }
    }, this);
}, Object.create(null));

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

Комментарии:

1. Только для последнего Europe среднего значения: 6.25 !== 6.4 ; Также «средние объекты» имеют America и Europe . Я сомневаюсь, что ваш алгоритм соответствует требованиям

2. @Andreas прав, это не то, что я должен делать, мне не нужно делать среднее значение числа в поле значения, слишком просто!! смотрите ожидаемые результаты в вопросе

3. @Giox Правильно ли вычисление в моем комментарии к вашему вопросу? Если это так, вы могли бы добавить это в свой вопрос в качестве примера

4. @Andreas да, именно то, что мне нужно! но учтите, что я не могу жестко закодировать название региона, поскольку они динамические

5. @NinaScholz идеально

Ответ №2:

Уменьшите исходные данные, отслеживая промежуточные результаты в объекте:

  • Объект создает новый ключ для каждой новой области, с которой он сталкивается
  • Когда рассматривается первая запись данных для региона, сохраните две вещи:
    • Среднее значение, которое начинается как копия значений первого элемента
    • Количество, которое представляет собой среднее значение, которое начинается с 1
  • Когда появится второй или n-й элемент, вычислите новое среднее значение, используя формулу скользящего среднего
  • Возвращайте результирующий объект до тех пор, пока не будут обработаны все записи
  • Преобразуйте объект в две точки данных и соедините их с исходным массивом

 var data = [
    {id: 1, region: "America", country:"USA", values:[1,2,null,4] },
    {id: 2, region: "America", country:"Canada", values:[3,4,5,6] },
    {id: 3, region: "Europe", country:"France", values:[1,2,3,4] },
    {id: 4, region: "Europe", country:"Italy", values:[1,2,3,4] },
    {id: 5, region: "Europe", country:"Spain", values:[5,9,1,7] },
    {id: 6, region: "Europe", country:"Germany", values:[1,6,2,8] },
    {id: 7, region: "Europe", country:"Ireland", values:[6,4,6,9]}
];

var avg = data.reduce(function(result, current) {
  if (result[current.region]) {
    var obj = result[current.region];
    obj.avg = current.values
      // Map to a moving average: 
      //  - the current avg at pos `i` represents `count` samples
      .map(function(v, i) { 
        if (v === null) return obj.avg[i];
      
        return (v   (obj.avg[i] * obj.count[i])) / (  obj.count[i]);
       });
  } else {
    result[current.region] = {
      count: current.values.map(function(v) { 
        return v !== null ? 1 : 0; 
      }),
      avg: current.values.map(function(v) {
        return v !== null ? v : 0;
      })
    };
  }
  
  return resu<
}, {});

// Add to array (assume sorted by id)
var extendedData = data.concat(Object.keys(avg).map(function(k, i) {
  return {
    id: data[data.length - 1].id   1   i,
    region: k,
    country: "avg",
    values: avg[k].avg
  };
}));

console.log(extendedData);  

Комментарии:

1. Спасибо, это идеально, а также подход с уменьшением карты очень хорош. Правильным термином для этого вопроса должно быть Скользящее среднее … вы правы.

2. Я обнаружил проблему, когда есть нулевое значение, поэтому данные недоступны… вот так: {идентификатор: 1, регион: «Америка», страна: «США», значения: [1,2, null, 4] }, {идентификатор: 2, регион: «Америка», страна: «Канада», значения: [3,4,5,6] } должно получиться 2,3,5,5

3. Ах, я подумал, что вы хотели бы, чтобы null значения обрабатывались как 0 … Не видел этого требования. Хотя это легко исправить, я внесу правку.

4. Я исправил это, также создав count массив. Теперь у каждого индекса значения может быть свой собственный счетчик, что означает, что когда null находится в наборе значений, количество этого индекса значения не будет увеличиваться.

5. map это способ сделать две вещи одновременно: вы используете массив для создания нового массива равной длины, обрабатывая его значения с помощью переданной функции. Самое замечательное в reduce том, что он позволяет выполнять цикл по массиву, возвращая значение на следующей итерации. Дайте мне знать, если в этом фрагменте есть определенные строки, которые нуждаются в уточнении; Я буду рад добавить комментарии или предоставить альтернативные способы записи материала, если это необходимо!

Ответ №3:

Я бы просто прошелся по всему массиву и подсчитал среднее значение значений, затем добавил avg свойство к массиву.

 var data = [
    {id: 1, region: "America", country:"USA", values:[1,2,3,4] },
    {id: 2, region: "America", country:"Canada", values:[3,4,5,6] },
    {id: 3, region: "Europe", country:"France", values:[1,2,3,4] },
    {id: 4, region: "Europe", country:"Italy", values:[1,2,3,4] },
    {id: 5, region: "Europe", country:"Spain", values:[5,9,1,7] },
    {id: 6, region: "Europe", country:"Germany", values:[1,6,2,8] },
    {id: 7, region: "Europe", country:"Ireland", values:[6,4,6,9]}
];

data.forEach(function(element,index,array){
  var sum = 0;
  element.values.forEach(function(element,index,array){
    sum  = element;
  });
  element.avg = sum / element.values.length;
});

console.log(data);  

Комментарии:

1. нет, это неправильно, не соответствует требованиям. Это делает среднее значение по странам, а не по регионам. Смотрите ожидаемый результат в вопросе

Ответ №4:

Вы можете сделать что-то вроде этого:

 var data = [{ id: 1, region: "America", country: "USA", values: [1, 2, null, 4] }, { id: 2, region: "America", country: "Canada", values: [3, 4, 5, 6] }, { id: 3, region: "Europe", country: "France", values: [1, 2, 3, 4] }, { id: 4, region: "Europe", country: "Italy", values: [1, 2, 3, 4] }, { id: 5, region: "Europe", country: "Spain", values: [5, 9, 1, 7] }, { id: 6, region: "Europe", country: "Germany", values: [1, 6, 2, 8] }, { id: 7, region: "Europe", country: "Ireland", values: [6, 4, 6, 9] }]

// So that regions are configurable
var regions = ['America', 'Europe'];
var result = [];

// for cases when data is not sorted by id.
var lastIndex = Math.max.apply(null, data.map(function(x){ return x.id}))

regions.forEach(function(r) {
  var val = [];
  data.forEach(function(c,i){
    if(c.region === r amp;amp; c.values amp;amp; c.values.length > 0){
      c.values.forEach(function(v, i) {
        if (!v amp;amp; v!== 0) return
          val[i] = (val[i] || []);
          val[i].push(v)
      })
    }
  });
  
  var avg = val.map(function(v){
    return (v.reduce(function(p,c){ return p c }) / v.length);
  })
  
  // not pushing into data to prevent extra iterations
  result.push({
    id:   lastIndex,
    region: r,
    country: 'avg',
    values: avg
  })

});

data = data.concat(result)
console.log(data)  

Комментарии:

1. Вы близки, но … «когда есть значение null, мне не нужно учитывать среднее значение: (2 4 null) / 2 = 3»

2. @Andreas Вы принимаете во внимание случай, когда values длина является переменной?

3. values Массив может содержать null , и в этом случае это не должно рассматриваться как 0 ( (val[i] || 0) ) и не должно увеличивать количество элементов ( len ) — по крайней мере, это моя интерпретация вопроса и комментария К…

4. Обновлена скрипка . И спасибо, что согласились со мной. Я обновил условие в своем ответе на !v amp;amp; v!== 0

Ответ №5:

Ниже приведено общее решение, которое будет работать для любого количества регионов. Я знаю, что вопросу уже много лет (2 часа), но учтите простоту подхода. Для каждого уникального региона вычислите среднее значение как таковое:

1 — получить все values массивы как один 2d массив для данной области
2 — транспонировать этот массив
3 — усреднить каждый массив

 var data = // your array here

function getArr(data,reg) {
    return data.filter(el => el.region === reg)
               .map(el => el.values)
}

const xpose = x => x[0].map( (c,i) => x.map( r => r[i] ) )

function avgArr(a) {
    a = a.filter(el => el !== null);
    return a.reduce((x,y) => x y) / a.length;
}

function calcAverages(data) {
    let i = 1   Math.max.apply(null, data.map( el => el.id )) // max id
    let regions = [...new Set (data.map (a => a.region))]  // unique regions

    regions.forEach( region => {
        data.push( { id: i  ,
                     region: region,
                     country: "avg",
                     values: xpose(getArr(data,region)).map(a => avgArr(a))})})

    return data
}

// > calcAverages(data)
//
// ...
//  { id: 8,
//    region: 'America',
//    country: 'avg',
//    values: [ 2, 3, 4, 5 ] },
//  { id: 9,
//    region: 'Europe',
//    country: 'avg',
//    values: [ 2.8, 4.6, 3, 6.4 ] } ]
  

Ответ №6:

Я хотел бы сделать следующее

 var data = [
    {id: 1, region: "America", country:"USA", values:[1,2,3,4] },
    {id: 2, region: "America", country:"Canada", values:[3,4,5,6] },
    {id: 3, region: "Europe", country:"France", values:[1,2,3,4] },
    {id: 4, region: "Europe", country:"Italy", values:[1,2,3,4] },
    {id: 5, region: "Europe", country:"Spain", values:[5,9,1,7] },
    {id: 6, region: "Europe", country:"Germany", values:[1,6,2,8] },
    {id: 7, region: "Europe", country:"Ireland", values:[6,4,6,9]}
      ],
 newData = [...new Set(data.map(c => c.region))]
           .reduce((f,c,i) => (f.push(...data.filter(o => o.region === c)
                                             .reduce((p,q,_,a) => { p[0].values = q.values.map((v,j) => v/a.length   p[0].values[j]);
                                                                    p.push(q);
                                                                    return p;
                                                                  } ,[{     id: data.length   1   i,
                                                                        region: c,
                                                                       country: "avg",
                                                                        values: Array(data[0].values.length).fill(0)
                                                                      }])),f),[]);
console.log(newData);