Suppression des doublons d'une liste de listes

116

J'ai une liste de listes en Python:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

Et je veux en supprimer les éléments en double. Était-ce une liste normale et non des listes que je pourrais utiliser set. Mais malheureusement, cette liste ne peut pas être hachée et ne peut pas constituer un ensemble de listes. Seulement des tuples. Je peux donc transformer toutes les listes en tuples, puis utiliser set et revenir en listes. Mais ce n'est pas rapide.

Comment cela peut-il être fait de la manière la plus efficace?

Le résultat de la liste ci-dessus devrait être:

k = [[5, 6, 2], [1, 2], [3], [4]]

Je me fiche de la préservation de l'ordre.

Remarque: cette question est similaire mais pas tout à fait ce dont j'ai besoin. Recherche SO mais n'a pas trouvé le double exact.


Analyse comparative:

import itertools, time


class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print '[%s]' % self.name,
        print 'Elapsed: %s' % (time.time() - self.tstart)


k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] * 5
N = 100000

print len(k)

with Timer('set'):
    for i in xrange(N):
        kt = [tuple(i) for i in k]
        skt = set(kt)
        kk = [list(i) for i in skt]


with Timer('sort'):
    for i in xrange(N):
        ks = sorted(k)
        dedup = [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]


with Timer('groupby'):
    for i in xrange(N):
        k = sorted(k)
        dedup = list(k for k, _ in itertools.groupby(k))

with Timer('loop in'):
    for i in xrange(N):
        new_k = []
        for elem in k:
            if elem not in new_k:
                new_k.append(elem)

"loop in" (méthode quadratique) le plus rapide de tous pour les listes courtes. Pour les longues listes, c'est plus rapide que tout le monde sauf la méthode groupby. Est-ce que ça a du sens?

Pour la liste courte (celle du code), 100000 itérations:

[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

Pour une liste plus longue (celle du code dupliquée 5 fois):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
zaharpopov
la source
1
Par "ce n'est pas rapide", voulez-vous dire que vous l'avez chronométré et que ce n'est pas assez rapide pour votre application, ou que vous pensez que ce n'est pas rapide?
Torsten Marek
@Torsten, cela semble juste trop de copies pour être une méthode intelligente. désolé, instinct. copier les listes dans les tuples, puis dans l'ensemble, puis revenir à la liste des listes (copier à nouveau les tuples dans les listes)
zaharpopov
@zaharpopov: ce n'est pas comme ça que fonctionne Python, rien ne sera copié , juste de nouveaux conteneurs pour les éléments existants (bien que pour les ints, c'est à peu près la même chose)
Jochen Ritzel
3
1. les durées des méthodes utilisant le tri sont dégonflées, car "k" est rebondi sur la variante triée. 2. La dernière méthode est plus rapide car la façon dont vous générez les données de test vous laisse au plus 4 éléments distincts. Essayez qc. comme K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek
@Torsten: réparé grâce. mais la méthode en boucle est toujours rapide même s'il n'y a qu'un seul doublon dans la liste des 10
zaharpopov

Réponses:

167
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> import itertools
>>> k.sort()
>>> list(k for k,_ in itertools.groupby(k))
[[1, 2], [3], [4], [5, 6, 2]]

itertoolsoffre souvent les plus rapides et les plus puissantes solutions à ce genre de problèmes, et il est bien la peine de se connaît très bien -)

Edit : comme je le mentionne dans un commentaire, les efforts d'optimisation normaux se concentrent sur les gros intrants (l'approche big-O) car c'est tellement plus facile que cela offre de bons retours sur efforts. Mais parfois (essentiellement pour les «goulots d'étranglement tragiquement cruciaux» dans les boucles internes profondes de code qui repoussent les limites des limites de performances), il peut être nécessaire d'entrer beaucoup plus en détail, en fournissant des distributions de probabilité, en décidant quelles mesures de performance optimiser (peut-être la limite supérieure ou le 90e centile est plus important qu'une moyenne ou une médiane, en fonction de ses applications), en effectuant des vérifications éventuellement heuristiques au début pour choisir différents algorithmes en fonction des caractéristiques des données d'entrée, etc.

Des mesures soigneuses des performances "ponctuelles" (code A vs code B pour une entrée spécifique) font partie de ce processus extrêmement coûteux, et le module de bibliothèque standard est timeitutile ici. Cependant, il est plus facile de l'utiliser à l'invite du shell. Par exemple, voici un court module pour présenter l'approche générale de ce problème, enregistrez-le sous nodup.py:

import itertools

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

def doset(k, map=map, list=list, set=set, tuple=tuple):
  return map(list, set(map(tuple, k)))

def dosort(k, sorted=sorted, xrange=xrange, len=len):
  ks = sorted(k)
  return [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]

def dogroupby(k, sorted=sorted, groupby=itertools.groupby, list=list):
  ks = sorted(k)
  return [i for i, _ in itertools.groupby(ks)]

def donewk(k):
  newk = []
  for i in k:
    if i not in newk:
      newk.append(i)
  return newk

