【问题标题】:What techniques can be used to measure performance of pandas/numpy solutions可以使用哪些技术来衡量 pandas/numpy 解决方案的性能
【发布时间】:2017-11-12 01:32:11
【问题描述】:

问题

如何简洁全面地衡量以下各种功能的性能。

示例

考虑数据框df

df = pd.DataFrame({
        'Group': list('QLCKPXNLNTIXAWYMWACA'),
        'Value': [29, 52, 71, 51, 45, 76, 68, 60, 92, 95,
                  99, 27, 77, 54, 39, 23, 84, 37, 99, 87]
    })

我想总结由Group 中不同值分组的Value 列。我有三种方法可以做到。

import pandas as pd
import numpy as np
from numba import njit


def sum_pd(df):
    return df.groupby('Group').Value.sum()

def sum_fc(df):
    f, u = pd.factorize(df.Group.values)
    v = df.Value.values
    return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()

@njit
def wbcnt(b, w, k):
    bins = np.arange(k)
    bins = bins * 0
    for i in range(len(b)):
        bins[b[i]] += w[i]
    return bins

def sum_nb(df):
    b, u = pd.factorize(df.Group.values)
    w = df.Value.values
    bins = wbcnt(b, w, u.size)
    return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()

它们是一样的吗?

print(sum_pd(df).equals(sum_nb(df)))
print(sum_pd(df).equals(sum_fc(df)))

True
True

他们有多快?

%timeit sum_pd(df)
%timeit sum_fc(df)
%timeit sum_nb(df)

1000 loops, best of 3: 536 µs per loop
1000 loops, best of 3: 324 µs per loop
1000 loops, best of 3: 300 µs per loop

【问题讨论】:

标签: python pandas numpy


【解决方案1】:

框架

以前有人问过我这个问题。所以我只是把它作为问答发布,希望其他人觉得它有用。

我欢迎所有反馈和建议。

改变大小

我通常检查的首要任务是解决方案在不同大小的输入数据上的速度。我们应该如何缩放数据的“大小”并不总是显而易见的。

我们用一个名为creator 的函数封装了这个概念,该函数接受一个指定大小的参数n。在这种情况下,creator 生成一个长度为 n 的数据帧,其中包含两列 GroupValue

from string import ascii_uppercase

def creator(n):
    letters = list(ascii_uppercase)
    np.random.seed([3,1415])
    df = pd.DataFrame(dict(
            Group=np.random.choice(letters, n),
            Value=np.random.randint(100, size=n)
        ))
    return df

尺寸

我想测试列表中指定的各种尺寸

sizes = [1000, 3000, 10000, 30000, 100000]

方法

我想要一个要测试的函数列表。每个函数都应采用单个输入,即creator 的输出。

我们有OP的功能

import pandas as pd
import numpy as np
from numba import njit


def sum_pd(df):
    return df.groupby('Group').Value.sum()

def sum_fc(df):
    f, u = pd.factorize(df.Group.values)
    v = df.Value.values
    return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()

@njit
def wbcnt(b, w, k):
    bins = np.arange(k)
    bins = bins * 0
    for i in range(len(b)):
        bins[b[i]] += w[i]
    return bins

def sum_nb(df):
    b, u = pd.factorize(df.Group.values)
    w = df.Value.values
    bins = wbcnt(b, w, u.size)
    return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()

methods = [sum_pd, sum_fc, sum_nb]

测试人员

最后,我们构建了 tester 函数

import pandas as pd
from timeit import timeit

def tester(sizes, methods, creator, k=100, v=False):
    results = pd.DataFrame(
        index=pd.Index(sizes, name='Size'),
        columns=pd.Index([m.__name__ for m in methods], name='Method')
    )

    methods = {m.__name__: m for m in methods}
    for n in sizes:
        x = creator(n)
        for m in methods.keys():
            stmt = '%s(x)' % m
            setp = 'from __main__ import %s, x' % m
            if v:
                print(stmt, setp, n)
            t = timeit(stmt, setp, number=k)
            results.set_value(n, m, t)
    return results

我们用

捕获结果
results = tester(sizes, methods, creator)

print(results)

Method     sum_pd     sum_fc     sum_nb
Size                                   
1000    0.0632993  0.0316809  0.0364261
3000    0.0596143   0.031896  0.0319997
10000   0.0609055  0.0324342  0.0363031
30000   0.0646989    0.03237  0.0376961
100000  0.0656784  0.0363296  0.0331994

