Remover duplicatas de uma lista de listas

116

Tenho uma lista de listas em Python:

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

E eu quero remover elementos duplicados dele. Era se fosse uma lista normal não de listas que eu poderia usar set. Mas, infelizmente, essa lista não é hashable e não pode fazer um conjunto de listas. Apenas de tuplas. Assim, posso transformar todas as listas em tuplas e usar definir e voltar para listas. Mas isso não é rápido.

Como isso pode ser feito da maneira mais eficiente?

O resultado da lista acima deve ser:

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

Eu não me importo em preservar a ordem.

Nota: esta questão é semelhante, mas não exatamente o que eu preciso. Procurado SO, mas não encontrei duplicata exata.


Avaliação comparativa:

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étodo quadrático) o mais rápido de todos para listas curtas. Para listas longas, é mais rápido que todos, exceto o método groupby. Isso faz sentido?

Para uma lista curta (a do código), 100.000 iterações:

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

Para uma lista mais longa (aquela no código duplicada 5 vezes):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
Zaharpopov
fonte
1
Por "isso não é rápido", você quer dizer que você cronometrou e não é rápido o suficiente para sua aplicação, ou que você acha que não é rápido?
Torsten Marek
@Torsten, parece que copiar demais para ser um método inteligente. desculpe, pressentimento. copie as listas para as tuplas, depois para o conjunto e de volta para a lista de listas (copie novamente as tuplas para as listas)
zaharpopov
@zaharpopov: não é assim que Python funciona, nada será copiado , apenas novos contêineres para os elementos existentes (embora para ints, seja praticamente o mesmo)
Jochen Ritzel
3
1. os tempos para os métodos que usam classificação são deflacionados, porque "k" é devolvido à variante classificada. 2. O último método é mais rápido porque a maneira como você gera os dados de teste deixa você com no máximo 4 elementos distintos. Experimente o sth. como K = [[int (u) para u em str (random.randrange (1, 1000))] para _ no intervalo (100)]
Torsten Marek
@Torsten: obrigado fixo. mas ainda assim o método de loop é rápido mesmo quando há apenas uma duplicata na lista de 10
zaharpopov

Respostas:

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

itertoolsmuitas vezes oferece as soluções mais rápidas e poderosas para este tipo de problemas, e é bem vale a pena ficar intimamente familiarizado com -!)

Edit : como mencionei em um comentário, os esforços de otimização normais são focados em grandes entradas (a abordagem big-O) porque é muito mais fácil que oferece bons retornos sobre os esforços. Mas às vezes (essencialmente para "gargalos tragicamente cruciais" em profundos loops internos de código que estão ultrapassando os limites dos limites de desempenho), pode ser necessário entrar em muito mais detalhes, fornecendo distribuições de probabilidade, decidindo quais medidas de desempenho otimizar (talvez o limite superior ou o percentil 90 é mais importante do que uma média ou mediana, dependendo dos aplicativos), realizando verificações possivelmente heurísticas no início para escolher algoritmos diferentes dependendo das características dos dados de entrada e assim por diante.

Medidas cuidadosas de desempenho de "ponto" (código A versus código B para uma entrada específica) fazem parte desse processo extremamente caro e o módulo de biblioteca padrão timeitajuda aqui. No entanto, é mais fácil usá-lo em um prompt de shell. Por exemplo, aqui está um pequeno módulo para mostrar a abordagem geral para este problema, salve-o 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))

Observe a verificação de sanidade (realizada quando você acabou de fazer python nodup.py) e a técnica de içamento básica (faça nomes globais constantes locais para cada função para velocidade) para colocar as coisas em pé de igualdade.

Agora podemos executar verificações na pequena lista de exemplos:

$ 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 a abordagem quadrática tem constantes pequenas o suficiente para torná-la atraente para listas minúsculas com poucos valores duplicados. Com uma pequena lista sem duplicatas:

$ 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

a abordagem quadrática não é ruim, mas as classificadas e agrupadas são melhores. Etc etc.

Se (como a obsessão com o desempenho sugere) esta operação está em um loop interno central de seu aplicativo extraindo limites, vale a pena tentar o mesmo conjunto de testes em outras amostras de entrada representativas, possivelmente detectando alguma medida simples que poderia permitir heuristicamente escolha uma ou outra abordagem (mas a medida deve ser rápida, é claro).

Também vale a pena considerar manter uma representação diferente para k- por que tem que ser uma lista de listas em vez de um conjunto de tuplas em primeiro lugar? Se a tarefa de remoção de duplicatas for frequente e a criação de perfil mostrar que ela é o gargalo de desempenho do programa, manter um conjunto de tuplas o tempo todo e obter uma lista de listas apenas se e onde necessário, pode ser mais rápido no geral, por exemplo.

