Pandas 掩码 / where 方法与 NumPy np.where
Posted
技术标签:
【中文标题】Pandas 掩码 / where 方法与 NumPy np.where【英文标题】:Pandas mask / where methods versus NumPy np.where 【发布时间】:2019-01-29 15:12:15 【问题描述】:在有条件地更新系列中的值时,我经常使用 Pandas mask
和 where
方法来实现更清晰的逻辑。但是,对于相对性能关键的代码,我注意到相对于numpy.where
的性能显着下降。
虽然我很高兴在特定情况下接受这一点,但我很想知道:
-
Pandas
mask
/ where
方法是否提供任何其他功能,除了 inplace
/ errors
/ try-cast
参数?我了解这 3 个参数,但很少使用它们。例如,我不知道level
参数指的是什么。
是否有任何重要的反例,mask
/ where
优于 numpy.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
【问题讨论】:
@ead,这很有趣。在这种情况下,答案可能会评论更改的内容(以及在哪个版本中?)。我没有看到任何关于实施更改的提及。另外,您是否看到我的示例的 both 具有相同的性能? 好吧,我忽略了第二个例子。第一个我得到96.9ms vs 92.7ms
,第二个得到276ms vs 120ms
。
我必须搜索源代码以获得复杂的答案。但是看看内存消耗,它看起来 df[0].mask 做了很多临时内存分配,而 np.where 没有。我还在 Numba 中实现了一个并行版本,它的 inplace 速度比 np.where 快 8 倍,但它只快 3 倍。我假设 np.where 是一个简单的编译 for,if,else 循环,就像我的 Numba 解决方案一样,而 pandas 创建一个临时掩码数组。
@max9111,这非常有趣;实际上,超出了我的确切问题。我假设numba
仅适用于数字输入,这对我的用例来说很好。如果有一个临时掩码数组,我想mask
/ where
的性能永远优于np.where
。
不适用于大型阵列。在较小的情况下,临时数据适合 CPU 缓存,这可能相同也可能不同(取决于精确实现和编译器/编译器设置)。缓存效果示例:***.com/q/48887461/4045774
【参考方案1】:
我使用的是 pandas 0.23.3 和 Python 3.6,因此仅在您的第二个示例中,我可以看到运行时间的真正差异。
但是,让我们研究您的第二个示例的稍微不同的版本(因此我们将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 的版本比 pandas 快大约 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
完全不同的情况:
pandas 不会在后台使用PyArray_Where
- 最显着的耗时是vm_engine_iter_task
,即numexpr-functionality。
正在进行一些繁重的内存复制 - __memmove_ssse3_back
大约使用了 25
% 的时间!可能内核的一些功能也与内存访问有关。
实际上,pandas-0.19 在后台使用了 PyArray_Where
,对于旧版本,性能报告看起来像:
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 的 0.19 版中,我没有看到 pandas 比 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%。性能说:
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 版本多。
我的主要收获:
pandas 有可能至少比 numpy 快一点(因为它可能更快)。但是,pandas 对数据复制的处理有些不透明,因此很难预测这种潜力何时会被(不必要的)数据复制所掩盖。
当where
/mask
的性能成为瓶颈时,我会使用 numba/cython 来提高性能 - 请参阅下面我相当天真的尝试使用 numba 和 cython。
想法是采取
np.where(df[0] > 0.5, df[0]*2, df[0])
版本并消除创建临时的需要 - 即df[0]*2
。
按照@max9111 的建议,使用 numba:
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% 的加速。不确定,为什么 cython 比 numba 慢得多。
列表:
答: 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)
【讨论】:
@jpp 我的理解是,如果other
-argument 的维度比df[0]
有另一个数字(即更少),则使用level
(与axis
-argument 一起)为了控制(至少在某种程度上)执行对齐的方式。我很确定,它对您的情况没有任何影响,并且不确定它是否应该成为此答案的一部分。
好的,将尝试使用不同的维度来解决问题。当然,无需更新您的答案。它本来就很棒。
@jpp 您可能已经遵循了代码,但是为了将信息保存在某处/为其他人:github.com/pandas-dev/pandas/blob/v0.23.4/pandas/core/… 和 pandas.pydata.org/pandas-docs/stable/generated/… 和
我猜 Numba 的表现非常好,因为循环的 SIMD 向量化。您可以使用 llvmlite.binding 将其检查为 llvm llvm.set_option('', '--debug-only=loop-vectorize') 。使用正确的 C 编译器设置,Cython 也可能执行相同的操作。 (使用 Clang 编译器的等价物是 (-O3, -march=native))以上是关于Pandas 掩码 / where 方法与 NumPy np.where的主要内容,如果未能解决你的问题,请参考以下文章
pandas新字段(数据列)生成使用np.where或者apply lambda函数结合if else生成新的字段,详解及实战
Pandas 获得与 SQL 语句相同结果的 Pythonic 方式是啥:“UPDATE-LEFT JOIN - SET - WHERE”?