代码之家  ›  专栏  ›  技术社区  ›  John Zwinck

为什么Pandas.eval()和numexpr的速度这么慢?

  •  6
  • John Zwinck  · 技术社区  · 6 年前

    测试代码:

    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)
    

    我理解为什么第二行要快一点(它忽略了熊猫指数)。但是为什么 eval() 方法使用 numexpr 这么慢?它不应该比第一种方法更快吗?文档确实让人觉得: https://pandas.pydata.org/pandas-docs/stable/enhancingperf.html

    1 回复  |  直到 6 年前
        1
  •  9
  •   ead    5 年前

    只是表达的一小部分 y[(10 < z) & (z < 50)].mean() 是通过 numexpr -模块。 numexpr公司 doesn't support indexing 因此,我们只能寄希望于 (10 < z) & (z < 50) 加速-其他任何东西都会被映射到 pandas -操作。

    然而,

    %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.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 (或 %prun -s cumulative <...> 在IPython中,我可以看到以下配置文件。

    对于熊猫功能的直接调用:

       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)
    

    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)
    

    系列。\u getitem__ frame.py:2861(eval) 大约2秒后 expr.py:461(visit_Subscript)

    我只是做了一个肤浅的调查(见下面的更多细节),但是这个开销似乎不仅仅是常数,至少在序列中元素的数量上是线性的。例如有 method 'copy' of 'numpy.ndarray' objects 这意味着数据是被复制的(目前还不清楚,为什么这本身是必要的)。

    pd.eval 只要可以使用 numexpr公司 一个人。一旦情况不是这样,可能就不再有收益,而是由于相当大的管理费用而造成的损失。


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

    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) -复制发生了。