【问题标题】:Pandas pd.Series.isin performance with set versus arrayPandas pd.Series.isin 性能与集合与数组
【发布时间】:2018-11-19 15:14:15
【问题描述】:

通常在 Python 中,可散列集合的成员资格最好通过 set 进行测试。我们知道这一点,因为散列的使用使我们的查找复杂度为 O(1),而 listnp.ndarray 的查找复杂度为 O(n)。

在 Pandas 中,我经常需要检查大型集合中的成员资格。我认为这同样适用,即检查系列中的每个项目是否属于set 比使用listnp.ndarray 更有效。但是,情况似乎并非如此:

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 的开销很大。

否定构建输入的成本,对 Pandas 的影响:

  • 如果您知道x_listx_arr 的元素是独一无二的,请不要费心转换为x_set。与 Pandas 一起使用会很昂贵(转换和成员资格测试)。
  • 使用列表推导是受益于 O(1) 集合查找的唯一方法。

我的问题是:

  1. 我上面的分析正确吗?这似乎是如何实现 pd.Series.isin 的一个明显但未记录的结果。
  2. 是否有一种解决方法,不使用列表解析或pd.Series.apply 使用 O(1) 集查找?或者这是不可避免的设计选择和/或将 NumPy 作为 Pandas 主干的必然结果?

更新:在较旧的设置(Pandas / NumPy 版本)上,x_set 的性能优于 x_arrpd.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'

【问题讨论】:

  • 请注意,in1d 仅用于大小大于 1000000 的系列。
  • 那里有对np.unique的调用,所以你自己调用set并没有什么不同。
  • myvalues 小到无关紧要时,O(1) 查找就无关紧要了。当myvalues 足够大以至于 O(1) 查找仍然不够时......这就是唯一 + 合并排序开始的地方。这是巧妙的 imo。
  • 你的 randint 并没有达到集合的最大值,如果你使用 200000,你可能会看到不同的数字。使用 numpy.in1d 似乎是一个奇怪的选择。但总的来说......什么时候出现? (你什么时候在一个大集合上测试包含?)另一个解决方法是使用ser.apply(x_set.__contains__)。这很奇怪,因为我认为有一个 pandas 内部类似 dict 的数据结构可以在这种情况下使用(khash?)。
  • 我认为我可以用x_idx = pd.RangeIndex(100000); %timeit ser.isin(x_idx) 做得更好,但令人抓狂的是它比你所有的方法都慢。直觉在这里似乎行不通。

标签: python performance pandas numpy series


【解决方案1】:

这可能并不明显,但pd.Series.isin 使用O(1)-查找每个元素。

在证明上述陈述的分析之后,我们将利用其洞察力创建一个 Cython 原型,该原型可以轻松击败最快的开箱即用解决方案。


假设“集合”有n 元素,而“系列”有m 元素。那么运行时间是:

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

对于纯python版本,这意味着:

  • T_preprocess(n)=0 - 无需预处理
  • T_lookup(n)=O(1) - python 集合的众所周知的行为
  • 结果为@​​987654338@

pd.Series.isin(x_arr) 会发生什么?显然,如果我们跳过预处理和线性时间搜索,我们将得到O(n*m),这是不可接受的。

在调试器或分析器的帮助下很容易看到(我使用了 valgrind-callgrind+kcachegrind),发生了什么:工作的马是函数__pyx_pw_6pandas_5_libs_9hashtable_23ismember_int64。其定义见here

  • 在预处理步骤中,使用来自x_arrn 元素创建哈希映射(熊猫使用khash from klib),即在运行时O(n)
  • m 查找发生在 O(1) 每个或 O(m) 中,在构造的哈希映射中。
  • 结果为@​​987654348@

我们必须记住 - numpy-array 的元素是原始 C 整数,而不是原始集合中的 Python 对象 - 所以我们不能按原样使用集合。

将 Python 对象集转换为 C-int 集的另一种方法是将单个 C-int 转换为 Python-object,从而能够使用原始集。这就是[i in x_set for i in ser.values]-variant 中发生的情况:

  • 无预处理。
  • m 次查找发生在每次O(1) 时间或总共O(m),但由于需要创建 Python 对象,查找速度较慢。
  • 结果为@​​987654352@

显然,您可以使用 Cython 稍微加快这个版本的速度。

但是足够的理论,让我们看看不同ns 和固定ms 的运行时间:

我们可以看到:预处理的线性时间在大ns 的 numpy 版本中占主导地位。从 numpy 转换为 pure-python 的版本 (numpy->python) 具有与 pure-python 版本相同的恒定行为,但速度较慢,因为需要转换 - 这一切都符合我们的分析。

这在图中不能很好地看出:如果n < m numpy 版本变得更快 - 在这种情况下,khash-lib 的更快查找起着最重要的作用,而不是预处理部分。

我的分析总结:

  • n < m:应该采用pd.Series.isin,因为O(n)-预处理成本并不高。

  • n > m:(可能是 cythonized 版本)[i in x_set for i in ser.values] 应该被采用,因此 O(n) 应该被避免。

  • 显然存在一个灰色区域,其中nm 大致相等,如果不进行测试,很难判断哪种解决方案是最好的。

  • 如果您可以控制它:最好的办法是将set 直接构建为 C 整数集(khash (already wrapped in pandas) 甚至可能是一些 c++ 实现) ,从而消除了预处理的需要。我不知道 pandas 中是否有可以复用的东西,但是用 Cython 编写函数可能没什么大不了的。


问题是最后一个建议不能开箱即用,因为 pandas 和 numpy 在它们的界面中都没有集合的概念(至少据我所知)。但是拥有 raw-C-set-interfaces 将是两全其美:

  • 无需预处理,因为值已作为集合传递
  • 不需要转换,因为传递的集合包含原始 C 值

我编写了一个又快又脏的 Cython-wrapper for khash(灵感来自 pandas 中的包装器),它可以通过 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 倍(但无论如何,纯 python 不是我们想要的),甚至比 cpp 的版本快约 3 倍。


列表

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:使用不同的 set-wrappers 绘制结果:

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()

【讨论】:

  • 这太棒了。我犯了从时间假设复杂性的菜鸟错误。我喜欢你解释n &lt; m 的哈希映射创建时间和性能与n &gt; m 的性能之间的权衡的方式。
  • 男孩,这个答案比我的博士论文还要长。
猜你喜欢
  • 2016-12-24
  • 2018-06-03
  • 1970-01-01
  • 2014-08-02
  • 2014-05-04
  • 1970-01-01
  • 2019-09-09
  • 1970-01-01
相关资源
最近更新 更多