2012-04-20 10 views
8

Con frecuencia uso sorted y groupby para encontrar elementos duplicados en un iterable. Ahora veo que no es fiable:¿Cuál es una buena manera pitonica de encontrar objetos duplicados?

from itertools import groupby 
data = 3 * ('x ', (1,), u'x') 
duplicates = [k for k, g in groupby(sorted(data)) if len(list(g)) > 1] 
print duplicates 
# [] printed - no duplicates found - like 9 unique values 

La razón por la que el código anterior falla en Python 2.x se explica here.

¿Cuál es una manera confiable de encontrar duplicados?

Busqué preguntas/respuestas similares en SO. El mejor de ellos es "In Python, how do I take a list and reduce it to a list of duplicates?", pero la solución aceptada no es pitónico (es multilínea procesal para ... si ... agregar ... más ... agregar ... devolver resultado) y otras soluciones no son confiables (depende de la transitividad no realizada del operador "<") o es lenta (O n * n).

[EDIT] Cerrado. La respuesta aceptada me ayudó a resumir las conclusiones en mi respuesta a continuación más general.

Me gustaría usar tipos incorporados para representar, p. estructuras de árbol. Es por eso que ahora tengo miedo de mezclar.

Respuesta

11

Nota: Se asume entradas son hashable

>>> from collections import Counter 
>>> data = 3 * ('x ', (1,), u'x') 
>>> [k for k, c in Counter(data).iteritems() if c > 1] 
[u'x', 'x ', (1,)] 
+0

¿Cuál es la complejidad de tiempo aquí? O (n * ln (n))? – Akavall

+0

¿Asume que todas las entradas de la lista son hashable (ya que está creando claves de contador con ellas)? ¿Qué pasa si hay una lista como uno de los elementos de datos? – PaulMcG

+0

Cuando lo pienso más, creo que la complejidad del tiempo es en realidad O (n). Cuando Counter crea un diccionario, tenemos que pasar por cada elemento una vez, luego cuando estamos recorriendo un diccionario, también observas cada elemento una vez (y el tiempo de búsqueda es constante). Por lo tanto, cuando aumenta el tamaño del problema, el tiempo para ejecutarlo aumenta en proporción lineal, es decir O (n). – Akavall

1

Conclusión:

  • Si todos los elementos son hashable y es al menos 2,7 Python, la mejor solución es más arriba con collections.Counter.
  • Si algunos artículos no son hashable o Python 2.7+ no está presente, entonces la solución groupby(sorted(..)) es muy buena en condiciones
    • no combinan str y unicode o
    • no utilizar cualquier tipo con el nombre colocado aphabetically entre "str" ​​y "unicode". (Por lo general "tupla" o "tipo")
  • Si los datos son unhashable y de tipo mixto, nada por encima se pueden utilizar y entonces el mejor es utilizar:
    • Counter(map(pickled.dumps, data)) en lugar de Counter(data) y finalmente unpickle o
    • groupby(sorted(data, key=pickled.dumps)) si desestibado no es deseable o no pitón 2,7
  • a "naive solution" se discute a continuación puede ser mejor que para el decapado de muy pequeño número de elementos approximatel y menos de 300.

Todas las demás soluciones en otras preguntas son peores actualmente.

Notas:

  • Parece que el contador de la clase puede ser copiado & pegado a bajar versiones de Python.
  • Un "naive solution" que busca cada elemento en datos completos se puede utilizar para una cantidad muy pequeña de elementos. Tiene la ventaja de que no requiere datos transitables y no depende de la transitividad del operador predeterminado "<", pero para más de 25 artículos lavables es mejor el contador y para más de 300 elementos "salvajes" indesarmables es mejor que el contador en escabeche artículos.

pensé en la preclasificación artículos por tipo o extenderlos por hash para artículos hashable, que ayuda en la actualidad, pero no es una solución segura ya que el mismo problema sería con "<" operador listas insite, tuplas etc.

1

Este tema me resulta interesante, por lo que calculé la solución anterior frente a la solución aceptada en el otro hilo.

