A veces necesita escribir código numpy no idiomático si realmente quiere acelerar su cálculo, lo que no puede hacer con numpy nativo.
numba
compila su código de Python a bajo nivel C. Dado que una gran cantidad de numpy suele ser tan rápido como C, esto generalmente resulta útil si su problema no se presta a la vectorización nativa con numpy. Este es un ejemplo (donde supuse que los índices son contiguos y ordenados, lo que también se refleja en los datos del ejemplo):
import numpy as np
import numba
# use the inflated example of roganjosh https://stackoverflow.com/a/58788534
data = [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0, 0, 1, 1, 1, 1, 2, 3, 3]
data = np.array(data * 500) # using arrays is important for numba!
index = np.sort(np.random.randint(0, 30, 4500))
# jit-decorate; original is available as .py_func attribute
@numba.njit('f8[:](f8[:], i8[:])') # explicit signature implies ahead-of-time compile
def diffmedian_jit(data, index):
res = np.empty_like(data)
i_start = 0
for i in range(1, index.size):
if index[i] == index[i_start]:
continue
# here: i is the first _next_ index
inds = slice(i_start, i) # i_start:i slice
res[inds] = data[inds] - np.median(data[inds])
i_start = i
# also fix last label
res[i_start:] = data[i_start:] - np.median(data[i_start:])
return res
Y aquí hay algunos tiempos usando la %timeit
magia de IPython :
>>> %timeit diffmedian_jit.py_func(data, index) # non-jitted function
... %timeit diffmedian_jit(data, index) # jitted function
...
4.27 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
65.2 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Usando los datos de ejemplo actualizados en la pregunta, estos números (es decir, el tiempo de ejecución de la función python frente al tiempo de ejecución de la función acelerada por JIT) son
>>> %timeit diffmedian_jit.py_func(data, groups)
... %timeit diffmedian_jit(data, groups)
2.45 s ± 34.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93.6 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Esto equivale a una aceleración de 65x en el caso más pequeño y una aceleración de 26x en el caso más grande (en comparación con el código de bucle lento, por supuesto) usando el código acelerado. Otra ventaja es que (a diferencia de la vectorización típica con numpy nativo) no necesitábamos memoria adicional para lograr esta velocidad, se trata de código de bajo nivel optimizado y compilado que termina ejecutándose.
La función anterior supone que las matrices numpy int son int64
por defecto, lo cual no es el caso en Windows. Así que una alternativa es eliminar la firma de la llamada a numba.njit
, lo que provocó adecuada compilación justo a tiempo. Pero esto significa que la función se compilará durante la primera ejecución, lo que puede interferir con los resultados de temporización (podemos ejecutar la función una vez manualmente, utilizando tipos de datos representativos, o simplemente aceptar que la primera ejecución de temporización será mucho más lenta, lo que debería ser ignorado). Esto es exactamente lo que intenté evitar al especificar una firma, que desencadena una compilación anticipada.
De todos modos, en el caso adecuado de JIT, el decorador que necesitamos es solo
@numba.njit
def diffmedian_jit(...):
Tenga en cuenta que los tiempos anteriores que mostré para la función compilada jit solo se aplican una vez que la función se ha compilado. Esto ocurre ya sea en la definición (con la compilación ansiosos, cuando una firma explícita se pasa a numba.njit
), o durante la primera llamada de función (con la compilación perezoso, cuando no hay ninguna firma se pasa a numba.njit
). Si la función solo se ejecutará una vez, el tiempo de compilación también debe considerarse para la velocidad de este método. Por lo general, solo vale la pena compilar funciones si el tiempo total de compilación + ejecución es menor que el tiempo de ejecución sin compilar (que en realidad es cierto en el caso anterior, donde la función nativa de Python es muy lenta). Esto ocurre principalmente cuando llama a su función compilada muchas veces.
Como max9111 señaló en un comentario, una característica importante de numba
es la cache
palabra clave to jit
. Pasar cache=True
a numba.jit
almacenará la función compilada en el disco, de modo que durante la próxima ejecución del módulo python dado, la función se cargará desde allí en lugar de volver a compilarse, lo que de nuevo puede ahorrarle tiempo de ejecución a largo plazo.
scipy.ndimage.median
sugerencia en la respuesta vinculada? No me parece que necesite un número igual de elementos por etiqueta. ¿O me perdí algo?