为啥 statistics.mean() 这么慢?

Posted

技术标签:

【中文标题】为啥 statistics.mean() 这么慢?【英文标题】:Why is statistics.mean() so slow?为什么 statistics.mean() 这么慢? 【发布时间】:2016-09-28 18:08:24 【问题描述】:

我比较了statistics 模块的mean 函数和简单的sum(l)/len(l) 方法的性能,发现mean 函数由于某种原因非常慢。我用timeit 和下面的两个代码sn-ps 来比较它们,有谁知道是什么导致了执行速度的巨大差异?我正在使用 Python 3.5。

from timeit import repeat
print(min(repeat('mean(l)',
                 '''from random import randint; from statistics import mean; \
                 l=[randint(0, 10000) for i in range(10000)]''', repeat=20, number=10)))

上面的代码在我的机器上执行大约需要 0.043 秒。

from timeit import repeat
print(min(repeat('sum(l)/len(l)',
                 '''from random import randint; from statistics import mean; \
                 l=[randint(0, 10000) for i in range(10000)]''', repeat=20, number=10)))

上面的代码在我的机器上执行大约需要 0.000565 秒。

【问题讨论】:

tl;dr 是 mean 比琐碎的 sum(x)/len(x) 做了更多的错误处理和工作。 不仅慢,而且也很刻薄。 :( 【参考方案1】:

Python 的 statistics 模块不是为速度而构建的,而是为精确度而构建的

在the specs for this module 中,似乎

在处理大量浮点数时,内置总和可能会失去准确性 幅度不同。因此,上述天真均值未能实现这一点 《酷刑试验》

assert mean([1e30, 1, 3, -1e30]) == 1

返回 0 而不是 1,纯计算误差为 100%。

在 mean 中使用 math.fsum 会使 float 更准确 数据,但它也有将任何参数转换为的副作用 即使在不必要的时候也会漂浮。例如。我们应该期望列表的平均值 分数是分数,而不是浮点数。

反之,如果我们看一下这个模块中_sum()的实现,方法的文档字符串seem to confirm that的第一行:

def _sum(data, start=0):
    """_sum(data [, start]) -> (type, sum, count)

    Return a high-precision sum of the given numeric data as a fraction,
    together with the type to be converted to and the count of items.

    [...] """

是的,statistics 实现 sum,而不是对 Python 的内置 sum() 函数的简单单行调用,它本身需要大约 20 行代码,并在其主体中嵌套一个 for 循环.

这是因为statistics._sum 选择保证它可能遇到的所有类型的数字的最大精度(即使它们彼此相差很大),而不是简单地强调速度。

因此,内置的sum 被证明快一百倍似乎很正常。在你碰巧用奇异的数字调用它时,它的精度要低得多。

其他选项

如果你需要在算法中优先考虑速度,你应该看看Numpy,它的算法是用 C 实现的。

NumPy 的平均值远不如 statistics 精确,但它实现了(自 2013 年以来)routine based on pairwise summation,这比天真的 sum/len 更好(链接中的更多信息)。

不过……

import numpy as np
import statistics

np_mean = np.mean([1e30, 1, 3, -1e30])
statistics_mean = statistics.mean([1e30, 1, 3, -1e30])

print('NumPy mean: '.format(np_mean))
print('Statistics mean: '.format(statistics_mean))

> NumPy mean: 0.0
> Statistics mean: 1.0

【讨论】:

我对模块的 stdev 函数做了类似的测试,发现它比我自己的简单实现慢了大约 120 倍。这些函数确实必须做很多额外的工作才能获得我现在不需要的额外精度。谢谢你启发我。 我对 Pandas 不太熟悉,但是 NumPy 是为速度而生的。我认为它没有像statistics.mean 这样的为数值稳定性优化的平均程序;它只是做简单的 sum/len,但在 C 中。 足够新的 NumPy 版本确实有 an improved summation routine based on pairwise summation,但没有选择 Kahan 求和,更不用说任何与 statistics 模块为准确性所做的长度有关的事情了。【参考方案2】:

如果您关心速度,请改用 numpy/scipy/pandas:

In [119]: from random import randint; from statistics import mean; import numpy as np;

In [122]: l=[randint(0, 10000) for i in range(10**6)]

In [123]: mean(l)
Out[123]: 5001.992355

In [124]: %timeit mean(l)
1 loop, best of 3: 2.01 s per loop

In [125]: a = np.array(l)

In [126]: np.mean(a)
Out[126]: 5001.9923550000003

In [127]: %timeit np.mean(a)
100 loops, best of 3: 2.87 ms per loop

结论:它将快几个数量级 - 在我的示例中它快 700 倍,但可能没有那么精确(因为 numpy 不使用 Kahan 求和算法)。

【讨论】:

“您将获得相同的精确结果” - 不,您将失去精确度。您损失了多少以及您是否关心将取决于输入的内容以及您使用结果的目的。 @user2357112,感谢您的评论。我已经更新了答案【参考方案3】:

我前段时间问过同样的问题,但是一旦我注意到源代码中 317 线上的平均调用 _sum 函数,我就明白了原因:

def _sum(data, start=0):
    """_sum(data [, start]) -> (type, sum, count)
    Return a high-precision sum of the given numeric data as a fraction,
    together with the type to be converted to and the count of items.
    If optional argument ``start`` is given, it is added to the total.
    If ``data`` is empty, ``start`` (defaulting to 0) is returned.
    Examples
    --------
    >>> _sum([3, 2.25, 4.5, -0.5, 1.0], 0.75)
    (<class 'float'>, Fraction(11, 1), 5)
    Some sources of round-off error will be avoided:
    >>> _sum([1e50, 1, -1e50] * 1000)  # Built-in sum returns zero.
    (<class 'float'>, Fraction(1000, 1), 3000)
    Fractions and Decimals are also supported:
    >>> from fractions import Fraction as F
    >>> _sum([F(2, 3), F(7, 5), F(1, 4), F(5, 6)])
    (<class 'fractions.Fraction'>, Fraction(63, 20), 4)
    >>> from decimal import Decimal as D
    >>> data = [D("0.1375"), D("0.2108"), D("0.3061"), D("0.0419")]
    >>> _sum(data)
    (<class 'decimal.Decimal'>, Fraction(6963, 10000), 4)
    Mixed types are currently treated as an error, except that int is
    allowed.
    """
    count = 0
    n, d = _exact_ratio(start)
    partials = d: n
    partials_get = partials.get
    T = _coerce(int, type(start))
    for typ, values in groupby(data, type):
        T = _coerce(T, typ)  # or raise TypeError
        for n,d in map(_exact_ratio, values):
            count += 1
            partials[d] = partials_get(d, 0) + n
    if None in partials:
        # The sum will be a NAN or INF. We can ignore all the finite
        # partials, and just look at this special one.
        total = partials[None]
        assert not _isfinite(total)
    else:
        # Sum all the partial sums using builtin sum.
        # FIXME is this faster if we sum them in order of the denominator?
        total = sum(Fraction(n, d) for d, n in sorted(partials.items()))
    return (T, total, count)

与仅调用内置函数sum 相比,发生了大量操作,根据文档字符串mean 计算高精度总和

你可以看到使用 mean vs sum 可以给你不同的输出:

In [7]: l = [.1, .12312, 2.112, .12131]

In [8]: sum(l) / len(l)
Out[8]: 0.6141074999999999

In [9]: mean(l)
Out[9]: 0.6141075

【讨论】:

【参考方案4】:

len() 和 sum() 都是 Python 内置函数(功能有限),它们是用 C 语言编写的,更重要的是,它们经过优化,可以快速处理某些类型或对象(列表)。

你可以在这里查看内置函数的实现:

https://hg.python.org/sandbox/python2.7/file/tip/Python/bltinmodule.c

statistics.mean() 是用 Python 编写的高级函数。看看这里是如何实现的:

https://hg.python.org/sandbox/python2.7/file/tip/Lib/statistics.py

您可以看到稍后在内部使用了另一个名为 _sum() 的函数,与内置函数相比,它做了一些额外的检查。

【讨论】:

【参考方案5】:

如果您想要更快的均值函数,statistics 模块在 python 3.8 中引入了fmean 函数。它在计算平均值之前将其数据转换为float

(实现here)

快速比较:

import timeit, statistics

def test_basic_mean(): return sum(range(10000)) / 10000
def test_mean(): return statistics.mean(range(10000))
def test_fmean(): return statistics.fmean(range(10000))

print("basic mean:", min(timeit.repeat(stmt=test_basic_mean, setup="from __main__ import test_basic_mean", repeat=20, number=10)))
print("statistics.mean:", min(timeit.repeat(stmt=test_mean, setup="from __main__ import statistics, test_mean", repeat=20, number=10)))
print("statistics.fmean:", min(timeit.repeat(stmt=test_fmean, setup="from __main__ import statistics, test_fmean", repeat=20, number=10)))

给我:

basic mean: 0.0013072469737380743
statistics.mean: 0.025932796066626906
statistics.fmean: 0.001833588001318276

【讨论】:

【参考方案6】:

根据那个帖子: Calculating arithmetic mean (average) in Python

这应该是“由于统计中求和运算符的特别精确实现”。

mean 函数是用一个内部 _sum 函数编码的,它应该比普通的加法更精确,但速度要慢得多(代码在这里:https://hg.python.org/cpython/file/3.5/Lib/statistics.py)。

在 PEP 中指定: https://www.python.org/dev/peps/pep-0450/ 精度被认为比该模块的速度更重要。

【讨论】:

Link to _sum function in statistics.py 它绝对偏爱分数...

以上是关于为啥 statistics.mean() 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥垃圾回收这么慢?

为啥这个 select 语句这么慢?

为啥这个查询运行这么慢?

为啥 QuerySet 迭代这么慢?

为啥videoview这么慢?

为啥 putImageData 这么慢?