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.
df.query
ipd.eval
wydaje się, że dobrze pasuje do twojego przypadku użycia. Aby uzyskać informacje na tematpd.eval()
rodziny funkcji, ich funkcji i przypadków użycia, odwiedź stronę Ocena wyrażeń dynamicznych w pandach przy użyciu pd.eval () .Odpowiedzi:
Pandy (i numpy) pozwalają na indeksowanie boolowskie , które będzie znacznie wydajniejsze:
Jeśli chcesz napisać do tego funkcje pomocnicze, rozważ coś w tym zakresie:
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):
źródło
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 prostumap
zastosowaćand
do nich operator?f()
trzeba brać*b
zamiast po prostub
? Czy to jest tak, że użytkownikf()
może nadal używać opcjonalnegoout
parametrulogical_and()
? Prowadzi to do kolejnego małego pobocznego pytania. Jaka jest korzyść / kompromis w zakresie wydajności przekazywania w tablicy przez wout()
porównaniu z użyciem zwracanego zlogical_and()
? Dzięki jeszcze raz!*b
to konieczne, ponieważ przekazujesz dwie tabliceb1
ib2
musisz je rozpakować podczas wywoływanialogical_and
. Jednak drugie pytanie wciąż pozostaje aktualne. Czy istnieje korzyść w zakresie wydajności przekazywania tablicy za pośrednictwemout
parametru dological_and()
zamiast używania jej wartości zwracanej?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
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_or
zamiast tego:źródło
c_1
,c_2
,c_3
, ...c_n
na liście, a następnie przechodzącdata[conjunction(conditions_list)]
ale dostać błądValueError: Item wrong length 5 instead of 37.
próbowali takżedata[conjunction(*conditions_list)]
, ale pojawia się inny wynik niżdata[conjunction(c_1, c_2, c_3, ... c_n )]
, nie wiem co się dzieje.data[conjunction(*conditions_list)]
działa po spakowaniu ramek danych do listy i rozpakowaniu listy na miejscudf[f_2 & f_3 & f_4 & f_5 ]
zf_2 = df["a"] >= 0
itp. Nie ma potrzeby tej funkcji ... (chociaż przyjemne użycie funkcji wyższego rzędu ...)Najprostsze ze wszystkich rozwiązań:
Posługiwać się:
Inny przykład , aby przefiltrować ramkę danych pod kątem wartości należących do lutego 2018 r., Użyj poniższego kodu
źródło
Od aktualizacji pandy 0.22 dostępne są opcje porównania:
i wiele więcej. Te funkcje zwracają tablicę logiczną. Zobaczmy, jak możemy je wykorzystać:
źródło
Dlaczego tego nie zrobisz?
Próbny:
Wynik:
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.
źródło
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
źródło