我们可以用

results.plot()

【讨论】:

    【解决方案2】:

    这方面的术语是“比较基准测试”,与所有基准测试一样,指定(即使只是为了您自己)您想要进行基准测试也很重要。此外,一个糟糕的基准比根本没有基准更糟糕。因此,任何框架都需要仔细根据您的设置进行调整。

    通常,当您分析算法时,您会对“增长顺序”感兴趣。因此,通常您希望针对不同长度的输入对算法进行基准测试(但其他指标也可能很重要,例如创建set 时的“重复次数”,或基准测试sorting 算法时的初始顺序)。但不仅渐近性能很重要,常数因子(特别是如果这些是高阶项的常数因子)也很重要。

    关于前言,我经常发现自己在使用某种“简单框架”:

    # Setup
    
    import pandas as pd
    import numpy as np
    from numba import njit
    
    @njit
    def numba_sum(arr):
        return np.sum(arr)
    
    # Timing setup
    timings = {sum: [], np.sum: [], numba_sum: []}
    sizes = [2**i for i in range(1, 20, 2)]
    
    # Timing
    for size in sizes:
        func_input = np.random.random(size=size)
        for func in timings:
            res = %timeit -o func(func_input)   # if you use IPython, otherwise use the "timeit" module
            timings[func].append(res)
    

    这就是制作一些基准的全部内容。更重要的问题是如何将它们可视化。我常用的一种方法是以对数方式绘制它们。这样您就可以看到小数组的常数因子,但也可以看到渐近的执行情况:

    %matplotlib notebook
    
    import matplotlib.pyplot as plt
    import numpy as np
    
    fig = plt.figure(1)
    ax = plt.subplot(111)
    
    for func in timings:
        ax.plot(sizes, 
                [time.best for time in timings[func]], 
                label=str(func))  # you could also use "func.__name__" here instead
    ax.set_xscale('log')
    ax.set_yscale('log')
    ax.set_xlabel('size')
    ax.set_ylabel('time [seconds]')
    ax.grid(which='both')
    ax.legend()
    plt.tight_layout()
    

    但另一种方法是找到基线并绘制相对差异:

    %matplotlib notebook
    
    import matplotlib.pyplot as plt
    import numpy as np
    
    fig = plt.figure(1)
    ax = plt.subplot(111)
    
    baseline = sum_nb # choose one function as baseline
    for func in timings:
        ax.plot(sizes, 
                [time.best / ref.best for time, ref in zip(timings[func], timings[baseline])], 
                label=str(func))  # you could also use "func.__name__" here instead
    ax.set_yscale('log')
    ax.set_xscale('log')
    ax.set_xlabel('size')
    ax.set_ylabel('time relative to {}'.format(baseline)) # you could also use "func.__name__" here instead
    ax.grid(which='both')
    ax.legend()
    plt.tight_layout()
    

    传奇可能需要更多的工作......现在已经很晚了......希望现在可以理解。


    只是一些额外的随机评论:

    • timeit.Timer.repeat 文档包含一个非常重要的说明:

      从结果向量计算平均值和标准差并报告这些是很诱人的。但是,这不是很有用。在典型情况下,最小值给出了机器运行给定代码 sn-p 的速度的下限;结果向量中的较高值通常不是由 Python 速度的变化引起的,而是由其他进程干扰您的计时精度引起的。所以结果的 min() 可能是您应该感兴趣的唯一数字。之后,您应该查看整个向量并应用常识而不是统计数据。

      这意味着mean 可能存在偏见,因此sum 也是如此。这就是为什么我使用.best%timeit 结果。这是“分钟”。 当然,最小值也不是完全的事实,只要确保minmean(或sum)不会显示不同的趋势。

    • 我使用了上面的对数图。这些使得解释整体性能变得容易(“当它超过 1000 个元素时,x 比 y 快”),但它们很难量化(例如,“做 x 比 y 快 3 倍”)。所以在某些情况下,其他类型的可视化可能更合适。

    • %timeit 很棒,因为它会计算重复次数,因此每个基准测试大约需要 1-3 秒。但是在某些情况下,显式重复可能会更好。

    • 始终确保计时实际上是正确的!在执行修改全局状态或修改输入的操作时要特别小心。例如,就地排序的计时需要在每个基准测试之前进行设置步骤,否则您正在对已经排序的事物进行排序(这是几种排序算法的最佳情况)。

    【讨论】:

      【解决方案3】:

      它们可能不会归类为“简单框架”,因为它们是需要安装的第三方模块,但我经常使用两个框架:

      例如simple_benchmark 库允许装饰函数以进行基准测试:

      from simple_benchmark import BenchmarkBuilder
      b = BenchmarkBuilder()
      
      import pandas as pd
      import numpy as np
      from numba import njit
      
      @b.add_function()
      def sum_pd(df):
          return df.groupby('Group').Value.sum()
      
      @b.add_function()
      def sum_fc(df):
          f, u = pd.factorize(df.Group.values)
          v = df.Value.values
          return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()
      
      @njit
      def wbcnt(b, w, k):
          bins = np.arange(k)
          bins = bins * 0
          for i in range(len(b)):
              bins[b[i]] += w[i]
          return bins
      
      @b.add_function()
      def sum_nb(df):
          b, u = pd.factorize(df.Group.values)
          w = df.Value.values
          bins = wbcnt(b, w, u.size)
          return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()
      

      同时装饰一个产生基准值的函数:

      from string import ascii_uppercase
      
      def creator(n):  # taken from another answer here
          letters = list(ascii_uppercase)
          np.random.seed([3,1415])
          df = pd.DataFrame(dict(
                  Group=np.random.choice(letters, n),
                  Value=np.random.randint(100, size=n)
              ))
          return df
      
      @b.add_arguments('Rows in DataFrame')
      def argument_provider():
          for exponent in range(4, 22):
              size = 2**exponent
              yield size, creator(size)
      

      然后运行基准测试所需要做的就是:

      r = b.run()
      

      之后,您可以将结果作为绘图检查(为此您需要 matplotlib 库):

      r.plot()
      

      如果函数在运行时非常相似,则百分比差异而不是绝对数字可能更重要:

      r.plot_difference_percentage(relative_to=sum_nb) 
      

      或者将基准测试的时间设为DataFrame(这需要pandas

      r.to_pandas_dataframe()
      
                 sum_pd    sum_fc    sum_nb
      16       0.000796  0.000515  0.000502
      32       0.000702  0.000453  0.000454
      64       0.000702  0.000454  0.000456
      128      0.000711  0.000456  0.000458
      256      0.000714  0.000461  0.000462
      512      0.000728  0.000471  0.000473
      1024     0.000746  0.000512  0.000513
      2048     0.000825  0.000515  0.000514
      4096     0.000902  0.000609  0.000640
      8192     0.001056  0.000731  0.000755
      16384    0.001381  0.001012  0.000936
      32768    0.001885  0.001465  0.001328
      65536    0.003404  0.002957  0.002585
      131072   0.008076  0.005668  0.005159
      262144   0.015532  0.011059  0.010988
      524288   0.032517  0.023336  0.018608
      1048576  0.055144  0.040367  0.035487
      2097152  0.112333  0.080407  0.072154
      

      如果您不喜欢装饰器,您也可以在一个调用中设置所有内容(在这种情况下,您不需要 BenchmarkBuilderadd_function/add_arguments 装饰器):

      from simple_benchmark import benchmark
      r = benchmark([sum_pd, sum_fc, sum_nb], {2**i: creator(2**i) for i in range(4, 22)}, "Rows in DataFrame")
      

      这里perfplot 提供了一个非常相似的界面(和结果):

      import perfplot
      r = perfplot.bench(
          setup=creator,
          kernels=[sum_pd, sum_fc, sum_nb],
          n_range=[2**k for k in range(4, 22)],
          xlabel='Rows in DataFrame',
          )
      import matplotlib.pyplot as plt
      plt.loglog()
      r.plot()
      

      【讨论】:

      • 该问题已作为题外话关闭并发布我meta question here我做了一些适度的编辑,但您可能希望从您的帖子中删除一些提及我的问题中不再存在的措辞的措辞.我的赏金已退还。如果关闭解决了,我会再试一次。
      猜你喜欢
      • 1970-01-01
      • 2010-09-27
      • 1970-01-01
      • 1970-01-01
      • 2010-10-10
      • 2010-09-07
      • 1970-01-01
      • 1970-01-01
      • 2010-09-19
      相关资源
      最近更新 更多