web-dev-qa-db-ja.com

pd.eval()を使用したpandasの動的式評価

2つのDataFrameがある場合

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

pd.evalを使用して、1つ以上の列で算術演算を実行したいと思います。具体的には、次のコードを移植したいと思います。

x = 5
df2['D'] = df1['A'] + (df1['B'] * x) 

...evalを使用してコーディングします。 evalを使用する理由は、多くのワークフローを自動化するためです。そのため、動的にワークフローを作成すると便利です。

engineおよびparser引数をよりよく理解して、問題を解決する最善の方法を判断しようとしています。私は documentation を経験しましたが、違いは私には明らかではありませんでした。

  1. コードが最大のパフォーマンスで動作するようにするには、どの引数を使用する必要がありますか?
  2. 式の結果をdf2に戻す方法はありますか?
  3. また、物事をより複雑にするために、文字列式内の引数としてxをどのように渡すのですか?
45
cs95

この答えは、 pd.evaldf.query 、および df.eval によって提供されるさまざまな機能に掘り下げます。

セットアップ
例には、これらのデータフレームが含まれます(特に指定されていない限り)。

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

pandas.eval -「Missing Manual」


説明されている3つの関数のうち、pd.evalが最も重要です。 df.evalおよびdf.queryは、内部でpd.evalを呼び出します。動作と使用法は、3つの関数間で多少の一貫性がありますが、いくつかのマイナーなセマンティクスのバリエーションについては後で説明します。このセクションでは、3つの機能すべてに共通する機能を紹介します-これには、許可された構文、優先規則、およびキーワードが含まれますが、これらに限定されません引数

pd.evalは、変数やリテラルで構成される算術式を評価できます。これらの式は文字列として渡す必要があります。だから、質問に答えるために、あなたができる

x = 5
pd.eval("df1.A + (df1.B * x)")  

ここで注意すべき点がいくつかあります。

  1. 式全体は文字列です
  2. df1df2、およびxは、グローバル名前空間の変数を参照します。これらは、式の解析時にevalによって取得されます
  3. 属性アクセサインデックスを使用して、特定の列にアクセスします。 "df1['A'] + (df1['B'] * x)"を同じ効果に使用することもできます。

以下のtarget=...属性を説明するセクションで、再割り当ての特定の問題に対処します。しかし、今のところ、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

...等々。条件式も同じ方法でサポートされます。以下のステートメントはすべて有効な式であり、エンジンによって評価されます。

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

