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.

  1. jakich argumentów należy użyć, aby upewnić się, że mój kod działa z maksymalną wydajnością?
  2. czy istnieje sposób na przypisanie wyniku wyrażenia z powrotem do df2?
  3. Ponadto, aby skomplikować sprawę, jak przekazać x jako argument wewnątrz wyrażenia łańcuchowego?
Author: smci, 2018-12-14

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 i df.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:

  1. całe wyrażenie jest ciągiem
  2. df1, df2, i x odnoszą się do zmiennych w globalnej przestrzeni nazw, są one pobierane przez eval podczas parsowania wyrażenia
  3. 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 lub not df_bool list i tuple 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 do DataFrame.__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.

 96
Author: cs95,
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

Tutaj wpisz opis obrazka

 5
Author: astro123,
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