Python group by

125

Suponga que tengo un conjunto de pares de datos donde el índice 0 es el valor y el índice 1 es el tipo:

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

Quiero agruparlos por su tipo (por la primera cadena indexada) como tal:

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

¿Cómo puedo lograr esto de manera eficiente?

Hellnar
fuente

Respuestas:

153

Hazlo en 2 pasos. Primero, crea un diccionario.

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

Luego, convierta ese diccionario al 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'}]

También es posible con itertools.groupby pero requiere que la entrada se ordene primero.

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

Tenga en cuenta que ambos no respetan el orden original de las teclas. Necesita un OrderedDict si necesita conservar el 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
fuente
¿Cómo se puede hacer esto si la tupla de entrada tiene una clave y dos o más valores, como este: [('11013331', 'red', 'KAT'), ('9085267', 'blue' 'KAT')]donde el último elemento de la tupla es clave y los dos primeros como valor? El resultado debería ser así: resultado = [{tipo: 'KAT', elementos: [('11013331', rojo), ('9085267', azul)]}]
usuario1144616
1
from operator import itemgetter
Baumann
1
el paso 1 se puede realizar sin la importación:d= {}; for k,v in input: d.setdefault(k, []).append(v)
ecoe
Estoy trabajando en un programa MapReduce en Python, solo me pregunto si hay alguna forma de agrupar por valores en una lista sin tener que lidiar con diccionarios o bibliotecas externas como pandas. Si no es así, ¿cómo puedo deshacerme de los elementos y escribir mi resultado?
Kourosh
54

El itertoolsmódulo integrado de Python en realidad tiene una groupbyfunción, pero para eso los elementos que se van a agrupar primero deben ordenarse de manera que los elementos que se agrupen sean contiguos en la 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)

Ahora la entrada se ve así:

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

groupbydevuelve una secuencia de 2 tuplas, de la forma (key, values_iterator). Lo que queremos es convertir esto en una lista de dictados donde el "tipo" es la clave, y "elementos" es una lista de los elementos número 0 de las tuplas devueltas por el valor_iterador. Me gusta esto:

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

Ahora resultcontiene su dict deseado, como se indica en su pregunta.

Sin embargo, puede considerar hacer un solo resumen de esto, tecleado por tipo, y cada valor que contenga la lista de valores. En su forma actual, para encontrar los valores de un tipo en particular, deberá iterar sobre la lista para encontrar el dict que contiene la clave correspondiente 'tipo' y luego obtener el elemento 'elementos' de él. Si usa un solo dict en lugar de una lista de dictados de 1 elemento, puede encontrar los elementos para un tipo particular con una sola búsqueda con clave en el dict maestro. Usando groupby, esto se vería así:

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

resultahora contiene este dict (es similar al resdefaultdictdict intermedio en la respuesta de @ KennyTM)

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

(Si desea reducir esto a una sola línea, puede:

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

o usando la nueva forma de comprensión dict:

result = {key:list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn)}
PaulMcG
fuente
Estoy trabajando en un programa MapReduce en Python, solo me pregunto si hay alguna forma de agrupar por valores en una lista sin tener que lidiar con diccionarios o bibliotecas externas como pandas. Si no es así, ¿cómo puedo deshacerme de los elementos y escribir mi resultado?
Kourosh
@Kourosh: publique como una nueva pregunta, pero asegúrese de indicar lo que quiere decir con "deshacerse de los elementos y escribir mi resultado" y "sin tratar con los diccionarios".
PaulMcG
7

También me gustó la agrupación simple de pandas . es potente, simple y más adecuado para grandes conjuntos de datos

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

akiva
fuente
3

Esta respuesta es similar a la respuesta de @ PaulMcG pero no requiere ordenar la entrada.

Para aquellos en programación funcional, groupByse puede escribir en una línea (¡sin incluir las importaciones!), Y a diferencia de itertools.groupbyesto, no requiere que se ordene la entrada:

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

(La razón de ... or grpla lambdaes que para que este reduce()funcione, las lambdanecesidades para devolver su primer argumento, porque list.append()siempre devuelve Noneel orsiempre volverá grp. Es decir, se trata de un truco para conseguir alrededor de restricción del pitón que una lambda sólo puede evaluar una sola expresión.)

Esto devuelve un dict cuyas claves se encuentran al evaluar la función dada y cuyos valores son una lista de los elementos originales en el orden original. Para el ejemplo del OP, llamar esto como groupBy(lambda pair: pair[1], input)devolverá este 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')]}

Y según la respuesta de @ PaulMcG, se puede encontrar el formato solicitado por el OP envolviéndolo en una lista de comprensión. Entonces esto lo hará:

result = {key: [pair[0] for pair in values],
          for key, values in groupBy(lambda pair: pair[1], input).items()}
ronen
fuente
Mucho menos código, pero comprensible. También es bueno porque no reinventa la rueda.
devdanke
2

La siguiente función agrupará rápidamente ( no se requiere clasificación ) tuplas de cualquier longitud por una clave que tenga algún í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

En el caso de su pregunta, el índice de clave por el que desea agrupar es 1, por lo tanto:

group_by(input,1)

da

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

que no es exactamente el resultado que solicitó, pero que bien podría satisfacer sus necesidades.

mmj
fuente
Estoy trabajando en un programa MapReduce en Python, solo me pregunto si hay alguna forma de agrupar por valores en una lista sin tener que lidiar con diccionarios o bibliotecas externas como pandas. Si no es así, ¿cómo puedo deshacerme de los elementos y escribir mi 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'}]

fuente