Почему statistics.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 секунд на моей машине.

Ответ 1

Модуль Python statistics не построен для скорости, но для точности

В спецификации для этого модуля, похоже, что

Встроенная сумма может потерять точность при работе с поплавками дико различной величины. Следовательно, приведенное выше наивное значение "испытание пытками"

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

возвращает 0 вместо 1, чисто вычислительную ошибку 100%.

Использование math.fsum внутри середины сделает его более точным с float данных, но также имеет побочный эффект преобразования любых аргументов в плавать, даже если это не нужно. Например. мы должны ожидать, что среднее из списка фракций, чтобы быть фракцией, а не плавать.

Обратно, если взглянуть на реализацию _sum() в этом модуле, первые строки метода docstring похоже, подтверждают, что:

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 вместо простого однострочного вызова встроенной функции sum() на Python занимает около 20 строк сама по себе с вложенным циклом for в своем теле.

Это происходит потому, что statistics._sum выбирает, чтобы гарантировать максимальную точность для всех типов номеров, с которыми он мог столкнуться (даже если они сильно отличаются друг от друга), а не просто подчеркивая скорость.

Следовательно, кажется нормальным, что встроенный sum доказывает сто раз быстрее. Стоимость того, что у вас намного более низкая точность, вы называете его экзотическими числами.

Другие параметры

Если вам нужно определить приоритетность скорости в ваших алгоритмах, вы должны взглянуть на Numpy, алгоритмы которой реализованы в С.

Среднее значение NumPy не так точно, как statistics, но оно реализует (с 2013 года) процедуру на основе парного суммирования, которая лучше, чем наивный 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

Ответ 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).

Ответ 3

Я задал тот же вопрос некоторое время назад, но как только я заметил, что функция _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, так как строки doc mean вычисляют высокоточную сумму.

Вы можете видеть, что использование средней суммы vs может дать вам другой результат:

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

Stat.mean() - это функция высокого уровня, написанная на Python. Посмотрите здесь, как это реализовано:

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

Вы можете видеть, что позже использует внутренне другую функцию, называемую _sum(), которая выполняет несколько дополнительных проверок по сравнению со встроенными функциями.

Ответ 5

Согласно этому сообщению: Вычисление среднего арифметического значения (в среднем) на Python

Это должно быть "из-за особенно точной реализации оператора сумм в статистике".

Средняя функция кодируется внутренней функцией _sum, которая должна быть более точной, чем нормальная сложение, но которая намного медленнее (код доступен здесь: https://hg.python.org/cpython/file/3.5/Lib/statistics.py).

Он указан в PEP: https://www.python.org/dev/peps/pep-0450/ Точность считается более важной как скорость для этого модуля.