Dynamicznie Oceniać wyrażenie ze wzoru w Pandzie?
Chciałbym wykonać arytmetykę na jednej lub kilku kolumnach ramek danych używając pd.eval
. W szczególności chciałbym portować następujący kod, który ocenia formułę:
x = 5
df2['D'] = df1['A'] + (df1['B'] * x)
... do kodu za pomocą pd.eval
. Powodem używania pd.eval
jest to, że chciałbym zautomatyzować wiele przepływów pracy, więc tworzenie ich dynamicznie będzie dla mnie przydatne.
Moje dwa wejściowe ramki danych to:
import pandas as pd
import numpy as np
np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df1
A B C D
0 5 0 3 3
1 7 9 3 5
2 2 4 7 6
3 8 8 1 6
4 7 7 8 1
df2
A B C D
0 5 9 8 9
1 4 3 0 3
2 5 0 2 3
3 8 1 3 3
4 3 7 0 1
Staram się lepiej zrozumieć pd.eval
'S engine
i parser
argumenty do ustalenia jak najlepiej rozwiązać mój problem. Przejrzałem dokumentację , ale różnica nie była dla mnie jasna.
- jakich argumentów należy użyć, aby upewnić się, że mój kod działa z maksymalną wydajnością?
- czy istnieje sposób na przypisanie wyniku wyrażenia z powrotem do
df2
? - Ponadto, aby skomplikować sprawę, jak przekazać
x
jako argument wewnątrz wyrażenia łańcuchowego?
2 answers
Możesz użyć 1) pd.eval()
, 2) df.query()
, lub 3) df.eval()
. Ich różne funkcje i funkcjonalność zostały omówione poniżej.
Przykłady dotyczą tych ramek danych (o ile nie określono inaczej).
np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df3 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df4 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
1) pandas.eval
To jest "brakujący podręcznik", który powinien zawierać pandy doc. Uwaga: spośród trzech omawianych funkcji,
pd.eval
jest najważniejsza.df.eval
idf.query
call Pod maską. Zachowanie i użycie jest mniej więcej spójny w trzech funkcjach, z niewielkimi semantycznymi zmiany, które zostaną wyróżnione później. Ta sekcja będzie wprowadzenie funkcjonalności, która jest wspólna dla wszystkich trzech funkcji-obejmuje to (ale nie wyłącznie) dozwoloną składnię, reguły pierwszeństwa i argumenty słów kluczowych .
pd.eval
może oceniać wyrażenia arytmetyczne, które mogą składać się ze zmiennych i / lub literały. Wyrażenia te muszą być przekazywane jako ciągi znaków. Więc, aby odpowiedzieć na pytanie jak stwierdzono, możesz zrobić
x = 5
pd.eval("df1.A + (df1.B * x)")
Kilka rzeczy do odnotowania tutaj:
- całe wyrażenie jest ciągiem
-
df1
,df2
, ix
odnoszą się do zmiennych w globalnej przestrzeni nazw, są one pobierane przezeval
podczas parsowania wyrażenia - dostęp do określonych kolumn odbywa się za pomocą atrybutu accessor index. Możesz również użyć
"df1['A'] + (df1['B'] * x)"
do tego samego efekt.
Poruszę konkretną kwestię ponownego przypisania w sekcji wyjaśniającej atrybut target=...
poniżej. Ale na razie, oto bardziej proste przykłady ważnych operacji z pd.eval
:
pd.eval("df1.A + df2.A") # Valid, returns a pd.Series object
pd.eval("abs(df1) ** .5") # Valid, returns a pd.DataFrame object
...i tak dalej. Wyrażenia warunkowe są również obsługiwane w ten sam sposób. Poniższe instrukcje są poprawnymi wyrażeniami i będą oceniane przez silnik.
pd.eval("df1 > df2")
pd.eval("df1 > 5")
pd.eval("df1 < df2 and df3 < df4")
pd.eval("df1 in [1, 2, 3]")
pd.eval("1 < 2 < 3")
Listę opisującą wszystkie obsługiwane funkcje i składnię można znaleźć w dokumentacja . W skrócie,
- operacje arytmetyczne z wyjątkiem operatorów left shift (
<<
) I right shift (>>
), np.df + 2 * pi / s ** 4 % 42
- the_golden_ratio- operacje porównawcze, w tym porównania łańcuchowe, np.
2 < df < df2
- operacje logiczne, np.
df < df2 and df3 < df4
lubnot df_bool
list
ituple
literały, np.[1, 2]
lub(1, 2)
- dostęp do atrybutów, np.
df.a
- wyrażenia indeksowe, np.,
df[0]
- prosta ocena zmiennych, np.
pd.eval('df')
(nie jest to zbyt przydatne)- funkcje matematyczne: sin, cos, exp, log, expm1, log1p, sqrt, sinh, cosh, tanh, arcsin, arccos, arctan, arccosh, arcsinh, arctanh, abs i arctan2
Ta sekcja dokumentacji określa również reguły składni, które nie są obsługiwane, w tym set
/dict
literały, wyrażenia if-else, pętle i wyrażenia generatora.
Z listy, jest oczywiste, że można również przekazać wyrażenia zawierające indeks, takie jak
pd.eval('df1.A * (df1.index > 1)')
1a) wybór parsera: argument parser=...
pd.eval
obsługuje dwie różne opcje parsera podczas parsowania ciągu wyrażeń w celu wygenerowania drzewa składni: pandas
i python
. Główna różnica między nimi jest podkreślona przez nieco różniące się zasady pierwszeństwa.
Używając domyślnego parsera pandas
, przeciążone operatory bitowe &
i |
, które implementują wektoryzowane i operacje AND OR z obiektami Panda będą miały ten sam priorytet operatora co and
i or
.
pd.eval("(df1 > df2) & (df3 < df4)")
Będzie taka sama jak
pd.eval("df1 > df2 & df3 < df4")
# pd.eval("df1 > df2 & df3 < df4", parser='pandas')
A także to samo co
pd.eval("df1 > df2 and df3 < df4")
Tutaj, nawiasy są niezbędne. Aby to zrobić konwencjonalnie, parens musiałby zastąpić wyższy priorytet operatorów bitowych: {169]}
(df1 > df2) & (df3 < df4)
Bez tego, kończymy z
df1 > df2 & df3 < df4
ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
Użyj parser='python'
jeśli chcesz zachować spójność z rzeczywistym operatorem Pythona reguły pierwszeństwa podczas oceniania ciągu znaków.
pd.eval("(df1 > df2) & (df3 < df4)", parser='python')
Inną różnicą między tymi dwoma typami parserów jest semantyka operatorów ==
i !=
z węzłami list i tuple, które mają podobną semantykę jak in
i not in
, gdy używają parsera 'pandas'
. Na przykład,
pd.eval("df1 == [1, 2, 3]")
Jest poprawna i będzie działać z tą samą semantyką co
pd.eval("df1 in [1, 2, 3]")
OTOH, pd.eval("df1 == [1, 2, 3]", parser='python')
rzuci NotImplementedError
błąd.
1b) wybór zaplecza: The engine=...
argument
Istnieją dwie opcje - numexpr
(domyślna) i python
. Opcja numexpr
wykorzystuje backend numexpr , który jest zoptymalizowany pod kątem wydajności.
Z backendem 'python'
Twoje wyrażenie jest obliczane podobnie do przekazania wyrażenia do funkcji eval
Pythona. Masz elastyczność wykonywania więcej wewnątrz wyrażeń, takich jak na przykład operacje łańcuchowe.
df = pd.DataFrame({'A': ['abc', 'def', 'abacus']})
pd.eval('df.A.str.contains("ab")', engine='python')
0 True
1 False
2 True
Name: A, dtype: bool
Niestety, ta metoda oferuje nie korzyści z wydajności na silniku numexpr
i jest bardzo niewiele środków bezpieczeństwa, aby zapewnić, że niebezpieczne wyrażenia nie są oceniane, więc używaj na własne ryzyko ! Generalnie nie zaleca się zmiany tej opcji na 'python'
, chyba że wiesz, co robisz.
1C) local_dict
i global_dict
argumenty
Czasami przydatne jest podanie wartości zmiennych używanych w wyrażeniach, ale obecnie nie zdefiniowanych w przestrzeni nazw. Możesz przekazać słownik local_dict
Dla przykład:
pd.eval("df1 > thresh")
UndefinedVariableError: name 'thresh' is not defined
To się nie powiedzie, ponieważ {[98] } nie jest zdefiniowane. Jednak to działa:
pd.eval("df1 > thresh", local_dict={'thresh': 10})
Jest to przydatne, gdy masz zmienne do dostarczenia ze słownika. Można to zrobić za pomocą silnika 'python'
:
mydict = {'thresh': 5}
# Dictionary values with *string* keys cannot be accessed without
# using the 'python' engine.
pd.eval('df1 > mydict["thresh"]', engine='python')
Ale to będzie prawdopodobnie dużo wolniejsze niż używanie silnika 'numexpr'
i przekazywanie słownika do local_dict
lub {96]}. Miejmy nadzieję, że powinno to stanowić przekonujący argument za wykorzystaniem tych parametry.
1d) The target
(+ inplace
) argument i wyrażenia przypisania
Nie jest to często wymagane, ponieważ zazwyczaj istnieją prostsze sposoby na to, ale możesz przypisać wynik pd.eval
do obiektu, który implementuje __getitem__
, takie jak dict
s, i (zgadłeś) ramki danych.
Rozważ przykład w pytaniu
x = 5 df2['D'] = df1['A'] + (df1['B'] * x)
Aby przypisać kolumnę" D " do df2
, robimy
pd.eval('D = df1.A + (df1.B * x)', target=df2)
A B C D
0 5 9 8 5
1 4 3 0 52
2 5 0 2 22
3 8 1 3 48
4 3 7 0 42
To nie jest miejsce modyfikacja df2
} (ale może być... Czytaj dalej). Rozważmy inny przykład:
pd.eval('df1.A + df2.A')
0 10
1 11
2 7
3 16
4 10
dtype: int32
Jeśli chcesz (na przykład) przypisać to z powrotem do ramki danych, możesz użyć argumentu target
w następujący sposób:
df = pd.DataFrame(columns=list('FBGH'), index=df1.index)
df
F B G H
0 NaN NaN NaN NaN
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
df = pd.eval('B = df1.A + df2.A', target=df)
# Similar to
# df = df.assign(B=pd.eval('df1.A + df2.A'))
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Jeśli chcesz wykonać mutację in-place NA df
, Ustaw inplace=True
.
pd.eval('B = df1.A + df2.A', target=df, inplace=True)
# Similar to
# df['B'] = pd.eval('df1.A + df2.A')
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Jeśli inplace
jest ustawione bez celu, to ValueError
jest podniesione.
Podczas gdy argument target
jest zabawny, rzadko będziesz musiał go używać.
Jeśli jeśli chcesz to zrobić za pomocą df.eval
, użyj wyrażenia obejmującego przypisanie:
df = df.eval("B = @df1.A + @df2.A")
# df.eval("B = @df1.A + @df2.A", inplace=True)
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Uwaga
Jednym z niezamierzonych zastosowań pd.eval
jest parsowanie ciągów literalnych w sposób bardzo podobny do ast.literal_eval
:
pd.eval("[1, 2, 3]")
array([1, 2, 3], dtype=object)
Może również analizować zagnieżdżone listy za pomocą silnika 'python'
:
pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python')
[[1, 2, 3], [4, 5], [10]]
I listy ciągów:
pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]
Problem dotyczy jednak list o długości większej niż 100:
pd.eval(["[1]"] * 100, engine='python') # Works
pd.eval(["[1]"] * 101, engine='python')
AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis'
Więcej informacji może ten błąd, powoduje, poprawki i obejścia można znaleźć tutaj .
2) DataFrame.eval
:
Jak wspomniano powyżej, df.eval
wywołuje pd.eval
pod maską, z odrobiną zestawienia argumentów. Kod źródłowy v0. 23 pokazuje to:
def eval(self, expr, inplace=False, **kwargs):
from pandas.core.computation.eval import eval as _eval
inplace = validate_bool_kwarg(inplace, 'inplace')
resolvers = kwargs.pop('resolvers', None)
kwargs['level'] = kwargs.pop('level', 0) + 1
if resolvers is None:
index_resolvers = self._get_index_resolvers()
resolvers = dict(self.iteritems()), index_resolvers
if 'target' not in kwargs:
kwargs['target'] = self
kwargs['resolvers'] = kwargs.get('resolvers', ()) + tuple(resolvers)
return _eval(expr, inplace=inplace, **kwargs)
eval
tworzy argumenty, wykonuje małą walidację i przekazuje argumenty pd.eval
.
Aby dowiedzieć się więcej, możesz przeczytać: kiedy używać DataFrame.eval () kontra pandy.eval () lub python eval()
2A) różnice w użytkowaniu
2A1) wyrażenia z ramkami danych a wyrażenia szeregowe
Dla dynamicznych zapytań związanych z całymi ramkami danych, powinieneś wybrać pd.eval
. Na przykład, nie ma prostego sposobu, aby określić odpowiednik pd.eval("df1 + df2")
podczas wywoływania df1.eval
lub df2.eval
.
2a2) określanie nazw kolumn
Inną ważną różnicą jest dostęp do kolumn. Na przykład, aby dodać dwie kolumny " A " i " B " w df1
, należy wywołać pd.eval
z następującym wyrażeniem:
pd.eval("df1.A + df1.B")
Z df.eval, wystarczy podać tylko nazwy kolumn:
df1.eval("A + B")
Ponieważ w kontekście df1
jest jasne, że "A "i" B " odnoszą się do nazw kolumn.
Możesz również odwoływać się do indeksu i kolumn za pomocą index
(chyba że indeks jest nazwany, w którym to przypadku użyjesz nazwy).
df1.eval("A + index")
Lub, ogólniej, dla dowolnego DataFrame z indeks mający 1 lub więcej poziomów, można odnieść się do poziomu KTH w wyrażeniu za pomocą zmiennej "ilevel_k", co oznacza "index na poziomie k ". IOW, powyższe wyrażenie można zapisać jako df1.eval("A + ilevel_0")
.
Te zasady dotyczą również df.query
.
2A3) dostęp do zmiennych w lokalnej / globalnej przestrzeni nazw
Zmienne dostarczane wewnątrz wyrażeń muszą być poprzedzone symbolem"@", aby uniknąć mylenie z nazwami kolumn.
A = 5
df1.eval("A > @A")
To samo dotyczy query
.
Jest rzeczą oczywistą, że nazwy kolumn muszą być zgodne z zasadami poprawnego nazewnictwa identyfikatorów w Pythonie, aby były dostępne wewnątrz eval
. Zobacz tutaj aby uzyskać listę reguł dotyczących nazywania identyfikatorów.
2a4) Multiline Queries and Assignment
Mało znanym faktem jest to, że eval
obsługuje wyrażenia Wielowierszowe, które zajmują się przypisywaniem (podczas gdy {135]} Nie). Na przykład, aby utworzyć dwie nowe kolumny " E " i " F "w df1 na podstawie pewnych operacji arytmetycznych na niektórych kolumnach, a trzecią kolumnę" G "na podstawie wcześniej utworzonych" E " i "F", możemy zrobić
df1.eval("""
E = A + B
F = @df2.A + @df2.B
G = E >= F
""")
A B C D E F G
0 5 0 3 3 5 14 False
1 7 9 3 5 16 7 True
2 2 4 7 6 6 5 True
3 8 8 1 6 16 9 True
4 7 7 8 1 14 10 True
3) eval
vs query
Pomaga myśleć o df.query
jako o funkcji, która używa pd.eval
jako podprogramu.
Zazwyczaj, query
(jak sama nazwa wskazuje) jest używany do oceny wyrażeń warunkowych (tj. wyrażeń, które dają wartości True / False) i zwracają wiersze odpowiadający wynikowi True
. Wynik wyrażenia jest następnie przekazywany do loc
(w większości przypadków), aby zwrócić wiersze, które spełniają wyrażenie. Zgodnie z dokumentacją,
Wynik oceny tego wyrażenia jest najpierw przekazywany do
DataFrame.loc
a jeśli to się nie powiedzie z powodu wielowymiarowego klucza (np. DataFrame) wtedy wynik zostanie przekazany doDataFrame.__getitem__()
.Ta metoda wykorzystuje funkcję najwyższego poziomu
pandas.eval()
do oceny przeszedł zapytanie.
Pod względem podobieństwa, query
i df.eval
są podobne pod względem dostępu do nazw kolumn i zmiennych.
Ta kluczowa różnica między tymi dwoma, jak wspomniano powyżej, polega na tym, jak obsługują wynik wyrażenia. Staje się to oczywiste, gdy faktycznie uruchamiasz wyrażenie za pomocą tych dwóch funkcji. Na przykład rozważmy
df1.A
0 5
1 7
2 2
3 8
4 7
Name: A, dtype: int32
df1.B
0 9
1 3
2 0
3 1
4 7
Name: B, dtype: int32
Aby uzyskać wszystkie wiersze, w których "A" >= "B" W df1
, użyjemy eval
w następujący sposób:
m = df1.eval("A >= B")
m
0 True
1 False
2 False
3 True
4 True
dtype: bool
m
reprezentuje wynik pośredni generowany przez ocenę wyrażenia "A > = B". Następnie używamy maski do filtrowania df1
:
df1[m]
# df1.loc[m]
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
Jednak z query
wynik pośredni "m" jest bezpośrednio przekazywany do loc
, więc z query
, po prostu musisz zrobić
df1.query("A >= B")
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
Pod względem wydajności, jest to dokładnie to samo.
df1_big = pd.concat([df1] * 100000, ignore_index=True)
%timeit df1_big[df1_big.eval("A >= B")]
%timeit df1_big.query("A >= B")
14.7 ms ± 33.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.7 ms ± 24.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Ale ta ostatnia jest bardziej zwięzła i wyraża tę samą operację w jednym kroku.
Zauważ, że możesz też robić dziwne rzeczy z query
w ten sposób (aby, powiedzmy, zwrócić wszystkie wiersze indeksowane przez df1.indeks)
df1.query("index")
# Same as df1.loc[df1.index] # Pointless,... I know
A B C D
0 5 0 3 3
1 7 9 3 5
2 2 4 7 6
3 8 8 1 6
4 7 7 8 1
Ale nie rób tego.]}
Bottom line: proszę używać query
podczas zapytań lub filtrowania wierszy na podstawie wyrażenia warunkowego.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-06 21:29:55
Świetny samouczek, ale pamiętaj, że zanim zaczniesz szaleńczo korzystać z eval/query
przyciągnięta prostszą składnią, ma poważne problemy z wydajnością, jeśli twój zbiór danych ma mniej niż 15 000 wierszy.
W takim przypadku wystarczy użyć df.loc[mask1, mask2]
.
Zobacz: https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-29 05:05:58