什么是时间测试熊猫和numpy相关解决方案的简单框架

piR*_*red 21 python numpy pandas

动机

我在这里和那里回答几个问题.我经常测试我和其他人的解决方案有多快.

是否有一个简单的框架来全面了解各种解决方案的速度有多快?

考虑数据帧 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]
    })
Run Code Online (Sandbox Code Playgroud)

我想总结Value按不同值分组的列Group.我有三种方法可以做到这一点.

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()
Run Code Online (Sandbox Code Playgroud)

它们是一样的吗?

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

True
True
Run Code Online (Sandbox Code Playgroud)

它们有多快?

%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
Run Code Online (Sandbox Code Playgroud)

MSe*_*ert 16

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

例如,该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()
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

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

r = b.run()
Run Code Online (Sandbox Code Playgroud)

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

r.plot()
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

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

r.plot_difference_percentage(relative_to=sum_nb) 
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

或获取基准时间DataFrame(需要pandas

r.to_pandas_dataframe()
Run Code Online (Sandbox Code Playgroud)
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()
Run Code Online (Sandbox Code Playgroud)

如果您不喜欢装饰器,也可以在一个调用中设置所有内容(在这种情况下,您不需要BenchmarkBuilder和装饰器add_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")
Run Code Online (Sandbox Code Playgroud)

这里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()
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明


MSe*_*ert 15

这个术语是"比较基准",与所有基准一样,重要的是指定(即使只是为了你自己)你想要的基准.一个糟糕的基准也比没有基准差.因此,任何框架都需要根据您的设置进行仔细调整.

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

对于前言这么多,我经常发现自己使用某种"简单框架":

# 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)
Run Code Online (Sandbox Code Playgroud)

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

%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()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

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

%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()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

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


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

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

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

    这意味着mean可能有偏见,因此也是如此sum.这就是为什么我用.best了的%timeit结果.这是"最小".当然,最低限度也不是完整的事实,只要确保minmean(或sum)不显示不同的趋势.

  • 我使用上面的log-log图.这使得解释整体性能变得容易("当长于1000个元素时,x比y快"),但是它们很难量化(例如"x比y快3倍").因此,在某些情况下,其他类型的可视化可能更合适.

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

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


piR*_*red 6

构架

人们以前曾要求我这样做。因此,我只是将其发布为问答,希望其他人觉得它有用。

我欢迎所有反馈和建议。

变化大小

我通常检查的事情的首要任务是解决输入数据大小变化时的解决方案速度。我们如何扩展数据的“大小”并不总是很明显。

我们使用称为的函数封装此概念,该函数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
Run Code Online (Sandbox Code Playgroud)

尺码

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

sizes = [1000, 3000, 10000, 30000, 100000]
Run Code Online (Sandbox Code Playgroud)

方法

我需要测试功能列表。每个函数应采用一个输入,该输入是的输出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]
Run Code Online (Sandbox Code Playgroud)

测试仪

最后,我们建立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
Run Code Online (Sandbox Code Playgroud)

我们用

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
Run Code Online (Sandbox Code Playgroud)

我们可以用

results.plot()
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明