Modo efficiente per applicare più filtri a Panda DataFrame o Series


148

Ho uno scenario in cui un utente vuole applicare diversi filtri a un oggetto Pandas DataFrame o Series. In sostanza, voglio unire in modo efficiente un insieme di filtri (operazioni di confronto) che sono specificati in fase di esecuzione dall'utente.

I filtri dovrebbero essere additivi (ovvero ognuno applicato dovrebbe restringere i risultati).

Attualmente sto usando reindex()ma questo crea ogni volta un nuovo oggetto e copia i dati sottostanti (se capisco correttamente la documentazione). Quindi, questo potrebbe essere davvero inefficiente quando si filtra una grande serie o DataFrame.

Sto pensando che l'uso apply(), map()o qualcosa di simile potrebbe essere migliore. Sono abbastanza nuovo per i panda, anche se sto ancora cercando di avvolgere la mia testa intorno a tutto.

TL; DR

Voglio prendere un dizionario del seguente modulo e applicare ogni operazione a un determinato oggetto Serie e restituire un oggetto Serie "filtrato".

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

Esempio lungo

Inizierò con un esempio di ciò che ho attualmente e solo filtrando un singolo oggetto Serie. Di seguito è la funzione che sto attualmente utilizzando:

   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

L'utente fornisce a un dizionario le operazioni che desidera eseguire:

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

Ancora una volta, il "problema" con il mio approccio di cui sopra è che penso che ci sia molta copia forse inutile dei dati per i passaggi intermedi.

Inoltre, vorrei espanderlo in modo che il dizionario passato possa includere le colonne su cui operare e filtrare un intero DataFrame in base al dizionario di input. Tuttavia, suppongo che qualunque cosa funzioni per la serie possa essere facilmente estesa a un DataFrame.


Inoltre, sono pienamente consapevole che questo approccio al problema potrebbe essere molto lontano. Quindi forse ripensare l'intero approccio sarebbe utile. Voglio solo consentire agli utenti di specificare una serie di operazioni di filtro in fase di esecuzione ed eseguirle.
durden2.0,

Mi chiedo se i panda possano fare cose simili a data.table in R: df [col1 <1 ,,] [col2> = 1]
xappppp

df.querye pd.evalsembrano adatti per il tuo caso d'uso. Per informazioni sulla pd.eval()famiglia di funzioni, le loro caratteristiche e i casi d'uso, visitare la valutazione delle espressioni dinamiche in Panda usando pd.eval () .
cs95,

Risposte:


245

I panda (e il numpy) consentono l'indicizzazione booleana , che sarà molto più efficiente:

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 vuoi scrivere funzioni di supporto per questo, considera qualcosa in questo senso:

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

Aggiornamento: Panda 0.13 ha un metodo di query per questo tipo di casi d'uso, supponendo che i nomi di colonna siano identificatori validi per i seguenti lavori (e può essere più efficiente per i frame di grandi dimensioni poiché utilizza numexpr dietro le quinte):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11

