Lista de resultados de Groupby en grupos vacíos.

Estaba jugando para sentir mejor el grupo de groupby , así que groupby una lista de tuplas por el número y traté de obtener una lista de los grupos resultantes. Sin embargo, cuando convierto el resultado de groupby a una lista, obtengo un resultado extraño: todos, excepto el último grupo, están vacíos. ¿Porqué es eso? Supuse que convertir un iterador en una lista sería menos eficiente pero nunca cambiaría el comportamiento. Supongo que las listas están vacías porque los iteradores internos están atravesados, pero ¿cuándo y dónde ocurre eso?

 import itertools l=list(zip([1,2,2,3,3,3],['a','b','c','d','e','f'])) #[(1, 'a'), (2, 'b'), (2, 'c'), (3, 'd'), (3, 'e'), (3, 'f')] grouped_l = list(itertools.groupby(l, key=lambda x:x[0])) #[(1, ), (2, ), (3, )] [list(x[1]) for x in grouped_l] [[], [], [(3, 'f')]] grouped_i = itertools.groupby(l, key=lambda x:x[0]) # [list(x[1]) for x in grouped_i] [[(1, 'a')], [(2, 'b'), (2, 'c')], [(3, 'd'), (3, 'e'), (3, 'f')]] 

De la documentación de itertools.groupby() :

El grupo devuelto es en sí mismo un iterador que comparte el iterable subyacente con groupby() . Debido a que la fuente es compartida, cuando el objeto groupby() avanza, el grupo anterior ya no es visible.

Al groupby() la salida de groupby() en una lista, se avanza el objeto groupby() .


Por lo tanto, no debe realizar la conversión itertools.groupby objeto itertools.groupby en la lista. Si desea almacenar los valores como list , entonces debería estar haciendo algo como esta comprensión de lista para crear una copia del objeto groupby :

 grouped_l = [(a, list(b)) for a, b in itertools.groupby(l, key=lambda x:x[0])] 

Esto le permitirá iterar su lista (transformada de groupby objeto groupby ) varias veces. Sin embargo, si está interesado en iterar el resultado solo una vez, entonces la segunda solución que mencionó en la pregunta será suficiente para su requerimiento.

groupby es super perezoso. Aquí hay una demo iluminadora. Agrupemos tres valores-a y cuatro valores- b , e imprimamos lo que está sucediendo:

 >>> from itertools import groupby >>> def letters(): for letter in 'a', 'a', 'a', 'b', 'b', 'b', 'b': print('yielding', letter) yield letter 

Pasando por los grupos SIN mirar a sus miembros.

Vamos a rodar:

 >>> groups = groupby(letters()) >>> 

¡Nada se ha impreso todavía! Así que hasta ahora, groupby no hizo nada . Qué vago perezoso. Pidámoslo por el primer grupo:

 >>> next(groups) yielding a ('a', ) 

Entonces groupby nos dice que este es un grupo de valores-a, y podríamos pasar por ese objeto _grouper para obtenerlos todos. Pero espere, ¿por qué “ceder” se imprimió solo una vez? Nuestro generador está rindiendo tres de ellos, ¿no es así? Bueno, eso es porque groupby es perezoso. Leyó un valor para identificar al grupo, porque necesita decirnos de qué se trata el grupo, es decir, que es un grupo de valores-a. Y nos ofrece ese objeto _grouper para que obtengamos todos los miembros del grupo si queremos . Pero no pedimos a los miembros, así que el vago no fue más lejos. Simplemente no tenía una razón para hacerlo. Preguntemos por el siguiente grupo:

 >>> next(groups) yielding a yielding a yielding b ('b', ) 

¿Esperar lo? ¿Por qué “ceder un” cuando ahora estamos tratando con el segundo grupo, el grupo de valores b ? Bueno, porque groupby se detuvo antes del primero porque fue suficiente para darnos todo lo que habíamos pedido. Pero ahora, para contarnos sobre el segundo grupo, tiene que encontrar el segundo grupo, y para esto pregunta a nuestro generador hasta que vea algo más que a . Tenga en cuenta que “rindiendo b” nuevamente se imprime solo una vez , a pesar de que nuestro generador produce cuatro de ellos. Preguntemos por el tercer grupo:

 >>> next(groups) yielding b yielding b yielding b Traceback (most recent call last): File "", line 1, in  next(groups) StopIteration 

