web-dev-qa-db-ja.com

Pandasセット対アレイでのpd.Series.isinパフォーマンス

Pythonでは、通常、ハッシュ可能なコレクションのメンバーシップはsetでテストするのが最適です。ハッシュを使用するとO(1)ルックアップの複雑さに対してlistまたは_np.ndarray_のO(n)が得られるため、これはわかっています。

パンダでは、非常に大規模なコレクションのメンバーシップを確認する必要があります。同じことが当てはまると思います。つまり、setまたは_np.ndarray_を使用するよりも、listのシリーズの各アイテムのメンバーシップをチェックする方が効率的です。しかし、これはそうではないようです:

_import numpy as np
import pandas as pd

np.random.seed(0)

x_set = {i for i in range(100000)}
x_arr = np.array(list(x_set))
x_list = list(x_set)

arr = np.random.randint(0, 20000, 10000)
ser = pd.Series(arr)
lst = arr.tolist()

%timeit ser.isin(x_set)                   # 8.9 ms
%timeit ser.isin(x_arr)                   # 2.17 ms
%timeit ser.isin(x_list)                  # 7.79 ms
%timeit np.in1d(arr, x_arr)               # 5.02 ms
%timeit [i in x_set for i in lst]         # 1.1 ms
%timeit [i in x_set for i in ser.values]  # 4.61 ms
_

テストに使用されたバージョン:

_np.__version__  # '1.14.3'
pd.__version__  # '0.23.0'
sys.version     # '3.6.5'
_

_pd.Series.isin_ のソースコードは _numpy.in1d_ を使用していると思います。これは、おそらくsetから_np.ndarray_への変換のオーバーヘッドが大きいことを意味します。

入力を構築するコストを否定すること、パンダへの影響:

  • _x_list_または_x_arr_の要素が一意であることがわかっている場合は、_x_set_に変換しないでください。これは、Pandasで使用するにはコストがかかります(変換テストとメンバーシップテストの両方)。
  • リスト内包表記を使用することは、O(1)セットルックアップのメリットを享受する唯一の方法です。

私の質問は:

  1. 上記の私の分析は正しいですか?これは、_pd.Series.isin_の実装方法の結果であり、文書化されていない明白な結果のようです。
  2. リスト内包表記または_pd.Series.apply_を使用せずに、回避策はありますかdoes O(1) set lookupを利用しますか?それとも、これは避けられないデザインの選択、および/またはパンダのバックボーンとしてNumPyを持つことの帰結でしょうか?

更新:古いセットアップ(Pandas/NumPyバージョン)では、_x_set_が_x_arr_で_pd.Series.isin_を実行した場合のパフォーマンスが向上します。したがって、追加の質問:setのパフォーマンスを悪化させる原因となる、根本的に古いものから新しいものへの変更はありますか?

_%timeit ser.isin(x_set)                   # 10.5 ms
%timeit ser.isin(x_arr)                   # 15.2 ms
%timeit ser.isin(x_list)                  # 9.61 ms
%timeit np.in1d(arr, x_arr)               # 4.15 ms
%timeit [i in x_set for i in lst]         # 1.15 ms
%timeit [i in x_set for i in ser.values]  # 2.8 ms

pd.__version__  # '0.19.2'
np.__version__  # '1.11.3'
sys.version     # '3.6.0'
_
28
jpp

これは明らかではないかもしれませんが、_pd.Series.isin_はO(1)-look upを使用します。

上記のステートメントを証明する分析の後、その洞察を使用して、最速の箱から出してすぐのソリューションを簡単に打ち破ることができるCythonプロトタイプを作成します。


「セット」にn要素があり、「シリーズ」にm要素があるとしましょう。実行時間は次のとおりです。

_ T(n,m)=T_preprocess(n)+m*T_lookup(n)
_

純粋なpythonバージョンの場合、これは次のことを意味します。

  • T_preprocess(n)=0-前処理は不要
  • T_lookup(n)=O(1)-Pythonのセットのよく知られた動作
  • 結果はT(n,m)=O(m)になります

pd.Series.isin(x_arr)はどうなりますか?明らかに、前処理をスキップして線形時間で検索すると、O(n*m)が得られますが、これは受け入れられません。

