为什么statistics.mean()这么慢?

时间:2016-05-30 21:05:05

标签: python performance mean

我将mean模块的statistics函数的性能与简单的sum(l)/len(l)方法进行了比较,发现mean函数由于某种原因非常慢。我使用timeit和下面的两个代码片段来比较它们,是否有人知道是什么导致执行速度的巨大差异?我使用的是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秒。

6 个答案:

答案 0 :(得分:68)

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

the specs for this module中,似乎

  

在处理花车时,内置总和可能会失去准确性   不同程度。因此,上述天真的意思就是失败了   "酷刑测试"

     

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

     

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

     

在里面使用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

答案 1 :(得分:6)

如果你关心速度,请使用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求和算法)。

答案 2 :(得分:5)

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

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

答案 3 :(得分:5)

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()的函数,与内置函数相比,它会进行一些额外的检查。

答案 4 :(得分:2)

据该帖子说: Calculating arithmetic mean (average) in Python

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

均值函数使用内部_sum函数编码,该函数应该比正常加法更精确,但速度要慢很多(此处可用代码:https://hg.python.org/cpython/file/3.5/Lib/statistics.py)。

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

答案 5 :(得分:0)

如果你想要更快的均值函数,[Error: ENOENT: no such file or directory, open 'C:\repos\Puppeteer\output\0-aHR0cHM6Ly93d3cudTJ1ZS5jb20vdGF4L2hvbWUucGhwP2VtNWhwdDRucjZldzh5dnZzdTBpZmt xdGFuMTE5emxjZDc4c25xYnU0dTFlOHpiN3Nlb21pZ2x0eTc3c2p4cmQ3OXpza3FibXRyMGY5bnZmeGRycG4yMGZ4cDMwY3J0MGZpNGkwaG53dXF5cWtmbHRsaXBqd2c2YWN1cGNxbDZja2ZiOTd4YmJuYmdobmRkZnpweGp3bWg yb2lsY2ZtZW83ZmR0NGd1dWR1dm1tbnMwMWhhc2JvY2VheXNuMndkZGRlcWJjNmF5-index.png'] { errno: -4058, code: 'ENOENT', syscall: 'open', path: 'C:\\repos\\Puppeteer\\output\\0-aHR0cHM6Ly93d3cudTJ1ZS5jb20vdGF4L2hvbWUucGhwP2VtNWhwdDRucjZldzh5dnZzdTBpZmtxdGFuMTE5emxjZDc4c25xYnU0dTFlOHpiN 3Nlb21pZ2x0eTc3c2p4cmQ3OXpza3FibXRyMGY5bnZmeGRycG4yMGZ4cDMwY3J0MGZpNGkwaG53dXF5cWtmbHRsaXBqd2c2YWN1cGNxbDZja2ZiOTd4YmJuYmdobmRkZnpweGp3bWgyb2lsY2ZtZW83ZmR0NGd1dWR1dm1tbnMwM Whhc2JvY2VheXNuMndkZGRlcWJjNmF5-index.png' } 模块在 python 3.8 中引入了 fmean 函数。它在计算平均值之前将其数据转换为 statistics

(实现here

快速比较:

float

给我:

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)))