book - statistics python
¿Por qué es statistics.mean() tan lento? (5)
Comparé el rendimiento de la función mean
del módulo de statistics
con el método simple de sum(l)/len(l)
y encontré que la función mean
era muy lenta por alguna razón. Utilicé timeit
con los dos fragmentos de código siguientes para compararlos. ¿Alguien sabe qué causa la enorme diferencia en la velocidad de ejecución? Estoy usando 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)))
El código anterior se ejecuta en aproximadamente 0.043 segundos en mi máquina.
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)))
El código anterior se ejecuta en aproximadamente 0.000565 segundos en mi máquina.
El módulo de statistics
de Python no está diseñado para la velocidad, sino para la precisión
En las especificaciones para este módulo , parece que
La suma incorporada puede perder precisión cuando se trata de flotadores de magnitud muy diferente. En consecuencia, la media ingenua anterior falla esta "prueba de tortura"
assert mean([1e30, 1, 3, -1e30]) == 1
devolviendo 0 en lugar de 1, un error puramente computacional del 100%.
El uso de math.fsum inside mean lo hará más preciso con los datos flotantes, pero también tiene el efecto secundario de convertir cualquier argumento a flotar, incluso cuando no sea necesario. Por ejemplo, deberíamos esperar que la media de una lista de Fracciones sea una Fracción, no un flotador.
A la inversa, si echamos un vistazo a la implementación de _sum()
en este módulo, las primeras líneas de la cadena de documentación del método parecen confirmar 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.
[...] """
Así que sí, statistics
implementación statistics
de la sum
, en lugar de ser una simple llamada de una sola línea a la función sum()
incorporada de Python, toma aproximadamente 20 líneas por sí misma con un bucle anidado en su cuerpo.
Esto sucede porque las statistics._sum
deciden garantizar la máxima precisión para todos los tipos de números que pueden encontrar (incluso si difieren mucho entre sí), en lugar de simplemente enfatizar la velocidad.
Por lo tanto, parece normal que la sum
incorporada sea cien veces más rápida. El costo de ser una precisión mucho menor en ti lo llama con números exóticos.
Otras opciones
Si necesita priorizar la velocidad en sus algoritmos, debería echar un vistazo a Numpy , Numpy algoritmos se implementan en C.
La media de NumPy no es tan precisa como las statistics
de un tiro largo, pero implementa (desde 2013) una rutina basada en la suma por pares que es mejor que una sum/len
ingenua (más información en el enlace).
Sin embargo...
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
Hace un tiempo hice la misma pregunta, pero una vez que noté la función _sum
llamada in mean en la línea 317 en la fuente, entendí por qué:
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)
Hay una multitud de operaciones que suceden en comparación con solo llamar a la sum
incorporada, según las cadenas de documentos que la mean
calcula una suma de alta precisión .
Puedes ver que el uso de la media vs suma puede darte resultados diferentes:
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
Según ese post: Cálculo de la media aritmética (promedio) en Python
Debe ser "debido a una implementación particularmente precisa de la suma del operador en estadísticas".
La función media está codificada con una función _sum interna que se supone que es más precisa que la adición normal pero que es mucho más lenta (código disponible aquí: https://hg.python.org/cpython/file/3.5/Lib/statistics.py ).
Se especifica en el PEP: https://www.python.org/dev/peps/pep-0450/ La precisión se considera más importante como la velocidad para ese módulo.
Si se preocupa por la velocidad, use numpy / scipy / pandas en su lugar:
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
Conclusión: será órdenes de magnitud más rápido; en mi ejemplo, fue 700 veces más rápido, pero tal vez no sea tan preciso (ya que numpy no usa el algoritmo de suma de Kahan).
Tanto len () como sum () son funciones incorporadas de Python (con funcionalidad limitada), que están escritas en C y, lo que es más importante, están optimizadas para trabajar rápidamente con ciertos tipos u objetos (lista).
Puedes ver la implementación de las funciones incorporadas aquí:
https://hg.python.org/sandbox/python2.7/file/tip/Python/bltinmodule.c
Statistics.mean () es una función de alto nivel escrita en Python. Eche un vistazo aquí a cómo se implementa:
https://hg.python.org/sandbox/python2.7/file/tip/Lib/statistics.py
Puede ver que más tarde utiliza internamente otra función llamada _sum (), que realiza algunas comprobaciones adicionales en comparación con las funciones integradas.