デバッガーまたはプロファイラー(私はvalgrind-callgrind + kcachegrindを使用しました)の助けを借りて簡単に確認できます。何が起こっているのか:機能しているのは___pyx_pw_6pandas_5_libs_9hashtable_23ismember_int64_関数です。その定義は here にあります:

  • 前処理ステップでは、ハッシュマップ(pandasは khash from klib を使用)を_x_arr_のn要素から、つまり実行時にO(n)
  • mルックアップは、構築されたハッシュマップでそれぞれO(1)または合計O(m)で行われます。
  • 結果はT(n,m)=O(m)+O(n)になります

Numpy-arrayの要素はraw-C-integersであり、元のセットのPythonオブジェクトではないことを覚えておく必要があるため、セットをそのまま使用することはできません。

PythonオブジェクトのセットをC-intのセットに変換する代わりに、単一のC-intをPython-objectに変換して、元のセットを使用することができます。それが_[i in x_set for i in ser.values]_- variantで起こります:

  • 前処理なし。
  • mルックアップは、それぞれO(1)時間または合計O(m)で発生しますが、Pythonオブジェクトの作成が必要なため、ルックアップは遅くなります。
  • 結果はT(n,m)=O(m)になります

明らかに、Cythonを使用することで、このバージョンを少し高速化できます。

しかし、理論は十分です。nsを固定して、さまざまなmsの実行時間を見てみましょう。

enter image description here

わかります:前処理の線形時間がbig nsのnumpy-versionを支配しています。 numpyからpure-python(_numpy->python_)に変換したバージョンは、pure-pythonバージョンと同じ動作をしますが、必要な変換のために遅くなります。これはすべて私たちの分析によるものです。

これは図ではよくわかりません。_n < m_の場合、numpyバージョンが高速になります。この場合、khash- libの高速ルックアップが最も重要な役割を果たし、前処理部分ではありません。

この分析からの私の持ち帰り:

  • _n < m_:O(n)-前処理はそれほどコストがかからないため、_pd.Series.isin_を使用する必要があります。

  • _n > m_:(おそらくcythonizedバージョンの)_[i in x_set for i in ser.values]_を使用する必要があるため、O(n)は使用しないでください。

  • nmがほぼ等しい灰色のゾーンがあり、どのソリューションがテストなしで最適であるかを見分けるのは困難です。

  • あなたの管理下にある場合:C_integer-setとして直接setを構築するのが最善です(khashすでにpandasでラップされています )または、場合によっては一部のc ++実装も)、前処理の必要性を排除します。 pandasに再利用できるものがあるかどうかはわかりませんが、Cythonで関数を記述することはおそらく大したことではありません。


問題は、pandas=もnumpyも(少なくとも私の限られた知識によると)インターフェースにセットの概念がないため、最後の提案はそのままでは機能しません。しかし、 raw-C-set-interfacesは両方の世界で最高です:

  • 値はすでにセットとして渡されるため、前処理は不要
  • 渡されたセットは生のC値で構成されるため、変換は不要

私はすばやくダーティな khashのCython-wrapper (パンダのラッパーに触発された)をコード化しました。これは_pip install https://github.com/realead/cykhash/zipball/master_を介してインストールし、Cythonで使用してisinバージョン:

_%%cython
import numpy as np
cimport numpy as np

from cykhash.khashsets cimport Int64Set

def isin_khash(np.ndarray[np.int64_t, ndim=1] a, Int64Set b):
    cdef np.ndarray[np.uint8_t,ndim=1, cast=True] res=np.empty(a.shape[0],dtype=np.bool)
    cdef int i
    for i in range(a.size):
        res[i]=b.contains(a[i])
    return res
_

さらなる可能性として、c ++の_unordered_map_をラップすることができます(リストCを参照)。これには、c ++ライブラリが必要であり、(後で説明するように)少し遅いという欠点があります。

アプローチの比較(タイミングの作成については、リストDを参照):

enter image description here

khashは_numpy->python_より約20倍速く、純粋なpythonよりも約6倍高速ですが、pure-pythonは私たちが望むものではありません)、さらに約3より高速ですcppのバージョン。


