web-dev-qa-db-fra.com

Manière pythonique de supprimer les doublons inversés dans la liste

J'ai une liste de paires:

[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]

et je veux supprimer tous les doublons où

[a,b] == [b,a]

Donc on se retrouve avec juste

[0, 1], [0, 4], [1, 4]

Je peux faire une boucle interne et une boucle externe pour vérifier la paire inverse et les ajouter à une liste si ce n'est pas le cas, mais je suis sûr qu'il existe un moyen plus pythonique d'obtenir les mêmes résultats.

17
Mr Mystery Guest

Si vous devez conserver l'ordre des éléments dans la liste, vous pouvez utiliser une fonction sorted et définir la compréhension avec map comme ceci:

lst = [0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]
data = {Tuple(item) for item in map(sorted, lst)}
# {(0, 1), (0, 4), (1, 4)}

ou simplement sans map comme ceci:

data = {Tuple(sorted(item)) for item in lst}

Une autre méthode consiste à utiliser une frozenset comme indiqué ici mais notez que cela ne fonctionne que si vous avez des éléments distincts dans votre liste. Parce que, comme set, frozenset contient toujours des valeurs uniques. Ainsi, vous vous retrouverez avec une valeur unique dans votre sous-liste (perte de données) qui peut ne pas être ce que vous voulez.

Pour sortir une liste, vous pouvez toujours utiliser list(map(list, result)) où result est un ensemble de Tuple uniquement dans Python-3.0 ou plus récent.

18
styvane

Si vous souhaitez uniquement supprimer les paires inversées et ne souhaitez pas utiliser de bibliothèques externes, vous pouvez utiliser une fonction de générateur simple (basée sur la recette itertools "unique_everseen" ):

def remove_reversed_duplicates(iterable):
    # Create a set for already seen elements
    seen = set()
    for item in iterable:
        # Lists are mutable so we need tuples for the set-operations.
        tup = Tuple(item)
        if tup not in seen:
            # If the Tuple is not in the set append it in REVERSED order.
            seen.add(tup[::-1])
            # If you also want to remove normal duplicates uncomment the next line
            # seen.add(tup)
            yield item

>>> list(remove_reversed_duplicates(a))
[[0, 1], [0, 4], [1, 4]]

La fonction de générateur pourrait être un moyen assez rapide de résoudre ce problème car les recherches sur les ensembles sont vraiment peu coûteuses. Cette approche conserve également l'ordre de votre liste initiale et seulement supprime les doublons inversés tout en étant plus rapide que la plupart des alternatives!


Si l'utilisation d'une bibliothèque externe ne vous dérange pas et que vous souhaitez supprimer tous les doublons (inversés et identiques), une alternative est: iteration_utilities.unique_everseen

>>> a = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]

>>> from iteration_utilities import unique_everseen

>>> list(unique_everseen(a, key=set))
[[0, 1], [0, 4], [1, 4]]

Ceci vérifie si un élément a le même contenu dans un ordre arbitraire (donc le key=set) comme un autre. Dans ce cas, cela fonctionne comme prévu, mais cela supprime également les duplications [a, b] au lieu des seules occurrences [b, a]. Vous pouvez également utiliser key=sorted (comme le suggèrent les autres réponses). Le unique_everseen comme celui-ci a une mauvaise complexité algorithmique car le résultat de la fonction key n'est pas hashable et la recherche rapide est donc remplacée par une recherche lente. Pour accélérer le processus, vous devez rendre les clés accessibles, par exemple en les convertissant en n-uplets triés (comme le suggèrent d’autres réponses):

>>> from iteration_utilities import chained
>>> list(unique_everseen(a, key=chained(sorted, Tuple)))
[[0, 1], [0, 4], [1, 4]]

Le chained n'est rien d'autre qu'une alternative plus rapide à lambda x: Tuple(sorted(x)).

EDIT: Comme mentionné par @ jpmc26, on pourrait utiliser frozenset au lieu des ensembles normaux:

>>> list(unique_everseen(a, key=frozenset))
[[0, 1], [0, 4], [1, 4]]

Pour avoir une idée de la performance, j’ai fait quelques comparaisons timeit pour les différentes suggestions:

>>> a = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]

>>> %timeit list(remove_reversed_duplicates(a))
100000 loops, best of 3: 16.1 µs per loop
>>> %timeit list(unique_everseen(a, key=frozenset))
100000 loops, best of 3: 13.6 µs per loop
>>> %timeit list(set(map(frozenset, a)))
100000 loops, best of 3: 7.23 µs per loop

>>> %timeit list(unique_everseen(a, key=set))
10000 loops, best of 3: 26.4 µs per loop
>>> %timeit list(unique_everseen(a, key=chained(sorted, Tuple)))
10000 loops, best of 3: 25.8 µs per loop
>>> %timeit [list(tpl) for tpl in list(set([Tuple(sorted(pair)) for pair in a]))]
10000 loops, best of 3: 29.8 µs per loop
>>> %timeit set(Tuple(item) for item in map(sorted, a))
10000 loops, best of 3: 28.5 µs per loop

