gpt4 book ai didi

python - 为什么带有 numexpr 的 Pandas.eval() 这么慢?

转载 作者:太空狗 更新时间:2023-10-29 22:28:33 26 4
gpt4 key购买 nike

测试代码:

import numpy as np
import pandas as pd

COUNT = 1000000

df = pd.DataFrame({
'y': np.random.normal(0, 1, COUNT),
'z': np.random.gamma(50, 1, COUNT),
})

%timeit df.y[(10 < df.z) & (df.z < 50)].mean()
%timeit df.y.values[(10 < df.z.values) & (df.z.values < 50)].mean()
%timeit df.eval('y[(10 < z) & (z < 50)].mean()', engine='numexpr')

我的机器(一个相当快的带有 Python 3.6 的 x86-64 Linux 桌面)上的输出是:

17.8 ms ±  1.3 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
8.44 ms ± 502 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
46.4 ms ± 2.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

我明白为什么第二行要快一点(它忽略了 Pandas 索引)。但为什么使用 numexpreval() 方法这么慢?它不应该至少比第一种方法更快吗?文档确实使它看起来像:https://pandas.pydata.org/pandas-docs/stable/enhancingperf.html

最佳答案

从下面的调查来看,性能较差的一个不起眼的原因似乎是“开销”。

只是表达式y[(10 < z) & (z < 50)].mean()的一小部分通过 numexpr 完成-模块。 numexpr doesn't support indexing , 因此我们只能希望 (10 < z) & (z < 50)加速 - 其他任何内容都将映射到 pandas -操作。

然而,(10 < z) & (z < 50)不是这里的瓶颈,可以很容易地看出:

%timeit df.y[(10 < df.z) & (df.z < 50)].mean()  # 16.7 ms
mask=(10 < df.z) & (df.z < 50)
%timeit df.y[mask].mean() # 13.7 ms
%timeit df.y[mask] # 13.2 ms

df.y[mask] -占运行时间的大部分。

我们可以比较 df.y[mask] 的分析器输出和 df.eval('y[mask]')看看有什么不同。

当我使用以下脚本时:

import numpy as np
import pandas as pd

COUNT = 1000000

df = pd.DataFrame({
'y': np.random.normal(0, 1, COUNT),
'z': np.random.gamma(50, 1, COUNT),
})

mask = (10 < df.z) & (df.z < 50)
df['m']=mask

for _ in range(500):
df.y[df.m]
# OR
#df.eval('y[m]', engine='numexpr')

并用 python -m cProfile -s cumulative run.py 运行它(或 IPython 中的 %prun -s cumulative <...>),我可以看到以下配置文件。

直接调用 pandas 功能:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
419/1 0.013 0.000 7.228 7.228 {built-in method builtins.exec}
1 0.006 0.006 7.228 7.228 run.py:1(<module>)
500 0.005 0.000 6.589 0.013 series.py:764(__getitem__)
500 0.003 0.000 6.475 0.013 series.py:812(_get_with)
500 0.003 0.000 6.468 0.013 series.py:875(_get_values)
500 0.009 0.000 6.445 0.013 internals.py:4702(get_slice)
500 0.006 0.000 3.246 0.006 range.py:491(__getitem__)
505 3.146 0.006 3.236 0.006 base.py:2067(__getitem__)
500 3.170 0.006 3.170 0.006 internals.py:310(_slice)
635/2 0.003 0.000 0.414 0.207 <frozen importlib._bootstrap>:958(_find_and_load)

我们可以看到几乎 100% 的时间花在了 series.__getitem__ 上。没有任何开销。

