Grupuj w Pythonie według

125

Załóżmy, że mam zestaw par danych, w którym indeks 0 to wartość, a indeks 1 to typ:

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

Chcę pogrupować je według ich typu (według pierwszego zindeksowanego ciągu) jako takie:

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

Jak mogę to osiągnąć w efektywny sposób?

Hellnar
źródło

Odpowiedzi:

154

Zrób to w 2 krokach. Najpierw utwórz słownik.

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

Następnie przekonwertuj ten słownik na oczekiwany format.

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

Jest to również możliwe z itertools.groupby, ale wymaga to najpierw posortowania danych wejściowych.

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

Należy pamiętać, że oba te elementy nie są zgodne z oryginalną kolejnością kluczy. Jeśli chcesz zachować zamówienie, potrzebujesz OrderedDict.

>>> 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
źródło
Jak można to zrobić, jeśli krotka wejściowa ma jeden klucz i dwie lub więcej wartości, na przykład: [('11013331', 'red', 'KAT'), ('9085267', 'blue' 'KAT')]gdzie ostatni element krotki to klucz, a pierwsze dwie jako wartość. Wynik powinien wyglądać następująco: wynik = [{typ: 'KAT', elementy: [('11013331', czerwony), ('9085267', niebieski)]}]
user1144616
1
from operator import itemgetter
Baumann
1
krok 1 można wykonać bez importu:d= {}; for k,v in input: d.setdefault(k, []).append(v)
ecoe
Pracuję nad programem MapReduce w Pythonie, zastanawiam się tylko, czy istnieje sposób na grupowanie według wartości na liście bez zajmowania się słownikami lub zewnętrzną biblioteką, taką jak pandy? Jeśli nie, to w jaki sposób mogę pozbyć się elementów i wpisać wynik?
Kourosh
54

Wbudowany itertoolsmoduł Pythona w rzeczywistości ma groupbyfunkcję, ale w tym celu elementy do zgrupowania muszą najpierw zostać posortowane w taki sposób, aby elementy do zgrupowania były ciągłe na liście:

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)

Teraz dane wejściowe wyglądają następująco:

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

groupbyzwraca sekwencję 2-krotek postaci (key, values_iterator). Chcemy przekształcić to w listę dykt, w których „typ” jest kluczem, a „pozycje” jest listą zerowych elementów krotek zwracanych przez wartość_iterator. Lubię to:

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

Teraz resultzawiera żądany dykt, zgodnie z pytaniem.

Możesz jednak rozważyć po prostu zrobienie z tego pojedynczego dyktu, kluczowanego według typu i każdej wartości zawierającej listę wartości. W obecnym formularzu, aby znaleźć wartości dla określonego typu, będziesz musiał iterować po liście, aby znaleźć dict zawierający pasujący klucz „type”, a następnie pobrać z niego element „items”. Jeśli używasz pojedynczego dyktowania zamiast listy jednopozycyjnych dykt, możesz znaleźć pozycje dla określonego typu za pomocą pojedynczego klucza wyszukiwania w głównym dyktowaniu. Używając groupby, wyglądałoby to następująco:

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

resultzawiera teraz ten dykt (jest podobny do pośredniego resdomyślnego słowa w odpowiedzi @ KennyTM):

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

(Jeśli chcesz zredukować to do jednej linijki, możesz:

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

lub korzystając z nowomodnego formularza ze zrozumieniem:

result = {key:list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn)}
PaulMcG
źródło
Pracuję nad programem MapReduce w Pythonie, zastanawiam się tylko, czy istnieje sposób na grupowanie według wartości na liście bez zajmowania się słownikami lub zewnętrzną biblioteką, taką jak pandy? Jeśli nie, to w jaki sposób mogę pozbyć się elementów i wpisać wynik?
Kourosh
@Kourosh - opublikuj jako nowe pytanie, ale pamiętaj, aby wskazać, co masz na myśli, mówiąc „pozbądź się elementów i wpisz mój wynik” oraz „bez korzystania ze słowników”.
PaulMcG
7

Podobało mi się też proste grupowanie pand . jest potężny, prosty i najbardziej odpowiedni dla dużych zbiorów danych

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

akiva
źródło
3

Ta odpowiedź jest podobna do odpowiedzi @ PaulMcG, ale nie wymaga sortowania danych wejściowych.

Dla tych, którzy zajmują się programowaniem funkcjonalnym, groupBymożna je zapisać w jednej linii (bez importu!) Iw przeciwieństwie do itertools.groupbytego nie wymaga sortowania danych wejściowych:

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

(Powodem ... or grpw lambdato, że przez to reduce()do pracy, lambdapotrzeby, aby powrócić swój pierwszy argument, ponieważ list.append()zawsze zwraca zawsze powrócić . Ie to hack obejść ograniczenie Pythona że lambda może jedynie ocenić jeden wyraz).Noneorgrp

Zwraca to dict, którego klucze zostały znalezione przez ocenę danej funkcji i którego wartości są listą oryginalnych elementów w pierwotnej kolejności. Na przykład OP, wywołanie this as groupBy(lambda pair: pair[1], input)zwróci ten dykt:

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

I jak na użytkownika @ PaulMcG odpowiedź wymaganym formacie PO można znaleźć poprzez owinięcie że na liście zrozumieniem. Więc to zrobi:

result = {key: [pair[0] for pair in values],
          for key, values in groupBy(lambda pair: pair[1], input).items()}
ronen
źródło
Dużo mniej kodu, ale zrozumiałe. Również dobre, ponieważ nie wymyśla koła na nowo.
devdanke
2

Następująca funkcja szybko ( bez sortowania ) grupuje krotki o dowolnej długości według klucza mającego dowolny indeks:

# 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

W przypadku twojego pytania indeks klucza, według którego chcesz pogrupować, to 1, dlatego:

group_by(input,1)

daje

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

co nie jest dokładnie tym, o co prosiłeś, ale równie dobrze może odpowiadać Twoim potrzebom.

mmj
źródło
Pracuję nad programem MapReduce w Pythonie, zastanawiam się tylko, czy istnieje sposób na grupowanie według wartości na liście bez zajmowania się słownikami lub zewnętrzną biblioteką, taką jak pandy? Jeśli nie, to w jaki sposób mogę pozbyć się elementów i wpisać wynik?
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'}]

źródło