web-dev-qa-db-fra.com

Pourquoi statistics.mean () est-il si lent?

J'ai comparé les performances de la fonction mean du module statistics avec la méthode simple sum(l)/len(l) et j'ai trouvé que la fonction mean était très lente pour une raison quelconque . J'ai utilisé timeit avec les deux extraits de code ci-dessous pour les comparer, est-ce que quelqu'un sait ce qui cause l'énorme différence de vitesse d'exécution? J'utilise 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)))

Le code ci-dessus s'exécute en environ 0,043 seconde sur ma machine.

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

Le code ci-dessus s'exécute en environ 0,000565 secondes sur ma machine.

45
Just some guy

Le module statistics de Python n'est pas construit pour la vitesse, mais pour la précision

Dans les spécifications de ce module , il semble que

La somme intégrée peut perdre en précision lorsqu'il s'agit de flotteurs de magnitude très différente. Par conséquent, le moyen naïf ci-dessus échoue à ce "test de torture"

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

renvoyant 0 au lieu de 1, une erreur purement informatique de 100%.

L'utilisation de math.fsum à l'intérieur de la moyenne le rendra plus précis avec les données flottantes, mais il a également pour effet secondaire de convertir tous les arguments en flottants même lorsqu'ils ne sont pas nécessaires. Par exemple. nous devons nous attendre à ce que la moyenne d'une liste de fractions soit une fraction, pas un flottant.

Inversement, si nous regardons l'implémentation de _sum() dans ce module, les premières lignes de la docstring de la méthode semblent confirmer que :

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.

    [...] """

Alors oui, l'implémentation de statistics de sum, au lieu d'être un simple appel unilatéral à la fonction sum() intégrée de Python, prend environ 20 lignes à elle seule avec un imbriqué for boucle dans son corps.

Cela se produit car statistics._sum Choisit de garantir la précision maximale pour tous les types de nombres qu'il pourrait rencontrer (même s'ils diffèrent largement les uns des autres), au lieu de simplement mettre l'accent sur la vitesse.

Par conséquent, il semble normal que le sum intégré se révèle cent fois plus rapide. Le coût d'une précision beaucoup plus faible en vous l'appelle avec des nombres exotiques.

Autres options

Si vous avez besoin de prioriser la vitesse dans vos algorithmes, vous devriez plutôt regarder Numpy , dont les algorithmes sont implémentés en C.

La moyenne de NumPy n'est pas aussi précise que statistics de loin, mais elle implémente (depuis 2013) une routine basée sur la sommation par paire qui est meilleure qu'une sum/len Naïve ( plus d'infos dans le lien).

Toutefois...

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
69
Jivan

si vous vous souciez de la vitesse, utilisez numpy/scipy/pandas à la place:

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

Conclusion: ce sera des ordres de grandeur plus rapides - dans mon exemple, c'était 700 fois plus rapide, mais peut-être pas si précis (car numpy n'utilise pas l'algorithme de sommation de Kahan).

6
MaxU

J'ai posé la même question il y a quelque temps, mais une fois que j'ai remarqué le _sum fonction appelée en moyenne en ligne 17 dans la source j'ai compris pourquoi:

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)

Il y a une multitude d'opérations qui se produisent par rapport à un simple appel à la fonction intégrée sum, selon les chaînes de documentation mean calcule une somme de haute précision.

Vous pouvez voir que l'utilisation de la moyenne par rapport à la somme peut vous donner une sortie différente:

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
5

Len () et sum () sont des fonctions intégrées Python (avec des fonctionnalités limitées), qui sont écrites en C et, plus important encore, sont optimisées pour fonctionner rapidement avec certains types ou objets (liste) .

Vous pouvez regarder l'implémentation des fonctions intégrées ici:

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

Le fichier statistics.mean () est une fonction de haut niveau écrite en Python. Jetez un œil ici sur la façon dont il est mis en œuvre:

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

Vous pouvez voir que plus tard utilise en interne une autre fonction appelée _sum (), qui effectue quelques vérifications supplémentaires par rapport aux fonctions intégrées.

5
grepe

Selon cet article: Calcul de la moyenne arithmétique (moyenne) en Python

Elle devrait être "due à une implémentation particulièrement précise de l'opérateur de somme dans les statistiques".

La fonction moyenne est codée avec une fonction _sum interne qui est censée être plus précise que l'addition normale mais qui est beaucoup plus lente (code disponible ici: https://hg.python.org/cpython/file/ 3.5/Lib/statistics.py ).

Il est spécifié dans le PEP: https://www.python.org/dev/peps/pep-0450/ La précision est considérée comme plus importante que la vitesse pour ce module.

2