Maneira eficiente de aplicar vários filtros aos pandas DataFrame ou Series

148

Eu tenho um cenário em que um usuário deseja aplicar vários filtros a um objeto Pandas DataFrame ou Series. Essencialmente, quero encadear com eficiência um conjunto de filtros (operações de comparação) que são especificados em tempo de execução pelo usuário.

Os filtros devem ser aditivos (ou seja, cada um aplicado deve restringir os resultados).

Atualmente, estou usando, reindex()mas isso cria um novo objeto a cada vez e copia os dados subjacentes (se eu entender a documentação corretamente). Portanto, isso pode ser realmente ineficiente ao filtrar uma grande série ou DataFrame.

Estou pensando que o uso apply(), map()ou algo semelhante poderia ser melhor. Eu sou muito novo no Pandas, embora ainda esteja tentando entender tudo.

TL; DR

Eu quero pegar um dicionário do seguinte formulário e aplicar cada operação a um determinado objeto Series e retornar um objeto Series 'filtrado'.

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

Exemplo longo

Começarei com um exemplo do que tenho atualmente e apenas filtrarei um único objeto Series. Abaixo está a função que estou usando atualmente:

   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

O usuário fornece um dicionário com as operações que deseja executar:

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

Novamente, o "problema" da minha abordagem acima é que acho que há muitas cópias possivelmente desnecessárias dos dados para as etapas intermediárias.

Além disso, eu gostaria de expandir isso para que o dicionário transmitido possa incluir as colunas para o operador e filtrar um DataFrame inteiro com base no dicionário de entrada. No entanto, estou assumindo que tudo o que funciona para a série pode ser facilmente expandido para um DataFrame.

durden2.0
fonte
Além disso, estou plenamente ciente de que essa abordagem para o problema pode estar longe. Talvez repensar toda a abordagem seria útil. Eu só quero permitir que os usuários especifiquem um conjunto de operações de filtro em tempo de execução e as executem.
durden2.0
Eu estou querendo saber se pandas pode fazer coisas semelhantes como data.table em R: df [col1 <1 ,,] [col2> = 1]
xappppp
df.querye pd.evalparece ser um bom ajuste para o seu caso de uso. Para obter informações sobre a pd.eval()família de funções, seus recursos e casos de uso, visite Avaliação de Expressão Dinâmica em pandas usando pd.eval () .
cs95

Respostas:

245

Pandas (e numpy) permitem indexação booleana , que será muito mais 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

Se você deseja escrever funções auxiliares para isso, considere algo como estas:

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

Atualização: o pandas 0.13 possui um método de consulta para esse tipo de casos de uso, assumindo que os nomes das colunas sejam identificadores válidos para os seguintes trabalhos (e podem ser mais eficientes para quadros grandes, pois usa o numexpr nos bastidores):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11
Andy Hayden
fonte
1
Seu direito, booleano é mais eficiente, pois não faz uma cópia dos dados. No entanto, meu cenário é um pouco mais complicado que o seu exemplo. A entrada que recebo é um dicionário que define quais filtros aplicar. Meu exemplo poderia fazer algo parecido df[(ge(df['col1'], 1) & le(df['col1'], 1)]. O problema para mim realmente é que o dicionário com os filtros pode conter muitos operadores e encaderná-los é complicado. Talvez eu possa adicionar cada matriz booleana intermediária a uma matriz grande e usar apenas mappara aplicar o andoperador a elas?
durden2.0
@ durden2.0 Eu adicionei uma idéia para uma função auxiliar, que eu acho que é semelhante ao que você está procurando :)
Andy Hayden
Parece muito próximo do que eu criei! Obrigado pelo exemplo. Por que f()precisa tomar em *bvez de apenas b? É assim que o usuário f()ainda pode usar o outparâmetro opcional logical_and()? Isso leva a outra pequena questão paralela. Qual é o benefício / troca de desempenho da transmissão na matriz via out()vs. usando a retornada logical_and()? Obrigado novamente!
durden2.0
Deixa pra lá, eu não parecia perto o suficiente. Isso *bé necessário porque você está passando as duas matrizes b1e b2precisa descompactá-las ao ligar logical_and. No entanto, a outra questão ainda permanece. Existe um benefício de desempenho em passar uma matriz via outparâmetro para logical_and()vs apenas usando seu 'valor de retorno?
durden2.0
2
@dwanderson, você pode passar uma lista de condições para np.logical_and.reduce para várias condições. Exemplo: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo
39

As condições de encadeamento criam longas filas, que são desencorajadas pelo pep8. O uso do método .query força o uso de strings, o que é poderoso, mas não-tônico e não muito dinâmico.

Depois que cada um dos filtros estiver no lugar, uma abordagem será

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)]

O np.logical opera e é rápido, mas não recebe mais de dois argumentos, que são tratados pelo functools.reduce.

Observe que isso ainda possui alguns redundâncias: a) o atalho não ocorre em nível global b) Cada uma das condições individuais é executada em todos os dados iniciais. Ainda assim, espero que seja eficiente o suficiente para muitos aplicativos e seja muito legível.

Você também pode fazer uma disjunção (em que apenas uma das condições precisa ser verdadeira) usando np.logical_or:

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)]
Lagartixa
fonte
1
Existe uma maneira de implementar isso para um número variável de condições? Eu tentei acrescentar cada c_1, c_2, c_3, ... c_nem uma lista, e em seguida, passando data[conjunction(conditions_list)], mas obter um erro ValueError: Item wrong length 5 instead of 37.também tentou data[conjunction(*conditions_list)], mas eu recebo um resultado diferente do que data[conjunction(c_1, c_2, c_3, ... c_n )], não sei o que está acontecendo.
user5359531
Encontrei uma solução para o erro em outro lugar. data[conjunction(*conditions_list)]faz o trabalho após a embalagem os dataframes em uma lista, e desembalar a lista no lugar
user5359531
1
Acabei de deixar um comentário na resposta acima com uma versão muito mais desleixada e depois notei a sua resposta. Muito limpo, eu gosto muito!
dwanderson
Esta é uma ótima resposta!
Charlie Crown
1
i tinha usado: df[f_2 & f_3 & f_4 & f_5 ]com f_2 = df["a"] >= 0etc. Não há necessidade de que a função ... (bom uso da função de ordem superior embora ...)
A. Rabus
19

Mais simples de todas as soluções:

Usar:

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

Outro exemplo , para filtrar o dataframe por valores pertencentes a fevereiro de 2018, use o código abaixo

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
Gil Baggio
fonte
Eu estou usando variável em vez de constante. recebendo erro. df [df []] [df []] envia uma mensagem de aviso, mas fornece a resposta correta.
Nguai al
8

Desde a atualização do pandas 0.22 , as opções de comparação estão disponíveis como:

  • gt (maior que)
  • lt (menor que)
  • eq (igual a)
  • ne (não é igual a)
  • ge (maior que ou igual a)

e muitos mais. Essas funções retornam matriz booleana. Vamos ver como podemos usá-los:

# 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
fonte
2

Por que não fazer isso?

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

Demo:

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

Você pode ver que a coluna 'a' foi filtrada em que a> = 2.

Isso é um pouco mais rápido (tempo de digitação, não desempenho) do que o encadeamento do operador. Obviamente, você pode colocar a importação na parte superior do arquivo.

Obol
fonte
1

Também é possível selecionar linhas com base nos valores de uma coluna que não estão em uma lista ou são iteráveis. Criaremos uma variável booleana como antes, mas agora negaremos a variável booleana colocando ~ na frente.

Por exemplo

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