通过 df.eval(...) 调用电话,情况就大不相同了:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
453/1 0.013 0.000 12.702 12.702 {built-in method builtins.exec}
1 0.015 0.015 12.702 12.702 run.py:1(<module>)
500 0.013 0.000 12.090 0.024 frame.py:2861(eval)
1000/500 0.025 0.000 10.319 0.021 eval.py:153(eval)
1000/500 0.007 0.000 9.247 0.018 expr.py:731(__init__)
1000/500 0.004 0.000 9.236 0.018 expr.py:754(parse)
4500/500 0.019 0.000 9.233 0.018 expr.py:307(visit)
1000/500 0.003 0.000 9.105 0.018 expr.py:323(visit_Module)
1000/500 0.002 0.000 9.102 0.018 expr.py:329(visit_Expr)
500 0.011 0.000 9.096 0.018 expr.py:461(visit_Subscript)
500 0.007 0.000 6.874 0.014 series.py:764(__getitem__)
500 0.003 0.000 6.748 0.013 series.py:812(_get_with)
500 0.004 0.000 6.742 0.013 series.py:875(_get_values)
500 0.009 0.000 6.717 0.013 internals.py:4702(get_slice)
500 0.006 0.000 3.404 0.007 range.py:491(__getitem__)
506 3.289 0.007 3.391 0.007 base.py:2067(__getitem__)
500 3.282 0.007 3.282 0.007 internals.py:310(_slice)
500 0.003 0.000 1.730 0.003 generic.py:432(_get_index_resolvers)
1000 0.014 0.000 1.725 0.002 generic.py:402(_get_axis_resolvers)
2000 0.018 0.000 1.685 0.001 base.py:1179(to_series)
1000 0.003 0.000 1.537 0.002 scope.py:21(_ensure_scope)
1000 0.014 0.000 1.534 0.002 scope.py:102(__init__)
500 0.005 0.000 1.476 0.003 scope.py:242(update)
500 0.002 0.000 1.451 0.003 inspect.py:1489(stack)
500 0.021 0.000 1.449 0.003 inspect.py:1461(getouterframes)
11000 0.062 0.000 1.415 0.000 inspect.py:1422(getframeinfo)
2000 0.008 0.000 1.276 0.001 base.py:1253(_to_embed)
2035 1.261 0.001 1.261 0.001 {method 'copy' of 'numpy.ndarray' objects}
1000 0.015 0.000 1.226 0.001 engines.py:61(evaluate)
11000 0.081 0.000 1.081 0.000 inspect.py:757(findsource)

再次在 series.__getitem__ 中花费了大约 7 秒, 但也有大约 6 秒的开销 - 例如 frame.py:2861(eval) 中大约 2 秒在 expr.py:461(visit_Subscript) 中大约 2 秒.

我只进行了粗浅的调查(请参阅下面的更多详细信息),但这种开销似乎并不仅仅是恒定的,而且至少与系列中的元素数量呈线性关系。例如有 method 'copy' of 'numpy.ndarray' objects这意味着数据被复制(目前尚不清楚,为什么这本身是必要的)。

我的收获:使用 pd.eval只要计算的表达式可以用 numexpr 计算就具有优势独自的。一旦不是这种情况,可能就会因相当大的开销而不再是 yield 而是损失。


使用 line_profiler (这里我使用 %lprun-magic(在用 %load_ext line_profliler 加载它之后)用于函数 run(),它或多或少是上面脚本的副本)我们可以很容易地找到时间在 Frame.eval 中丢失的地方。 :

%lprun -f pd.core.frame.DataFrame.eval
-f pd.core.frame.DataFrame._get_index_resolvers
-f pd.core.frame.DataFrame._get_axis_resolvers
-f pd.core.indexes.base.Index.to_series
-f pd.core.indexes.base.Index._to_embed
run()

在这里我们可以看到是否花费了额外的 10%:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
2861 def eval(self, expr,
....
2951 10 206.0 20.6 0.0 from pandas.core.computation.eval import eval as _eval
2952
2953 10 176.0 17.6 0.0 inplace = validate_bool_kwarg(inplace, 'inplace')
2954 10 30.0 3.0 0.0 resolvers = kwargs.pop('resolvers', None)
2955 10 37.0 3.7 0.0 kwargs['level'] = kwargs.pop('level', 0) + 1
2956 10 17.0 1.7 0.0 if resolvers is None:
2957 10 235850.0 23585.0 9.0 index_resolvers = self._get_index_resolvers()
2958 10 2231.0 223.1 0.1 resolvers = dict(self.iteritems()), index_resolvers
2959 10 29.0 2.9 0.0 if 'target' not in kwargs:
2960 10 19.0 1.9 0.0 kwargs['target'] = self
2961 10 46.0 4.6 0.0 kwargs['resolvers'] = kwargs.get('resolvers', ()) + tuple(resolvers)
2962 10 2392725.0 239272.5 90.9 return _eval(expr, inplace=inplace, **kwargs)

_get_index_resolvers()可以向下钻取到 Index._to_embed :

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
1253 def _to_embed(self, keep_tz=False, dtype=None):
1254 """
1255 *this is an internal non-public method*
1256
1257 return an array repr of this object, potentially casting to object
1258
1259 """
1260 40 73.0 1.8 0.0 if dtype is not None:
1261 return self.astype(dtype)._to_embed(keep_tz=keep_tz)
1262
1263 40 201490.0 5037.2 100.0 return self.values.copy()

哪里O(n) - 发生复制。

关于python - 为什么带有 numexpr 的 Pandas.eval() 这么慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52588653/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com