Como uso o itertools.groupby ()?

507

Não consegui encontrar uma explicação compreensível de como realmente usar a itertools.groupby()função do Python . O que estou tentando fazer é o seguinte:

  • Faça uma lista - neste caso, os filhos de um objeto lxml elemento
  • Divida-o em grupos com base em alguns critérios
  • Depois, repita cada um desses grupos separadamente.

Revi a documentação e os exemplos , mas tive problemas para tentar aplicá-los além de uma simples lista de números.

Então, como eu uso itertools.groupby()? Existe outra técnica que eu deveria estar usando? Indicadores para uma boa leitura de "pré-requisito" também serão apreciados.

James Sulak
fonte
um caso útil para o seria leetcode.com/problems/string-compression
ShawnLee

Respostas:

656

NOTA IMPORTANTE: Você deve classificar seus dados primeiro.


A parte que eu não entendi é que na construção de exemplo

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

ké a chave de agrupamento atual e gé um iterador que você pode usar para iterar sobre o grupo definido por essa chave de agrupamento. Em outras palavras, ogroupby próprio iterador retorna iteradores.

Aqui está um exemplo disso, usando nomes de variáveis ​​mais claros:

from itertools import groupby

things = [("animal", "bear"), ("animal", "duck"), ("plant", "cactus"), ("vehicle", "speed boat"), ("vehicle", "school bus")]

for key, group in groupby(things, lambda x: x[0]):
    for thing in group:
        print "A %s is a %s." % (thing[1], key)
    print " "

Isso fornecerá a saída:

Um urso é um animal.
Um pato é um animal.

Um cacto é uma planta.

Uma lancha é um veículo.
Um ônibus escolar é um veículo.

Neste exemplo, thingshá uma lista de tuplas em que o primeiro item em cada tupla é o grupo ao qual o segundo item pertence.

A groupby()função usa dois argumentos: (1) os dados para agrupar e (2) a função para agrupá-los.

Aqui, lambda x: x[0]diz groupby()para usar o primeiro item em cada tupla como a chave de agrupamento.

Na forinstrução acima , groupbyretorna três pares (chave, iterador de grupo) - uma vez para cada chave exclusiva. Você pode usar o iterador retornado para iterar sobre cada item individual nesse grupo.

Aqui está um exemplo um pouco diferente com os mesmos dados, usando uma compreensão de lista:

for key, group in groupby(things, lambda x: x[0]):
    listOfThings = " and ".join([thing[1] for thing in group])
    print key + "s:  " + listOfThings + "."

Isso fornecerá a saída:

animais: urso e pato.
plantas: cacto.
veículos: lancha e ônibus escolar.

James Sulak
fonte
1
Existe uma maneira de especificar os grupos antecipadamente e não exigir classificação?
John Salvatier
2
o itertools geralmente clica em mim, mas eu também tinha um 'bloqueio' para este. Apreciei seus exemplos - muito mais claros que os documentos. Eu acho que as ferramentas tendem a clicar ou não, e são muito mais fáceis de entender se você tiver problemas semelhantes. Ainda não precisava deste na natureza.
Profano
3
Os documentos do Python do Julian parecem ótimos para a maioria das coisas, mas quando se trata de iteradores, geradores e cherrypy, os documentos geralmente me intrigam. Os documentos do Django são duplamente desconcertantes.
Marc Maxmeister
6
+1 na classificação - não entendi o que você quis dizer até agrupar meus dados.
Cody
4
@DavidCrook muito tarde para a festa, mas pode ajudar alguém. Provavelmente, porque sua matriz não está classificada, tente groupby(sorted(my_collection, key=lambda x: x[0]), lambda x: x[0]))com a suposição de que my_collection = [("animal", "bear"), ("plant", "cactus"), ("animal", "duck")]você deseja agrupar eanimal or plant
Robin Nemeth
72

O exemplo nos documentos do Python é bastante direto:

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

Portanto, no seu caso, dados são uma lista de nós, keyfuncé para onde a lógica da sua função de critérios vai e depois groupby()agrupa os dados.

Você deve ter o cuidado de classificar os dados pelos critérios antes de ligar groupbyou eles não funcionarão. groupbyNa verdade, o método itera através de uma lista e, sempre que a chave muda, ele cria um novo grupo.