Alex Martelli
fonte
@alex obrigado pela alternativa. este método é quase a mesma velocidade do de danben, um pouco mais rápido
zaharpopov
@alex: estranhamente, isso é mais lento do que um método quadrático ingênuo para listas mais curtas (ver edição da pergunta)
zaharpopov
@zaharpopov: é assim apenas no seu caso especial, cf. meu comentário à pergunta.
Torsten Marek
@zaharpopov, se você der uma distribuição de probabilidade de comprimentos de lista e sublista e chance de duplicatas, é possível (com grande esforço) calcular / medir a distribuição de probabilidade de tempos de execução para qualquer código e otimizar qualquer medida de que você precisa (mediana, média, 90º centil, qualquer que seja). Isso quase nunca é feito por causa do ROI muito baixo: normalmente o foco é no caso muito mais fácil de grandes entradas (a abordagem big-O), onde algoritmos inferiores realmente prejudicariam terrivelmente o desempenho. E eu não vejo você especificar nenhuma distribuição de probabilidade em seu Q de qualquer maneira ;-).
Alex Martelli
@zaharpov, que bom que gostou!
Alex Martelli
21

Fazendo manualmente, criando uma nova klista e adicionando entradas não encontradas até agora:

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

Simples de compreender, e você preserva a ordem da primeira ocorrência de cada elemento, se isso for útil, mas acho que é quadrática em complexidade, pois você está procurando new_kcada elemento por completo .

Paul Stephenson
fonte
@paul: muito estranho - este método é mais rápido que todos os outros
zaharpopov
Suspeito que esse método não será mais rápido para listas muito longas. Depende da sua aplicação: se você realmente tem apenas listas de seis elementos com duas duplicatas, então qualquer solução provavelmente será rápida o suficiente e você deve ir com o código mais claro.
Paul Stephenson
@zaharpopov, não é quadrático em seu benchmark porque você duplica a mesma lista indefinidamente. Você está fazendo um benchmarking com uma caixa de canto linear.
Mike Graham
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5mostrará o comportamento quadrático muito bem
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]]

Não sei se é necessariamente mais rápido, mas você não precisa usar para tuplas e conjuntos.

danben
fonte
Obrigado danben. isso mais rápido do que virar para tuplas, 'definir' e voltar para as listas?
zaharpopov
Você poderia testar isso facilmente - escreva os dois métodos de desduplicação, gere algumas listas aleatórias usando randome cronometrar time.
danben
4

Todas as setsoluções relacionadas a esse problema até agora requerem a criação de um todo setantes da iteração.

É possível tornar isso preguiçoso e, ao mesmo tempo, preservar a ordem, iterando a lista de listas e adicionando a um "visto" set. Então, só produzirá uma lista se ela não for encontrada neste rastreador set.

Esta unique_everseenreceita está disponível na itertools documentação . Também está disponível na toolzbiblioteca de terceiros :

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

Observe que a tupleconversão é necessária porque as listas não são hashable.

jpp
fonte
3

Mesmo a sua lista "longa" é muito curta. Além disso, você os escolheu para corresponder aos dados reais? O desempenho varia de acordo com a aparência real desses dados. Por exemplo, você tem uma lista curta repetida indefinidamente para fazer uma lista mais longa. Isso significa que a solução quadrática é linear em seus benchmarks, mas não na realidade.

Para listas realmente grandes, o código definido é sua melhor aposta - é linear (embora precise de muito espaço). Os métodos sort e groupby são O (n log n) e o método loop in é obviamente quadrático, então você sabe como eles escalarão conforme n se tornar realmente grande. Se esse for o tamanho real dos dados que você está analisando, quem se importa? É minúsculo.

Aliás, estou vendo uma notável aceleração se não formar uma lista intermediária para fazer o conjunto, ou seja, se eu substituir

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

com

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

A solução real pode depender de mais informações: Tem certeza de que uma lista de listas é realmente a representação de que você precisa?

Mike Graham
fonte
3

Lista de tupla e {} pode ser usada para remover duplicatas

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

Crie um dicionário com tupla como chave e imprima as chaves.

  • criar dicionário com tupla como chave e índice como valor
  • imprimir lista de chaves do dicionário

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]]
Super Nova
fonte
1

Isso deve 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
fonte
0

Estranhamente, as respostas acima remove as 'duplicatas', mas e se eu quiser remover também o valor duplicado ?? O seguinte deve ser útil e não cria um novo objeto na memória!

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)

e o / p é:

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

Outra solução provavelmente mais genérica e simples é criar um dicionário codificado pela versão da string dos objetos e obter os valores () no final:

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

O problema é que isso só funciona para objetos cuja representação de string é uma chave única boa o suficiente (o que é verdadeiro para a maioria dos objetos nativos).

Jacmkno
fonte