Remova linhas com índices duplicados (Pandas DataFrame e TimeSeries)


251

Estou lendo alguns dados meteorológicos automatizados da web. As observações ocorrem a cada 5 minutos e são compiladas em arquivos mensais para cada estação meteorológica. Quando terminar de analisar um arquivo, o DataFrame se parece com isso:

                      Sta  Precip1hr  Precip5min  Temp  DewPnt  WindSpd  WindDir  AtmPress
Date                                                                                      
2001-01-01 00:00:00  KPDX          0           0     4       3        0        0     30.31
2001-01-01 00:05:00  KPDX          0           0     4       3        0        0     30.30
2001-01-01 00:10:00  KPDX          0           0     4       3        4       80     30.30
2001-01-01 00:15:00  KPDX          0           0     3       2        5       90     30.30
2001-01-01 00:20:00  KPDX          0           0     3       2       10      110     30.28

O problema que estou tendo é que, às vezes, um cientista volta e corrige as observações - não editando as linhas incorretas, mas anexando uma linha duplicada ao final de um arquivo. Um exemplo simples desse caso é ilustrado abaixo:

import pandas 
import datetime
startdate = datetime.datetime(2001, 1, 1, 0, 0)
enddate = datetime.datetime(2001, 1, 1, 5, 0)
index = pandas.DatetimeIndex(start=startdate, end=enddate, freq='H')
data1 = {'A' : range(6), 'B' : range(6)}
data2 = {'A' : [20, -30, 40], 'B' : [-50, 60, -70]}
df1 = pandas.DataFrame(data=data1, index=index)
df2 = pandas.DataFrame(data=data2, index=index[:3])
df3 = df2.append(df1)
df3
                       A   B
2001-01-01 00:00:00   20 -50
2001-01-01 01:00:00  -30  60
2001-01-01 02:00:00   40 -70
2001-01-01 03:00:00    3   3
2001-01-01 04:00:00    4   4
2001-01-01 05:00:00    5   5
2001-01-01 00:00:00    0   0
2001-01-01 01:00:00    1   1
2001-01-01 02:00:00    2   2

E então eu preciso df3me tornar:

                       A   B
2001-01-01 00:00:00    0   0
2001-01-01 01:00:00    1   1
2001-01-01 02:00:00    2   2
2001-01-01 03:00:00    3   3
2001-01-01 04:00:00    4   4
2001-01-01 05:00:00    5   5

Eu pensei que adicionar uma coluna de números de linha ( df3['rownum'] = range(df3.shape[0])) me ajudaria a selecionar a linha mais inferior para qualquer valor de DatetimeIndex, mas estou preso em descobrir as instruções group_byou pivot(ou ???) para fazer esse trabalho.


1
Outra maneira de obter duplicatas são dados por hora durante a noite, quando os relógios são definidos de volta para o horário de verão: 01:00, 2, 3, 2, 3, novamente, 4 ...
— denis

Respostas:


467

Eu sugeriria o uso do método duplicado no próprio Índice Pandas:

df3 = df3.loc[~df3.index.duplicated(keep='first')]

Enquanto todos os outros métodos funcionam, a resposta atualmente aceita é de longe o menos eficiente para o exemplo fornecido. Além disso, enquanto o método groupby é apenas um pouco menos eficiente, acho o método duplicado mais legível.

Usando os dados de amostra fornecidos:

>>> %timeit df3.reset_index().drop_duplicates(subset='index', keep='first').set_index('index')
1000 loops, best of 3: 1.54 ms per loop

>>> %timeit df3.groupby(df3.index).first()
1000 loops, best of 3: 580 µs per loop

>>> %timeit df3[~df3.index.duplicated(keep='first')]
1000 loops, best of 3: 307 µs per loop

Observe que você pode manter o último elemento alterando o argumento keep.

Deve-se notar também que esse método também funciona MultiIndex(usando o df1 conforme especificado no exemplo de Paul ):

>>> %timeit df1.groupby(level=df1.index.names).last()
1000 loops, best of 3: 771 µs per loop

>>> %timeit df1[~df1.index.duplicated(keep='last')]
1000 loops, best of 3: 365 µs per loop

3
locpode não ser necessário. Simplesmente faça df3 = df3[~df3.index.duplicated(keep='first')], que eliminará todas as linhas com índice duplicado, exceto a primeira ocorrência.
— lingjiankong 16/09/19

1
faria sentido usar isso para séries temporais muito grandes em que as duplicatas geralmente são apenas o primeiro ou o último valor?
— cheesus

1
o que ~ faz em df3 = df3.loc [~ df3.index.duplicated (keep = 'first')] se alguém não se importa em responder?
— jsl5703 27/02

3
@ jsl5703 Inverte a máscara. Então, transforma tudo o que era Verdadeiro Falso e vice-versa. Nesse caso, isso significa que selecionaremos os que não serão duplicados de acordo com o método.
— n8yoder 27/02

115

Minha resposta original, que agora está desatualizada, foi mantida para referência.

Uma solução simples é usar drop_duplicates

df4 = df3.drop_duplicates(subset='rownum', keep='last')

Para mim, isso operou rapidamente em grandes conjuntos de dados.

Isso requer que 'rownum' seja a coluna com duplicatas. No exemplo modificado, 'rownum' não possui duplicatas; portanto, nada é eliminado. O que realmente queremos é que os 'cols' sejam configurados para o índice. Eu não encontrei uma maneira de dizer ao drop_duplicates para considerar apenas o índice.

Aqui está uma solução que adiciona o índice como uma coluna de quadro de dados, elimina duplicatas e remove a nova coluna:

