pandas: filtrar filas de DataFrame con encadenamiento de operador


329

La mayoría de las operaciones en pandasque se puede lograr con el encadenamiento de operador ( groupby, aggregate, apply, etc.), pero la única manera que he encontrado para las filas de filtro es a través de la indexación de soporte de la normalidad

df_filtered = df[df['column'] == value]

Esto no es atractivo, ya que requiere asignar dfa una variable antes de poder filtrar sus valores. ¿Hay algo más como lo siguiente?

df_filtered = df.mask(lambda x: x['column'] == value)

df.queryy pd.evalparece que encaja bien en este 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:


384

No estoy completamente seguro de lo que quieres, y tu última línea de código tampoco ayuda, pero de todos modos:

El filtrado "encadenado" se realiza "encadenando" los criterios en el índice booleano.

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [99]: df[(df.A == 1) & (df.D == 6)]
Out[99]:
   A  B  C  D
d  1  3  9  6

Si desea encadenar métodos, puede agregar su propio método de máscara y usarlo.

In [90]: def mask(df, key, value):
   ....:     return df[df[key] == value]
   ....:

In [92]: pandas.DataFrame.mask = mask

In [93]: df = pandas.DataFrame(np.random.randint(0, 10, (4,4)), index=list('abcd'), columns=list('ABCD'))

In [95]: df.ix['d','A'] = df.ix['a', 'A']

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [97]: df.mask('A', 1)
Out[97]:
   A  B  C  D
a  1  4  9  1
d  1  3  9  6

In [98]: df.mask('A', 1).mask('D', 6)
Out[98]:
   A  B  C  D
d  1  3  9  6

2
¡Gran respuesta! Entonces (df.A == 1) & (df.D == 6), ¿es el "&" un operador sobrecargado en Pandas?
Shawn


Esa es una solución realmente agradable: ni siquiera sabía que se podrían manipular métodos como este en Python. Una función como esta sería realmente agradable de tener en Pandas.
naught101

El único problema que tengo con esto es el uso de pandas.. Que debiera import pandas as pd.
Daisuke Aramaki

3
De hecho, import pandas as pdes una práctica común ahora. Dudo que fuera cuando respondí la pregunta.
Wouter Overmeire

108

Los filtros se pueden encadenar mediante una consulta Pandas :

df = pd.DataFrame(np.random.randn(30, 3), columns=['a','b','c'])
df_filtered = df.query('a > 0').query('0 < b < 2')

Los filtros también se pueden combinar en una sola consulta:

df_filtered = df.query('a > 0 and 0 < b < 2')

3
Si necesita referirse a las variables de Python en su consulta, la documentación dice: "Puede referirse a las variables en el entorno al prefijarlas con un carácter '@' como @a + b". Tenga en cuenta que lo siguiente es válido: df.query('a in list([1,2])'), s = set([1,2]); df.query('a in @s').
user3780389

2
Por otro lado, parece que la evaluación de la consulta fallará si el nombre de su columna tiene ciertos caracteres especiales: por ejemplo, "Place.Name".
user3780389

2
El encadenamiento es para lo que está diseñada la consulta.
piRSquared

66

La respuesta de @lodagro es genial. Lo ampliaría generalizando la función de máscara como:

def mask(df, f):
  return df[f(df)]

Entonces puedes hacer cosas como:

df.mask(lambda x: x[0] < 0).mask(lambda x: x[1] > 0)

8
¡Una útil generalización! ¡Ojalá ya se integrara directamente en DataFrames!
Duckworthd

24

Desde la versión 0.18.1, el .locmétodo acepta un llamado para selección. Junto con las funciones lambda, puede crear filtros encadenables muy flexibles:

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.randint(0,100,size=(100, 4)), columns=list('ABCD'))
df.loc[lambda df: df.A == 80]  # equivalent to df[df.A == 80] but chainable

df.sort_values('A').loc[lambda df: df.A > 80].loc[lambda df: df.B > df.A]

Si todo lo que está haciendo es filtrar, también puede omitir el .loc.


16

Ofrezco esto para ejemplos adicionales. Esta es la misma respuesta que https://stackoverflow.com/a/28159296/