リスト

1)valgrindによるプロファイリング:

_#isin.py
import numpy as np
import pandas as pd

np.random.seed(0)

x_set = {i for i in range(2*10**6)}
x_arr = np.array(list(x_set))


arr = np.random.randint(0, 20000, 10000)
ser = pd.Series(arr)


for _ in range(10):
   ser.isin(x_arr)
_

そしていま:

_>>> valgrind --tool=callgrind python isin.py
>>> kcachegrind
_

次の呼び出しグラフにつながります。

enter image description here

B:実行時間を生成するためのipythonコード:

_import numpy as np
import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt

np.random.seed(0)

x_set = {i for i in range(10**2)}
x_arr = np.array(list(x_set))
x_list = list(x_set)

arr = np.random.randint(0, 20000, 10000)
ser = pd.Series(arr)
lst = arr.tolist()

n=10**3
result=[]
while n<3*10**6:
    x_set = {i for i in range(n)}
    x_arr = np.array(list(x_set))
    x_list = list(x_set)

    t1=%timeit -o  ser.isin(x_arr) 
    t2=%timeit -o  [i in x_set for i in lst]
    t3=%timeit -o  [i in x_set for i in ser.values]

    result.append([n, t1.average, t2.average, t3.average])
    n*=2

#plotting result:
for_plot=np.array(result)
plt.plot(for_plot[:,0], for_plot[:,1], label='numpy')
plt.plot(for_plot[:,0], for_plot[:,2], label='python')
plt.plot(for_plot[:,0], for_plot[:,3], label='numpy->python')
plt.xlabel('n')
plt.ylabel('running time')
plt.legend()
plt.show()
_

C:cpp-wrapper:

_%%cython --cplus -c=-std=c++11 -a

from libcpp.unordered_set cimport unordered_set

cdef class HashSet:
    cdef unordered_set[long long int] s
    cpdef add(self, long long int z):
        self.s.insert(z)
    cpdef bint contains(self, long long int z):
        return self.s.count(z)>0

import numpy as np
cimport numpy as np

cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)

def isin_cpp(np.ndarray[np.int64_t, ndim=1] a, HashSet b):
    cdef np.ndarray[np.uint8_t,ndim=1, cast=True] res=np.empty(a.shape[0],dtype=np.bool)
    cdef int i
    for i in range(a.size):
        res[i]=b.contains(a[i])
    return res
_

D:異なるセットラッパーで結果をプロットする:

_import numpy as np
import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt
from cykhash import Int64Set

np.random.seed(0)

x_set = {i for i in range(10**2)}
x_arr = np.array(list(x_set))
x_list = list(x_set)


arr = np.random.randint(0, 20000, 10000)
ser = pd.Series(arr)
lst = arr.tolist()

n=10**3
result=[]
while n<3*10**6:
    x_set = {i for i in range(n)}
    x_arr = np.array(list(x_set))
    cpp_set=HashSet()
    khash_set=Int64Set()

    for i in x_set:
        cpp_set.add(i)
        khash_set.add(i)


    assert((ser.isin(x_arr).values==isin_cpp(ser.values, cpp_set)).all())
    assert((ser.isin(x_arr).values==isin_khash(ser.values, khash_set)).all())


    t1=%timeit -o  isin_khash(ser.values, khash_set)
    t2=%timeit -o  isin_cpp(ser.values, cpp_set) 
    t3=%timeit -o  [i in x_set for i in lst]
    t4=%timeit -o  [i in x_set for i in ser.values]

    result.append([n, t1.average, t2.average, t3.average, t4.average])
    n*=2

#ploting result:
for_plot=np.array(result)
plt.plot(for_plot[:,0], for_plot[:,1], label='khash')
plt.plot(for_plot[:,0], for_plot[:,2], label='cpp')
plt.plot(for_plot[:,0], for_plot[:,3], label='pure python')
plt.plot(for_plot[:,0], for_plot[:,4], label='numpy->python')
plt.xlabel('n')
plt.ylabel('running time')
ymin, ymax = plt.ylim()
plt.ylim(0,ymax)
plt.legend()
plt.show()
_
32
ead