Manera eficiente de aplicar múltiples filtros a pandas DataFrame o Series

148

Tengo un escenario en el que un usuario quiere aplicar varios filtros a un objeto Pandas DataFrame o Series. Esencialmente, quiero encadenar eficientemente un conjunto de filtros (operaciones de comparación) que el usuario especifica en tiempo de ejecución.

Los filtros deben ser aditivos (es decir, cada uno aplicado debe reducir los resultados).

Actualmente estoy usando, reindex()pero esto crea un nuevo objeto cada vez y copia los datos subyacentes (si entiendo la documentación correctamente). Por lo tanto, esto podría ser realmente ineficiente al filtrar una gran serie o un marco de datos.

Estoy pensando que el uso de apply(), map()o algo similar podría ser mejor. Sin embargo, soy bastante nuevo en Pandas, así que todavía estoy tratando de entender todo.

TL; DR

Quiero tomar un diccionario de la siguiente forma y aplicar cada operación a un objeto Serie dado y devolver un objeto Serie 'filtrado'.

relops = {'>=': [1], '<=': [1]}

Ejemplo largo

Comenzaré con un ejemplo de lo que tengo actualmente y simplemente filtrando un solo objeto de la Serie. A continuación se muestra la función que estoy usando actualmente:

   def apply_relops(series, relops):
        """
        Pass dictionary of relational operators to perform on given series object
        """
        for op, vals in relops.iteritems():
            op_func = ops[op]
            for val in vals:
                filtered = op_func(series, val)
                series = series.reindex(series[filtered])
        return series

El usuario proporciona un diccionario con las operaciones que desea realizar:

>>> df = pandas.DataFrame({'col1': [0, 1, 2], 'col2': [10, 11, 12]})
>>> print df
>>> print df
   col1  col2
0     0    10
1     1    11
2     2    12

>>> from operator import le, ge
>>> ops ={'>=': ge, '<=': le}
>>> apply_relops(df['col1'], {'>=': [1]})
col1
1       1
2       2
Name: col1
>>> apply_relops(df['col1'], relops = {'>=': [1], '<=': [1]})
col1
1       1
Name: col1

Nuevamente, el 'problema' con mi enfoque anterior es que creo que hay muchas copias posiblemente innecesarias de los datos para los pasos intermedios.

Además, me gustaría expandir esto para que el diccionario aprobado pueda incluir las columnas para operar y filtrar un DataFrame completo basado en el diccionario de entrada. Sin embargo, supongo que todo lo que funcione para la Serie se puede expandir fácilmente a un Marco de datos.

durden2.0
fuente
Además, soy plenamente consciente de que este enfoque del problema podría estar muy lejos. Entonces quizás repensar todo el enfoque sería útil. Solo quiero permitir que los usuarios especifiquen un conjunto de operaciones de filtro en tiempo de ejecución y ejecutarlos.
durden2.0
Me pregunto si los pandas pueden hacer cosas similares a data.table en R: df [col1 <1 ,,] [col2> = 1]
xappppp
df.queryy pd.evalparece que encaja bien con su caso de uso. Para obtener información sobre la pd.eval()familia de funciones, sus características y casos de uso, visite Evaluación de expresión dinámica en pandas usando pd.eval () .
cs95

Respuestas:

245

Los pandas (y numpy) permiten la indexación booleana , que será mucho más eficiente:

In [11]: df.loc[df['col1'] >= 1, 'col1']
Out[11]: 
1    1
2    2
Name: col1

In [12]: df[df['col1'] >= 1]
Out[12]: 
   col1  col2
1     1    11
2     2    12

In [13]: df[(df['col1'] >= 1) & (df['col1'] <=1 )]
Out[13]: 
   col1  col2
1     1    11

Si desea escribir funciones de ayuda para esto, considere algo en este sentido:

In [14]: def b(x, col, op, n): 
             return op(x[col],n)

In [15]: def f(x, *b):
             return x[(np.logical_and(*b))]

In [16]: b1 = b(df, 'col1', ge, 1)

In [17]: b2 = b(df, 'col1', le, 1)

In [18]: f(df, b1, b2)
Out[18]: 
   col1  col2
1     1    11

Actualización: pandas 0.13 tiene un método de consulta para este tipo de casos de uso, suponiendo que los nombres de columna son identificadores válidos de los siguientes trabajos (y pueden ser más eficientes para marcos grandes, ya que usa numexpr detrás de escena):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11
Andy Hayden
fuente
1
A la derecha, boolean es más eficiente ya que no hace una copia de los datos. Sin embargo, mi escenario es un poco más complicado que tu ejemplo. La entrada que recibo es un diccionario que define qué filtros aplicar. Mi ejemplo podría hacer algo así df[(ge(df['col1'], 1) & le(df['col1'], 1)]. El problema para mí es que el diccionario con los filtros podría contener muchos operadores y encadenarlos es engorroso. ¿Tal vez podría agregar cada matriz booleana intermedia a una gran matriz y luego usarla mappara aplicarles el andoperador?
durden2.0
@ durden2.0 He agregado una idea para una función auxiliar, que creo que es similar a lo que estás buscando :)
Andy Hayden
¡Eso se parece mucho a lo que se me ocurrió! Gracias por el ejemplo ¿Por qué f()necesita tomar en *blugar de solo b? ¿Es esto así que el usuario f()todavía podría usar el outparámetro opcional logical_and()? Esto lleva a otra pequeña pregunta secundaria. ¿Cuál es el beneficio de rendimiento / compensación de pasar en matriz a través de out()usar el devuelto logical_and()? ¡Gracias de nuevo!
durden2.0
No importa, no miré lo suficientemente cerca. Esto *bes necesario porque está pasando las dos matrices b1y b2necesita desempaquetarlas al llamar logical_and. Sin embargo, la otra pregunta sigue en pie. ¿Existe un beneficio de rendimiento al pasar una matriz a través de outparámetros a logical_and()vs simplemente usando su 'valor de retorno?
durden2.0
2
@dwanderson puede pasar una lista de condiciones a np.logical_and.reduce para múltiples condiciones. Ejemplo: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo
39

