熊猫矢量化函数cumsum与numpy


问题内容

在回答熊猫数据框的向量化计算问题时,我注意到了有关性能的一个有趣问题。

我的印象是功能,比如下df.min()df.mean()df.cumsum(),等,都矢量化。但是,我看到在之间的性能差异巨大的df.cumsum()numpy替代。

考虑到在其基础架构中pandas使用numpy阵列,我希望性能会更接近。我尝试调查其源代码df.cumsum()但发现它很难处理。有人可以解释为什么它这么慢吗?

从@HYRY的答案中可以看出,该问题简化为以下两个命令为何在时间安排上存在如此巨大差异的问题:

import pandas as pd, numpy as np
df_a = pd.DataFrame(np.arange(1,1000*1000+1).reshape(1000,1000))

%timeit pd.DataFrame(np.nancumsum(df_a.values))    #  4.18 ms
%timeit df_a.cumsum()                              # 15.7  ms

(由于我的numpy v1.11没有nancumsum,因此由评论者之一进行了计时。)


问题答案:

这里似乎有几件事毫无价值。

首先,df_a.cumsum()默认值为axis=0(Pandas没有将一次调用中的整个DataFrame求和的概念),而NumPy调用默认为axis=None。因此,通过在一个操作上指定一个轴并有效地展平另一个操作,就可以将苹果与橘子进行比较。

也就是说,您可以比较三个调用:

>>> np.cumsum(df_a, axis=0)
>>> df_a.cumsum()
>>> val.cumsum(axis=0)  # val = df_a.values

在最后一次调用中,这里val是基础的NumPy数组,我们不算.values在运行时获取属性。

因此,如果您使用的是IPython
Shell,请%prun尝试进行行分析:

>>> %prun -q -T pdcumsum.txt df_a.cumsum()

>>> val = df_a.values
>>> %prun -q -T ndarraycumsum.txt val.cumsum(axis=0)

>>> %prun -q -T df_npcumsum.txt np.cumsum(df_a, axis=0)

-T将输出保存为文本,以便您可以查看彼此匹配的所有三个。这是您最终得到的结果:

  • df_a.cumsum()186个 函数调用,.022秒。其中0.013用于numpy.ndarray.cumsum()。(我的猜测是,如果没有NaN,则nancumsum()不需要,但请不要在此引用我的意思)。另一个块花在复制阵列上。
  • val.cumsum(axis=0):5个函数调用,0.020秒。不进行任何复制(尽管这不是就地操作)。
  • np.cumsum(df_a, axis=0)204个 函数调用,0.026秒 可以说,将Pandas对象传递给顶级NumPy函数似乎最终会在Pandas对象上调用等效方法,该方法要处理大量开销,然后重新调用NumPy函数。

现在,不像%timeit您那样,您在这里只打了1个电话%time,所以我不会过多地依赖与的相对定时差异%prun。也许比较内部函数调用是有用的。但是在这种情况下,当您为两者指定相同的轴时,即使Pandas发出的呼叫数量与NumPy的数量相比,计时差异实际上并没有那么大。换句话说,在这种情况下,所有三个通话的时间都由主导np.ndarray.cumsum(),而辅助Pandas通话不会占用太多时间。在其他情况下,辅助Pandas调用确实会消耗更多的运行时,但这似乎不是其中之一。

大局观-正如韦斯·麦金尼(Wes McKinney)所承认的那样,

从索引编制到汇总统计信息,相当简单的操作可能会在到达最低计算层之前经过多层支架。

可以权衡的是灵活性和增加的功能。

最后一个细节:在NumPy中,可以通过调用实例方法而不是顶层函数来避免一点点开销,因为后者只会路由到前者。但是正如一位智者曾经说过的那样,过早的优化是万恶之源。ndarray.cumsum()``np.cumsum()


以供参考:

>>> pd.__version__, np.__version__
('0.22.0', '1.14.0')