El método de contador es este hilo es muy elegante; sin embargo, la respuesta aceptada en este hilo In Python, how do I take a list and reduce it to a list of duplicates? parece ser aproximadamente 2 veces más rápida.

import random as rn 
import timeit 
from collections import Counter 

a = [rn.randint(0,100000) for i in xrange(10000)] 

def counter_way(x): 
    return [k for k,v in Counter(x).iteritems() if v > 1] 

def accepted_way(x): #accepted answer in the linked thread 
    duplicates = set() 
    found = set() 
    for item in x: 
     if item in found: 
      duplicates.add(item) 
     else:   
      found.add(item) 
    return duplicates 


t1 = timeit.timeit('counter_way(a)', 'from __main__ import counter_way, a', number = 100) 
print "counter_way: ", t1 
t2 = timeit.timeit('accepted_way(a)','from __main__ import accepted_way, a', number = 100) 
print "accepted_way: ", t2 

Resultados:

counter_way: 1.15775845813 
accepted_way: 0.531060022992 

Probé este bajo diferentes especificaciones y el resultado siempre es el mismo.

+0

Aprecio en * collections.Counter * esa frecuencia puede ser fácil descubierto. Lo deseé en la mente, pero no quería perder una buena idea por condiciones demasiado restrictivas. ¿Puedes escribir un contador más eficiente? – hynekcer

+0

Creo que lo que hace Counter es tan eficiente como lo es. Y tiene razón en que Counter también proporciona frecuencia para cada artículo, pero el precio es más largo. En el método de contador, primero revisamos cada elemento de la lista y obtenemos frecuencias para cada elemento, y luego hacemos un bucle en el diccionario creado por Contador para obtener elementos que aparecen más de uno. En el otro método, solo revisamos cada elemento una vez. – Akavall

0

Sin embargo, hay una trampa en ambas soluciones. La razón es que combina los valores con el mismo hash. Entonces, depende de si los valores utilizados pueden tener el mismo hash. No es tan loco comentario como se puede pensar (también me sorprendió antes), porque Python hashes algunos valores de la manera especial. Proveedores:

from collections import Counter 


def counter_way(x): 
    return [k for k,v in Counter(x).iteritems() if v > 1] 

def accepted_way(x): #accepted answer in the linked thread 
    duplicates = set() 
    found = set() 
    for item in x: 
     if item in found: 
      duplicates.add(item) 
     else:   
      found.add(item) 
    return duplicates 


a = ('x ', (1,), u'x') * 2 

print 'The values:', a 
print 'Counter way duplicates:', counter_way(a) 
print 'Accepted way duplicates:', accepted_way(a) 
print '-' * 50 

# Now the problematic values. 
a = 2 * (0, 1, True, False, 0.0, 1.0) 

print 'The values:', a 
print 'Counter way duplicates:', counter_way(a) 
print 'Accepted way duplicates:', accepted_way(a) 

La 1, 1,0, y Verdadero tienen el mismo hash, por definición, y de manera similar al 0, 0,0, y False. Se imprime el siguiente en mi consola (pensar en las dos últimas líneas - todos los valores realmente debería ser duplicados):

c:\tmp\___python\hynekcer\so10247815>python d.py 
The values: ('x ', (1,), u'x', 'x ', (1,), u'x') 
Counter way duplicates: [u'x', 'x ', (1,)] 
Accepted way duplicates: set([u'x', 'x ', (1,)]) 
-------------------------------------------------- 
The values: (0, 1, True, False, 0.0, 1.0, 0, 1, True, False, 0.0, 1.0) 
Counter way duplicates: [0, 1] 
Accepted way duplicates: set([False, True]) 
+0

Esto no es un pitfal, así es como se define la igualdad '==' en Python: 'assert 0 == 0.0 == False y 1 == 1.0 == True y 'a' == u'a''. Incluso los números complejos pueden ser iguales a '1 == 1.0 + 0.0j'. Si desea comparar exactamente y usar también tipos compuestos como tuplas, debe comparar la representación en escabeche como la única solución posible. Puede verificar que las claves del diccionario sean únicas incluso si tienen el mismo hash. (Cree un diccionario en un sistema de 32 bits con un millón de tuplas aleatorias como claves. Con probabilidad de más del 99%, encontrará algunos mismos valores hash pero diferentes claves). – hynekcer

