¿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 me pueda ayudar?

La estructura de datos de acceso a usar para todos los tipos de agrupación es el dict . La idea es usar algo que identifique de forma única a un grupo como las claves del dict, 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 número entero que está utilizando para agrupar los valores se utiliza 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 agrupación 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 agrupación: recorrer sus datos y gradualmente rellenar un dict con valores agrupados. El uso de un defaultdict lugar de un dict regular hace que todo el proceso sea aún más fácil, ya que 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 Agrupación

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

 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 se puede / debe personalizar según su caso de uso.

Entrada

El formato de sus datos de entrada determina cómo iterar sobre ellos.

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

  • 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 bucle for :

     for value in data: 
  • Listas multiples

    Si tiene varias listas, cada una de ellas contiene el valor de un atributo diferente, como

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

    usa la función zip para iterar sobre todas las listas simultáneamente:

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

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

  • Dictados múltiples o una lista de dictados.

    Si quieres combinar varios dicts como

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

    Primero ponlos a todos en una lista:

     dicts = [dict1, dict2] 

    Y luego use dos bucles nesteds para iterar sobre todos (key, value) pares (key, value) :

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

    En este caso, la variable de value 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 elemento nth:

     group = value[n] 

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

     group = value['firstname'] 
  • Agrupando por un atributo

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

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

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

     group = len(value) 
  • Agrupación por múltiples valores.

    Si desea agrupar sus datos por más de un solo valor, puede utilizar una tupla como identificador de grupo. Por ejemplo, para agrupar cadenas por su primera letra y su longitud:

     group = (value[0], len(value)) 
  • Agrupando por algo inestable.

    Debido a que las claves de dict deben ser hashables , se encontrará con problemas si intenta agruparse por algo que no se puede hash. En tal caso, debe encontrar una manera de convertir el valor que no se puede lavar en una representación de hashable.

    1. sets : Convierte sets a frozensets , que son hashable:

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

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

Modificar los valores agregados.

A veces querrás modificar los valores que estás agrupando. Por ejemplo, si está 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']} lugar de {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 el value = ??? línea de su código.

  • Mantener solo un elemento de lista / tupla / dict

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

     value = value[1] 

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

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

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

     value = value[1:] 
  • Eliminar / mantener elementos de lista / tupla / dict arbitraria

    Si tus valores son listas como ['foo', 'bar', 'baz'] o frases como {'firstname': 'foo', 'middlename': 'bar', 'lastname': 'baz'} y quieres elimine o mantenga solo algunos de estos elementos, comience por crear un conjunto de elementos que desee conservar o eliminar. Por ejemplo:

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

    Luego 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 la 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 se completa la agrupación, tenemos un defaultdict lleno de listas. Pero el resultado deseado no siempre es un dict (predeterminado).

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

  • Un dict regular

    Para convertir el valor predeterminado en un dict regular, simplemente llame al constructor de dict 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]} , use 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]} , use 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]} , {group1: [value1, value2], group2: [value3]} el dict con una lista de comprensión :

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

    Si tus 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. Aplanar las listas con un dictado de dictado :

       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, utilizando list.extend lugar de list.append . En otras palabras, cambiar

       groupdict[group].append(value) 

      a

       groupdict[group].extend(value) 

      Y luego simplemente establece el result = groupdict .

  • Una lista ordenada

    Los dictados son estructuras de datos desordenadas. Si recorres un dictado, nunca sabes en qué orden se enumerarán sus elementos. Si no te importa el pedido, puedes usar las recetas que se muestran arriba. Pero si le importa el orden, debe ordenar la salida en consecuencia.

    Usaré el siguiente dictado para demostrar cómo ordenar su salida de varias maneras:

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

    Tenga en cuenta que esto es un poco 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'] 

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

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

    Una vez que haya ordenado las claves, úselas 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 conveniencia, aquí hay algunos tipos de orden de uso común:

    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 el número de elementos asociados con cada grupo, use la función len :

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

    Si desea contar el número de elementos distintos , use 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 a partir de esta receta, tratemos de activar una entrada de

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

dentro

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

En otras palabras, agrupamos las listas por su segundo elemento.

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

 import collections groupdict = collections.defaultdict(list) 

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

 for value in data: 

A continuación 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. Ya que solo queremos mantener el primer elemento de cada lista, una vez más usamos la indexación de listas:

  value = value[0] 

Finalmente, tenemos que descubrir cómo convertir el dictado que generamos en una lista. Lo que queremos es una lista de valores, sin los grupos. Consultamos la sección Salida de la receta para encontrar el fragmento de aplanamiento de 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"]] 

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 a cambiar en la receta son:

  • defina la clave de agrupación ( extractKey ): en este caso, obtenga el primer elemento de la tupla:

    lambda x: x[0]

  • resultados agrupados agregados (si es necesario) ( agregado ): g contiene todas las tuplas coincidentes para cada clave k (por ejemplo, (1, 'a') , (1, 'b') para la clave 1 y (2, 'x') para la clave 2 ), queremos tomar solo el segundo elemento de la tupla y concatenar todos esos 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 , extractKey , o ambos pueden ser incorporados en una sola línea (también omitimos la clave de clasificación, ya que es redundante para este 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

Al comparar esta receta con la receta que usa defaultdict existen ventajas y desventajas en ambos casos.

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

Por otro lado, 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 todos ellos.

Grupo de pandas

Esta no es una receta como tal, sino una forma intuitiva y flexible de agrupar datos usando 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 bien a los flujos de trabajo que solo requieren resultados de list al final de una secuencia de pasos vectorizables.
  • Fácilmente adaptable cambiando ''.join a la list u otra función de reducción.

Desventajas

  • Overkill para una tarea aislada: requiere list -> pd.DataFrame -> list conversion.
  • Introduce la dependencia en una biblioteca de terceros.

Comprensión de lista de análisis múltiple

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

Sin embargo, para listas pequeñas donde el rendimiento no es un problema , puede realizar una comprensión de la lista 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 bucle for 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 necesitamos para la salida deseada.