1
Il tuo diritto, booleano, è più efficiente poiché non crea una copia dei dati. Tuttavia, il mio scenario è un po 'più complicato del tuo esempio. L'input che ricevo è un dizionario che definisce quali filtri applicare. Il mio esempio potrebbe fare qualcosa del genere df[(ge(df['col1'], 1) & le(df['col1'], 1)]. Il problema per me è davvero che il dizionario con i filtri potrebbe contenere molti operatori e concatenarli è ingombrante. Forse potrei aggiungere ogni array booleano intermedio a un array grande e poi usare solo mapper applicare l' andoperatore ad essi?
durden2.0,

@ durden2.0 Ho aggiunto un'idea per una funzione di supporto, che penso sia simile a ciò che stai cercando :)
Andy Hayden,

Sembra molto vicino a quello che mi è venuto in mente! Grazie per l'esempio Perché è f()necessario prendere *binvece di solo b? È così che l'utente f()potrebbe ancora utilizzare il outparametro opzionale per logical_and()? Questo porta a un'altra piccola domanda secondaria. Qual è il vantaggio / compromesso in termini di prestazioni out()derivante dal passaggio in array tramite rispetto a quello da cui è stato restituito logical_and()? Grazie ancora!
durden2.0

Non importa, non sembravo abbastanza vicino. Il *bè necessaria perché si sta passando i due array b1e b2ed è necessario scompattare loro quando si chiama logical_and. Tuttavia, l'altra domanda è ancora valida. C'è un vantaggio in termini di prestazioni nel passare un array tramite outparametro a logical_and()vs semplicemente usando il suo 'valore di ritorno?
durden2.0,

2
@dwanderson è possibile passare un elenco di condizioni a np.logical_and.reduce per più condizioni. Esempio: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo,

39

Le condizioni di concatenamento creano lunghe file, che sono scoraggiate da pep8. L'uso del metodo .query obbliga a utilizzare le stringhe, che è potente ma non ritmico e non molto dinamico.

Una volta che ciascuno dei filtri è attivo, un approccio è

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 funziona ed è veloce, ma non accetta più di due argomenti, che è gestito da functools.reduce.

Si noti che ciò presenta ancora alcuni esuberi: a) la scorciatoia non avviene a livello globale b) Ciascuna delle singole condizioni viene eseguita su tutti i dati iniziali. Tuttavia, mi aspetto che questo sia abbastanza efficiente per molte applicazioni ed è molto leggibile.

Puoi anche fare una disgiunzione (in cui solo una delle condizioni deve essere vera) usando np.logical_orinvece:

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

1
C'è un modo per implementarlo per un numero variabile di condizioni? Ho provato aggiungendo ogni c_1, c_2, c_3, ... c_nin un elenco, e poi passando data[conjunction(conditions_list)], ma un errore ValueError: Item wrong length 5 instead of 37.ha provato anche data[conjunction(*conditions_list)], ma io ottenere un risultato diverso da quello data[conjunction(c_1, c_2, c_3, ... c_n )], non so cosa sta succedendo.
user5359531

Ho trovato una soluzione all'errore altrove. data[conjunction(*conditions_list)]funziona dopo aver compresso i frame di dati in un elenco e aver decompresso l'elenco in posizione
user5359531

1
Ho appena lasciato un commento sulla risposta sopra con una versione molto più sciatta, e poi ho notato la tua risposta. Molto pulito, mi piace molto!
Dwanderson,

Questa è un'ottima risposta!
Charlie Crown,

1
avevo usato: df[f_2 & f_3 & f_4 & f_5 ]con f_2 = df["a"] >= 0ecc. Non c'è bisogno di quella funzione ... (buon uso della funzione di ordine superiore però ...)
A. Rabus,

19

La più semplice di tutte le soluzioni:

Uso:

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

Un altro esempio , per filtrare il frame di dati per i valori appartenenti a febbraio-2018, utilizzare il codice seguente

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]

sto usando la variabile anziché la costante. errore. df [df []] [df []] fornisce un messaggio di avviso ma fornisce una risposta corretta.
Nguai al

8

Dal momento che i panda 0,22 aggiornamento , sono disponibili opzioni di confronto come:

  • GT (maggiore di)
  • lt (minore di)
  • eq (uguale a)
  • ne (non uguale a)
  • ge (maggiore di o uguale a)

e molti altri. Queste funzioni restituiscono un array booleano. Vediamo come possiamo usarli:

# 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

2

Perché non farlo?

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

Risultato:

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

Puoi vedere che la colonna 'a' è stata filtrata dove a> = 2.

Questo è leggermente più veloce (tempo di digitazione, non prestazioni) rispetto al concatenamento dell'operatore. Ovviamente potresti mettere l'importazione in cima al file.


1

È inoltre possibile selezionare le righe in base ai valori di una colonna che non sono in un elenco o in alcun iterabile. Creeremo una variabile booleana proprio come prima, ma ora annulleremo la variabile booleana posizionando ~ in primo piano.

Per esempio

list = [1, 0]
df[df.col1.isin(list)]
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.