Longue liste avec beaucoup de doublons:

>>> import random
>>> a = [[random.randint(0, 10), random.randint(0,10)] for _ in range(10000)]

>>> %timeit list(remove_reversed_duplicates(a))
100 loops, best of 3: 12.5 ms per loop
>>> %timeit list(unique_everseen(a, key=frozenset))
100 loops, best of 3: 10 ms per loop
>>> %timeit set(map(frozenset, a))
100 loops, best of 3: 10.4 ms per loop

>>> %timeit list(unique_everseen(a, key=set))
10 loops, best of 3: 47.7 ms per loop
>>> %timeit list(unique_everseen(a, key=chained(sorted, Tuple)))
10 loops, best of 3: 22.4 ms per loop
>>> %timeit [list(tpl) for tpl in list(set([Tuple(sorted(pair)) for pair in a]))]
10 loops, best of 3: 24 ms per loop
>>> %timeit set(Tuple(item) for item in map(sorted, a))
10 loops, best of 3: 35 ms per loop

Et avec moins de doublons:

>>> a = [[random.randint(0, 100), random.randint(0,100)] for _ in range(10000)]

>>> %timeit list(remove_reversed_duplicates(a))
100 loops, best of 3: 15.4 ms per loop
>>> %timeit list(unique_everseen(a, key=frozenset))
100 loops, best of 3: 13.1 ms per loop
>>> %timeit set(map(frozenset, a))
100 loops, best of 3: 11.8 ms per loop


>>> %timeit list(unique_everseen(a, key=set))
1 loop, best of 3: 1.96 s per loop
>>> %timeit list(unique_everseen(a, key=chained(sorted, Tuple)))
10 loops, best of 3: 24.2 ms per loop
>>> %timeit [list(tpl) for tpl in list(set([Tuple(sorted(pair)) for pair in a]))]
10 loops, best of 3: 31.1 ms per loop
>>> %timeit set(Tuple(item) for item in map(sorted, a))
10 loops, best of 3: 36.7 ms per loop

Ainsi, les variantes avec remove_reversed_duplicates, unique_everseen (key=frozenset) et set(map(frozenset, a)) semblent être de loin les solutions les plus rapides. Lequel dépend de la longueur de l'entrée et du nombre de doublons.

14
MSeifert

TL; DR

set(map(frozenset, lst))

Explication

Si les paires sont logiquement non ordonnées, elles sont plus naturellement exprimées en ensembles. Il serait préférable de les avoir en tant qu'ensembles avant même d'arriver à ce point, mais vous pouvez les convertir comme ceci:

lst = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
lst_as_sets = map(frozenset, lst)

Et puis, le moyen naturel d’éliminer les doublons dans un itératif est de le convertir en un set:

deduped = set(lst_as_sets)

(C’est la raison principale pour laquelle j’ai choisi frozenset dans la première étape. Les sets mutables ne peuvent pas être ajoutés, ils ne peuvent donc pas être ajoutés à un set.)

Ou vous pouvez le faire en une seule ligne, comme dans la section TL; DR.

Je pense que cela est beaucoup plus simple, plus intuitif et correspond mieux à la façon dont vous pensez à propos des données que de vous occuper du tri et des n-uplets.

Reconvertir

Si, pour une raison quelconque, vous avez vraiment besoin d'un list sur lists comme résultat final, la conversion en arrière est triviale:

result_list = list(map(list, deduped))

Mais il est probablement plus logique de tout laisser aussi longtemps que possible avec sets. Je ne peux penser qu'à une seule raison pour laquelle vous pourriez en avoir besoin, à savoir la compatibilité avec le code/les bibliothèques existants.

6
jpmc26

Vous pouvez trier chaque paire, convertir votre liste de paires en un ensemble de n-uplets et inversement:

l = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
[list(tpl) for tpl in list(set([Tuple(sorted(pair)) for pair in l]))]
#=> [[0, 1], [1, 4], [0, 4]]

Les étapes peuvent être plus faciles à comprendre qu’une longue ligne:

>>> l = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
>>> [sorted(pair) for pair in l]
# [[0, 1], [0, 4], [0, 1], [1, 4], [0, 4], [1, 4]]
>>> [Tuple(pair) for pair in _]
# [(0, 1), (0, 4), (0, 1), (1, 4), (0, 4), (1, 4)]
>>> set(_)
# set([(0, 1), (1, 4), (0, 4)])
>>> list(_)
# [(0, 1), (1, 4), (0, 4)]
>>> [list(tpl) for tpl in _]
# [[0, 1], [1, 4], [0, 4]]
4
Eric Duminil

Vous pouvez utiliser la fonction intégrée filter.

from __future__ import print_function

def my_filter(l):
    seen = set()

    def not_seen(it):
        s = min(*it), max(*it)
        if s in seen:
            return False
        else:
            seen.add(s)
            return True

    out = filter(not_seen, l)

    return out

myList = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
print(my_filter(myList)) # [[0, 1], [0, 4], [1, 4]]