Seb
fonte
46
Então você leu keyfunce ficou tipo "sim, eu sei exatamente o que é isso porque essa documentação é bastante direta". Incrível!
Jarad 07/04
5
Acredito que a maioria das pessoas já conhece esse exemplo "simples", mas inútil, pois não diz que tipo de 'dados' e 'teclas funcionais' usar! Mas acho que você também não sabe, caso contrário, ajudaria as pessoas a esclarecer e não apenas copiar e colar. Ou você?
Apostolos
69

itertools.groupby é uma ferramenta para agrupar itens.

A partir dos documentos , analisamos ainda mais o que isso pode fazer:

# [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B

# [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D

groupby objetos produzem pares de grupos de chaves em que o grupo é um gerador.

Recursos

  • A. Agrupe itens consecutivos
  • B. Agrupe todas as ocorrências de um item, considerando uma iterável classificada
  • C. Especifique como agrupar itens com uma função de tecla *

Comparações

# Define a printer for comparing outputs
>>> def print_groupby(iterable, keyfunc=None):
...    for k, g in it.groupby(iterable, keyfunc):
...        print("key: '{}'--> group: {}".format(k, list(g)))

# Feature A: group consecutive occurrences
>>> print_groupby("BCAACACAADBBB")
key: 'B'--> group: ['B']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A', 'A']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A', 'A']
key: 'D'--> group: ['D']
key: 'B'--> group: ['B', 'B', 'B']

# Feature B: group all occurrences
>>> print_groupby(sorted("BCAACACAADBBB"))
key: 'A'--> group: ['A', 'A', 'A', 'A', 'A']
key: 'B'--> group: ['B', 'B', 'B', 'B']
key: 'C'--> group: ['C', 'C', 'C']
key: 'D'--> group: ['D']

# Feature C: group by a key function
>>> # keyfunc = lambda s: s.islower()                      # equivalent
>>> def keyfunc(s):
...     """Return a True if a string is lowercase, else False."""   
...     return s.islower()
>>> print_groupby(sorted("bCAaCacAADBbB"), keyfunc)
key: 'False'--> group: ['A', 'A', 'A', 'B', 'B', 'C', 'C', 'D']
key: 'True'--> group: ['a', 'a', 'b', 'b', 'c']

Usos

Nota: Vários dos exemplos anteriores derivam do PyCon de Víctor Terrón () (espanhol) , "Kung Fu ao amanhecer com ferramentas". Veja também o groupbycódigo fonte escrito em C.

* Uma função na qual todos os itens são passados ​​e comparados, influenciando o resultado. Outros objetos com funções principais incluem sorted(), max()e min().


Resposta

