Efektywny sposób na zastosowanie wielu filtrów do pand DataFrame lub Series

148

Mam scenariusz, w którym użytkownik chce zastosować kilka filtrów do obiektu Pandas DataFrame lub Series. Zasadniczo chcę wydajnie łączyć kilka operacji filtrowania (operacji porównania), które są określane w czasie wykonywania przez użytkownika.

Filtry powinny być addytywne (każdy zastosowany powinien zawęzić wyniki).

Obecnie używam, reindex()ale za każdym razem tworzy to nowy obiekt i kopiuje podstawowe dane (jeśli dobrze rozumiem dokumentację). Tak więc może to być naprawdę nieefektywne podczas filtrowania dużej serii lub ramki DataFrame.

Myślę, że za pomocą apply(), map()lub coś podobnego mogłoby być lepiej. Jestem całkiem nowy w Pandach, więc wciąż próbuję wszystko ogarnąć.

TL; DR

Chcę wziąć słownik w poniższej formie i zastosować każdą operację do danego obiektu Series i zwrócić „przefiltrowany” obiekt Series.

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

Długi przykład

Zacznę od przykładu tego, co mam obecnie i tylko filtrując pojedynczy obiekt Series. Poniżej znajduje się funkcja, której obecnie używam:

   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

Użytkownik udostępnia słownik z operacjami, które chce wykonać:

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

Ponownie, „problem” z moim powyższym podejściem polega na tym, że wydaje mi się, że jest dużo prawdopodobnie niepotrzebnego kopiowania danych w krokach pośrednich.

Chciałbym również rozszerzyć to, aby przekazywany słownik mógł zawierać kolumny do operatora i filtrować całą ramkę DataFrame na podstawie słownika wejściowego. Zakładam jednak, że wszystko, co działa w serii, można łatwo rozszerzyć do ramki DataFrame.

durden2.0
źródło
Mam też pełną świadomość, że takie podejście do problemu może być dalekie. Więc może przydałoby się przemyślenie całego podejścia. Chcę tylko zezwolić użytkownikom na określenie zestawu operacji filtrujących w czasie wykonywania i ich wykonanie.
durden2.0
Zastanawiam się, czy pandy potrafią robić podobne rzeczy jak data.table w R: df [col1 <1 ,,] [col2> = 1]
xappppp
df.queryi pd.evalwydaje się, że dobrze pasuje do twojego przypadku użycia. Aby uzyskać informacje na temat pd.eval()rodziny funkcji, ich funkcji i przypadków użycia, odwiedź stronę Ocena wyrażeń dynamicznych w pandach przy użyciu pd.eval () .
cs95

Odpowiedzi:

245

Pandy (i numpy) pozwalają na indeksowanie boolowskie , które będzie znacznie wydajniejsze:

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

Jeśli chcesz napisać do tego funkcje pomocnicze, rozważ coś w tym zakresie:

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

Aktualizacja: pandas 0.13 ma metodę zapytania dla tego rodzaju przypadków użycia, zakładając, że nazwy kolumn są prawidłowymi identyfikatorami, co działa poniżej (i może być bardziej wydajne dla dużych ramek, ponieważ używa numexpr w tle):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11
Andy Hayden
źródło
1
Twoje prawo, boolean jest bardziej wydajny, ponieważ nie tworzy kopii danych. Jednak mój scenariusz jest nieco bardziej skomplikowany niż twój przykład. Dane wejściowe, które otrzymuję, to słownik określający, jakie filtry zastosować. Mój przykład mógłby zrobić coś takiego df[(ge(df['col1'], 1) & le(df['col1'], 1)]. Dla mnie problemem jest to, że słownik z filtrami może zawierać wiele operatorów, a łączenie ich w łańcuchy jest uciążliwe. Może mógłbym dodać każdą pośrednią tablicę logiczną do dużej tablicy, a następnie po prostu mapzastosować anddo nich operator?
durden2.0
@ durden2.0 Dodałem pomysł na funkcję pomocniczą, która moim zdaniem jest podobna do tego, czego szukasz :)
Andy Hayden
Wygląda to bardzo blisko tego, co wymyśliłem! Dzięki za przykład. Dlaczego f()trzeba brać *bzamiast po prostu b? Czy to jest tak, że użytkownik f()może nadal używać opcjonalnego outparametru logical_and()? Prowadzi to do kolejnego małego pobocznego pytania. Jaka jest korzyść / kompromis w zakresie wydajności przekazywania w tablicy przez w out()porównaniu z użyciem zwracanego z logical_and()? Dzięki jeszcze raz!
durden2.0
Nieważne, nie spojrzałem wystarczająco blisko. Jest *bto konieczne, ponieważ przekazujesz dwie tablice b1i b2musisz je rozpakować podczas wywoływania logical_and. Jednak drugie pytanie wciąż pozostaje aktualne. Czy istnieje korzyść w zakresie wydajności przekazywania tablicy za pośrednictwem outparametru do logical_and()zamiast używania jej wartości zwracanej?
durden2.0
2
@dwanderson możesz przekazać listę warunków do np.logical_and.reduce dla wielu warunków. Przykład: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo
39

