Группа Python по

125

Предположим, что у меня есть набор пар данных, где индекс 0 - это значение, а индекс 1 - это тип:

input = [
          ('11013331', 'KAT'), 
          ('9085267',  'NOT'), 
          ('5238761',  'ETH'), 
          ('5349618',  'ETH'), 
          ('11788544', 'NOT'), 
          ('962142',   'ETH'), 
          ('7795297',  'ETH'), 
          ('7341464',  'ETH'), 
          ('9843236',  'KAT'), 
          ('5594916',  'ETH'), 
          ('1550003',  'ETH')
        ]

Я хочу сгруппировать их по типу (по 1-й индексированной строке) как таковые:

result = [ 
           { 
             type:'KAT', 
             items: ['11013331', '9843236'] 
           },
           {
             type:'NOT', 
             items: ['9085267', '11788544'] 
           },
           {
             type:'ETH', 
             items: ['5238761', '962142', '7795297', '7341464', '5594916', '1550003'] 
           }
         ] 

Как я могу добиться этого эффективным способом?

Hellnar
источник

Ответы:

154

Сделайте это за 2 шага. Сначала создайте словарь.

>>> input = [('11013331', 'KAT'), ('9085267', 'NOT'), ('5238761', 'ETH'), ('5349618', 'ETH'), ('11788544', 'NOT'), ('962142', 'ETH'), ('7795297', 'ETH'), ('7341464', 'ETH'), ('9843236', 'KAT'), ('5594916', 'ETH'), ('1550003', 'ETH')]
>>> from collections import defaultdict
>>> res = defaultdict(list)
>>> for v, k in input: res[k].append(v)
...

Затем преобразуйте этот словарь в ожидаемый формат.

>>> [{'type':k, 'items':v} for k,v in res.items()]
[{'items': ['9085267', '11788544'], 'type': 'NOT'}, {'items': ['5238761', '5349618', '962142', '7795297', '7341464', '5594916', '1550003'], 'type': 'ETH'}, {'items': ['11013331', '9843236'], 'type': 'KAT'}]

Это также возможно с помощью itertools.groupby, но для этого требуется сначала отсортировать ввод.

>>> sorted_input = sorted(input, key=itemgetter(1))
>>> groups = groupby(sorted_input, key=itemgetter(1))
>>> [{'type':k, 'items':[x[0] for x in v]} for k, v in groups]
[{'items': ['5238761', '5349618', '962142', '7795297', '7341464', '5594916', '1550003'], 'type': 'ETH'}, {'items': ['11013331', '9843236'], 'type': 'KAT'}, {'items': ['9085267', '11788544'], 'type': 'NOT'}]

Обратите внимание, что в обоих случаях не соблюдается исходный порядок клавиш. Вам понадобится OrderedDict, если вам нужно сохранить порядок.

>>> from collections import OrderedDict
>>> res = OrderedDict()
>>> for v, k in input:
...   if k in res: res[k].append(v)
...   else: res[k] = [v]
... 
>>> [{'type':k, 'items':v} for k,v in res.items()]
[{'items': ['11013331', '9843236'], 'type': 'KAT'}, {'items': ['9085267', '11788544'], 'type': 'NOT'}, {'items': ['5238761', '5349618', '962142', '7795297', '7341464', '5594916', '1550003'], 'type': 'ETH'}]
kennytm
источник
Как это можно сделать, если входной кортеж имеет один ключ и два или более значений, например: [('11013331', 'red', 'KAT'), ('9085267', 'blue' 'KAT')]где последний элемент кортежа является ключевым, а первые два - значением. Результат должен быть таким: result = [{type: 'KAT', items: [('11013331', red), ('9085267', blue)]}]
user1144616 06
1
from operator import itemgetter
Baumann
1
шаг 1 можно выполнить без импорта:d= {}; for k,v in input: d.setdefault(k, []).append(v)
ecoe
Я работаю над программой MapReduce на python, просто интересно, есть ли способ группировать по значениям в списке, не имея дело со словарями или внешней библиотекой, такой как pandas? Если нет, то как мне избавиться от элементов и ввести свой результат?
Kourosh
55

Встроенный itertoolsмодуль Python на самом деле имеет groupbyфункцию, но для этого элементы, которые нужно сгруппировать, должны быть сначала отсортированы таким образом, чтобы элементы, которые нужно сгруппировать, были смежными в списке:

from operator import itemgetter
sortkeyfn = itemgetter(1)
input = [('11013331', 'KAT'), ('9085267', 'NOT'), ('5238761', 'ETH'), 
 ('5349618', 'ETH'), ('11788544', 'NOT'), ('962142', 'ETH'), ('7795297', 'ETH'), 
 ('7341464', 'ETH'), ('9843236', 'KAT'), ('5594916', 'ETH'), ('1550003', 'ETH')] 
input.sort(key=sortkeyfn)

Теперь ввод выглядит так:

[('5238761', 'ETH'), ('5349618', 'ETH'), ('962142', 'ETH'), ('7795297', 'ETH'),
 ('7341464', 'ETH'), ('5594916', 'ETH'), ('1550003', 'ETH'), ('11013331', 'KAT'),
 ('9843236', 'KAT'), ('9085267', 'NOT'), ('11788544', 'NOT')]

groupbyвозвращает последовательность из двух кортежей формы (key, values_iterator). Мы хотим превратить это в список dicts, где 'type' является ключом, а 'items' - это список 0-х элементов кортежей, возвращаемых values_iterator. Как это:

from itertools import groupby
result = []
for key,valuesiter in groupby(input, key=sortkeyfn):
    result.append(dict(type=key, items=list(v[0] for v in valuesiter)))

Теперь resultсодержит желаемый диктат, как указано в вашем вопросе.