Las condiciones de encadenamiento crean largas colas, que no son recomendadas por pep8. El uso del método .query obliga a usar cadenas, que son potentes pero poco pitónicas y no muy dinámicas.

Una vez que cada uno de los filtros está en su lugar, un enfoque es

import numpy as np
import functools
def conjunction(*conditions):
    return functools.reduce(np.logical_and, conditions)

c_1 = data.col1 == True
c_2 = data.col2 < 64
c_3 = data.col3 != 4

data_filtered = data[conjunction(c1,c2,c3)]

np.logical funciona y es rápido, pero no toma más de dos argumentos, que es manejado por functools.reduce.

Tenga en cuenta que esto todavía tiene algunas redundancias: a) el acceso directo no ocurre a nivel global b) Cada una de las condiciones individuales se ejecuta en los datos iniciales completos. Aún así, espero que esto sea lo suficientemente eficiente para muchas aplicaciones y que sea muy legible.

También puede hacer una disyunción (en la que solo una de las condiciones debe ser verdadera) usando en su np.logical_orlugar:

import numpy as np
import functools
def disjunction(*conditions):
    return functools.reduce(np.logical_or, conditions)

c_1 = data.col1 == True
c_2 = data.col2 < 64
c_3 = data.col3 != 4

data_filtered = data[disjunction(c1,c2,c3)]
Geco
fuente
1
¿Hay alguna manera de implementar esto para un número variable de condiciones? He tratado añadiendo cada uno c_1, c_2, c_3, ... c_nen una lista, y luego pasar data[conjunction(conditions_list)], pero obtendrá un error ValueError: Item wrong length 5 instead of 37.también intentó data[conjunction(*conditions_list)]pero conseguir un resultado diferente que data[conjunction(c_1, c_2, c_3, ... c_n )], sin saber lo que está pasando.
user5359531
Encontró una solución al error en otro lugar. data[conjunction(*conditions_list)]funciona después de empaquetar los marcos de datos en una lista y desempaquetar la lista en su lugar
user5359531
1
Acabo de dejar un comentario sobre la respuesta anterior con una versión mucho más descuidada, y luego noté su respuesta. Muy limpio, me gusta mucho!
dwanderson
¡Esta es una respuesta genial!
Charlie Crown
1
yo había usado: df[f_2 & f_3 & f_4 & f_5 ]con f_2 = df["a"] >= 0etc. No hay necesidad de esa función ... (buen uso de la función de orden superior ...)
A. Rabus
19

La más simple de todas las soluciones:

Utilizar:

filtered_df = df[(df['col1'] >= 1) & (df['col1'] <= 5)]

Otro ejemplo , para filtrar el marco de datos para los valores que pertenecen a febrero de 2018, use el siguiente código

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
Gil Baggio
fuente
Estoy usando variable en lugar de constante. obteniendo error. df [df []] [df []] da un mensaje de advertencia pero da la respuesta correcta.
Nguai al
8

Desde la actualización de pandas 0.22 , las opciones de comparación están disponibles como:

  • gt (mayor que)
  • lt (menor que)
  • eq (igual a)
  • ne (no es igual a)
  • ge (mayor o igual que)

y muchos más. Estas funciones devuelven una matriz booleana. Veamos cómo podemos usarlos:

# sample data
df = pd.DataFrame({'col1': [0, 1, 2,3,4,5], 'col2': [10, 11, 12,13,14,15]})

# get values from col1 greater than or equals to 1
df.loc[df['col1'].ge(1),'col1']

1    1
2    2
3    3
4    4
5    5

# where co11 values is better 0 and 2
df.loc[df['col1'].between(0,2)]

 col1 col2
0   0   10
1   1   11
2   2   12

# where col1 > 1
df.loc[df['col1'].gt(1)]

 col1 col2
2   2   12
3   3   13
4   4   14
5   5   15
Yolo
fuente
2

¿Por qué no haces esto?

def filt_spec(df, col, val, op):
    import operator
    ops = {'eq': operator.eq, 'neq': operator.ne, 'gt': operator.gt, 'ge': operator.ge, 'lt': operator.lt, 'le': operator.le}
    return df[ops[op](df[col], val)]
pandas.DataFrame.filt_spec = filt_spec

Manifestación:

df = pd.DataFrame({'a': [1,2,3,4,5], 'b':[5,4,3,2,1]})
df.filt_spec('a', 2, 'ge')

Resultado:

   a  b
 1  2  4
 2  3  3
 3  4  2
 4  5  1

Puede ver que la columna 'a' se ha filtrado donde a> = 2.

Esto es un poco más rápido (tiempo de escritura, no rendimiento) que el encadenamiento del operador. Por supuesto, puede colocar la importación en la parte superior del archivo.

Obol
fuente
1

También podemos seleccionar filas en función de los valores de una columna que no están en una lista ni en ninguna iterable. Crearemos una variable booleana como antes, pero ahora negaremos la variable booleana colocando ~ en el frente.

Por ejemplo

list = [1, 0]
df[df.col1.isin(list)]
Ram Prajapati
fuente