df3 = df3.reset_index().drop_duplicates(subset='index', keep='last').set_index('index')

E se você quiser as coisas de volta na ordem correta, basta chamar sorto quadro de dados.

df3 = df3.sort()

10
Outra variação é:df.reset_index().drop_duplicates(cols='index',take_last=True).set_index('index')
— Luciano

Embora esse método funcione, ele também cria duas cópias temporárias do DataFrame e tem um desempenho significativamente menor do que o uso do índice duplicado ou dos métodos de grupo sugeridos como respostas alternativas.
— N8yoder

Se o seu índice for um MultiIndex, reset_index()adicione as colunas level_0, level_1, etc. E se o seu índice tiver um nome, esse nome será usado no lugar do rótulo "index". Isso torna isso um pouco mais do que uma linha para fazer o certo para qualquer DataFrame. index_label = getattr(df.index, 'names', getattr(df.index, 'name', 'index'))então cols=index_label, set_index(index_labels)e mesmo isso não é infalível (não funcionará para multi-índices não nomeados).
— hobs

1
Mover o índice para uma coluna, limpar duplicatas e redefinir o índice foi incrível, era exatamente o que eu precisava!
— Mxplusb

Dada idx = df.index.name or 'index', pode-se também fazer df2 = df.reset_index(); df2.drop_duplicates(idx, inplace=True); df2.set_index(idx, inplace=True)a evitar as cópias intermédias (devido ao inplace=True)
— Anakhand

67

Oh meu. Isto é realmente tão simples!

grouped = df3.groupby(level=0)
df4 = grouped.last()
df4
                      A   B  rownum

2001-01-01 00:00:00   0   0       6
2001-01-01 01:00:00   1   1       7
2001-01-01 02:00:00   2   2       8
2001-01-01 03:00:00   3   3       3
2001-01-01 04:00:00   4   4       4
2001-01-01 05:00:00   5   5       5

Acompanhamento editar 2013-10-29 No caso em que tenho um bastante complexo MultiIndex, acho que prefiro a groupbyabordagem. Aqui está um exemplo simples para a posteridade:

import numpy as np
import pandas

# fake index
idx = pandas.MultiIndex.from_tuples([('a', letter) for letter in list('abcde')])

# random data + naming the index levels
df1 = pandas.DataFrame(np.random.normal(size=(5,2)), index=idx, columns=['colA', 'colB'])
df1.index.names = ['iA', 'iB']

# artificially append some duplicate data
df1 = df1.append(df1.select(lambda idx: idx[1] in ['c', 'e']))
df1
#           colA      colB
#iA iB                    
#a  a  -1.297535  0.691787
#   b  -1.688411  0.404430
#   c   0.275806 -0.078871
#   d  -0.509815 -0.220326
#   e  -0.066680  0.607233
#   c   0.275806 -0.078871  # <--- dup 1
#   e  -0.066680  0.607233  # <--- dup 2

e aqui está a parte importante

# group the data, using df1.index.names tells pandas to look at the entire index
groups = df1.groupby(level=df1.index.names)  
groups.last() # or .first()
#           colA      colB
#iA iB                    
#a  a  -1.297535  0.691787
#   b  -1.688411  0.404430
#   c   0.275806 -0.078871
#   d  -0.509815 -0.220326
#   e  -0.066680  0.607233

se eles tiverem nomes, caso contrário (se um nome for Nenhum), digamos level=[0,1]que funcionará se houver dois níveis df1.groupby(level=[0,1]).last(). Isto deve ser parte de pandas como uma cortesia paradrop_duplicates
— dashesy

@dashesy yeah. O uso df.index.namesé apenas uma maneira fácil de agrupar por todos os níveis do índice.
— 21715 Paul H das

Ótima solução, obrigado! Além disso, vou acrescentar que isso funciona em xarraypara lidar com índices de DateTime duplicados bem que fazem ds.resamplee ds.groupbyoperações falhar
— DRG

Alteração do meu comentário anterior: ele funciona em xarraycontanto que você mudar o grouped = df3.groupby(level=0)que grouped = df3.groupby(dim='time')ou qualquer que seja a dimensão é que contém duplicatas
— DRG

4

Infelizmente, não acho que o Pandas permita que alguém jogue fora dos índices. Eu sugeriria o seguinte:

df3 = df3.reset_index() # makes date column part of your data
df3.columns = ['timestamp','A','B','rownum'] # set names
df3 = df3.drop_duplicates('timestamp',take_last=True).set_index('timestamp') #done!

1

Se alguém como eu gosta de manipulação de dados encadeados usando a notação de ponto pandas (como canalização), o seguinte pode ser útil:

df3 = df3.query('~index.duplicated()')

Isso permite instruções de encadeamento como este:

df3.assign(C=2).query('~index.duplicated()').mean()

Eu tentei isso, mas não consegui fazê-lo funcionar .. Eu recebo um erro como este: TypeError: 'Series' objects are mutable, thus they cannot be hashed.. Isso realmente funcionou para você?
— Onno Eberhard

1

Remover duplicatas (mantendo primeiro)

idx = np.unique( df.index.values, return_index = True )[1]
df = df.iloc[idx]

Remover duplicatas (mantendo por último)

df = df[::-1]
df = df.iloc[ np.unique( df.index.values, return_index = True )[1] ]

Testes: 10k loops usando dados do OP

numpy method - 3.03 seconds
df.loc[~df.index.duplicated(keep='first')] - 4.43 seconds
df.groupby(df.index).first() - 21 seconds
reset_index() method - 29 seconds
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.