# OP: Yes, you can use `groupby`, e.g. 
[do_something(list(g)) for _, g in groupby(lxml_elements, criteria_func)]
pylang
fonte
1
Tecnicamente, os documentos provavelmente devem dizer [''.join(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D.
Mateen Ulhaq
1
Sim. A maioria dos documentos de iteração é "abreviada" dessa maneira. Como todos os itertools são iteradores, eles devem ser convertidos em um builtin ( list(), tuple()) ou consumidos em um loop / compreensão para exibir o conteúdo. São redundâncias que o autor provavelmente excluiu para economizar espaço.
Pylang # 25/18
39

Um truque neato com groupby é executar a codificação de comprimento em uma linha:

[(c,len(list(cgen))) for c,cgen in groupby(some_string)]

fornecerá uma lista de duas tuplas em que o primeiro elemento é o caractere e o segundo é o número de repetições.

Edit: Observe que é isso que separa itertools.groupbya GROUP BYsemântica do SQL : o itertools não (e geralmente não pode) classificar o iterador antecipadamente, para que grupos com a mesma "chave" não sejam mesclados.

nimish
fonte
27

Outro exemplo:

for key, igroup in itertools.groupby(xrange(12), lambda x: x // 5):
    print key, list(igroup)

resulta em

0 [0, 1, 2, 3, 4]
1 [5, 6, 7, 8, 9]
2 [10, 11]

Observe que o igroup é um iterador (um sub-iterador como a documentação chama).

Isso é útil para dividir um gerador:

def chunker(items, chunk_size):
    '''Group items in chunks of chunk_size'''
    for _key, group in itertools.groupby(enumerate(items), lambda x: x[0] // chunk_size):
        yield (g[1] for g in group)

with open('file.txt') as fobj:
    for chunk in chunker(fobj):
        process(chunk)

Outro exemplo de groupby - quando as chaves não estão classificadas. No exemplo a seguir, os itens em xx são agrupados por valores em yy. Nesse caso, um conjunto de zeros é emitido primeiro, seguido por um conjunto de unidades, seguido novamente por um conjunto de zeros.

xx = range(10)
yy = [0, 0, 0, 1, 1, 1, 0, 0, 0, 0]
for group in itertools.groupby(iter(xx), lambda x: yy[x]):
    print group[0], list(group[1])

Produz:

0 [0, 1, 2]
1 [3, 4, 5]
0 [6, 7, 8, 9]
user650654
fonte
Isso é interessante, mas as ferramentas não seriam melhores. Por que escolher um iterável? Ele retorna um objeto que itera como um gerador, mas usa o código C.
trojjer
@trojjer islice seria melhor se os grupos fossem de tamanho consistente.
woodm1979
Eu quero obter: [0, 1, 2], [1, 2, 3], [2, 3, 4] ...
GilbertS
21

ATENÇÃO:

A lista de sintaxe (groupby (...)) não funcionará da maneira que você pretende. Parece destruir os objetos do iterador interno, portanto, usando

for x in list(groupby(range(10))):
    print(list(x[1]))

vai produzir:

[]
[]
[]
[]
[]
[]
[]
[]
[]
[9]

Em vez de list (groupby (...)), tente [(k, list (g)) para k, g em groupby (...)] ou, se você usa essa sintaxe com frequência,

def groupbylist(*args, **kwargs):
    return [(k, list(g)) for k, g in groupby(*args, **kwargs)]

e tenha acesso à funcionalidade groupby, evitando os iteradores incômodos (para pequenos dados) todos juntos.

RussellStewart
fonte
3
Muitas das respostas se referem ao obstáculo que você deve classificar antes do grupo para obter os resultados esperados. Acabei de encontrar esta resposta, que explica o comportamento estranho que não tinha visto antes. Eu não tinha visto antes, porque só agora eu estava tentando listar (groupby (range (10)) como o @singular diz. Antes disso, eu sempre usava a abordagem "recomendada" de iterar "manualmente" através dos objetos groupby, em vez de deixando o construtor list () "automaticamente" fazê-lo.
The Red Pea
9

Eu gostaria de dar outro exemplo em que groupby sem classificação não está funcionando. Adaptado do exemplo por James Sulak

from itertools import groupby

things = [("vehicle", "bear"), ("animal", "duck"), ("animal", "cactus"), ("vehicle", "speed boat"), ("vehicle", "school bus")]

for key, group in groupby(things, lambda x: x[0]):
    for thing in group:
        print "A %s is a %s." % (thing[1], key)
    print " "

saída é

A bear is a vehicle.

A duck is a animal.
A cactus is a animal.

A speed boat is a vehicle.
A school bus is a vehicle.

existem dois grupos com veículo, enquanto um poderia esperar apenas um grupo

Kiriloff
fonte
5
Você deve classificar os dados primeiro, usando como tecla a função pela qual você está agrupando. Isso é mencionado nos dois posts acima, mas não é destacado.
mbatchkarov
Eu estava fazendo uma compreensão de dict para preservar os sub-iteradores por chave, até perceber que isso era tão simples quanto dict (groupby (iterator, key)). Doce.
Trojjer
Pensando bem e após a experimentação, a chamada de ditado envolvida pelo grupo esgotará os sub-iteradores do grupo. Droga.
trojjer
Qual é o objetivo desta resposta? Como está se baseando na resposta original ?
codeforester
7

@CaptSolo, eu tentei o seu exemplo, mas não funcionou.

from itertools import groupby 
[(c,len(list(cs))) for c,cs in groupby('Pedro Manoel')]

Resultado:

[('P', 1), ('e', 1), ('d', 1), ('r', 1), ('o', 1), (' ', 1), ('M', 1), ('a', 1), ('n', 1), ('o', 1), ('e', 1), ('l', 1)]

Como você pode ver, existem dois o's e dois e's, mas eles entraram em grupos separados. Foi quando percebi que você precisava classificar a lista passada para a função groupby. Portanto, o uso correto seria:

name = list('Pedro Manoel')
name.sort()
[(c,len(list(cs))) for c,cs in groupby(name)]

Resultado:

[(' ', 1), ('M', 1), ('P', 1), ('a', 1), ('d', 1), ('e', 2), ('l', 1), ('n', 1), ('o', 2), ('r', 1)]

Apenas lembrando, se a lista não estiver classificada, a função groupby não funcionará !

pedromanoel
fonte
7
Na verdade funciona. Você pode considerar esse comportamento quebrado, mas é útil em alguns casos. Veja as respostas a esta pergunta para um exemplo: stackoverflow.com/questions/1553275/…
Denis Otkidach
6

Classificação e agrupamento

from itertools import groupby

val = [{'name': 'satyajit', 'address': 'btm', 'pin': 560076}, 
       {'name': 'Mukul', 'address': 'Silk board', 'pin': 560078},
       {'name': 'Preetam', 'address': 'btm', 'pin': 560076}]


for pin, list_data in groupby(sorted(val, key=lambda k: k['pin']),lambda x: x['pin']):
...     print pin
...     for rec in list_data:
...             print rec
... 
o/p:

560076
{'name': 'satyajit', 'pin': 560076, 'address': 'btm'}
{'name': 'Preetam', 'pin': 560076, 'address': 'btm'}
560078
{'name': 'Mukul', 'pin': 560078, 'address': 'Silk board'}
Satyajit Das
fonte
5

Como uso o itertools.groupby () do Python?

Você pode usar groupby para agrupar coisas para repetir. Você atribui ao grupo uma iterável e uma função- chave opcional / que pode ser chamada pela qual verificar os itens à medida que eles saem do iterável, e ele retorna um iterador que fornece duas tuplas do resultado da chave que pode ser chamada e dos itens reais em outro iterável. Da ajuda:

groupby(iterable[, keyfunc]) -> create an iterator which returns
(key, sub-iterator) grouped by each value of key(value).

Aqui está um exemplo de groupby usando uma corotina para agrupar por uma contagem, ele usa uma chave que pode ser chamada (neste caso coroutine.send) para cuspir a contagem para quantas iterações e um sub-iterador agrupado de elementos:

import itertools


def grouper(iterable, n):
    def coroutine(n):
        yield # queue up coroutine
        for i in itertools.count():
            for j in range(n):
                yield i
    groups = coroutine(n)
    next(groups) # queue up coroutine

    for c, objs in itertools.groupby(iterable, groups.send):
        yield c, list(objs)
    # or instead of materializing a list of objs, just:
    # return itertools.groupby(iterable, groups.send)

list(grouper(range(10), 3))

impressões

[(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])]
Aaron Hall
fonte
1

Um exemplo útil que me deparei pode ser útil:

from itertools import groupby

#user input

myinput = input()

#creating empty list to store output

myoutput = []

for k,g in groupby(myinput):

    myoutput.append((len(list(g)),int(k)))

print(*myoutput)

Entrada de amostra: 14445221

Resultado da amostra: (1,1) (3,4) (1,5) (2,2) (1,1)

Arko
fonte
1

Essa implementação básica me ajudou a entender essa função. Espero que ajude outras pessoas também:

arr = [(1, "A"), (1, "B"), (1, "C"), (2, "D"), (2, "E"), (3, "F")]

for k,g in groupby(arr, lambda x: x[0]):
    print("--", k, "--")
    for tup in g:
        print(tup[1])  # tup[0] == k
-- 1 --
A
B
C
-- 2 --
D
E
-- 3 --
F
Tiago
fonte
0

Você pode escrever a própria função groupby:

           def groupby(data):
                kv = {}
                for k,v in data:
                    if k not in kv:
                         kv[k]=[v]
                    else:
                        kv[k].append(v)
           return kv

     Run on ipython:
       In [10]: data = [('a', 1), ('b',2),('a',2)]

        In [11]: groupby(data)
        Out[11]: {'a': [1, 2], 'b': [2]}
Céu
fonte
1
reinventar roda não é uma ótima idéia, também questão é explicar itertools groupby, não escrever própria
user2678074
1
@ user2678074 Você está certo. É algo que você deseja escrever para um ponto de vista de aprendizado.
Sky
2
Também usar melhor a defaultdict (lista) por isso é ainda mais curto
Mickey Perlstein
@MickeyPerlstein e mais rápido.
funnydman 26/02