Groupe Python par

125

Supposons que j'ai un ensemble de paires de données où l' index 0 est la valeur et l' index 1 est le type:

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

Je souhaite les regrouper par leur type (par la 1ère chaîne indexée) comme tel:

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

Comment puis-je y parvenir de manière efficace?

Hellnar
la source

Réponses:

153

Faites-le en 2 étapes. Commencez par créer un dictionnaire.

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

Ensuite, convertissez ce dictionnaire au format attendu.

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

C'est également possible avec itertools.groupby mais cela nécessite que l'entrée soit triée en premier.

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

Notez que ces deux éléments ne respectent pas l'ordre d'origine des touches. Vous avez besoin d'un OrderedDict si vous devez conserver la commande.

>>> 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
la source
Comment cela peut-il être fait si le tuple d'entrée a une clé et deux valeurs ou plus, comme ceci: [('11013331', 'red', 'KAT'), ('9085267', 'blue' 'KAT')]où le dernier élément du tuple est la clé et les deux premiers comme valeur. Le résultat devrait ressembler à ceci: result = [{type: 'KAT', items: [('11013331', red), ('9085267', blue)]}]
user1144616
1
from operator import itemgetter
Baumann
1
l'étape 1 peut être effectuée sans l'importation:d= {}; for k,v in input: d.setdefault(k, []).append(v)
ecoe
Je travaille sur un programme MapReduce en python, je me demande simplement s'il existe un moyen de regrouper par valeurs dans une liste sans avoir à traiter avec des dictionnaires ou une bibliothèque externe comme les pandas? Sinon, comment puis-je me débarrasser des éléments et saisir mon résultat?
Kourosh
54

Le itertoolsmodule intégré de Python a en fait une groupbyfonction, mais pour cela les éléments à grouper doivent d'abord être triés de telle sorte que les éléments à grouper soient contigus dans la liste:

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)

Maintenant, l'entrée ressemble à:

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

groupbyrenvoie une séquence de 2-tuples, de la forme (key, values_iterator). Ce que nous voulons, c'est transformer cela en une liste de dictionnaires où le 'type' est la clé, et 'items' est une liste des 0 'éléments des tuples retournés par le values_iterator. Comme ça:

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

resultContient maintenant votre dict souhaité, comme indiqué dans votre question.

Vous pouvez cependant envisager de créer un seul dict à partir de cela, indexé par type, et chaque valeur contenant la liste de valeurs. Dans votre formulaire actuel, pour trouver les valeurs d'un type particulier, vous devrez parcourir la liste pour trouver le dict contenant la clé 'type' correspondante, puis en extraire l'élément 'items'. Si vous utilisez un seul dict au lieu d'une liste de dictés à 1 élément, vous pouvez trouver les éléments d'un type particulier avec une seule recherche à clé dans le dict maître. En utilisant groupby, cela ressemblerait à:

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

resultcontient maintenant ce dict (c'est similaire au resdefaultdict intermédiaire dans la réponse de @ KennyTM):

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

(Si vous souhaitez réduire cela à une seule ligne, vous pouvez:

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

ou en utilisant le nouveau formulaire de compréhension de dict:

result = {key:list(v[0] for v in valuesiter)
              for key,valuesiter in groupby(input, key=sortkeyfn)}
PaulMcG
la source
Je travaille sur un programme MapReduce en python, je me demande simplement s'il existe un moyen de regrouper par valeurs dans une liste sans avoir à traiter avec des dictionnaires ou une bibliothèque externe comme les pandas? Sinon, comment puis-je me débarrasser des éléments et saisir mon résultat?
Kourosh
@Kourosh - Postez en tant que nouvelle question, mais assurez-vous d'indiquer ce que vous entendez par «se débarrasser des éléments et saisir mon résultat» et «sans utiliser de dictionnaires».
PaulMcG
7

J'ai aussi aimé le regroupement simple des pandas . il est puissant, simple et le plus adéquat pour les grands ensembles de données

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

Akiva
la source
3

Cette réponse est similaire à la réponse de @ PaulMcG mais ne nécessite pas de trier l'entrée.

Pour ceux qui s'intéressent à la programmation fonctionnelle, groupBypeut être écrit sur une seule ligne (sans compter les importations!), Et contrairement à itertools.groupbycela, il ne nécessite pas le tri des entrées:

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

(La raison ... or grpen lambdaest que pour cela reduce()au travail, les lambdabesoins de retourner son premier argument, parce que list.append()toujours retourne Nonele orsera toujours revenir grp. Ie c'est un hack pour contourner la restriction de python qu'un lambda ne peut évaluer une expression unique.)

Cela renvoie un dict dont les clés sont trouvées en évaluant la fonction donnée et dont les valeurs sont une liste des éléments d'origine dans l'ordre d'origine. Pour l'exemple de l'OP, appeler ceci comme groupBy(lambda pair: pair[1], input)retournera ce dict:

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

Et selon la réponse de @ PaulMcG, le format demandé par l'OP peut être trouvé en l'enveloppant dans une compréhension de liste. Alors ça va le faire:

result = {key: [pair[0] for pair in values],
          for key, values in groupBy(lambda pair: pair[1], input).items()}
Ronen
la source
Beaucoup moins de code, mais compréhensible. Aussi bien car il ne réinvente pas la roue.
devdanke le
2

La fonction suivante regroupera rapidement ( aucun tri requis) des tuples de n'importe quelle longueur par une clé ayant n'importe quel index:

# 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

Dans le cas de votre question, l'index de la clé que vous souhaitez regrouper est 1, donc:

group_by(input,1)

donne

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

qui n'est pas exactement le résultat que vous avez demandé, mais qui pourrait tout aussi bien répondre à vos besoins.

mmj
la source
Je travaille sur un programme MapReduce en python, je me demande simplement s'il existe un moyen de regrouper par valeurs dans une liste sans avoir à traiter avec des dictionnaires ou une bibliothèque externe comme les pandas? Sinon, comment puis-je me débarrasser des éléments et saisir mon résultat?
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'}]

la source