Agrupar Python por

125

Suponha que eu tenha um conjunto de pares de dados em que o índice 0 seja o valor e o índice 1 seja o tipo:

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

Quero agrupá-los por seu tipo (pela primeira string indexada) da seguinte forma:

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

Como posso conseguir isso de maneira eficiente?

Hellnar
fonte

Respostas:

153

Faça isso em 2 etapas. Primeiro, crie um dicionário.

>>> 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)
...

Em seguida, converta esse dicionário no formato esperado.

>>> [{'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'}]

Também é possível com itertools.groupby, mas requer que a entrada seja classificada primeiro.

>>> 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'}]

Observe que ambos não respeitam a ordem original das chaves. Você precisa de um OrderedDict se precisar manter o pedido.

>>> 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
fonte
Como isso pode ser feito se a tupla de entrada tiver uma chave e dois ou mais valores, assim: [('11013331', 'red', 'KAT'), ('9085267', 'blue' 'KAT')]onde o último elemento da tupla é a chave e os dois primeiros como valor. O resultado deve ser assim: result = [{type: 'KAT', itens: [('11013331', vermelho), ('9085267', azul)])]]
user1144616
1
from operator import itemgetter
Baumann
1
o passo 1 pode ser feito sem a importação:d= {}; for k,v in input: d.setdefault(k, []).append(v)
ecoe 19/10/18
Estou trabalhando em um programa MapReduce em python, apenas imaginando, existe alguma maneira de agrupar valores em uma lista sem lidar com dicionários ou bibliotecas externas, como pandas? Caso contrário, como posso me livrar dos itens e digitar meu resultado?
Kourosh #
54

O itertoolsmódulo interno do Python realmente tem uma groupbyfunção, mas para que os elementos a serem agrupados sejam primeiro classificados, de modo que os elementos a serem agrupados sejam contíguos na lista:

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)

Agora, a entrada se parece com:

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

groupbyretorna uma sequência de 2 tuplas, do formulário (key, values_iterator). O que queremos é transformar isso em uma lista de dictos onde o 'tipo' é a chave e 'itens' é uma lista dos 0'ésimos elementos das tuplas retornadas pelo values_iterator. Como isso:

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)))

Agora resultcontém o ditado desejado, conforme indicado na sua pergunta.

Você pode considerar, no entanto, criar um único ditado, digitado por tipo e cada valor contendo a lista de valores. Em seu formulário atual, para encontrar os valores para um tipo específico, você precisará percorrer a lista para encontrar o ditado que contém a chave 'type' correspondente e, em seguida, obter o elemento 'items'. Se você usar um único ditado em vez de uma lista de ditados de 1 item, poderá encontrar os itens para um tipo específico com uma única pesquisa com chave no ditado mestre. Usando groupby, seria assim:

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

resultagora contém este ditado (é semelhante ao respadrão intermediário na resposta do @ KennyTM):

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

(Se você deseja reduzir isso para uma linha, você pode:

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

ou usando o novo formulário de compreensão de ditados:

result = {key:list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn)}
PaulMcG
fonte
Estou trabalhando em um programa MapReduce em python, apenas imaginando, existe alguma maneira de agrupar valores em uma lista sem lidar com dicionários ou bibliotecas externas, como pandas? Caso contrário, como posso me livrar dos itens e digitar meu resultado?
Kourosh #
@Kourosh - Publique como uma nova pergunta, mas não deixe de indicar o que você quer dizer com "livrar-se de itens e digitar o meu resultado" e "sem lidar com dicionários".
26918 PaulMcG
7

Eu também gostei de agrupamento simples de pandas . é poderoso, simples e mais adequado para grandes conjuntos de dados

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

akiva
fonte
3

Esta resposta é semelhante à resposta de @ PaulMcG, mas não requer classificação da entrada.

Para aqueles em programação funcional, groupBypode ser escrito em uma linha (não incluindo importações!) E, ao contrário itertools.groupby, não exige que a entrada seja classificada:

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))

(A razão para ... or grpnos lambdaé que para este reduce()ao trabalho, as lambdanecessidades para retornar seu primeiro argumento, porque list.append()sempre retorna Nonea orvoltar sempre grp. Ou seja, é um hack para contornar a restrição de python que uma lambda só pode avaliar uma única expressão.)

Isso retorna um ditado cujas chaves são encontradas avaliando a função especificada e cujos valores são uma lista dos itens originais na ordem original. Para o exemplo do OP, chamar isso como groupBy(lambda pair: pair[1], input)retornará este ditado:

{'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')]}

E de acordo com a resposta do @ PaulMcG, o formato solicitado do OP pode ser encontrado envolvendo-o em uma lista de compreensão. Então, isso fará isso:

result = {key: [pair[0] for pair in values],
          for key, values in groupBy(lambda pair: pair[1], input).items()}
Ronen
fonte
Muito menos código, mas compreensível. Também é bom porque não reinventa a roda.
devdanke 11/07
2

A função seguinte irá rapidamente ( sem ordenação necessária) tuplos do grupo de qualquer comprimento por uma chave que tem qualquer índice:

# 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

No caso da sua pergunta, o índice da chave que você deseja agrupar é 1, portanto:

group_by(input,1)

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

que não é exatamente a saída solicitada, mas também pode atender às suas necessidades.

mmj
fonte
Estou trabalhando em um programa MapReduce em python, apenas imaginando, existe alguma maneira de agrupar valores em uma lista sem lidar com dicionários ou bibliotecas externas, como pandas? Caso contrário, como posso me livrar dos itens e digitar meu resultado?
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'}]

fonte