サポートされているすべての機能と構文の詳細なリストは、 documentation にあります。要約すれば、

  • 左シフト(<<)および右シフト(>>)演算子を除く算術演算(例:df + 2 * pi / s ** 4 % 42-the_golden_ratio)
  • 連鎖比較を含む比較演算(例:2 < df < df2
  • ブール演算、例えばdf < df2 and df3 < df4またはnot df_boollistおよびTupleリテラル、例えば[1, 2]または(1, 2)
  • 属性アクセス、例:df.a
  • 添字式、例えばdf[0]
  • 単純な変数評価、たとえば、pd.eval('df')(これはあまり役に立ちません)
  • 数学関数:sin、cos、exp、log、expm1、log1p、sqrt、sinh、cosh、tanh、arcsin、arccos、arctan、arccosh、arcsinh、arctanh、abs、arctan2.

ドキュメントのこのセクションでは、set/dictリテラル、if-elseステートメント、ループ、内包表記、ジェネレーター式など、サポートされていない構文規則も指定し​​ます。

リストから、次のようなインデックスを含む式を渡すこともできます。

pd.eval('df1.A * (df1.index > 1)')

パーサーの選択:parser=...引数

pd.evalは、式文字列を解析して構文ツリーを生成するときに、2つの異なるパーサーオプションをサポートします:pandasおよびpython。 2つの主な違いは、優先順位ルールがわずかに異なることで強調されています。

デフォルトのパーサーpandasを使用すると、ORオブジェクトでベクトル化されたANDおよびpandas演算を実装するオーバーロードされたビット演算子&および|は、andおよびorと同じ演算子優先順位を持ちます。そう、

pd.eval("(df1 > df2) & (df3 < df4)")

と同じになります

pd.eval("df1 > df2 & df3 < df4")
# pd.eval("df1 > df2 & df3 < df4", parser='pandas')

そしてまた同じ

pd.eval("df1 > df2 and df3 < df4")

ここでは、括弧が必要です。これを従来どおりに行うには、ビット単位演算子のより高い優先順位を上書きする必要があります。

(df1 > df2) & (df3 < df4)

それがなければ、

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().

文字列を評価する際にPythonの実際の演算子優先順位規則との一貫性を維持する場合は、parser='python'を使用します。

pd.eval("(df1 > df2) & (df3 < df4)", parser='python')

2つのタイプのパーサーのその他の違いは、リストおよびタプルノードを使用する==および!=演算子のセマンティクスです。これらは、not inパーサーを使用する場合のinおよび'pandas'と同様のセマンティクスを持っています。例えば、

pd.eval("df1 == [1, 2, 3]")

有効であり、次と同じセマンティクスで実行されます

pd.eval("df1 in [1, 2, 3]")

OTOH、pd.eval("df1 == [1, 2, 3]", parser='python')NotImplementedErrorエラーをスローします。

バックエンドの選択:engine=...引数

numexpr(デフォルト)とpythonの2つのオプションがあります。 numexprオプションは、パフォーマンスのために最適化された numexpr バックエンドを使用します。

'python'バックエンドを使用すると、式はPythonのeval関数に式を渡すだけのように評価されます。たとえば、文字列操作などの式の内部をより柔軟に行うことができます。

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

残念ながら、このメソッドはnumexprエンジンよりもnoパフォーマンス上の利点を提供し、危険な式が評価されないようにするためのセキュリティ対策が非常に少ないため、AT自分のリスクを使用してください!通常、このオプションを'python'に変更するのは、何をしているのかわからない限りお勧めしません。

local_dictおよびglobal_dict引数

式内で使用されているが、現在ネームスペースで定義されていない変数の値を指定すると便利な場合があります。辞書をlocal_dictに渡すことができます

例えば、

pd.eval("df1 > thresh")

UndefinedVariableError: name 'thresh' is not defined

threshが定義されていないため、これは失敗します。ただし、これは機能します。

pd.eval("df1 > thresh", local_dict={'thresh': 10})

これは、辞書から提供する変数がある場合に便利です。または、'python'エンジンを使用して、これを簡単に行うことができます。

mydict = {'thresh': 5}
# Dictionary values with *string* keys cannot be accessed without 
# using the 'python' engine.
pd.eval('df1 > mydict["thresh"]', engine='python')

しかし、これはおそらく'numexpr'エンジンを使用して辞書をlocal_dictまたはglobal_dictに渡すよりもmuch遅くなります。うまくいけば、これはこれらのパラメータの使用について説得力のある議論をするはずです。

target(+ inplace)引数、および割り当て式

通常、これを行うにはより簡単な方法があるため、これは要件ではありませんが、pd.evalの結果を、dictsなどの__getitem__を実装するオブジェクトに割り当てることができます。

質問の例を考えてください

x = 5
df2['D'] = df1['A'] + (df1['B'] * x)

列「D」をdf2に割り当てるには、次のようにします

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

これは、df2のインプレース変更ではありません(ただし、読み進めることができます)。別の例を考えてみましょう:

pd.eval('df1.A + df2.A')

0    10
1    11
2     7
3    16
4    10
dtype: int32

(たとえば)これをDataFrameに割り当てたい場合、次のようにtarget引数を使用できます。

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

dfでインプレースミューテーションを実行する場合は、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

inplaceがターゲットなしで設定されている場合、ValueErrorが発生します。

target引数はいじって楽しいですが、使用する必要はほとんどありません。

df.evalを使用してこれを実行する場合は、割り当てを含む式を使用します。

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


pd.evalの意図しない用途の1つは、ast.literal_evalと非常によく似た方法でリテラル文字列を解析することです。

pd.eval("[1, 2, 3]")
array([1, 2, 3], dtype=object)

'python'エンジンを使用して、ネストされたリストを解析することもできます。

pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python')
[[1, 2, 3], [4, 5], [10]]

そして、文字列のリスト:

pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]

ただし、問題は、長さが100を超えるリストの場合です。

pd.eval(["[1]"] * 100, engine='python') # Works
pd.eval(["[1]"] * 101, engine='python') 

AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis'

このエラー、原因、修正、および回避策の詳細については、 here を参照してください。


DataFrame.eval -pandas.evalとの並置

