草庐IT

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

coder 2023-08-27 原文

测试代码:

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/

有关python - 为什么带有 numexpr 的 Pandas.eval() 这么慢?的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  3. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  4. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  5. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  6. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

    为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

  7. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

    它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

  8. ruby - Infinity 和 NaN 的类型是什么? - 2

    我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

  9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  10. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

随机推荐