web-dev-qa-db-ja.com

Pandas mask / whereメソッドとNumPy np.where

私はよくPandas mask および where メソッドを使用して、系列の値を更新するときにロジックをより明確にしますただし、比較的パフォーマンスが重要なコードの場合、 numpy.where に比べてパフォーマンスが大幅に低下しています。

特定のケースでこれを受け入れて満足ですが、知りたいです。

  1. Pandas mask/whereメソッドは追加機能を提供しますinplace/errors/try-castパラメータ?これらの3つのパラメータは理解していますが、めったに使用しません。たとえば、levelパラメータが何を指しているのかわかりません。
  2. mask/wherenumpy.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
30
jpp

私は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)
22
ead