Eliminar duplicados de una lista de listas

116

Tengo una lista de listas en Python:

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

Y quiero eliminar elementos duplicados de él. Si fuera una lista normal, no de listas que pudiera usar set. Pero, lamentablemente, esa lista no se puede usar con hash y no se pueden crear conjuntos de listas. Solo de tuplas. Entonces puedo convertir todas las listas en tuplas y luego usar set y volver a listas. Pero esto no es rápido.

¿Cómo se puede hacer esto de la manera más eficiente?

El resultado de la lista anterior debería ser:

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

No me importa mantener el orden.

Nota: esta pregunta es similar pero no es exactamente lo que necesito. Busqué SO pero no encontró un duplicado exacto.


Benchmarking:

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)

"bucle" (método cuadrático) más rápido de todos para listas cortas. Para listas largas es más rápido que todos, excepto el método groupby. ¿Esto tiene sentido?

Para una lista corta (la que está en el código), 100000 iteraciones:

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

Para una lista más larga (el que está en el código duplicado 5 veces):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
zaharpopov
fuente
1
Por "esto no es rápido", ¿quiere decir que lo ha cronometrado y no es lo suficientemente rápido para su aplicación, o que cree que no es rápido?
Torsten Marek
@Torsten, parece demasiada copia para ser un método inteligente. lo siento, presentimiento. copiar listas de tuplas, a continuación, en conjunto, a continuación, volver a la lista de listas (copiar tuplas de nuevo a listas)
zaharpopov
@zaharpopov: no es así como funciona Python, no se copiará nada , solo nuevos contenedores para los elementos existentes (aunque para los ints, es más o menos lo mismo)
Jochen Ritzel
3
1. Los tiempos de los métodos que utilizan la clasificación se desinflan, porque "k" se recupera a la variante clasificada. 2. El último método es más rápido porque la forma en que genera los datos de prueba le deja como máximo 4 elementos distintos. Prueba algo. como K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek
@Torsten: arreglado gracias. pero aún así, el método de bucle es rápido incluso cuando solo hay un duplicado en la lista de 10
zaharpopov

Respuestas:

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

itertoolsa menudo ofrece las soluciones más rápidas y poderosas para este tipo de problemas, y está bien vale la pena conseguir íntimamente familiarizados con -!)

Editar : como mencioné en un comentario, los esfuerzos de optimización normales se centran en grandes entradas (el enfoque de Big-O) porque es mucho más fácil que ofrece buenos retornos de los esfuerzos. Pero a veces (esencialmente para "cuellos de botella trágicamente cruciales" en bucles internos profundos de código que superan los límites de los límites de rendimiento) es posible que sea necesario entrar en muchos más detalles, proporcionar distribuciones de probabilidad, decidir qué medidas de rendimiento optimizar (tal vez el límite superior o el percentil 90 es más importante que el promedio o la mediana, dependiendo de las aplicaciones), realizando comprobaciones posiblemente heurísticas al principio para elegir diferentes algoritmos según las características de los datos de entrada, y así sucesivamente.

Las medidas cuidadosas del rendimiento del "punto" (código A frente al código B para una entrada específica) son parte de este proceso extremadamente costoso, y el módulo de biblioteca estándar timeitayuda aquí. Sin embargo, es más fácil usarlo en un indicador de shell. Por ejemplo, aquí hay un módulo corto para mostrar el enfoque general para este problema, guárdelo como 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))

Tenga en cuenta la verificación de cordura (realizada cuando acaba de hacerlo python nodup.py) y la técnica de elevación básica (haga que los nombres globales constantes sean locales para cada función para la velocidad) para poner las cosas en pie de igualdad.

Ahora podemos ejecutar comprobaciones en la pequeña lista de ejemplos:

$ 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

confirmando que el enfoque cuadrático tiene constantes lo suficientemente pequeñas como para hacerlo atractivo para listas pequeñas con pocos valores duplicados. Con una lista corta sin duplicados:

$ 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

el enfoque cuadrático no es malo, pero el tipo y el grupo por grupo son mejores. Etcétera etcétera.

Si (como sugiere la obsesión con el rendimiento) esta operación se encuentra en un bucle interno central de su aplicación de superación de los límites, vale la pena probar el mismo conjunto de pruebas en otras muestras de entrada representativas, posiblemente detectando alguna medida simple que podría permitirle heurísticamente elija uno u otro enfoque (pero la medida debe ser rápida, por supuesto).

También vale la pena considerar mantener una representación diferente para k: ¿ por qué tiene que ser una lista de listas en lugar de un conjunto de tuplas en primer lugar? Si la tarea de eliminación de duplicados es frecuente y la creación de perfiles muestra que es el cuello de botella del rendimiento del programa, mantener un conjunto de tuplas todo el tiempo y obtener una lista de listas de él solo si es necesario y donde sea necesario, podría ser más rápido en general, por ejemplo.