En complément, je vous orienterais vers le module Python itertools , qui décrit une fonction unique_everseen qui fait fondamentalement la même chose que ci-dessus mais dans une version paresseuse, basée sur un générateur, utilisant efficacement la mémoire. Peut-être mieux que n’importe laquelle de nos solutions si vous travaillez sur de grandes baies. Voici comment l'utiliser:

from itertools import ifilterfalse

def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in ifilterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

gen = unique_everseen(myList, lambda x: (min(x), max(x))) # gen is an iterator
print(gen) # <generator object unique_everseen at 0x7f82af492fa0>
result = list(gen) # consume generator into a list.
print(result) # [[0, 1], [0, 4], [1, 4]]

Je n'ai fait aucune métrique pour voir qui est le plus rapide. Cependant, l'efficacité de la mémoire et la complexité d'O semblent mieux dans cette version.

Timing min/max vs triés

La fonction sorted intégrée peut être passée à unique_everseen pour commander des éléments dans les vecteurs internes. Au lieu de cela, je passe lambda x: (min(x), max(x)). Puisque je connais la taille du vecteur qui est exactement 2, je peux procéder comme ceci. 

Pour utiliser sorted, il faudrait que je transmette lambda x: Tuple(sorted(x)), ce qui augmente les frais généraux. Pas de façon spectaculaire, mais quand même.

myList = [[random.randint(0, 10), random.randint(0,10)] for _ in range(10000)]
timeit.timeit("list(unique_everseen(myList, lambda x: (min(x), max(x))))", globals=globals(), number=20000)
>>> 156.81979029000013
timeit.timeit("list(unique_everseen(myList, lambda x: Tuple(sorted(x))))", globals=globals(), number=20000)
>>> 168.8286430349999

Timings effectués dans Python 3, qui ajoute globals kwarg à timeit.timeit.

4
daragua

ÉDITÉ pour mieux expliquer

Commencez par trier chaque liste, puis utilisez les clés de dictionnaires pour obtenir un ensemble unique d’éléments et leur compréhension.

Pourquoi tuples?
Il est nécessaire de remplacer les listes par des nuplets pour éviter l’erreur "inutile" lors du passage à travers la fonction fromkeys ()

my_list = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
Tuple_list = [ Tuple(sorted(item)) for item in my_list ]
final_list = [ list(item) for item in list({}.fromkeys(Tuple_list)) ]

En utilisant OrderedDict, vous conservez même l'ordre de la liste.

from collections import OrderedDict

my_list = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
Tuple_list = [ Tuple(sorted(item)) for item in my_list ]
final_list = [ list(item) for item in list(OrderedDict.fromkeys(Tuple_list)) ]

Le code ci-dessus donnera la liste désirée

[[0, 1], [0, 4], [1, 4]]
3

Une solution facile et unnested:

pairs = [[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
s=set()
for p in pairs:
    # Lists are unhashable so make the "elements" into tuples
    p = Tuple(p)
    if p not in s and p[::-1] not in s:
        s.add(p)

print s
3
UlfR

Si l'ordre des paires et des éléments de paire est important, la création d'une nouvelle liste en testant l'adhésion est peut-être la solution. 

pairs = [0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]
no_dups = []
for pair in pairs:
    if not any( all( i in p for i in pair ) for p in no_dups ):
        no_dups.append(pair)

Sinon, j'irais avec la réponse de Styvane

Incidemment, la solution ci-dessus ne fonctionnera pas pour les cas dans lesquels vous avez paires correspondantes. Par exemple, [0,0] ne serait pas ajouté à la liste. Pour cela, vous devez ajouter une vérification supplémentaire: 

for pair in pairs:
    if not any( all( i in p for i in pair ) for p in no_dups ) or ( len(set(pair)) == 1 and not pair in no_dups ):
        no_dups.append(pair)

Cependant, cette solution ne récupérera pas empty "paires" (par exemple, []). Pour cela, vous aurez besoin d'un ajustement supplémentaire: 

    if not any( all( i in p for i in pair ) for p in no_dups ) or ( len(set(pair)) in (0,1) and not pair in no_dups ):
        no_dups.append(pair)

Le bit and not pair in no_dups est requis pour empêcher l'ajout du [0,0] ou du [] à no_dupsdeux fois

1
Rick Teachey

Eh bien, je "vérifie la paire inversée et l'ajoute à une liste si ce n'est pas le cas" comme vous l'avez dit, mais j'utilise une boucle unique.

x=[[0, 1], [0, 4], [1, 0], [1, 4], [4, 0], [4, 1]]
out = []
for pair in x:
    if pair[::-1] not in out:
        out.append(pair)
print out

L'avantage par rapport aux réponses existantes est d'être, IMO, plus lisible. Aucune connaissance approfondie de la bibliothèque standard n'est nécessaire ici. Et pas de suivi de quelque chose de complexe. Le seul concept qui puisse ne pas être familier aux débutants est que [::-1] rétablit la paire.

La performance est O (n ** 2) cependant, ne l'utilisez pas si la performance pose problème et/ou si les listes sont volumineuses.

1
Emilio M Bumachar