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で使用するにはコストがかかります(変換テストとメンバーシップテストの両方)。私の質問は:
pd.Series.isin
_の実装方法の結果であり、文書化されていない明白な結果のようです。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'
_
これは明らかではないかもしれませんが、_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 にあります:
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で起こります:
O(1)
時間または合計O(m)
で発生しますが、Pythonオブジェクトの作成が必要なため、ルックアップは遅くなります。T(n,m)=O(m)
になります明らかに、Cythonを使用することで、このバージョンを少し高速化できます。
しかし、理論は十分です。n
sを固定して、さまざまなm
sの実行時間を見てみましょう。
わかります:前処理の線形時間がbig n
sの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)
は使用しないでください。
n
とm
がほぼ等しい灰色のゾーンがあり、どのソリューションがテストなしで最適であるかを見分けるのは困難です。
あなたの管理下にある場合:C_integer-setとして直接set
を構築するのが最善です(khash
( すでにpandasでラップされています )または、場合によっては一部のc ++実装も)、前処理の必要性を排除します。 pandasに再利用できるものがあるかどうかはわかりませんが、Cythonで関数を記述することはおそらく大したことではありません。
問題は、pandas=もnumpyも(少なくとも私の限られた知識によると)インターフェースにセットの概念がないため、最後の提案はそのままでは機能しません。しかし、 raw-C-set-interfacesは両方の世界で最高です:
私はすばやくダーティな 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を参照):
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
_
次の呼び出しグラフにつながります。
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()
_