A volte è necessario scrivere codice numpy non idiomatico se si desidera davvero accelerare il calcolo che non si può fare con numpy nativo.
numba
compila il tuo codice Python a basso livello C. Dato che un sacco di numpy stesso è di solito veloce quanto C, questo finisce per essere utile se il tuo problema non si presta alla vettorizzazione nativa con numpy. Questo è un esempio (dove ho assunto che gli indici siano contigui e ordinati, che si riflette anche nei dati di esempio):
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
E qui ci sono alcuni tempi usando la %timeit
magia di 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)
Utilizzando i dati di esempio aggiornati nella domanda, questi numeri (ovvero il runtime della funzione python rispetto al runtime della funzione accelerata JIT) sono
>>> %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)
Ciò equivale a uno speedup 65x nel caso più piccolo e uno speedup 26x nel caso più grande (rispetto al codice a ciclo lento, ovviamente) usando il codice accelerato. Un altro aspetto positivo è che (a differenza della vettorializzazione tipica con intorpidimento nativo) non abbiamo avuto bisogno di memoria aggiuntiva per raggiungere questa velocità, è tutto basato sul codice di basso livello ottimizzato e compilato che finisce per essere eseguito.
La funzione sopra presuppone che gli array int intorpiditi siano int64
predefiniti, il che non è effettivamente il caso su Windows. Quindi un'alternativa è rimuovere la firma dalla chiamata a numba.njit
, innescando una corretta compilazione just-in-time. Ciò significa che la funzione verrà compilata durante la prima esecuzione, che può intromettersi con i risultati di temporizzazione (possiamo eseguire la funzione una volta manualmente, utilizzando tipi di dati rappresentativi, o semplicemente accettare che la prima esecuzione di temporizzazione sarà molto più lenta, il che dovrebbe essere ignorato). Questo è esattamente ciò che ho cercato di prevenire specificando una firma, che attiva la compilazione anticipata.
Ad ogni modo, nel caso correttamente JIT il decoratore di cui abbiamo bisogno è giusto
@numba.njit
def diffmedian_jit(...):
Si noti che i tempi sopra indicati per la funzione compilata jit si applicano solo dopo che la funzione è stata compilata. Ciò accade sia in fase di definizione (con compilation ansiosa, quando viene passata una firma esplicita numba.njit
), sia durante la prima chiamata di funzione (con compilation lazy, quando non viene passata alcuna firma numba.njit
). Se la funzione verrà eseguita solo una volta, anche il tempo di compilazione dovrebbe essere considerato per la velocità di questo metodo. In genere vale la pena di compilare le funzioni solo se il tempo totale di compilazione + esecuzione è inferiore al runtime non compilato (che è effettivamente vero nel caso precedente, in cui la funzione nativa di Python è molto lenta). Ciò accade soprattutto quando si chiama la funzione compilata molte volte.
Come max9111 ha osservato in un commento, una caratteristica importante del numba
è la cache
parola chiave per jit
. Passando cache=True
a numba.jit
, la funzione compilata verrà archiviata su disco, in modo che durante la successiva esecuzione del modulo python dato la funzione verrà caricata da lì anziché ricompilata, il che può di nuovo risparmiare tempo di esecuzione nel lungo periodo.
scipy.ndimage.median
suggerimento nella risposta collegata? Non mi sembra che abbia bisogno di un uguale numero di elementi per etichetta. O mi sono perso qualcosa?