データ科学IPythonノート7.15高性能Pandas


#7.15高性能Pandas:eval()query()原文:High-Performance Pandas:eval()andquery()ドラゴン
プロトコル:CC BY-NC-SA 4.0
本節は『Pythonデータ科学マニュアル』(Python Data Science Handbook)の抜粋です.
前述の章では、PyDataテクノロジースタックの力は、NumPyとPandasが直感的な文法を通じて、基本的な操作をCにプッシュする能力に基づいて構築されていることを示しています.例えば、NumPyにおける量子化/ブロードキャスト操作、およびPandasのパケットタイプ操作などです.これらの抽象は多くの一般的な使用例に対して効率的で有効ですが、通常は一時的な中間オブジェクトの作成に依存し、計算時間とメモリ使用のオーバーヘッドを発生させる可能性があります.
バージョン0.13(2014年1月リリース)から、Pandasには、高価な中間配列割り当てを必要とせずに、Cと同じ速度の操作に直接アクセスできる実験的なツールが含まれています.これらは、Numexprパケットに依存するeval()およびquery()関数である.このノートでは、それらの使用方法を徐々に紹介し、いつ使用するかを考慮できる経験則を提供します.query()およびeval()の動機:複合式
NumPyとPandasが高速量子化操作をサポートしているのを見たことがあります.たとえば、2つの配列の要素を加算する場合:
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(1000000)
y = rng.rand(1000000)
%timeit x + y

# 100 loops, best of 3: 3.39 ms per loop

「NumPy配列の計算:汎用関数」で説明したように、Pythonループまたは導出式による加算よりもはるかに高速です.
%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))

# 1 loop, best of 3: 266 ms per loop

しかし、複合式を計算する場合、この抽象はそれほど有効ではない可能性があります.たとえば、次の式を考慮します.
mask = (x > 0.5) & (y < 0.5)

NumPyは各サブエクスプレッションを計算するので、次のようになります.
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2

すなわち、各中間ステップはメモリに明確に割り当てられる.xおよびy配列が非常に大きい場合、メモリおよび計算オーバーヘッドが大量に発生する可能性があります.Numexprライブラリを使用すると、完全な中間配列を割り当てることなく、要素ごとにこのタイプの複合式を計算できます.Numexprドキュメントには詳細がありますが、このライブラリは文字列を受け入れており、計算したいNumPyスタイルの式を提供しています.
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)

# True

ここでの利点は,Numexprが完全な一時配列を用いないように式を計算するので,特に大型配列に対してNumPyよりも効率的であることである.ここで議論するPandaseval()およびquery()ツールは,概念的に類似しており,Numexprパケットに依存する.
効率的な操作のためのpandas.eval()Pandasのeval()関数は、DataFrameを使用して操作を効率的に計算する文字列式を受け入れます.例えば、以下のDataFrame:
import pandas as pd
nrows, ncols = 100000, 100
rng = np.random.RandomState(42)
df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols))
                      for i in range(4))

4つのDataFrameの合計を計算するには、典型的なPandasメソッドを使用します.
%timeit df1 + df2 + df3 + df4

# 10 loops, best of 3: 87.1 ms per loop

式を文字列にすることで、pd.evalで同じ結果を計算できます.
%timeit pd.eval('df1 + df2 + df3 + df4')

# 10 loops, best of 3: 42.2 ms per loop

この式のeval()バージョンの速度は約50%(メモリの使用量が少ない)向上し、同じ結果が得られました.
np.allclose(df1 + df2 + df3 + df4,
            pd.eval('df1 + df2 + df3 + df4'))
            
# True
pd.eval()サポートされているアクション
Pandas v 0から.16から、pd.eval()は幅広い操作をサポートします.これらを実証するために、次の整数DataFrameを使用します.
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
                           for i in range(5))

算術演算子pd.eval()すべての演算子をサポートします.たとえば、次のようになります.
result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)

# True

比較演算子pd.eval()チェーン式を含むすべての比較演算子をサポートします.
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)

# True

ビット単位演算子pd.eval()サポート&および|ビット別演算子:
result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')
np.allclose(result1, result2)

# True

さらに、ブール式での字面andおよびorの使用をサポートします.
result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)

# True

オブジェクトのプロパティとインデックスpd.eval()は、obj.attr構文によるオブジェクト属性へのアクセスをサポートし、obj[index]構文によるインデックスをサポートします.
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)

# True

その他の演算子
関数呼び出し、条件文、ループ、その他のより複雑な構造などの他の操作は、現在pd.eval()で実装されていません.これらのより複雑な式を実行する場合は、Numexprライブラリ自体を使用します.
列単位の演算に使用DataFrame.eval()Pandasにトップクラスのpd.eval()関数があるように、DataFrameにはeval()メソッドがあり、その動作は似ています.eval()メソッドの利点は、カラムが名前で参照できることです.このラベル付き配列を例として使用します.
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()