+0

Bueno, las expectativas pueden diferir. Sin embargo, puedo imaginar que '[0, False]' se considere como la lista de valores únicos. – pepr

+0

pepr: ¿Cuál es su método? ¿Cómo se prueba que '0' y' False' son diferentes en * Python *? – hynekcer

0

El hecho de que yo era curiosa, aquí está la solución que marca la diferencia entre el 0, Falso , 0.0, etc. Se basa en ordenar la secuencia con my_cmp que toma en consideración también el tipo de artículo. Es muy lento en comparación con las soluciones mencionadas anteriormente, por supuesto. Esto es debido a la clasificación. ¡Pero compare los resultados!

import sys 
import timeit 
from collections import Counter 

def empty(x): 
    return 

def counter_way(x): 
    return [k for k,v in Counter(x).iteritems() if v > 1] 

def accepted_way(x): #accepted answer in the linked thread 
    duplicates = set() 
    found = set() 
    for item in x: 
     if item in found: 
      duplicates.add(item) 
     else:   
      found.add(item) 
    return duplicates 


def my_cmp(a, b): 
    result = cmp(a, b) 
    if result == 0: 
     return cmp(id(type(a)), id(type(b))) 
    return result  


def duplicates_via_sort_with_types(x, my_cmp=my_cmp): 

    last = '*** the value that cannot be in the sequence by definition ***' 
    duplicates = [] 
    added = False 
    for e in sorted(x, cmp=my_cmp): 
     if my_cmp(e, last) == 0: 
      ##print 'equal:', repr(e), repr(last), added 
      if not added: 
       duplicates.append(e) 
       ##print 'appended:', e 
       added = True 
     else: 
      ##print 'different:', repr(e), repr(last), added 
      last = e 
      added = False 
    return duplicates 


a = [0, 1, True, 'a string', u'a string', False, 0.0, 1.0, 2, 2.0, 1000000, 1000000.0] * 1000 

print 'Counter way duplicates:', counter_way(a) 
print 'Accepted way duplicates:', accepted_way(a) 
print 'Via enhanced sort:', duplicates_via_sort_with_types(a) 
print '-' * 50 

# ... and the timing 
t3 = timeit.timeit('empty(a)','from __main__ import empty, a', number = 100) 
print "empty: ", t3 
t1 = timeit.timeit('counter_way(a)', 'from __main__ import counter_way, a', number = 100) 
print "counter_way: ", t1 
t2 = timeit.timeit('accepted_way(a)','from __main__ import accepted_way, a', number = 100) 
print "accepted_way: ", t2 
t4 = timeit.timeit('duplicates_via_sort_with_types(a)','from __main__ import duplicates_via_sort_with_types, a', number = 100) 
print "duplicates_via_sort_with_types: ", t4 

Imprime en mi consola:

c:\tmp\___python\hynekcer\so10247815>python e.py 
Counter way duplicates: [0, 1, 2, 'a string', 1000000] 
Accepted way duplicates: set([False, True, 2.0, u'a string', 1000000.0]) 
Via enhanced sort: [False, 0.0, 0, True, 1.0, 1, 2.0, 2, 1000000.0, 1000000, 'a string', u'a string'] 
-------------------------------------------------- 
empty: 2.11195471969e-05 
counter_way: 0.76977053612 
accepted_way: 0.496547434023 
duplicates_via_sort_with_types: 11.2378848197 
+0

Esta solución se centra en tipos simples pero falla en tipos compuestos. Ejemplo: 'a = [(0,), (False,)] + [('x',), ((),), (u'x ',)] * 10000', expresión' duplicates_via_sort_with_types (a) ' , resultado: '[(False,)]' No reconoce 0 y False como quiera que los considere diferentes, no reconoce valores que son exactamente los mismos en el otro lado. – hynekcer

+0

@hynekcer: Sí, tienes razón. – pepr

Cuestiones relacionadas