Pregunta ¿Una receta para agrupar / agregar datos?


Tengo algunos datos almacenados en una lista que me gustaría agrupar según un valor.

Por ejemplo, si mis datos son

data = [(1, 'a'), (2, 'x'), (1, 'b')]

y quiero agruparlo por el primer valor en cada tupla para obtener

result = [(1, 'ab'), (2, 'x')]

¿cómo lo haría?

De manera más general, ¿cuál es la forma recomendada de agrupar datos en Python? ¿Hay alguna receta que pueda ayudarme?


6
2018-04-29 12:14


origen


Respuestas:


La estructura de datos para utilizar para todo tipo de agrupación es la dict. La idea es usar algo que identifique de manera única a un grupo como las claves del dictado, y almacenar todos los valores que pertenecen al mismo grupo bajo la misma clave.

Como ejemplo, sus datos podrían almacenarse en un dict como este:

{1: ['a', 'b'],
 2: ['x']}

El entero que está utilizando para agrupar los valores se usa como la clave dict, y los valores se agregan en una lista.

La razón por la que estamos usando un dict es porque puede asignar claves a valores en tiempo O (1) constante. Esto hace que el proceso de agrupamiento sea muy eficiente y también muy fácil. La estructura general del código siempre será la misma para todos los tipos de tareas de agrupamiento: Usted itera sobre sus datos y llena gradualmente un dict con valores agrupados. Usando un defaultdict en lugar de un dict regular hace todo el proceso aún más fácil, porque no tenemos que preocuparnos por inicializar el dict con listas vacías.

import collections

groupdict = collections.defaultdict(list)
for value in data:
    group = value[0]
    value = value[1]
    groupdict[group].append(value)

# result:
# {1: ['a', 'b'],
#  2: ['x']}

Una vez que se agrupan los datos, todo lo que queda es convertir el dictado al formato de salida deseado:

result = [(key, ''.join(values)) for key, values in groupdict.items()]
# result: [(1, 'ab'), (2, 'x')]

La receta de agrupamiento

La siguiente sección proporcionará recetas para diferentes tipos de entradas y salidas, y mostrará cómo agrupar por varias cosas. La base de todo es el siguiente fragmento:

import collections

groupdict = collections.defaultdict(list)
for value in data:  # input
    group = ???  # group identifier
    value = ???  # value to add to the group
    groupdict[group].append(value)

result = groupdict  # output

Cada una de las líneas comentadas puede / debe personalizarse según su caso de uso.

Entrada

El formato de sus datos de entrada dicta cómo iterar sobre él.

En esta sección, estamos personalizando el for value in data: línea de la receta.

  • Una lista de valores

    La mayoría de las veces, todos los valores se almacenan en una lista plana:

    data = [value1, value2, value3, ...]
    

    En este caso, simplemente iteramos sobre la lista con un for lazo:

    for value in data:
    
  • Múltiples listas

    Si tiene varias listas con cada lista que contiene el valor de un atributo diferente como

    firstnames = [firstname1, firstname2, ...]
    middlenames = [middlename1, middlename2, ...]
    lastnames = [lastname1, lastname2, ...]
    

    utilizar el zip función para iterar sobre todas las listas simultáneamente:

    for value in zip(firstnames, middlenames, lastnames):
    

    Esto hará value una tupla de (firstname, middlename, lastname).

  • Múltiples dictados o una lista de dicts

    Si desea combinar múltiples dictados como

    dict1 = {'a': 1, 'b': 2}
    dict2 = {'b': 5}
    

    Primero ponlos todos en una lista:

    dicts = [dict1, dict2]
    

    Y luego usa dos bucles anidados para iterar sobre todos (key, value) pares:

    for dict_ in dicts:
        for value in dict_.items():
    

    En este caso, el value variable tomará la forma de una tupla de 2 elementos como ('a', 1) o ('b', 2).

Agrupamiento

Aquí cubriremos varias formas de extraer identificadores de grupo de sus datos.

