Eliminar filas con índices duplicados (Pandas DataFrame y TimeSeries)

251

Estoy leyendo algunos datos meteorológicos automatizados de la web. Las observaciones ocurren cada 5 minutos y se compilan en archivos mensuales para cada estación meteorológica. Una vez que termine de analizar un archivo, el DataFrame se ve así:

                      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

El problema que tengo es que a veces un científico regresa y corrige las observaciones, no editando las filas erróneas, sino agregando una fila duplicada al final de un archivo. A continuación se ilustra un ejemplo simple de tal caso:

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

Y, entonces, necesito df3convertirme en realidad:

                       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

Pensé que agregar una columna de números de fila ( df3['rownum'] = range(df3.shape[0])) me ayudaría a seleccionar la fila inferior para cualquier valor de la DatetimeIndex, pero estoy atascado en averiguar las declaraciones group_byo pivot(o ???) para que funcione.

Paul H
fuente
1
Otra forma de obtener duplicados son los datos por hora en la noche cuando los relojes se retrasan para el horario de verano: 1 AM, 2, 3, 2, 3 nuevamente, 4 ...
denis

Respuestas:

467

Sugeriría usar el método duplicado en el Índice Pandas:

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

Si bien todos los demás métodos funcionan, la respuesta actualmente aceptada es, con mucho, la menos eficaz para el ejemplo proporcionado. Además, si bien el método groupby es solo un poco menos eficaz , creo que el método duplicado es más legible.

Usando los datos de muestra proporcionados:

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

Tenga en cuenta que puede conservar el último elemento cambiando el argumento de mantenimiento.

También se debe tener en cuenta que este método también funciona MultiIndex(usando df1 como se especifica en el ejemplo 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
fuente
3
locPuede que no sea necesario. Simplemente hazlo df3 = df3[~df3.index.duplicated(keep='first')], lo que eliminará todas las filas con índice duplicado excepto la primera aparición.
lingjiankong
1
¿Tendría sentido usar esto para series de tiempo muy grandes donde los duplicados generalmente son solo el primer o el último valor?
cheesus
1
¿Qué hace ~ en df3 = df3.loc [~ df3.index.duplicated (keep = 'first')] si a alguien no le importa responder?
jsl5703
3
@ jsl5703 Invierte la máscara. Entonces convierte todo lo que era Verdadero Falso y viceversa. En este caso, eso significa que seleccionaremos los que no están duplicados de acuerdo con el método.
n8yoder
115

Mi respuesta original, que ahora está desactualizada, se mantuvo como referencia.

Una solución simple es usar drop_duplicates

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

Para mí, esto funcionó rápidamente en grandes conjuntos de datos.

Esto requiere que 'rownum' sea la columna con duplicados. En el ejemplo modificado, 'rownum' no tiene duplicados, por lo tanto, nada se elimina. Lo que realmente queremos es que los 'cols' se establezcan en el índice. No he encontrado una manera de decirle a drop_duplicates que solo considere el índice.

Aquí hay una solución que agrega el índice como una columna de marco de datos, coloca duplicados en eso y luego elimina la nueva columna:

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

Y si desea que las cosas vuelvan a estar en el orden correcto, solo llame sortal marco de datos.

df3 = df3.sort()
DA
fuente
10
Otra variación de esto es:df.reset_index().drop_duplicates(cols='index',take_last=True).set_index('index')
Luciano
Si bien este método funciona, también crea dos copias temporales del DataFrame y es significativamente menos eficaz que usar el índice duplicado o los métodos grupales sugeridos como respuestas alternativas.
n8yoder
Si su índice es un MultiIndex, reset_index()agrega columnas level_0, level_1, etc. Y si su índice tiene un nombre, ese nombre se usará en lugar de la etiqueta "index". Eso hace que esto sea un poco más que una línea para hacerlo bien para cualquier DataFrame. index_label = getattr(df.index, 'names', getattr(df.index, 'name', 'index'))entonces cols=index_label, set_index(index_labels)e incluso esto no es infalible (no funcionará para múltiples índices sin nombre).
encimeras
1
Mover el índice a una columna, borrar duplicados y restablecer el índice fue increíble, ¡eso era exactamente lo que necesitaba!
mxplusb
Dado idx = df.index.name or 'index', uno también podría hacer df2 = df.reset_index(); df2.drop_duplicates(idx, inplace=True); df2.set_index(idx, inplace=True)para evitar las copias intermedias (debido a la inplace=True)
Anakhand
67

Oh mi. ¡Esto es realmente tan simple!

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

Edición de seguimiento 29/10/2013 En el caso de que tenga bastante complejo MultiIndex, creo que prefiero el groupbyenfoque. Aquí hay un ejemplo simple para la posteridad:

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

y aquí está 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
Paul H
fuente
si tienen nombres, de lo contrario (si un nombre es Ninguno) digamos level=[0,1]que funcionará si hay 2 niveles df1.groupby(level=[0,1]).last(). Esto debería ser parte de Pandas como un complemento paradrop_duplicates
guiones el
@dashesy sí. Usar df.index.nameses solo una forma fácil de agrupar por todos los niveles del índice.
Paul H
Gran solución, gracias! También agregaré que esto funciona xarraypara tratar con índices de fecha ds.resampley ds.groupby
hora
Enmienda a mi comentario anterior: funciona en xarrayel tiempo que cambia el grouped = df3.groupby(level=0)que grouped = df3.groupby(dim='time')o lo que es la dimensión que contiene duplicados
DRG
4

Desafortunadamente, no creo que Pandas permita que uno baje duplicados de los índices. Sugeriría lo siguiente:

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!
usuario128754
fuente
1

Si a alguien como yo le gusta la manipulación de datos encadenables utilizando la notación de puntos pandas (como las tuberías), entonces lo siguiente puede ser útil:

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

Esto permite encadenar declaraciones como esta:

df3.assign(C=2).query('~index.duplicated()').mean()
bbiegel
fuente
Intenté esto pero no pude hacerlo funcionar. Obtuve un error como este: TypeError: 'Series' objects are mutable, thus they cannot be hashed¿Esto realmente funcionó para usted?
Onno Eberhard
1

Eliminar duplicados (Mantener primero)

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

Eliminar duplicados (Mantener último)

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

Pruebas: 10k bucles utilizando los datos de 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
fuente