2012-04-20 4 views
8

Uso frequentemente sorted e groupby per trovare elementi duplicati in un iterabile. Ora vedo che è inaffidabile:Qual è un buon metodo pitone per trovare oggetti duplicati?

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 

Il motivo per cui il codice di cui sopra non riesce in Python 2.x è spiegato here.

Che cosa è un modo attendibile pitonico per trovare duplicati?

Ho cercato domande simili/risposte su SO. Il meglio di loro è "In Python, how do I take a list and reduce it to a list of duplicates?", ma la soluzione accettata non è pitonica (è una multilinea procedurale per ... se ... aggiungi ... altro ... aggiungi ... restituisci risultato) e altre soluzioni sono inaffidabili (dipende dalla transitività insoddisfatta dell'operatore "<") o è lenta (O n * n).

[EDIT] Chiuso. La risposta accettata mi ha aiutato a riassumere le conclusioni nella mia risposta più generale.

Mi piace utilizzare i tipi incorporati per rappresentare ad es. strutture ad albero. Questo è il motivo per cui ho paura di mescolare ora.

risposta

11

Nota: Assume voci sono 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

Qual è la complessità tempo qui? O (n * ln (n))? – Akavall

+0

Questo presuppone che tutte le voci della lista siano lavabili (dato che si stanno creando chiavi contatore con esse)? E se ci fosse una lista come uno degli elementi di dati? – PaulMcG

+0

Quando ci penso di più, penso che la complessità del tempo sia in realtà O (n). Quando Counter crea un dizionario, dobbiamo passare attraverso ogni elemento una volta, quindi quando eseguiamo il looping di un dizionario, guardate anche ogni elemento una volta (e cercate il tempo è costante). Quindi, quando la dimensione del problema aumenta, il tempo di eseguirlo aumenta in proporzione lineare, cioè O (n). – Akavall

1

Conclusione:

  • Se tutte le voci sono hashable ed è almeno Python 2.7, la soluzione migliore è al di sopra di collections.Counter.
  • Se alcuni articoli non sono hashable o Python 2.7+ non c'è, allora la soluzione groupby(sorted(..)) è molto buona in condizioni
    • non combinano str e unicode o
    • non utilizzare qualsiasi tipo con il nome collocato tra aphabetically "str" ​​e "unicode". (Tipicamente "tupla" o "tipo")
  • Se i dati sono nel calcolo dell'hash e di tipo misto, niente sopra possono essere usate e quindi la migliore è di usare:
    • Counter(map(pickled.dumps, data)) anziché Counter(data) e infine deserializzare o
    • groupby(sorted(data, key=pickled.dumps)) se deserializzazione è indesiderabile o nessuna pitone 2,7
  • a "naive solution" discusso di seguito può essere migliore di decapaggio per numero molto limitato di elementi approximatel y meno di 300.

Tutti gli altri soluzioni in altre questioni sono peggiori attualmente.

Note:

  • E sembra che il contatore di classe può essere copiato & incollato ad abbassare versioni di Python.
  • Un "naive solution" che ricerca ogni elemento in dati interi può essere utilizzato per un numero molto piccolo di elementi. Ha il vantaggio che non richiede dati lavabili e non dipende dalla transitività dell'operatore "<" predefinito, ma per più di 25 oggetti lavabili è meglio il contatore e per più di 300 oggetti "selvaggi" non selezionabili è meglio il contatore su sottaceto elementi.

ho pensato di preselezione articoli per tipo o prorogare di hash per articoli hashable, aiuta attualmente, ma non è una soluzione sicura, perché lo stesso problema sarebbe stato con "<" Operatore liste insite, tuple etc.

1

Questo argomento è interessante per me, quindi ho cronometrato la soluzione precedente rispetto alla soluzione accettata nell'altro thread.

Il metodo Counter è molto elegante; tuttavia, la risposta accettata in questa discussione In Python, how do I take a list and reduce it to a list of duplicates? sembra essere circa 2 volte più veloce.

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 

Risultati:

counter_way: 1.15775845813 
accepted_way: 0.531060022992 

ho provato questo in diverse specifiche e il risultato sempre lo stesso.

+0

Apprezzo su * collections.Counter * che la frequenza può essere facile scoperto. Lo desideravo nella mente, ma non volevo perdere una buona idea in condizioni troppo restrittive. Puoi scrivere un contatore più efficiente? – hynekcer

+0

Penso che per quello che fa Counter è tanto efficiente quanto lo è. E hai ragione che Counter fornisce anche la frequenza per ogni articolo, ma il prezzo per questo è il tempo di esecuzione più lungo. Nel metodo contatore, dapprima passiamo in rassegna ogni elemento dell'elenco e otteniamo le frequenze per ogni elemento, quindi eseguiamo il ciclo del dizionario creato per contatore per ottenere elementi che si presentano più di uno. Nell'altro metodo, passiamo in rassegna ogni elemento una sola volta. – Akavall

0

Tuttavia, c'è una trappola in entrambe le soluzioni. Il motivo è che unisce i valori con lo stesso hash. Quindi, dipende se i valori utilizzati possono avere lo stesso hash. Non è un commento folle come si potrebbe pensare (sono stato anche sorpreso prima), perché Python ha alcuni valori in modo speciale. Prova:

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) 

Il 1, 1.0, e Vera hanno lo stesso hash per definizione, e allo stesso modo il 0, 0.0, e False. Esso stampa il seguente sulla mia console (pensare alle ultime due righe - tutti i valori dovrebbero in realtà essere duplicati):

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

Questo non è un problema, questo è il modo in cui l'equality '==' è definito in Python: 'assert 0 == 0.0 == False e 1 == 1.0 == True e 'a' == u'a''. Anche i numeri complessi possono essere uguali '1 == 1.0 + 0.0j'. Se si desidera confrontare esattamente e utilizzare anche tipi composti come tuple, è necessario confrontare la rappresentazione decapata come l'unica soluzione possibile. È possibile verificare che le chiavi del dizionario siano univoche anche se hanno lo stesso hash. (Creare un dizionario sul sistema a 32 bit con un milione di tuple casuali come chiavi: con una probabilità superiore al 99% si trovano gli stessi hash ma chiavi diverse). – hynekcer

+0

Bene, le aspettative potrebbero essere diverse. Tuttavia, posso immaginare che '[0, False]' sia considerato come la lista di valori unici. – pepr

+0

pepr: Qual è il tuo metodo? Come si prova che '0' un' False' sono diversi in * Python *? – hynekcer

0

Solo perché ero curioso, ecco la soluzione che fa la differenza fra 0, False , 0.0, ecc. Si basa sull'ordinamento della sequenza con my_cmp che prende in considerazione anche il tipo dell'articolo. È molto lento rispetto alle soluzioni sopra menzionate, ovviamente. Questo a causa dell'ordinamento. Ma confronta i risultati!

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 

Esso stampa sulla mia console:

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

Questa soluzione è focalizzata su tipi semplici ma non riesce su tipi composti. Esempio: 'a = [(0,), (False,)] + [('x',), ((),), (u'x ',)] * 10000', espressione' duplicates_via_sort_with_types (a) ' , risultato: '[(False,)]' Non riconosce 0 e False poiché li consideri diversi, non riconosce valori che sono esattamente uguali sull'altro lato. – hynekcer

+0

@hynekcer: Sì, hai ragione. – pepr