Как группировать данные, которые имеют общее свойство для нескольких свойств

#javascript #arrays #group-by #grouping

#javascript #массивы #групповое-по #группировка

Вопрос:

У меня есть проблема, над решением которой я работаю, и мне трудно найти хорошее решение.

У меня есть некоторые данные, которые выглядят так:

 [
  { id: 0, dad: 'Carl', mum: 'Amanda', cat: 'Mittens' },
  { id: 1, dad: 'Ron', mum: 'Amanda', cat: 'Scratch' },
  { id: 2, dad: 'Carl', mum: 'Lucy', cat: 'Tiddles' },
  { id: 3, dad: 'Barry', mum: 'Florence', cat: 'Nyanko' },
  { id: 4, dad: 'Barry', mum: 'Florence', cat: 'Fluffy' },
  { id: 5, dad: 'Carl', mum: 'Stefanie', cat: 'Snuggles' },
...
]
 

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

Я сделал это, сначала сгруппировав их по каждой категории следующим образом:

 const groupedByDad = groupBy(data, ({dad}) => dad);
const groupedByMum = groupBy(data, ({mum}) => mum);
const groupedByCat = groupBy(data, ({cat}) => cat);

const groups = [groupedByDad, groupedByMum, groupedByCat]; // array of groups
 

Затем я извлекаю только идентификаторы, поскольку мне больше не нужны дополнительные данные, только какие идентификаторы принадлежат к какой группе.

 // in next block I extract just the IDs from the original data
groups.forEach((group) => {
  Object.entries(group).forEach(([key, grouping]) => {
    group[key] = grouping.map(({id}) => id);
  });
});
 

Затем я придумал решение, в котором я помещаю все массивы идентификаторов в 1 объект и перебираю массивы идентификаторов, для каждого массива я нахожу все массивы идентификаторов, которые пересекают первый, группирую их вместе в новый массив и удаляю их из массива массивов. Затем я перехожу к следующему оставшемуся массиву и повторяю, пока исходный массив массивов не станет пустым. Проблема в этом:

  • 1: это медленно
  • 2: Если у меня есть следующие массивы идентификаторов в массиве массивов: 0: [0, 1, 2] 1: [0, 1, 9] 2: [9, 8, 4] затем мой алгоритм обнаруживает, что массив 1 пересекает массив 0 и добавляет его в соответствующие группы, но обнаруживает, что массив 2 этого не делает, поскольку он не пересекает массив 0. Однако массив 2 пересекает массив 1 (поскольку оба они имеют идентификатор 9 в), и поэтому они должны иметь общее имя кошки, папы или мамы, поэтому эта группа также должна быть добавлена в группу, но в моей реализации она отсутствует. Я мог бы повторить процедуру несколько раз, пока не будут найдены новые совпадения, но это кажется медленным и действительно неэффективным.

Должен быть лучший метод / алгоритм для группировки записей, которые имеют как минимум 1 общее свойство. Может кто-нибудь посоветовать мне, как лучше поступить?

Я поместил приведенный ниже код для генерации некоторых тестовых данных и выполнения начальной группировки:

 const {groupBy, intersection} = require("lodash");

const dadsNames = ["Barry", "John", "Bill", "Ron", "Carl", "Danny",
  "Dodger", "Filbert", "Charlie", "Frank"];
const mumsNames = ["Lucy", "Mary", "Alice", "Sarah", "Yvonne", "Sandra",
  "Suzie", "Stefanie", "Pearl", "Amanda", "Florence"];
const catsNames = ["Tiddles", "Nyanko", "Paws", "Fluffy", "Scratch", "Snuggles",
  "Impy", "Chris", "Mandrew", "Mittens", "Tuxedo", "Sultan"];

const getRandomEntry = (array) => {
  return array[Math.floor((Math.random() * array.length))];
};

const data = [];
for (let i = 0; i < 100; i  ) {
  data.push({id: i, dad: getRandomEntry(dadsNames), mum: getRandomEntry(mumsNames), cat: getRandomEntry(catsNames)});
}

const groupedByDad = groupBy(data, ({dad}) => dad);
const groupedByMum = groupBy(data, ({mum}) => mum);
const groupedByCat = groupBy(data, ({cat}) => cat);

const groups = [groupedByDad, groupedByMum, groupedByCat]; // array of groups

// in next block I extract just the IDs from the original data
groups.forEach((group) => {
  Object.entries(group).forEach(([key, grouping]) => {
    group[key] = grouping.map(({id}) => id);
  });
});
 

Ответ №1:

Похоже, вам нужно рекурсивное решение для поиска всех комбинаций, которые должны попасть в одну группу, в противном случае, как отмечалось выше в вашей небольшой выборке из 3 идентификаторов, последующие записи строк данных не будут должным образом включены в группу…

Другой альтернативой является присвоение битовых значений каждому из значений атрибутов dad, mum и cat, затем определение битовой маски строки и использование логического И для определения совпадений, принадлежащих группе. Конечно, как только совпадение найдено, битовая маска группы расширяется с помощью логического ИЛИ, и поиск продолжается до тех пор, пока для группы не будет найдено больше строк данных.

Используя пример данных в начале вопроса, битовые маски для каждого значения атрибута следующие…

 0: {"Carl" => 1n}
1: {"Amanda" => 2n}
2: {"Mittens" => 4n}
3: {"Ron" => 8n}
4: {"Scratch" => 16n}
5: {"Lucy" => 32n}
6: {"Tiddles" => 64n}
7: {"Barry" => 128n}
8: {"Florence" => 256n}
9: {"Nyanko" => 512n}
10: {"Fluffy" => 1024n}
11: {"Stefanie" => 2048n}
12: {"Snuggles" => 4096n}
 

