私はよくPandas mask
および where
メソッドを使用して、系列の値を更新するときにロジックをより明確にしますただし、比較的パフォーマンスが重要なコードの場合、 numpy.where
に比べてパフォーマンスが大幅に低下しています。
特定のケースでこれを受け入れて満足ですが、知りたいです。
mask
/where
メソッドは追加機能を提供します別inplace
/errors
/try-cast
パラメータ?これらの3つのパラメータは理解していますが、めったに使用しません。たとえば、level
パラメータが何を指しているのかわかりません。mask
/where
がnumpy.where
より優れている重要な反例はありますか?そのような例が存在する場合、それが今後の適切な方法の選択に影響を与える可能性があります。参考までに、Pandas 0.19.2/Python 3.6.0:
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
assert (df[0].mask(df[0] > 0.5, 1).values == np.where(df[0] > 0.5, 1, df[0])).all()
%timeit df[0].mask(df[0] > 0.5, 1) # 145 ms per loop
%timeit np.where(df[0] > 0.5, 1, df[0]) # 113 ms per loop
非スカラー値の場合、パフォーマンスは発散するように見えますさらに:
%timeit df[0].mask(df[0] > 0.5, df[0]*2) # 338 ms per loop
%timeit np.where(df[0] > 0.5, df[0]*2, df[0]) # 153 ms per loop
私はpandas 0.23.3およびPython 3.6を使用しているため、2番目の例でのみ実行時間の実際の違いを確認できます。
しかし、2番目の例の少し異なるバージョンを調べてみましょう(そうすれば、邪魔にならない2*df[0]
になります)。これが私のマシンのベースラインです:
twice = df[0]*2
mask = df[0] > 0.5
%timeit np.where(mask, twice, df[0])
# 61.4 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df[0].mask(mask, twice)
# 143 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpyのバージョンはパンダの約2.3倍高速です。
では、両方の関数のプロファイルを作成して違いを確認しましょう。プロファイリングは、コードの基礎に詳しくない場合に全体像を把握するのに適した方法です。デバッグよりも速く、何が起こっているのかを理解するよりもエラーが発生しにくくなります。コードを読むだけで。
私はLinuxを使用しており、 perf
を使用しています。取得するnumpyのバージョンについては(リストについては付録Aを参照):
>>> perf record python np_where.py
>>> perf report
Overhead Command Shared Object Symbol
68,50% python multiarray.cpython-36m-x86_64-linux-gnu.so [.] PyArray_Where
8,96% python [unknown] [k] 0xffffffff8140290c
1,57% python mtrand.cpython-36m-x86_64-linux-gnu.so [.] rk_random
ご覧のとおり、時間の大部分はPyArray_Where
に費やされています-約69%。不明なシンボルはカーネル関数です(実際にはclear_page
の問題です)-root権限なしで実行しているため、シンボルは解決されません。
pandasの場合(コードについては付録Bを参照)):
>>> perf record python pd_mask.py
>>> perf report
Overhead Command Shared Object Symbol
37,12% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
23,36% python libc-2.23.so [.] __memmove_ssse3_back
19,78% python [unknown] [k] 0xffffffff8140290c
3,32% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
1,48% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
まったく異なる状況:
PyArray_Where
を使用しません-最も顕著な時間消費者はvm_engine_iter_task
です numexpr-functionality です。__memmove_ssse3_back
は約25
%の時間を使用します!おそらく、カーネルの機能のいくつかは、メモリーアクセスにも関連しています。実際、pandas-0.19は内部でPyArray_Where
を使用していました。古いバージョンでは、perf-reportは次のようになります。
Overhead Command Shared Object Symbol
32,42% python multiarray.so [.] PyArray_Where
30,25% python libc-2.23.so [.] __memmove_ssse3_back
21,31% python [kernel.kallsyms] [k] clear_page
1,72% python [kernel.kallsyms] [k] __schedule
そのため、基本的には、内部でnp.where
を使用して+オーバーヘッド(すべてのデータコピー以上、__memmove_ssse3_back
を参照)を使用していました。
pandasがpandasのバージョン0.19でnumpyよりも高速になる可能性があるシナリオはありません-numpyの機能にオーバーヘッドが追加されるだけです。pandasのバージョン0.23.3はまったく異なるストーリーです-ここでnumexpr-moduleが使用されている場合、パンダのバージョンが(少なくともわずかに)速いシナリオがある可能性が非常に高くなります。
このメモリコピーが本当に必要/必要かどうかはわかりません-多分それをパフォーマンスバグと呼ぶことさえできますが、確かなことを十分に知りません。
pandasコピーしないようにするために、いくつかの間接参照を取り除くことで、np.array
の代わりにpd.Series
を渡す)ことができます。次に例を示します。
%timeit df[0].mask(mask.values > 0.5, twice.values)
# 75.7 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
今、pandasは25%遅いだけです。perfはこう言います:
Overhead Command Shared Object Symbol
50,81% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
14,12% python [unknown] [k] 0xffffffff8140290c
9,93% python libc-2.23.so [.] __memmove_ssse3_back
4,61% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
2,01% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
データのコピーははるかに少なくなりますが、オーバーヘッドの主な原因であるnumpyのバージョンよりも多くなります。
それから私の重要なポイント:
パンダは、numpyよりもわずかに高速になる可能性があります(高速になる可能性があるため)。ただし、パンダのデータコピーの処理はやや不透明であり、この可能性が(不要な)データコピーによって覆い隠される時期を予測することは困難です。
where
/mask
のパフォーマンスがボトルネックである場合は、numba/cythonを使用してパフォーマンスを改善します-かなり単純なnumbaとcythonの使用を以下で参照してください。
アイデアは
np.where(df[0] > 0.5, df[0]*2, df[0])
バージョンを作成し、一時ファイルを作成する必要をなくします-df[0]*2
。
Numbaを使用して@ max9111によって提案されたように:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert(np.where(df[0] > 0.5, twice, df[0])==nb_where(df[0].values)).all()
%timeit np.where(df[0] > 0.5, df[0]*2, df[0])
# 85.1 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit nb_where(df[0].values)
# 17.4 ms ± 673 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
これは、numpyのバージョンよりも約5倍高速です!
そして、ここにCythonの助けを借りてパフォーマンスを改善しようとする私のはるかに成功しない試みがあります:
%%cython -a
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[np.float64_t] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert (df[0].mask(df[0] > 0.5, 2*df[0]).values == cy_where(df[0].values)).all()
%timeit cy_where(df[0].values)
# 66.7± 753 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
25%スピードアップします。よくわからないが、なぜシトンはnumbaよりもはるかに遅いのですか?.
リスト:
A:np_where.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
for _ in range(50):
np.where(df[0] > 0.5, twice, df[0])
B:pd_mask.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
mask = df[0] > 0.5
for _ in range(50):
df[0].mask(mask, twice)