A
B
C
0
0.375506
0.406939
0.069938
1
0.069087
0.235615
0.154374
2
0.677945
0.433839
0.652324
3
0.264038
0.808055
0.347197
4
0.589161
0.252418
0.557789
上記のpd.eval()を使用すると、式を計算するために3つの列を使用できます.
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
np.allclose(result1, result2)

# True
DataFrame.eval()メソッドでは、カラムを使用して式をより簡潔に解くことができます.
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)

# True

カラム名は、解を要求する式の変数とみなされ、結果は私たちが望んでいる結果であることに注意してください.DataFrame.eval()の付与
先ほど説明したオプションに加えて、DataFrame.eval()は任意のカラムに値を割り当てることもできます.以前のDataFrameを使用します.列ABがあります.
df.head()

A
B
C
0
0.375506
0.406939
0.069938
1
0.069087
0.235615
0.154374
2
0.677945
0.433839
0.652324
3
0.264038
0.808055
0.347197
4
0.589161
0.252418
0.557789 Cを使用して、新しい列df.eval()を作成し、他の列から計算された値を割り当てることができます.
df.eval('D = (A + B) / C', inplace=True)
df.head()

A
B
C
D
0
0.375506
0.406939
0.069938
11.187620
1
0.069087
0.235615
0.154374
1.973796
2
0.677945
0.433839
0.652324
1.704344
3
0.264038
0.808055
0.347197
3.087857
4
0.589161
0.252418
0.557789
1.508776
同じ方法で、既存の列を変更できます.
df.eval('D = (A - B) / C', inplace=True)
df.head()

A
B
C
D
0
0.375506
0.406939
0.069938
-0.449425
1
0.069087
0.235615
0.154374
-1.078728
2
0.677945
0.433839
0.652324
0.374209
3
0.264038
0.808055
0.347197
-1.566886
4
0.589161
0.252418
0.557789
0.603708 'D'のローカル変数DataFrame.eval()メソッドは、Pythonローカル変数を使用することができる追加の構文をサポートします.以下を考慮します.
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)

# True

ここでは、DataFrame.eval()文字タグ変数名はカラム名ではなく、カラムの名前空間とPythonオブジェクトの名前空間の2つの「名前空間」に関する式を効率的に計算できます.この@文字は@メソッドのみでサポートされ、DataFrame.eval()関数ではサポートされません.pandas.eval()関数は1つの(Python)ネーミングスペースにしかアクセスできないためです.pandas.eval ()メソッドDataFrame.query()は、DataFrameメソッドと呼ばれる文字列に基づく別の評価方法がある.以下を考慮します.
result1 = df[(df.A < 0.5) & (df.B < 0.5)]
result2 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')
np.allclose(result1, result2)

# True
query()について説明するときに使用する例と同様に、DataFrame.eval()列に関する式です.しかし、DataFrame構文では表現できません!逆に、このタイプのフィルタリング操作では、DataFrame.eval()メソッドを使用できます.
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)

# True

これは、より効率的な計算として、マスク式に比べて読みやすく、理解しやすい.注意query()メソッドもquery()フラグを受け入れてローカル変数をマークします.
Cmean = df['C'].mean()
result1 = df[(df.A < Cmean) & (df.B < Cmean)]
result2 = df.query('A < @Cmean and B < @Cmean')
np.allclose(result1, result2)

# True

パフォーマンス:これらの関数はいつ使用されますか?
これらの関数を使用するかどうかを考慮すると、計算時間とメモリ使用の2つの注意点があります.メモリの使用は最も予測可能な側面です.前述したように、NumPy配列またはPandas@に関する各複合式は、暗黙的に作成された一時配列を生成します.たとえば、これは次のようになります.
x = df[(df.A < 0.5) & (df.B < 0.5)]

ほぼこれに相当します.
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3]

一時DataFrameのサイズが利用可能なシステムメモリ(通常は数千メガバイト)に比べて大きい場合、DataFrameまたはeval()式を使用するのは良いアイデアです.配列の大体のサイズ(バイト単位)は、次の方法で確認できます.
df.values.nbytes

# 32000

パフォーマンスの面では、システムメモリを超えていなくても、query()の方が高速です.問題は、あなたの一時eval()がシステム上のL 1またはL 2 CPUキャッシュのサイズと比較して(2016年は通常数メガバイト)どうですか.より大きい場合、DataFrameは、異なるメモリキャッシュ間のいくつかの値の移動を回避し、遅い場合があります.
実際には、従来の方法とeval()方法との間の計算時間の違いを発見しました.通常は大きくありません.もしあれば、従来の方法は小さい配列にとってもっと速いです.eval/queryの利点は、主にメモリを節約し、より明確な構文を提供することです.
私たちはすでにeval/queryeval()の大部分の詳細をカバーしています.これらの詳細については、Pandasドキュメントを参照してください.特に、これらのクエリを実行する異なる解析器とエンジンを指定することができる.詳細については、「パフォーマンスの向上」セクションの説明を参照してください.