# sanity check that all functions compute the same result and don't alter k
if __name__ == '__main__':
  savek = list(k)
  for f in doset, dosort, dogroupby, donewk:
    resk = f(k)
    assert k == savek
    print '%10s %s' % (f.__name__, sorted(resk))

Notez le contrôle de cohérence (effectué lorsque vous venez de le faire python nodup.py) et la technique de levage de base (rendre les noms globaux constants locaux à chaque fonction pour la vitesse) pour mettre les choses sur un pied d'égalité.

Nous pouvons maintenant effectuer des vérifications sur la petite liste d'exemples:

$ python -mtimeit -s'import nodup' 'nodup.doset(nodup.k)'
100000 loops, best of 3: 11.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort(nodup.k)'
100000 loops, best of 3: 9.68 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby(nodup.k)'
100000 loops, best of 3: 8.74 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.donewk(nodup.k)'
100000 loops, best of 3: 4.44 usec per loop

confirmant que l'approche quadratique a des constantes suffisamment petites pour la rendre intéressante pour les petites listes avec peu de valeurs dupliquées. Avec une courte liste sans doublons:

$ python -mtimeit -s'import nodup' 'nodup.donewk([[i] for i in range(12)])'
10000 loops, best of 3: 25.4 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby([[i] for i in range(12)])'
10000 loops, best of 3: 23.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.doset([[i] for i in range(12)])'
10000 loops, best of 3: 31.3 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort([[i] for i in range(12)])'
10000 loops, best of 3: 25 usec per loop

l'approche quadratique n'est pas mauvaise, mais les méthodes tri et groupby sont meilleures. Etc.