Warunki łańcuchowe tworzą długie linie, które są zniechęcane przez pep8. Korzystanie z metody .query wymusza użycie łańcuchów, które są potężne, ale nieszablonowe i niezbyt dynamiczne.

Gdy każdy z filtrów jest na miejscu, jest jedno podejście

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 działa i jest szybki, ale nie przyjmuje więcej niż dwóch argumentów, co jest obsługiwane przez functools.reduce.

Zauważ, że nadal ma to pewne nadmiarowości: a) skróty nie występują na poziomie globalnym b) Każdy z indywidualnych warunków działa na całych danych początkowych. Mimo to oczekuję, że będzie to wystarczająco wydajne dla wielu aplikacji i jest bardzo czytelne.

Możesz również utworzyć rozłączenie (w którym tylko jeden z warunków musi być spełniony), używając np.logical_orzamiast tego:

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)]
Gekon
źródło
1
Czy istnieje sposób na wdrożenie tego dla zmiennej liczby warunków? Próbowałem dołączanie każdy c_1, c_2, c_3, ... c_nna liście, a następnie przechodząc data[conjunction(conditions_list)]ale dostać błąd ValueError: Item wrong length 5 instead of 37.próbowali także data[conjunction(*conditions_list)], ale pojawia się inny wynik niż data[conjunction(c_1, c_2, c_3, ... c_n )], nie wiem co się dzieje.
user5359531
Znalazłem rozwiązanie błędu w innym miejscu. data[conjunction(*conditions_list)]działa po spakowaniu ramek danych do listy i rozpakowaniu listy na miejscu
user5359531
1
Po prostu zostawiłem komentarz do powyższej odpowiedzi w znacznie bardziej niechlujnej wersji, a potem zauważyłem twoją odpowiedź. Bardzo czysty, bardzo mi się podoba!
dwanderson
To świetna odpowiedź!
Charlie Crown,
1
użyłem: df[f_2 & f_3 & f_4 & f_5 ]z f_2 = df["a"] >= 0itp. Nie ma potrzeby tej funkcji ... (chociaż przyjemne użycie funkcji wyższego rzędu ...)
A. Rabus
19

Najprostsze ze wszystkich rozwiązań:

Posługiwać się:

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

Inny przykład , aby przefiltrować ramkę danych pod kątem wartości należących do lutego 2018 r., Użyj poniższego kodu

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
Gil Baggio
źródło
używam zmiennej zamiast stałej. błąd. df [df []] [df []] daje ostrzeżenie, ale daje poprawną odpowiedź.
Nguai al
8

Od aktualizacji pandy 0.22 dostępne są opcje porównania:

  • gt (większe niż)
  • lt (mniejszy niż)
  • eq (równa się)
  • ne (nie równa się)
  • ge (większe lub równe)

i wiele więcej. Te funkcje zwracają tablicę logiczną. Zobaczmy, jak możemy je wykorzystać:

# 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
źródło
2

Dlaczego tego nie zrobisz?

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

Próbny:

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

Wynik:

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

Jak widać, kolumna „a” została przefiltrowana, gdzie a> = 2.

Jest to nieco szybsze (czas pisania, a nie wydajność) niż łańcuchowanie operatorów. Możesz oczywiście umieścić import na początku pliku.

Obol
źródło
1

Można również wybierać wiersze na podstawie wartości kolumny, których nie ma na liście ani w żadnej iterowalnej. Stworzymy zmienną boolowską tak jak poprzednio, ale teraz zanegujemy zmienną boolowską umieszczając ~ na początku.

Na przykład

list = [1, 0]
df[df.col1.isin(list)]
Ram Prajapati
źródło