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