Ok, no hay un tercer grupo y, por lo tanto, groupby emite una StopIteration para que el consumidor (por ejemplo, un bucle o una lista de comprensión) sepa que debe detenerse. Pero antes de eso, el “b rendimiento” restante se imprime, porque groupby se quitó su trasero perezoso y caminó sobre los valores restantes con la esperanza de encontrar un nuevo grupo.

Pasando por los grupos SIN mirar a sus miembros.

Intentemos nuevamente, esta vez preguntemos por los miembros:

 >>> groups = groupby(letters()) >>> key, members = next(groups) yielding a >>> key 'a' 

Una vez más, groupby le pidió a nuestro generador un solo valor para identificar al grupo y así poder decirnos que es a grupo. Pero esta vez, también pediremos a los miembros del grupo:

 >>> list(members) yielding a yielding a yielding b ['a', 'a', 'a'] 

Jajaja Quedan los restantes “rindiendo a”. Además, ya es el primer “rendimiento b”! ¡A pesar de que ni siquiera pedimos el segundo grupo todavía! Pero, por supuesto, groupby tiene que llegar tan lejos porque pedimos a los miembros del grupo, por lo que tiene que seguir buscando hasta que obtenga un no miembro. Vayamos al siguiente grupo:

 >>> key, members = next(groups) >>> 

¿Esperar lo? ¿Nada se imprimió en absoluto? ¿ groupby está durmiendo? ¡Despierta! Oh, espera … eso es correcto … ya descubrió que el siguiente grupo es valores- b . Preguntemos por todos ellos:

 >>> list(members) yielding b yielding b yielding b ['b', 'b', 'b', 'b'] 

Ahora suceden los tres “b rendimientos” restantes , porque los solicitamos, por lo que groupby tiene que obtenerlos.

¿Por qué no funciona obtener los miembros del grupo después?

list(groupby(...)) forma inicial con la list(groupby(...)) :

 >>> groups = list(groupby(letters())) yielding a yielding a yielding a yielding b yielding b yielding b yielding b >>> [list(members) for key, members in groups] [[], ['b']] 

Tenga en cuenta que no solo el primer grupo está vacío, sino que el segundo grupo solo tiene un elemento (no lo mencionó).

¿Por qué?

Una vez más: groupby es super perezoso. Le ofrece esos objetos _grouper para que pueda ir a través de los miembros de cada grupo. Pero si no pides ver a los miembros del grupo y en su lugar solo pides que se identifique al siguiente grupo, groupby encoge de hombros y groupby : “Ok, tú eres el jefe, solo voy a buscar el siguiente grupo” .

Lo que hace su list(groupby(...)) es groupby a groupby que identifique todos los grupos. Así lo hace. Pero si al final pregunta por los miembros de cada grupo, groupby es como “Amigo … Lo siento, se los ofrecí pero no los quería. Y soy perezoso, así que No mantengas las cosas sin una buena razón . Puedo darte el último miembro del último grupo, porque todavía recuerdo eso, pero antes de eso … lo siento, ya no las tengo, tú Debería haberme dicho que los querías “.

PD: En todo esto, por supuesto, “perezoso” realmente significa “eficiente”. ¡No algo malo sino algo bueno!

Resumen: la razón es que los itertools generalmente no almacenan datos. Sólo consumen un iterador. Así que cuando el iterador externo avanza, el iterador interno también debe hacerlo.

Analogía: imagina que eres un asistente de vuelo parado en la puerta, admitiendo una sola línea de pasajeros en un avión. Los pasajeros están organizados por grupo de embarque, pero solo puede verlos y admitirlos uno por uno. Periódicamente, a medida que la gente ingrese, aprenderá cuándo un grupo de abordaje finalizó y luego comenzó el siguiente.

Para avanzar al siguiente grupo, tendrá que admitir a todos los pasajeros restantes en el grupo actual. No puede ver lo que hay en la línea descendente sin dejar pasar a todos los pasajeros actuales.

Comparación de Unix: El diseño de groupby () es algorítmicamente similar a la utilidad Unix de Unix.

Lo que dicen los documentos: “El grupo devuelto es en sí mismo un iterador que comparte el iterable subyacente con groupby (). Debido a que la fuente se comparte, cuando el objeto groupby () avanza, el grupo anterior ya no es visible”.

Cómo usarlo: Si los datos se necesitan más tarde, se deben almacenar como una lista:

 groups = [] uniquekeys = [] data = sorted(data, key=keyfunc) for k, g in groupby(data, keyfunc): groups.append(list(g)) # Store group iterator as a list uniquekeys.append(k)