En esta sección, estamos personalizando el group = ??? línea de la receta.

  • Agrupación por un elemento list / tuple / dict

    Si sus valores son listas o tuplas como (attr1, attr2, attr3, ...) y desea agruparlos por el enésimo elemento:

    group = value[n]
    

    La sintaxis es la misma para los dictados, por lo que si tiene valores como {'firstname': 'foo', 'lastname': 'bar'} y quieres agrupar por el primer nombre:

    group = value['firstname']
    
  • Agrupación por un atributo

    Si tus valores son objetos como datetime.date(2018, 5, 27) y quieres agruparlos por un atributo, como year:

    group = value.year
    
  • Agrupación por una función clave

    A veces tienes una función que devuelve un grupo de valores cuando se llama. Por ejemplo, podrías usar el len Función para agrupar valores por su longitud:

    group = len(value)
    
  • Agrupamiento por valores múltiples

    Si desea agrupar sus datos en más de un valor único, puede usar un tupla como el identificador de grupo. Por ejemplo, para agrupar cadenas por su primera letra. y su longitud:

    group = (value[0], len(value))
    
  • Agrupación por algo inestable.

    Porque las teclas dict deben ser hashable, te encontrarás con problemas si intentas agrupar por algo que no puede ser hash. En tal caso, debe encontrar una forma de convertir el valor indescompensable en una representación procesable.

    1. conjuntos: Convertir juegos a congelados, que son lavables:

      group = frozenset(group)
      
    2. dictados: Los dictados se pueden representar ordenados (key, value) tuplas:

      group = tuple(sorted(group.items()))
      

Modificando los valores agregados

A veces querrás modificar los valores que estás agrupando. Por ejemplo, si estás agrupando tuplas como (1, 'a') y (1, 'b') por el primer elemento, es posible que desee eliminar el primer elemento de cada tupla para obtener un resultado como {1: ['a', 'b']} más bien que {1: [(1, 'a'), (1, 'b')]}.