Si (comme l'obsession de la performance le suggère), cette opération est au cœur de la boucle interne de votre application repoussant les limites, cela vaut la peine d'essayer le même ensemble de tests sur d'autres échantillons d'entrée représentatifs, en détectant éventuellement une mesure simple qui pourrait vous permettre de manière heuristique choisissez l'une ou l'autre approche (mais la mesure doit être rapide, bien sûr).

Cela vaut également la peine d'envisager de conserver une représentation différente pour k- pourquoi doit-il s'agir d'une liste de listes plutôt que d'un ensemble de tuples en premier lieu? Si la tâche de suppression des doublons est fréquente et que le profilage montre qu'il s'agit du goulot d'étranglement des performances du programme, conserver un ensemble de tuples tout le temps et obtenir une liste de listes à partir de celui-ci uniquement si et où cela est nécessaire, peut être globalement plus rapide, par exemple.

Alex Martelli
la source
@alex merci pour l'alternative. cette méthode à peu près à la même vitesse que celle de danben, quelques% plus rapide
zaharpopov
@alex: étrangement, c'est plus lent qu'une méthode quadratique naïve pour des listes plus courtes (voir la modification de la question)
Zaharpopov
@zaharpopov: c'est comme ça que dans votre cas particulier, cf. mon commentaire à la question.
Torsten Marek
@zaharpopov, si vous donnez une distribution de probabilité des longueurs de liste et de sous-liste et le risque de doublons, il est possible (avec un effort énorme) de calculer / mesurer la distribution de probabilité d'exécution pour un code donné et d'optimiser la mesure dont vous avez besoin (médiane, moyenne, 90e centile, peu importe). Cela n'est pratiquement jamais fait à cause d'un retour sur investissement très faible: normalement, on se concentre sur le cas beaucoup plus facile des grandes entrées (l'approche big-O), où des algorithmes inférieurs nuiraient vraiment aux performances. Et je ne vois pas de toute façon que vous spécifiez des distributions de probabilité dans votre Q ;-).
Alex Martelli
@zaharpov, content que vous ayez aimé!
Alex Martelli
21

Faites-le manuellement, créez une nouvelle kliste et ajoutez des entrées introuvables jusqu'à présent:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
new_k = []
for elem in k:
    if elem not in new_k:
        new_k.append(elem)
k = new_k
print k
# prints [[1, 2], [4], [5, 6, 2], [3]]

Simple à comprendre, et vous conservez l'ordre de la première occurrence de chaque élément si cela est utile, mais je suppose que sa complexité est quadratique lorsque vous recherchez l'ensemble de new_kchaque élément.

Paul Stephenson
la source
@paul: très étrange - cette méthode est plus rapide que toutes les autres
zaharpopov
Je soupçonne que cette méthode ne sera pas plus rapide pour de très longues listes. Cela dépendra de votre application: si vous n'avez vraiment que des listes de six éléments avec deux doublons, alors toute solution sera probablement assez rapide et vous devriez utiliser le code le plus clair.
Paul Stephenson
@zaharpopov, ce n'est pas quadratique dans votre benchmark car vous dupliquez la même liste encore et encore. Vous comparez avec un boîtier d'angle linéaire.
Mike Graham
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5montrera bien le comportement quadratique
John La Rooy
17
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> k = sorted(k)
>>> k
[[1, 2], [1, 2], [3], [4], [4], [5, 6, 2]]
>>> dedup = [k[i] for i in range(len(k)) if i == 0 or k[i] != k[i-1]]
>>> dedup
[[1, 2], [3], [4], [5, 6, 2]]

Je ne sais pas si c'est forcément plus rapide, mais vous n'avez pas besoin d'utiliser des tuples et des ensembles.

Danben
la source
Merci Danben. est-ce plus rapide que de passer aux tuples, puis de «définir» puis de revenir aux listes?
zaharpopov
Vous pouvez facilement tester cela - écrire les deux méthodes de déduplication, générer des listes aléatoires en utilisant randomet chronométrer time.
danben
4

Toutes les setsolutions liées à ce problème jusqu'à présent nécessitent la création d'unset avant l'itération.

Il est possible de rendre cet ordre paresseux et en même temps de préserver l'ordre, en itérant la liste des listes et en ajoutant à un "vu" set. Ensuite, ne donnez une liste que si elle n'est pas trouvée dans ce trackerset .

Cette unique_everseenrecette est disponible dans la itertools documentation . Il est également disponible dans la toolzbibliothèque tierce :

from toolz import unique

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

# lazy iterator
res = map(list, unique(map(tuple, k)))

print(list(res))

[[1, 2], [4], [5, 6, 2], [3]]

Notez que la tupleconversion est nécessaire car les listes ne sont pas hachables.

jpp
la source
3

Même votre «longue» liste est assez courte. De plus, les avez-vous choisis pour correspondre aux données réelles? Les performances varieront en fonction de l'aspect réel de ces données. Par exemple, vous avez une courte liste répétée encore et encore pour faire une liste plus longue. Cela signifie que la solution quadratique est linéaire dans vos benchmarks, mais pas en réalité.

Pour les listes réellement volumineuses, le code défini est votre meilleur choix: il est linéaire (bien que gourmand en espace). Les méthodes de tri et de groupby sont O (n log n) et la méthode en boucle est évidemment quadratique, vous savez donc comment elles évolueront lorsque n devient vraiment grand. Si c'est la taille réelle des données que vous analysez, alors qui s'en soucie? C'est minuscule.

Au passage, je constate une accélération notable si je ne forme pas de liste intermédiaire pour faire l'ensemble, c'est-à-dire si je remplace

kt = [tuple(i) for i in k]
skt = set(kt)

avec

skt = set(tuple(i) for i in k)

La vraie solution peut dépendre de plus d'informations: êtes-vous sûr qu'une liste de listes est vraiment la représentation dont vous avez besoin?

Mike Graham
la source
3

La liste des tuple et {} peut être utilisée pour supprimer les doublons

>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>> 
SuperNova
la source
1

Créez un dictionnaire avec tuple comme clé et imprimez les clés.

  • créer un dictionnaire avec tuple comme clé et index comme valeur
  • imprimer la liste des clés du dictionnaire

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

dict_tuple = {tuple(item): index for index, item in enumerate(k)}

print [list(itm) for itm in dict_tuple.keys()]

# prints [[1, 2], [5, 6, 2], [3], [4]]
SuperNova
la source
1

Cela devrait fonctionner.

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

k_cleaned = []
for ele in k:
    if set(ele) not in [set(x) for x in k_cleaned]:
        k_cleaned.append(ele)
print(k_cleaned)

# output: [[1, 2], [4], [5, 6, 2], [3]]
Zoé L
la source
0

Étrangement, les réponses ci-dessus suppriment les «doublons», mais que faire si je veux également supprimer la valeur dupliquée ?? Ce qui suit devrait être utile et ne crée pas de nouvel objet en mémoire!

def dictRemoveDuplicates(self):
    a=[[1,'somevalue1'],[1,'somevalue2'],[2,'somevalue1'],[3,'somevalue4'],[5,'somevalue5'],[5,'somevalue1'],[5,'somevalue1'],[5,'somevalue8'],[6,'somevalue9'],[6,'somevalue0'],[6,'somevalue1'],[7,'somevalue7']]


print(a)
temp = 0
position = -1
for pageNo, item in a:
    position+=1
    if pageNo != temp:
        temp = pageNo
        continue
    else:
        a[position] = 0
        a[position - 1] = 0
a = [x for x in a if x != 0]         
print(a)

et le o / p est:

[[1, 'somevalue1'], [1, 'somevalue2'], [2, 'somevalue1'], [3, 'somevalue4'], [5, 'somevalue5'], [5, 'somevalue1'], [5, 'somevalue1'], [5, 'somevalue8'], [6, 'somevalue9'], [6, 'somevalue0'], [6, 'somevalue1'], [7, 'somevalue7']]
[[2, 'somevalue1'], [3, 'somevalue4'], [7, 'somevalue7']]
zorze
la source
-1

Une autre solution probablement plus générique et plus simple consiste à créer un dictionnaire indexé par la version chaîne des objets et à obtenir les valeurs () à la fin:

>>> dict([(unicode(a),a) for a in [["A", "A"], ["A", "A"], ["A", "B"]]]).values()
[['A', 'B'], ['A', 'A']]

Le hic, c'est que cela ne fonctionne que pour les objets dont la représentation sous forme de chaîne est une clé unique assez bonne (ce qui est vrai pour la plupart des objets natifs).

Jacmkno
la source