Agregaré otras ediciones para que esta publicación sea más útil.

pandas.DataFrame.query
queryfue hecho exactamente para este propósito. Considere el marco de datosdf

import pandas as pd
import numpy as np

np.random.seed([3,1415])
df = pd.DataFrame(
    np.random.randint(10, size=(10, 5)),
    columns=list('ABCDE')
)

df

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
2  0  2  0  4  9
3  7  3  2  4  3
4  3  6  7  7  4
5  5  3  7  5  9
6  8  7  6  4  7
7  6  2  6  6  5
8  2  8  7  5  8
9  4  7  6  1  5

Usemos querypara filtrar todas las filas dondeD > B

df.query('D > B')

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
2  0  2  0  4  9
3  7  3  2  4  3
4  3  6  7  7  4
5  5  3  7  5  9
7  6  2  6  6  5

Que encadenamos

df.query('D > B').query('C > B')
# equivalent to
# df.query('D > B and C > B')
# but defeats the purpose of demonstrating chaining

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
4  3  6  7  7  4
5  5  3  7  5  9
7  6  2  6  6  5

¿No es básicamente la misma respuesta que stackoverflow.com/a/28159296? ¿Le falta algo a esa respuesta que cree que debería aclararse?
bscan

9

Tenía la misma pregunta, excepto que quería combinar los criterios en una condición OR. El formato proporcionado por Wouter Overmeire combina los criterios en una condición AND de modo que ambos deben cumplirse:

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [99]: df[(df.A == 1) & (df.D == 6)]
Out[99]:
   A  B  C  D
d  1  3  9  6

Pero descubrí que, si envuelve cada condición (... == True)y une los criterios con una tubería, los criterios se combinan en una condición OR, satisfecha siempre que cualquiera de ellos sea cierto:

df[((df.A==1) == True) | ((df.D==6) == True)]

12
¿No df[(df.A==1) | (df.D==6)]sería suficiente para lo que estás tratando de lograr?
eenblam

No, no lo haría porque da resultados bollean (Verdadero o Falso) en lugar de porque está por encima, lo que filtra todos los datos que satisfacen la condición. Espero haberlo dejado claro.
MGB.py

8

pandas ofrece dos alternativas a la respuesta de Wouter Overmeire que no requieren ninguna anulación. Uno es .loc[.]con un invocable, como en

df_filtered = df.loc[lambda x: x['column'] == value]

el otro es .pipe() , como en

df_filtered = df.pipe(lambda x: x['column'] == value)

7

Mi respuesta es similar a las demás. Si no desea crear una nueva función, puede usar lo que los pandas ya han definido para usted. Usa el método de la tubería.

df.pipe(lambda d: d[d['column'] == value])

ESTO es lo que desea si desea encadenar comandos comoa.join(b).pipe(lambda df: df[df.column_to_filter == 'VALUE'])
displayname

4

Si desea aplicar todas las máscaras booleanas comunes, así como una máscara de uso general, puede arrojar lo siguiente en un archivo y luego simplemente asignarlas todas de la siguiente manera:

pd.DataFrame = apply_masks()

Uso:

A = pd.DataFrame(np.random.randn(4, 4), columns=["A", "B", "C", "D"])
A.le_mask("A", 0.7).ge_mask("B", 0.2)... (May be repeated as necessary

Es un poco hacky pero puede hacer las cosas un poco más limpias si continuamente está cortando y cambiando los conjuntos de datos según los filtros. También hay un filtro de propósito general adaptado de Daniel Velkov arriba en la función gen_mask que puede usar con funciones lambda o de lo contrario si lo desea.

Archivo para guardar (uso masks.py):

import pandas as pd

def eq_mask(df, key, value):
    return df[df[key] == value]

def ge_mask(df, key, value):
    return df[df[key] >= value]

def gt_mask(df, key, value):
    return df[df[key] > value]

def le_mask(df, key, value):
    return df[df[key] <= value]

def lt_mask(df, key, value):
    return df[df[key] < value]

def ne_mask(df, key, value):
    return df[df[key] != value]

def gen_mask(df, f):
    return df[f(df)]

def apply_masks():

    pd.DataFrame.eq_mask = eq_mask
    pd.DataFrame.ge_mask = ge_mask
    pd.DataFrame.gt_mask = gt_mask
    pd.DataFrame.le_mask = le_mask
    pd.DataFrame.lt_mask = lt_mask
    pd.DataFrame.ne_mask = ne_mask
    pd.DataFrame.gen_mask = gen_mask

    return pd.DataFrame

if __name__ == '__main__':
    pass

3

Esta solución es más agresiva en términos de implementación, pero me parece mucho más limpia en términos de uso, y ciertamente es más general que las otras propuestas.

https://github.com/toobaz/generic_utils/blob/master/generic_utils/pandas/where.py

No necesita descargar el repositorio completo: guardar el archivo y hacer

from where import where as W

Debería ser suficiente. Entonces lo usas así:

df = pd.DataFrame([[1, 2, True],
                   [3, 4, False], 
                   [5, 7, True]],
                  index=range(3), columns=['a', 'b', 'c'])
# On specific column:
print(df.loc[W['a'] > 2])
print(df.loc[-W['a'] == W['b']])
print(df.loc[~W['c']])
# On entire - or subset of a - DataFrame:
print(df.loc[W.sum(axis=1) > 3])
print(df.loc[W[['a', 'b']].diff(axis=1)['b'] > 1])

Un ejemplo de uso un poco menos estúpido:

data = pd.read_csv('ugly_db.csv').loc[~(W == '$null$').any(axis=1)]

Por cierto: incluso en el caso de que solo estés usando col booleanos,

df.loc[W['cond1']].loc[W['cond2']]

puede ser mucho más eficiente que

df.loc[W['cond1'] & W['cond2']]

porque evalúa cond2 dónde cond1está True.

DESCARGO DE RESPONSABILIDAD: Primero di esta respuesta en otro lugar porque no había visto esto.


2

Solo quiero agregar una demostración usando loc para filtrar no solo por filas sino también por columnas y algunos méritos a la operación encadenada.

El siguiente código puede filtrar las filas por valor.

df_filtered = df.loc[df['column'] == value]

Modificándolo un poco, también puede filtrar las columnas.

df_filtered = df.loc[df['column'] == value, ['year', 'column']]

Entonces, ¿por qué queremos un método encadenado? La respuesta es que es fácil de leer si tiene muchas operaciones. Por ejemplo,

res =  df\
    .loc[df['station']=='USA', ['TEMP', 'RF']]\
    .groupby('year')\
    .agg(np.nanmean)

2

Esto no es atractivo, ya que requiere asignar dfa una variable antes de poder filtrar sus valores.

df[df["column_name"] != 5].groupby("other_column_name")

parece funcionar: también puede anidar el []operador. Tal vez lo agregaron ya que usted hizo la pregunta.


1
Esto tiene poco sentido en una cadena porque dfahora no necesariamente hace referencia a la salida de la parte anterior de la cadena.
Daan Luttik

@DaanLuttik: de acuerdo, no está encadenando, sino anidando. ¿Mejor para ti?
serv-inc

1

Si configura sus columnas para buscar como índices, puede usar DataFrame.xs()para tomar una sección transversal. Esto no es tan versátil como las queryrespuestas, pero podría ser útil en algunas situaciones.

import pandas as pd
import numpy as np

np.random.seed([3,1415])
df = pd.DataFrame(
    np.random.randint(3, size=(10, 5)),
    columns=list('ABCDE')
)

df
# Out[55]: 
#    A  B  C  D  E
# 0  0  2  2  2  2
# 1  1  1  2  0  2
# 2  0  2  0  0  2
# 3  0  2  2  0  1
# 4  0  1  1  2  0
# 5  0  0  0  1  2
# 6  1  0  1  1  1
# 7  0  0  2  0  2
# 8  2  2  2  2  2
# 9  1  2  0  2  1

df.set_index(['A', 'D']).xs([0, 2]).reset_index()
# Out[57]: 
#    A  D  B  C  E
# 0  0  2  2  2  2
# 1  0  2  1  1  0

1

También puede aprovechar la biblioteca numpy para operaciones lógicas. Es bastante rápido

df[np.logical_and(df['A'] == 1 ,df['B'] == 6)]
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.