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.

Paul H
fonte
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
n8yoder
fonte
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()
DA
fonte
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
Paul H
fonte
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!
user128754
fonte
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()
bbiegel
fonte
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
Mott The Tuple
fonte