En esta sección, estamos personalizando el value = ??? línea de la receta.

  • Ningún cambio

    Si no desea cambiar el valor de ninguna manera, simplemente elimine value = ??? línea de su código.

  • Mantener un solo elemento lista / tupla / dict

    Si sus valores son listas como [1, 'a'] y solo quieres mantener el 'a':

    value = value[1]
    

    O si son dicts como {'firstname': 'foo', 'lastname': 'bar'} y solo quieres mantener el primer nombre:

    value = value['firstname']
    
  • Eliminando el primer elemento lista / tupla

    Si sus valores son listas como [1, 'a', 'foo'] y [1, 'b', 'bar'] y quieres descartar el primer elemento de cada tupla para obtener un grupo como [['a', 'foo], ['b', 'bar']], use la sintaxis de corte:

    value = value[1:]
    
  • Eliminar / Mantener elementos arbitrarios de la lista / tuple / dict

    Si sus valores son listas como ['foo', 'bar', 'baz'] o dictados como {'firstname': 'foo', 'middlename': 'bar', 'lastname': 'baz'} y desea eliminar o conservar solo algunos de estos elementos, comience creando un conjunto de elementos que desea conservar o borrar. Por ejemplo:

    indices_to_keep = {0, 2}
    keys_to_delete = {'firstname', 'middlename'}
    

    A continuación, elija el fragmento apropiado de esta lista:

    1. Para mantener los elementos de la lista:  value = [val for i, val in enumerate(value) if i in indices_to_keep]
    2. Para eliminar elementos de lista:  value = [val for i, val in enumerate(value) if i not in indices_to_delete]
    3. Para mantener los elementos dict:  value = {key: val for key, val in value.items() if key in keys_to_keep]
    4. Para eliminar los elementos dict:  value = {key: val for key, val in value.items() if key not in keys_to_delete]

Salida

Una vez que la agrupación está completa, tenemos un defaultdict lleno de listas Pero el resultado deseado no siempre es un dict (predeterminado).

En esta sección, estamos personalizando el result = groupdict línea de la receta.

  • Un dict regular

    Para convertir el defaultdict a un dict regular, simplemente llame al dict constructor en él:

    result = dict(groupdict)
    
  • Una lista de (group, value) pares

    Para obtener un resultado como [(group1, value1), (group1, value2), (group2, value3)] del dict {group1: [value1, value2], group2: [value3]}, usar una lista de comprensión:

    result = [(group, value) for group, values in groupdict.items()
                               for value in values]
    
  • Una lista anidada de valores justos

    Para obtener un resultado como [[value1, value2], [value3]] del dict {group1: [value1, value2], group2: [value3]}, utilizar dict.values:

    result = list(groupdict.values())
    
  • Una lista plana de valores justos

    Para obtener un resultado como [value1, value2, value3] del dict {group1: [value1, value2], group2: [value3]}, aplana el dict con un lista de comprensión:

    result = [value for values in groupdict.values() for value in values]
    
  • Aplanar valores iterables

    Si sus valores son listas u otros iterables como

    groupdict = {group1: [[list1_value1, list1_value2], [list2_value1]]}
    

    y quieres un resultado aplanado como

    result = {group1: [list1_value1, list1_value2, list2_value1]}
    

    tienes dos opciones:

    1. Acoplar las listas con un comprensión dict:

      result = {group: [x for iterable in values for x in iterable]
                                for group, values in groupdict.items()}
      
    2. Evite crear una lista de iterables en primer lugar, usando list.extend en lugar de list.append. En otras palabras, cambiar

      groupdict[group].append(value)
      

      a

      groupdict[group].extend(value)
      

      Y luego solo establece result = groupdict.

  • Una lista ordenada

    Los dicts son estructuras de datos desordenadas. Si itera sobre un dict, nunca se sabe en qué orden aparecerán sus elementos. Si no le importa el pedido, puede usar las recetas que se muestran arriba. Pero si tu hacer se preocupa por el orden, debe ordenar la salida en consecuencia.

    Usaré el siguiente dict para demostrar cómo ordenar tu salida de varias maneras:

    groupdict = {'abc': [1], 'xy': [2, 5]}
    

    Tenga en cuenta que se trata de una meta-receta que puede necesitar combinarse con otras partes de esta respuesta para obtener exactamente el resultado que desea. La idea general es ordenar las claves del diccionario antes de usarlas para extraer los valores del dict:

    groups = sorted(groupdict.keys())
    # groups = ['abc', 'xy']
    

    Manten eso en mente sorted acepta una función clave en caso de que quiera personalizar el orden de clasificación. Por ejemplo, si las claves dict son cadenas y desea ordenarlas por longitud:

    groups = sorted(groupdict.keys(), key=len)
    # groups = ['xy', 'abc']
    

    Una vez que haya ordenado las claves, utilícelas para extraer los valores del dict en el orden correcto:

    # groups = ['abc', 'xy']
    result = [groupdict[group] for group in groups]
    # result = [[1], [2, 5]]
    

    Recuerde que esto se puede combinar con otras partes de esta respuesta para obtener diferentes tipos de resultados. Por ejemplo, si desea mantener los identificadores de grupo:

    # groups = ['abc', 'xy']
    result = [(group, groupdict[group]) for group in groups]
    # result = [('abc', [1]), ('xy', [2, 5])]
    

    Para su comodidad, estos son algunos de los tipos de órdenes comúnmente utilizados:

    1. Ordenar por número de valores por grupo: 

       groups = sorted(groudict.keys(), key=lambda group: len(groupdict[group]))
       result = [groupdict[group] for group in groups]
       # result = [[2, 5], [1]]
      
  • Contando el número de valores en cada grupo

    Para contar la cantidad de elementos asociados con cada grupo, use la len función:

    result = {group: len(values) for group, values in groupdict.items()}
    

    Si quieres contar el número de distinto elementos, uso set para eliminar duplicados:

    result = {group: len(set(values)) for group, values in groupdict.items()}
    

Un ejemplo

Para demostrar cómo armar una solución de trabajo de esta receta, intentemos convertir una entrada de

data = [["A",0], ["B",1], ["C",0], ["D",2], ["E",2]]

dentro

result = [["A", "C"], ["B"], ["D", "E"]]

En otras palabras, estamos agrupando listas por su segundo elemento.

Las dos primeras líneas de la receta son siempre las mismas, así que comencemos copiándolas:

import collections

groupdict = collections.defaultdict(list)

Ahora tenemos que descubrir cómo hacer un bucle sobre la entrada. Dado que nuestra entrada es una lista simple de valores, una normal for loop será suficiente:

for value in data:

Luego tenemos que extraer el identificador de grupo del valor. Estamos agrupando por el segundo elemento de la lista, por lo que utilizamos la indexación:

    group = value[1]

El siguiente paso es transformar el valor. Como solo queremos mantener el primer elemento de cada lista, una vez más usamos indexación de lista:

    value = value[0]

Finalmente, tenemos que descubrir cómo convertir el dict que generamos en una lista. Lo que queremos es una lista de valores, sin los grupos. Nosotros consultamos el Salida sección de la receta para encontrar el fragmento de aplanamiento dict apropiado:

result = list(groupdict.values())

Et voilà:

data = [["A",0], ["B",1], ["C",0], ["D",2], ["E",2]]

import collections

groupdict = collections.defaultdict(list)
for value in data:
    group = value[1]
    value = value[0]
    groupdict[group].append(value)

result = list(groupdict.values())
# result: [["A", "C"], ["B"], ["D", "E"]]

7
2018-04-29 12:14



itertools.groupby

Hay una receta de propósito general en itertools y es groupby().

Un esquema de esta receta se puede dar en esta forma:

[(k, aggregate(g)) for k, g in groupby(sorted(data, key=extractKey), extractKey)]

Las dos partes relevantes para cambiar en la receta son:

  • definir la clave de agrupación (extractKey): en este caso obteniendo el primer elemento de la tupla:

    lambda x: x[0]

  • resultados agregados agrupados (si es necesario) (agregar) g contiene todas las tuplas correspondientes para cada clave k (p.ej. (1, 'a'), (1, 'b') para la llave 1y (2, 'x') para la llave 2), queremos tomar solo el segundo elemento de la tupla y concatenar todos los que están en una cadena:

    ''.join(x[1] for x in g)

Ejemplo:

from itertools import groupby

extractKey = lambda x: x[0]
aggregate = lambda g: ''.join(x[1] for x in g)

[(k, aggregate(g)) for k, g in groupby(sorted(data, key=extractKey), extractKey)]
# [(1, 'ab'), (2, 'x')]

A veces, extractKey, aggregate, o ambos pueden ser integrados en un solo trazo (también omitimos la clave de clasificación, ya que eso es redundante para esta ejemplo):

[(k, ''.join(x[1] for x in g)) for k, g in groupby(sorted(data), lambda x: x[0])]
# [(1, 'ab'), (2, 'x')]

Pros y contras

Comparando esta receta con la receta usando defaultdict hay pros y contras en ambos casos.

groupby() tiende a ser más lento (aproximadamente dos veces más lento en mis pruebas) que el defaultdict receta.

Por otra parte, groupby() tiene ventajas en el caso de memoria limitada donde los valores se producen sobre la marcha; puede procesar los grupos de forma continua, sin almacenarlos; defaultdict requerirá la memoria para almacenar todas de ellos.


2
2018-04-29 12:59



Pandas groupby

Esta no es una receta como tal, sino una forma intuitiva y flexible de agrupar datos utilizando una función. En este caso, la función es str.join.

import pandas as pd

data = [(1, 'a'), (2, 'x'), (1, 'b')]

# create dataframe from list of tuples
df = pd.DataFrame(data)

# group by first item and apply str.join
grp = df.groupby(0)[1].apply(''.join)

# create list of tuples from index and value
res = list(zip(grp.index, grp))

print(res)

[(1, 'ab'), (2, 'x')]

Ventajas

  • Se adapta muy bien a flujos de trabajo que solo requieren list salida al final de una secuencia de pasos vectorizables.
  • Fácilmente adaptable cambiando ''.join a list u otra función reductora.

Desventajas

  • Overkill para una tarea aislada: requiere list -> pd.DataFrame -> list conversión.
  • Presenta dependencia en una biblioteca de terceros.

0
2018-06-02 12:31



La comprensión de la lista de múltiples parse

Esto es ineficiente en comparación con el dict y groupby soluciones.

Sin embargo, para listas pequeñas donde el rendimiento no es una preocupación, puede realizar una lista de comprensión que analiza la lista para cada identificador único.

res = [(i, ''.join([j[1] for j in data if j[0] == i]))
       for i in set(list(zip(*data))[0])]

[(1, 'ab'), (2, 'x')]

La solución se puede dividir en 2 partes:

  1. set(list(zip(*data))[0]) extrae el conjunto único de identificadores que iteramos a través de un for lazo dentro de la lista de comprensión.
  2. (i, ''.join([j[1] for j in data if j[0] == i])) aplica la lógica que requerimos para la salida deseada.

-3
2018-04-30 09:23



Preguntas populares