Rimuovi le righe con indici duplicati (Pandas DataFrame e TimeSeries)


251

Sto leggendo alcuni dati meteorologici automatizzati dal web. Le osservazioni si verificano ogni 5 minuti e vengono compilate in file mensili per ciascuna stazione meteorologica. Una volta terminato di analizzare un file, DataFrame è simile al seguente:

                      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

Il problema che sto riscontrando è che a volte uno scienziato torna indietro e corregge le osservazioni, non modificando le righe errate, ma aggiungendo una riga duplicata alla fine di un file. Di seguito è riportato un semplice esempio di un caso del genere:

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 quindi devo df3diventare uniformemente:

                       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

Ho pensato che l'aggiunta di una colonna di numeri di riga ( df3['rownum'] = range(df3.shape[0])) mi avrebbe aiutato a selezionare la riga più in basso per qualsiasi valore di DatetimeIndex, ma sono bloccato nel capire le istruzioni group_byo pivot(o ???) per farlo funzionare.


1
Un altro modo per ottenere duplicati sono i dati orari durante la notte quando vengono ripristinati gli orologi per l'ora legale: 1:00, 2, 3, 2, 3 di nuovo, 4 ...
denis,

Risposte:


467

Suggerirei di utilizzare il metodo duplicato sull'indice Pandas stesso:

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

Mentre tutti gli altri metodi funzionano, la risposta attualmente accettata è di gran lunga la meno performante per l'esempio fornito. Inoltre, mentre il metodo groupby è solo leggermente meno performante, trovo che il metodo duplicato sia più leggibile.

Utilizzando i dati di esempio forniti:

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

Si noti che è possibile mantenere l'ultimo elemento modificando l'argomento keep.

Va anche notato che questo metodo funziona anche con MultiIndex(usando df1 come specificato nell'esempio di 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
locpotrebbe non essere necessario. Basta fare df3 = df3[~df3.index.duplicated(keep='first')], che lascerà cadere tutte le righe con indice duplicato tranne la prima occorrenza.
lingjiankong,

1
avrebbe senso usarlo per serie storiche molto grandi in cui i duplicati sono in genere solo il primo o l'ultimo valore?
cheesus,

1
cosa fa ~ in df3 = df3.loc [~ df3.index.duplicated (keep = 'first')] se a qualcuno non dispiace rispondere?
jsl5703,

3
@ jsl5703 Inverte la maschera. Quindi trasforma tutto ciò che era True False e viceversa. In questo caso, ciò significa che selezioneremo quelli che non sono duplicati secondo il metodo.
n8yoder

115

La mia risposta originale, che ora è obsoleta, è stata conservata come riferimento.

Una soluzione semplice è usare drop_duplicates

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

Per me, questo ha funzionato rapidamente su grandi set di dati.

Ciò richiede che "rownum" sia la colonna con duplicati. Nell'esempio modificato, "rownum" non ha duplicati, quindi nulla viene eliminato. Ciò che vogliamo veramente è che i "cols" siano impostati sull'indice. Non ho trovato un modo per dire a drop_duplicates di considerare solo l'indice.

Ecco una soluzione che aggiunge l'indice come colonna di un frame di dati, ne elimina i duplicati, quindi rimuove la nuova colonna:

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

E se vuoi che le cose sorttornino nell'ordine corretto, basta chiamare il frame di dati.

df3 = df3.sort()

10
Un'altra variazione su questo è:df.reset_index().drop_duplicates(cols='index',take_last=True).set_index('index')
Luciano,

Mentre questo metodo funziona, crea anche due copie temporanee di DataFrame ed è significativamente meno performante rispetto all'utilizzo dell'indice duplicato o dei metodi groupby suggeriti come risposte alternative.
n8yoder,

Se il tuo indice è un MultiIndex, reset_index()aggiunge le colonne level_0, level_1, ecc. E se il tuo indice ha un nome, quel nome verrà usato al posto dell'etichetta "index". Ciò rende questo un po 'più di una linea per farlo bene per qualsiasi DataFrame. index_label = getattr(df.index, 'names', getattr(df.index, 'name', 'index'))allora cols=index_labelallora set_index(index_labels)e anche questo non è infallibile (non funzionerà con multiindici senza nome).
piani cottura

1
Spostare l'indice in una colonna, cancellare i duplicati e reimpostare l'indice era fantastico, era esattamente quello di cui avevo bisogno!
mxplusb,

Dato idx = df.index.name or 'index', si potrebbe anche fare df2 = df.reset_index(); df2.drop_duplicates(idx, inplace=True); df2.set_index(idx, inplace=True)per evitare le copie intermedie (a causa del inplace=True)
Anakhand

67

Oh mio. Questo è in realtà così semplice!

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

Follow-up edit 29-10-2013 Nel caso in cui abbia un aspetto abbastanza complesso MultiIndex, penso di preferire l' groupbyapproccio. Ecco un semplice esempio per i posteri:

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

ed ecco la 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 hanno nomi, altrimenti (se un nome è Nessuno) diciamo level=[0,1]che funzionerà se ci sono 2 livelli df1.groupby(level=[0,1]).last(). Questo dovrebbe essere parte di Panda come un omaggio adrop_duplicates
dashesy

@dashesy sì. L'utilizzo df.index.namesè solo un modo semplice per raggruppare per tutti i livelli dell'indice.
Paul H,

Ottima soluzione, grazie! Aggiungerò anche che questo funziona anche xarrayper la gestione di indici DateTime duplicati e che falliscono ds.resamplee le ds.groupbyoperazioni
drg

Modifica il mio commento precedente: funziona in xarraytutto il tempo come si cambia il grouped = df3.groupby(level=0)per grouped = df3.groupby(dim='time')o qualunque sia la dimensione è che contiene i duplicati
DRG

4

Sfortunatamente, non credo che i panda permettano di eliminare gli indici dagli indici. Suggerirei quanto segue:

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 a qualcuno come me piace la manipolazione di dati concatenabili utilizzando la notazione pandas dot (come il piping), potrebbe essere utile quanto segue:

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

Ciò consente di concatenare dichiarazioni come questa:

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

Ho provato questo ma non sono riuscito a farlo funzionare .. Ho ricevuto un errore come questo: TypeError: 'Series' objects are mutable, thus they cannot be hashed.. Ha funzionato davvero per te?
Onno Eberhard,

1

Rimuovi duplicati (prima cosa)

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

Rimuovi duplicati (Keeping Last)

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

Test: 10k loop utilizzando i dati di 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.