Однако вы можете подумать о том, чтобы просто сделать из этого один диктант с ключом по типу и каждым значением, содержащим список значений. В вашей текущей форме, чтобы найти значения для определенного типа, вам придется перебирать список, чтобы найти dict, содержащий соответствующий ключ type, а затем получить из него элемент items. Если вы используете одиночный dict вместо списка dict из 1 элемента, вы можете найти элементы для определенного типа с помощью поиска с одним ключом в главном dict. При использовании groupbyэто будет выглядеть так:

result = {}
for key,valuesiter in groupby(input, key=sortkeyfn):
    result[key] = list(v[0] for v in valuesiter)

resultтеперь содержит этот dict (он похож на промежуточный resdefaultdict в ответе @ KennyTM):

{'NOT': ['9085267', '11788544'], 
 'ETH': ['5238761', '5349618', '962142', '7795297', '7341464', '5594916', '1550003'], 
 'KAT': ['11013331', '9843236']}

(Если вы хотите сократить это до однострочника, вы можете:

result = dict((key,list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn))

или используя новомодную форму понимания слов:

result = {key:list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn)}
PaulMcG
источник
Я работаю над программой MapReduce на python, просто интересно, есть ли способ группировать по значениям в списке, не имея дело со словарями или внешней библиотекой, такой как pandas? Если нет, то как мне избавиться от элементов и ввести свой результат?
Kourosh
@Kourosh - Задайте новый вопрос, но не забудьте указать, что вы имеете в виду под «избавиться от элементов и ввести мой результат» и «без работы со словарями».
PaulMcG
7

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

result = pandas.DataFrame(input).groupby(1).groups

Акива
источник
3

Этот ответ аналогичен ответу @ PaulMcG, но не требует сортировки ввода.

Для тех, кто занимается функциональным программированием, groupByможет быть записано в одну строку (не включая импорт!), И, в отличие от itertools.groupbyэтого, не требует сортировки ввода:

from functools import reduce # import needed for python3; builtin in python2
from collections import defaultdict

def groupBy(key, seq):
 return reduce(lambda grp, val: grp[key(val)].append(val) or grp, seq, defaultdict(list))

(Причина ... or grpв lambdaтом , что для этого reduce()на работу, то lambdaдолжен возвращать свой первый аргумент, потому что list.append()всегда возвращает всегда будет возвращать . Т.е. это хак , чтобы обойти ограничение питона , что лямбда может оценивать только одно выражение.)Noneorgrp

Это возвращает dict, ключи которого находятся путем вычисления данной функции, а значениями являются список исходных элементов в исходном порядке. Для примера OP вызов this as groupBy(lambda pair: pair[1], input)вернет этот dict:

{'KAT': [('11013331', 'KAT'), ('9843236', 'KAT')],
 'NOT': [('9085267', 'NOT'), ('11788544', 'NOT')],
 'ETH': [('5238761', 'ETH'), ('5349618', 'ETH'), ('962142', 'ETH'), ('7795297', 'ETH'), ('7341464', 'ETH'), ('5594916', 'ETH'), ('1550003', 'ETH')]}

И в соответствии с ответом @ PaulMcG запрошенный формат OP можно найти, обернув его в понимание списка. Итак, это сделает это:

result = {key: [pair[0] for pair in values],
          for key, values in groupBy(lambda pair: pair[1], input).items()}
Ронен
источник
Гораздо меньше кода, но понятно. Также хорошо, потому что не изобретает велосипед.
devdanke
2

Следующая функция быстро ( без сортировки ) группирует кортежи любой длины по ключу с любым индексом:

# given a sequence of tuples like [(3,'c',6),(7,'a',2),(88,'c',4),(45,'a',0)],
# returns a dict grouping tuples by idx-th element - with idx=1 we have:
# if merge is True {'c':(3,6,88,4),     'a':(7,2,45,0)}
# if merge is False {'c':((3,6),(88,4)), 'a':((7,2),(45,0))}
def group_by(seqs,idx=0,merge=True):
    d = dict()
    for seq in seqs:
        k = seq[idx]
        v = d.get(k,tuple()) + (seq[:idx]+seq[idx+1:] if merge else (seq[:idx]+seq[idx+1:],))
        d.update({k:v})
    return d

В случае вашего вопроса индекс ключа, который вы хотите сгруппировать, равен 1, поэтому:

group_by(input,1)

дает

{'ETH': ('5238761','5349618','962142','7795297','7341464','5594916','1550003'),
 'KAT': ('11013331', '9843236'),
 'NOT': ('9085267', '11788544')}

что не совсем то, что вы просили, но вполне может удовлетворить ваши потребности.

MMJ
источник
Я работаю над программой MapReduce на python, просто интересно, есть ли способ группировать по значениям в списке, не имея дело со словарями или внешней библиотекой, такой как pandas? Если нет, то как мне избавиться от элементов и ввести свой результат?
Kourosh
0
result = []
# Make a set of your "types":
input_set = set([tpl[1] for tpl in input])
>>> set(['ETH', 'KAT', 'NOT'])
# Iterate over the input_set
for type_ in input_set:
    # a dict to gather things:
    D = {}
    # filter all tuples from your input with the same type as type_
    tuples = filter(lambda tpl: tpl[1] == type_, input)
    # write them in the D:
    D["type"] = type_
    D["itmes"] = [tpl[0] for tpl in tuples]
    # append D to results:
    result.append(D)

result
>>> [{'itmes': ['9085267', '11788544'], 'type': 'NOT'}, {'itmes': ['5238761', '5349618', '962142', '7795297', '7341464', '5594916', '1550003'], 'type': 'ETH'}, {'itmes': ['11013331', '9843236'], 'type': 'KAT'}]

источник