… и затем эти значения используются для вычисления значений битовой маски строки данных…

 [
  { id: 0, dad: 'Carl', mum: 'Amanda', cat: 'Mittens' },    ==> 7n
  { id: 1, dad: 'Ron', mum: 'Amanda', cat: 'Scratch' },     ==> 26n
  { id: 2, dad: 'Carl', mum: 'Lucy', cat: 'Tiddles' },      ==> 97n
  { id: 3, dad: 'Barry', mum: 'Florence', cat: 'Nyanko' },  ==> 896n
  { id: 4, dad: 'Barry', mum: 'Florence', cat: 'Fluffy' },  ==> 1408n
  { id: 5, dad: 'Carl', mum: 'Stefanie', cat: 'Snuggles' }  ==> 6145n
]
 

Теперь строки просматриваются в поисках совпадений. Сначала маска группы устанавливается на первую доступную строку, в данном случае 7n. Затем строки обходятся, заменяя групповую маску маской строки. Итак, маска группы (7n) дополняется маской идентификатора строки 1 (26n), в результате чего получается 2n ( «Аманда»). Поскольку это указывает на совпадение, строка 1 затем добавляется в группу, а маска группы обновляется до 7n ИЛИ 26n, что равно 31n, что является битовой маской, представляющей сумму «Карл», «Аманда», «Варежки», «Рон» и «Скретч». Итак, теперь 31n — это маска группы, а 31n, дополненный значением идентификатора строки 2 (97n), приводит к 1n, представляющему «Carl» как общий элемент. Итак, идентификатор строки 2 добавляется в группу, и теперь 31n ИЛИ 97n приводят к 127n в качестве маски группы, которая представляет атрибуты «Carl» через «Tiddles» в списке выше. Это продолжается, просматривая строки данных, оставшиеся в списке (чтобы найти связанные строки, которые были переданы из-за добавления атрибутов в группу позже при поиске), пока список не будет пройден, и в текущую группу больше не будут добавлены строки данных. Затем, если все еще остаются строки данных, создается новая группа, и следующая доступная строка данных используется для создания новой группы, и цикл проверок повторяется…

Реализация (которая случайным образом создает 10 строк данных, используя код в вопросе)…

 const dadsNames = ["Barry", "John", "Bill", "Ron", "Carl", "Danny", "Dodger", "Filbert", "Charlie", "Frank"];
const mumsNames = ["Lucy", "Mary", "Alice", "Sarah", "Yvonne", "Sandra", "Suzie", "Stefanie", "Pearl", "Amanda", "Florence"];
const catsNames = ["Tiddles", "Nyanko", "Paws", "Fluffy", "Scratch", "Snuggles", "Impy", "Chris", "Mandrew", "Mittens", "Tuxedo", "Sultan"];

const getRandomEntry = (array) => {
  return array[Math.floor((Math.random() * array.length))];
};

var data = [];
for (let i = 0; i < 10; i  ) {
  data.push({id: i, dad: getRandomEntry(dadsNames), mum: getRandomEntry(mumsNames), cat: getRandomEntry(catsNames)});
}

function getGroupings( data, attr ) {

  function getAttrMap( data, attr ) {
    let attrMap = new Map();
    let attrMapValue = 1n;
    
    let dataMapValue = new Map();
    let dataIndexList = new Set();

    data.forEach( ( d, i ) => {
      dataIndexList.add( i );
      dataMapValue.set( i, 0n );
      attr.forEach( a => {
        let attrValue = d[ a ];
        if ( !attrMap.has( attrValue ) ) {
          attrMap.set( attrValue, attrMapValue );
          attrMapValue <<= 1n;
        }
        dataMapValue.set( i, dataMapValue.get( i )   attrMap.get( attrValue ) );
      } );
    } );
    
    console.log( `Binary mapping of attributes:` );
    attrMap.forEach( (v,k) => console.log( `${k}: ${v.toString()}` ) );
    
    console.log( `nBinary value of each row of data:` );
    dataMapValue.forEach( (v,k) => console.log( `${k}: ${v.toString()}` ) );
    
    return [ dataMapValue, dataIndexList ];
  }

  let groupings = [];
  let [ dataMapValue, dataIndexList ] = getAttrMap( data, ['dad','mum','cat'] );

  while ( dataIndexList.size ) {

    let group = new Set();
    let dataRow = dataIndexList.keys().next().value;
    let mask = dataMapValue.get( dataRow );
    do {
      let entryLength = dataIndexList.size;
      dataIndexList.forEach( k => {
        if ( mask amp; dataMapValue.get( k ) ) {
          group.add( k );
          dataIndexList.delete( k );
          mask |= dataMapValue.get( k );
        }
      } );
      if ( entryLength === dataIndexList.size ) break;
    
    } while ( true );
    
    groupings.push( group );
    
  }
  
  return groupings;
}

let result = getGroupings( data, ['dad','mum','cat'] );

console.log( `nData:` );
console.log( data );

console.log( `nFinal Groupings` );
console.log( result.map( s => [...s] ) ); 

Обратите внимание, что из-за небольшого количества атрибутов dad, mum и cat, чем больше вы увеличиваете количество строк данных, тем выше вероятность того, что все строки попадут в одну группу. Следовательно, приведенный выше код выбирает только 10 случайных записей.