上記のように、df.evalは内部でpd.evalを呼び出します。 v0.23ソースコード はこれを示しています:

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は引数を作成し、少し検証を行い、引数をpd.evalに渡します。

詳細については、以下をお読みください。 DataFrame.eval()とpandas.eval()またはpython eval() を使用する場合

使用法の違い

DataFrames v/s Series式を使用した式

データフレーム全体に関連付けられた動的クエリの場合は、pd.evalを選択する必要があります。たとえば、df1.evalまたはdf2.evalを呼び出すときに、pd.eval("df1 + df2")に相当するものを指定する簡単な方法はありません。

列名の指定

もう1つの大きな違いは、列へのアクセス方法です。たとえば、df1に2つの列「A」と「B」を追加するには、次の式でpd.evalを呼び出します。

pd.eval("df1.A + df1.B")

Df.evalを使用すると、列名を指定するだけで済みます。

df1.eval("A + B")

df1のコンテキスト内では、「A」と「B」が列名を指すことは明らかです。

indexを使用してインデックスと列を参照することもできます(インデックスに名前が付けられていない場合は、名前を使用します)。

df1.eval("A + index")

または、より一般的には、1つ以上のレベルを持つインデックスを持つDataFrameについては、kを参照できます番目 変数を使用する式のインデックスのレベル "ilevel_k"ndex atlevel k "。IOW、上記の式はdf1.eval("A + ilevel_0")と書くことができます。

これらの規則はqueryにも適用されます。

ローカル/グローバルネームスペースの変数へのアクセス

列名との混同を避けるために、式内で提供される変数の前には「@」記号を付ける必要があります。

A = 5
df1.eval("A > @A") 

queryについても同じことが言えます。

eval内でアクセスするには、列名がpythonの有効な識別子の命名規則に従う必要があることは言うまでもありません。識別子の命名に関する規則のリストについては、 here を参照してください。

複数行のクエリと割り当て

あまり知られていない事実は、evalが割り当てを扱う複数行の式をサポートしていることです。たとえば、いくつかの列の算術演算に基づいてdf1に2つの新しい列「E」と「F」を作成し、3つ目の列「G」を以前に作成した「E」と「F」に基づいて、

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

...気の利いた!ただし、これはqueryではサポートされていないことに注意してください。


eval v/s query-最終単語

df.queryをサブルーチンとしてpd.evalを使用する関数と考えると役立ちます。

通常、(名前が示すように)queryは、条件式(つまりTrue/False値になる式)を評価し、Trueの結果に対応する行を返すために使用されます。次に、式の結果がloc(ほとんどの場合)に渡され、式を満たす行が返されます。ドキュメントによると、

この式の評価結果は最初にDataFrame.locに渡され、多次元キー(たとえば、DataFrame)が原因で失敗した場合、結果はDataFrame.__getitem__()に渡されます。

このメソッドは、トップレベルのpandas.eval()関数を使用して、渡されたクエリを評価します。

類似性の観点から、querydf.evalは、列名と変数にアクセスする方法が似ています。

上記の2つの主な違いは、式の結果の処理方法です。これは、これら2つの関数を使用して式を実際に実行すると明らかになります。たとえば、考慮してください

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

df1で "A"> = "B"であるすべての行を取得するには、次のようにevalを使用します。

m = df1.eval("A >= B")
m
0     True
1    False
2    False
3     True
4     True
dtype: bool

mは、式「A> = B」を評価することによって生成された中間結果を表します。次に、マスクを使用して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

ただし、queryを使用すると、中間結果「m」がlocに直接渡されるため、queryを使用すると、単に

df1.query("A >= B")

   A  B  C  D
0  5  0  3  3
3  8  8  1  6
4  7  7  8  1

パフォーマンスに関しては、まったく同じです

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)

ただし、後者はより簡潔であり、同じ操作を1つのステップで表現します。

このようなqueryを使用して奇妙なこともできることに注意してください(たとえば、df1.indexによってインデックス付けされたすべての行を返すため)

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

しかし、しないでください。

結論:条件式に基づいて行をクエリまたはフィルタリングする場合は、queryを使用してください。

55
cs95

すでにすばらしいチュートリアルですが、より単純な構文に惹かれるeval/queryの使用法に乱入する前に、データセットの行数が15,000未満の場合、パフォーマンスに重大な問題があることに注意してください。

その場合、単にdf.loc[mask1, mask2]を使用します。

参照: https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval

enter image description here

3
astro123