Alex Martelli
fuente
@alex gracias por la alternativa. este método tiene aproximadamente la misma velocidad que el de danben, un poco más rápido
zaharpopov
@alex: extrañamente, esto es más lento que un método cuadrático ingenuo para listas más cortas (ver edición de preguntas)
zaharpopov
@zaharpopov: es así solo en tu caso especial, cf. mi comentario a la pregunta.
Torsten Marek
@zaharpopov, si proporciona una distribución de probabilidad de longitudes de lista y sublista y posibilidad de duplicados, es posible (con un gran esfuerzo) calcular / medir la distribución de probabilidad de tiempos de ejecución para cualquier código dado y optimizar cualquier medida que necesite (mediana, media, 90 ° percentil, lo que sea). Casi nunca se hace debido a un ROI muy bajo: normalmente uno se enfoca en el caso mucho más fácil de entradas grandes (el enfoque de gran O), donde los algoritmos inferiores realmente dañarían el rendimiento terriblemente. Y no veo que especifiques ninguna distribución de probabilidad en tu Q de todos modos ;-).
Alex Martelli
@zaharpov, ¡me alegro de que te haya gustado!
Alex Martelli
21

Haciéndolo manualmente, creando una nueva klista y agregando entradas no encontradas hasta ahora:

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 de comprender, y conserva el orden de la primera aparición de cada elemento en caso de que sea útil, pero supongo que es cuadrático en complejidad ya que está buscando la totalidad de new_kcada elemento.

Paul Stephenson
fuente
@paul: muy extraño - este método es más rápido que todos los demás
zaharpopov
Sospecho que este método no será más rápido para listas muy largas. Dependerá de su aplicación: si realmente solo tiene listas de seis elementos con dos duplicados, es probable que cualquier solución sea lo suficientemente rápida y debe elegir el código más claro.
Paul Stephenson
@zaharpopov, no es cuadrático en su punto de referencia porque duplica la misma lista una y otra vez. Está haciendo una evaluación comparativa con una caja de esquina lineal.
Mike Graham
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5mostrará muy bien el comportamiento cuadrático
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]]

No sé si es necesariamente más rápido, pero no tienes que usar tuplas y conjuntos.

danben
fuente
Gracias Danben. esto más rápido que pasar a tuplas, luego 'configurar' y luego volver a las listas?
zaharpopov
Puede probarlo fácilmente: escriba ambos métodos de deduplicación, genere algunas listas aleatorias utilizando randomy cronometre time.
danben
4

Todas las setsoluciones relacionadas con este problema hasta ahora requieren la creación de un entero setantes de la iteración.

Es posible hacer esto lento, y al mismo tiempo preservar el orden, iterando la lista de listas y agregando a un "visto" set. Luego, solo genere una lista si no se encuentra en este rastreadorset .

Esta unique_everseenreceta está disponible en los itertools documentos . También está disponible en la toolzbiblioteca de terceros :

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

Tenga en cuenta que la tupleconversión es necesaria porque las listas no son hash.

jpp
fuente
3

Incluso su lista "larga" es bastante corta. Además, ¿los eligió para que coincidieran con los datos reales? El rendimiento variará según el aspecto real de estos datos. Por ejemplo, tiene una lista corta repetida una y otra vez para hacer una lista más larga. Esto significa que la solución cuadrática es lineal en sus puntos de referencia, pero no en la realidad.

Para listas realmente grandes, el código de conjunto es su mejor opción: es lineal (aunque requiere mucho espacio). Los métodos de ordenar y agrupar son O (n log n) y el método de bucle es obviamente cuadrático, por lo que sabe cómo escalarán a medida que n se vuelva realmente grande. Si este es el tamaño real de los datos que está analizando, ¿a quién le importa? Es diminuto.

Por cierto, veo una aceleración notable si no formo una lista intermedia para hacer el conjunto, es decir, si reemplazo

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

con

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

La solución real puede depender de más información: ¿Está seguro de que una lista de listas es realmente la representación que necesita?

Mike Graham
fuente
3

La lista de tuplas y {} se pueden usar para eliminar duplicados.

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

Cree un diccionario con tupla como clave e imprima las claves.

  • crear diccionario con tupla como clave e índice como valor
  • imprimir lista de claves del diccionario

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
fuente
1

Esto debería funcionar.

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]]
Zoe L
fuente
0

Curiosamente, las respuestas anteriores eliminan los 'duplicados', pero ¿qué pasa si también quiero eliminar el valor duplicado? ¡Lo siguiente debería ser útil y no crea un nuevo objeto en la memoria!

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)

y el o / p es:

[[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
fuente
-1

Otra solución probablemente más genérica y simple es crear un diccionario codificado por la versión de cadena de los objetos y obtener los valores () al final:

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

El problema es que esto solo funciona para objetos cuya representación de cadena es una clave única suficientemente buena (lo cual es cierto para la mayoría de los objetos nativos).

jacmkno
fuente