Media mobile o media corrente


195

Esiste una funzione SciPy o una funzione o un modulo NumPy per Python che calcola la media corrente di un array 1D data una finestra specifica?

Risposte:


24

Per una soluzione breve e veloce che fa tutto in un ciclo, senza dipendenze, il codice qui sotto funziona alla grande.

mylist = [1, 2, 3, 4, 5, 6, 7]
N = 3
cumsum, moving_aves = [0], []

for i, x in enumerate(mylist, 1):
    cumsum.append(cumsum[i-1] + x)
    if i>=N:
        moving_ave = (cumsum[i] - cumsum[i-N])/N
        #can do stuff with moving_ave here
        moving_aves.append(moving_ave)

46
Veloce?! Questa soluzione è più lenta degli ordini di grandezza rispetto alle soluzioni con Numpy.
Bart,

3
Sebbene questa soluzione nativa sia interessante, l'OP ha richiesto una funzione intorpidita / scipy - presumibilmente quelle saranno notevolmente più veloci.
Demis,

256

UPD: le soluzioni più efficienti sono state proposte da Alleo e jasaarim .


Puoi usare np.convolveper quello:

np.convolve(x, np.ones((N,))/N, mode='valid')

Spiegazione

La media corrente è un caso dell'operazione matematica di convoluzione . Per la media corrente, si fa scorrere una finestra lungo l'input e si calcola la media del contenuto della finestra. Per segnali 1D discreti, la convoluzione è la stessa cosa, tranne per il fatto che si calcola una combinazione lineare arbitraria, cioè moltiplica ogni elemento per un coefficiente corrispondente e somma i risultati. Quei coefficienti, uno per ogni posizione nella finestra, sono talvolta chiamati kernel di convoluzione . Ora, la media aritmetica dei valori N è (x_1 + x_2 + ... + x_N) / N, quindi lo è il kernel corrispondente (1/N, 1/N, ..., 1/N), ed è esattamente quello che otteniamo usando np.ones((N,))/N.

bordi

L' modeargomento di np.convolvespecifica come gestire i bordi. Ho scelto la validmodalità qui perché penso che sia così che la maggior parte delle persone si aspetta che la corsa funzioni, ma potresti avere altre priorità. Ecco una trama che illustra la differenza tra le modalità:

import numpy as np
import matplotlib.pyplot as plt
modes = ['full', 'same', 'valid']
for m in modes:
    plt.plot(np.convolve(np.ones((200,)), np.ones((50,))/50, mode=m));
plt.axis([-10, 251, -.1, 1.1]);
plt.legend(modes, loc='lower center');
plt.show()

Esecuzione di modalità convolute medie


5
Mi piace questa soluzione perché è pulita (una riga) e relativamente efficiente (lavoro svolto all'interno di numpy). Ma la "soluzione efficiente" di Alleo numpy.cumsumha una maggiore complessità.
Ulrich Stern,

2
@denfromufa, credo che la documentazione copra abbastanza bene l'implementazione, e si collega anche a Wikipedia che spiega la matematica. Considerando il focus della domanda, pensi che questa risposta debba copiarli?
lapis,

@lapis l'uso di convolve per la media mobile è abbastanza insolito e non ovvio. Ecco la migliore spiegazione visiva che ho trovato: matlabtricks.com/post-11/moving-average-by-convolution
denfromufa

Per la stampa e le attività correlate sarebbe utile riempirlo con i valori Nessuno. Il mio suggerimento (non così bello ma breve): `` `moving_average def (x, N, fill = True): return np.concatenate ([x for x in [[None] * (N // 2 + N% 2) * fill, np.convolve (x, np.ones ((N,)) / N, mode = 'valid'), [None] * (N // 2) * fill,] if len (x)]) ` `` Il codice sembra così brutto nei commenti SO xD Non volevo aggiungere un'altra risposta perché ce n'erano così tanti ma potresti semplicemente copiarlo e incollarlo nel tuo IDE.
Chaoste,

146

Soluzione efficiente

La convoluzione è molto meglio dell'approccio diretto, ma (immagino) utilizza FFT e quindi piuttosto lenta. Tuttavia, specialmente per il calcolo della corsa, il seguente approccio funziona bene

def running_mean(x, N):
    cumsum = numpy.cumsum(numpy.insert(x, 0, 0)) 
    return (cumsum[N:] - cumsum[:-N]) / float(N)

Il codice da verificare

In[3]: x = numpy.random.random(100000)
In[4]: N = 1000
In[5]: %timeit result1 = numpy.convolve(x, numpy.ones((N,))/N, mode='valid')
10 loops, best of 3: 41.4 ms per loop
In[6]: %timeit result2 = running_mean(x, N)
1000 loops, best of 3: 1.04 ms per loop

Si noti che numpy.allclose(result1, result2)èTrue , due metodi sono equivalenti. Maggiore è la N, maggiore è la differenza nel tempo.

avvertenza: sebbene il cumsum sia più veloce, ci sarà un aumento dell'errore in virgola mobile che potrebbe causare risultati non validi / errati / inaccettabili

i commenti hanno sottolineato questo problema in virgola mobile qui ma lo sto rendendo più ovvio qui nella risposta. .

# demonstrate loss of precision with only 100,000 points
np.random.seed(42)
x = np.random.randn(100000)+1e6
y1 = running_mean_convolve(x, 10)
y2 = running_mean_cumsum(x, 10)
assert np.allclose(y1, y2, rtol=1e-12, atol=0)
  • più punti accumuli maggiore è l'errore in virgola mobile (quindi 1e5 punti è evidente, 1e6 punti è più significativo, più di 1e6 e potresti voler ripristinare gli accumulatori)
  • puoi imbrogliare usando np.longdouble ma l'errore in virgola mobile continuerà a essere significativo per un numero relativamente elevato di punti (circa> 1e5 ma dipende dai tuoi dati)
  • puoi tracciare l'errore e vederlo aumentare relativamente velocemente
  • la soluzione convolvale è più lenta ma non presenta questa perdita in virgola mobile di precisione
  • la soluzione uniform_filter1d è più veloce di questa soluzione cumsum E non ha questa perdita in virgola mobile di precisione

3
Bella soluzione! Il mio sospetto è numpy.convolveO (mn); la sua documentazione menziona che scipy.signal.fftconvolveutilizza FFT.
Ulrich Stern,

3
Questo metodo non si occupa dei bordi dell'array, vero?
JoVe,

6
Bella soluzione, ma nota che potrebbe soffrire di errori numerici per array di grandi dimensioni, poiché verso la fine dell'array, potresti sottrarre due numeri grandi per ottenere un piccolo risultato.
Bas Swinckels,

1
Questo utilizza la divisione intera invece della divisione float: running_mean([1,2,3], 2)array([1, 2]). Sostituire xcon [float(value) for value in x]fa il trucco.
ChrisW,

4
La stabilità numerica di questa soluzione può diventare un problema se xcontiene galleggianti. Esempio: running_mean(np.arange(int(1e7))[::-1] + 0.2, 1)[-1] - 0.2ritorna 0.003125mentre ci si aspetta 0.0. Ulteriori informazioni: en.wikipedia.org/wiki/Loss_of_significance
Milano,

80

Aggiornamento: L'esempio seguente mostra la vecchia pandas.rolling_meanfunzione che è stata rimossa nelle ultime versioni di Panda. Un equivalente moderno della chiamata di funzione qui sotto sarebbe

In [8]: pd.Series(x).rolling(window=N).mean().iloc[N-1:].values
Out[8]: 
array([ 0.49815397,  0.49844183,  0.49840518, ...,  0.49488191,
        0.49456679,  0.49427121])

Panda è più adatto a questo di NumPy o SciPy. La sua funzione rolling_mean fa comodamente il lavoro. Restituisce anche un array NumPy quando l'input è un array.

È difficile battere le rolling_meanprestazioni con qualsiasi implementazione Python pura personalizzata. Ecco un esempio di prestazione rispetto a due delle soluzioni proposte:

In [1]: import numpy as np

In [2]: import pandas as pd

In [3]: def running_mean(x, N):
   ...:     cumsum = np.cumsum(np.insert(x, 0, 0)) 
   ...:     return (cumsum[N:] - cumsum[:-N]) / N
   ...:

In [4]: x = np.random.random(100000)

In [5]: N = 1000

In [6]: %timeit np.convolve(x, np.ones((N,))/N, mode='valid')
10 loops, best of 3: 172 ms per loop

In [7]: %timeit running_mean(x, N)
100 loops, best of 3: 6.72 ms per loop

In [8]: %timeit pd.rolling_mean(x, N)[N-1:]
100 loops, best of 3: 4.74 ms per loop

In [9]: np.allclose(pd.rolling_mean(x, N)[N-1:], running_mean(x, N))
Out[9]: True

Ci sono anche buone opzioni su come gestire i valori dei bordi.


6
Pandas rolling_mean è un ottimo strumento per il lavoro ma è stato deprecato per ndarrays. Nelle versioni future di Pandas funzionerà solo sulla serie Pandas. Dove ci rivolgiamo ora per i dati dell'array non Pandas?
Mike,

5
@Mike rolling_mean () è obsoleto, ma ora puoi usare rolling e mean separatamente: df.rolling(windowsize).mean()ora funziona invece (molto rapidamente potrei aggiungere). per 6.000 serie di righe ha %timeit test1.rolling(20).mean()restituito 1000 loop, meglio di 3: 1,16 ms per loop
Vlox

5
@Vlox df.rolling()funziona abbastanza bene, il problema è che anche questo modulo non supporterà ndarrays in futuro. Per usarlo dovremo prima caricare i nostri dati in un Dataframe Pandas. Mi piacerebbe vedere questa funzione aggiunta a numpyo scipy.signal.
Mike,

1
@Mike è totalmente d'accordo. In particolare, sto lottando per abbinare la velocità dei panda .ewm (). Mean () per i miei array (invece di doverli prima caricare in un df). Voglio dire, è fantastico che sia veloce, ma sembra un po 'goffo muoversi dentro e fuori dai frame di dati troppo spesso.
Vlox,

6
%timeit bottleneck.move_mean(x, N)è da 3 a 15 volte più veloce dei metodi cumsum e panda sul mio pc. Dai un'occhiata al loro benchmark nel README del repository .
martedì

50

Puoi calcolare una media corrente con:

import numpy as np

def runningMean(x, N):
    y = np.zeros((len(x),))
    for ctr in range(len(x)):
         y[ctr] = np.sum(x[ctr:(ctr+N)])
    return y/N

Ma è lento.

Fortunatamente, NumPy include una convolve funzione che possiamo utilizzare per accelerare le cose. La media corrente equivale a contorcersi xcon un vettore Nlungo, con tutti i membri uguali 1/N. L'implementazione numpy di convolve include il transitorio iniziale, quindi è necessario rimuovere i primi punti N-1:

def runningMeanFast(x, N):
    return np.convolve(x, np.ones((N,))/N)[(N-1):]

Sulla mia macchina, la versione veloce è 20-30 volte più veloce, a seconda della lunghezza del vettore di input e delle dimensioni della finestra della media.

Si noti che convolve include una 'same'modalità che sembra che dovrebbe affrontare il problema del transitorio iniziale, ma lo divide tra l'inizio e la fine.


Si noti che la rimozione dei primi punti N-1 lascia comunque un effetto limite negli ultimi punti. Un modo più semplice per risolvere il problema è l'utilizzo mode='valid'in convolvecui non è necessaria alcuna post-elaborazione.
lapis,

1
@Psycho: mode='valid'rimuove il transitorio da entrambe le estremità, giusto? Se len(x)=10e N=4, per una media di corsa, vorrei 10 risultati ma validrestituisce 7.
mtrw,

1
Rimuove il transitorio dalla fine e l'inizio non ne ha uno. Bene, immagino sia una questione di priorità, non ho bisogno dello stesso numero di risultati a scapito di ottenere una pendenza verso lo zero che non è presente nei dati. A proposito, ecco un comando per mostrare la differenza tra le modalità: modes = ('full', 'same', 'valid'); [plot(convolve(ones((200,)), ones((50,))/50, mode=m)) for m in modes]; axis([-10, 251, -.1, 1.1]); legend(modes, loc='lower center')(con pyplot e numpy importati).
lapis,

runningMeanHo un effetto collaterale della media con zeri, quando esci dall'array con x[ctr:(ctr+N)]per il lato destro dell'array.
mrgloom,

runningMeanFastanche questo problema con l'effetto bordo.
mrgloom,

22

o modulo per Python che calcola

nei miei test su Tradewave.net TA-lib vince sempre:

import talib as ta
import numpy as np
import pandas as pd
import scipy
from scipy import signal
import time as t

PAIR = info.primary_pair
PERIOD = 30

def initialize():
    storage.reset()
    storage.elapsed = storage.get('elapsed', [0,0,0,0,0,0])

def cumsum_sma(array, period):
    ret = np.cumsum(array, dtype=float)
    ret[period:] = ret[period:] - ret[:-period]
    return ret[period - 1:] / period

def pandas_sma(array, period):
    return pd.rolling_mean(array, period)

def api_sma(array, period):
    # this method is native to Tradewave and does NOT return an array
    return (data[PAIR].ma(PERIOD))

def talib_sma(array, period):
    return ta.MA(array, period)

def convolve_sma(array, period):
    return np.convolve(array, np.ones((period,))/period, mode='valid')

def fftconvolve_sma(array, period):    
    return scipy.signal.fftconvolve(
        array, np.ones((period,))/period, mode='valid')    

def tick():

    close = data[PAIR].warmup_period('close')

    t1 = t.time()
    sma_api = api_sma(close, PERIOD)
    t2 = t.time()
    sma_cumsum = cumsum_sma(close, PERIOD)
    t3 = t.time()
    sma_pandas = pandas_sma(close, PERIOD)
    t4 = t.time()
    sma_talib = talib_sma(close, PERIOD)
    t5 = t.time()
    sma_convolve = convolve_sma(close, PERIOD)
    t6 = t.time()
    sma_fftconvolve = fftconvolve_sma(close, PERIOD)
    t7 = t.time()

    storage.elapsed[-1] = storage.elapsed[-1] + t2-t1
    storage.elapsed[-2] = storage.elapsed[-2] + t3-t2
    storage.elapsed[-3] = storage.elapsed[-3] + t4-t3
    storage.elapsed[-4] = storage.elapsed[-4] + t5-t4
    storage.elapsed[-5] = storage.elapsed[-5] + t6-t5    
    storage.elapsed[-6] = storage.elapsed[-6] + t7-t6        

    plot('sma_api', sma_api)  
    plot('sma_cumsum', sma_cumsum[-5])
    plot('sma_pandas', sma_pandas[-10])
    plot('sma_talib', sma_talib[-15])
    plot('sma_convolve', sma_convolve[-20])    
    plot('sma_fftconvolve', sma_fftconvolve[-25])

def stop():

    log('ticks....: %s' % info.max_ticks)

    log('api......: %.5f' % storage.elapsed[-1])
    log('cumsum...: %.5f' % storage.elapsed[-2])
    log('pandas...: %.5f' % storage.elapsed[-3])
    log('talib....: %.5f' % storage.elapsed[-4])
    log('convolve.: %.5f' % storage.elapsed[-5])    
    log('fft......: %.5f' % storage.elapsed[-6])

i risultati:

[2015-01-31 23:00:00] ticks....: 744
[2015-01-31 23:00:00] api......: 0.16445
[2015-01-31 23:00:00] cumsum...: 0.03189
[2015-01-31 23:00:00] pandas...: 0.03677
[2015-01-31 23:00:00] talib....: 0.00700  # <<< Winner!
[2015-01-31 23:00:00] convolve.: 0.04871
[2015-01-31 23:00:00] fft......: 0.22306

inserisci qui la descrizione dell'immagine


NameError: name 'info' is not defined. Ricevo questo errore, signore.
Md. Rezwanul Haque,

1
Sembra che le serie temporali vengano spostate dopo il livellamento, è l'effetto desiderato?
mrgloom,

@mrgloom yes, a scopo di visualizzazione; altrimenti apparirebbero come una riga sul grafico; Md. Rezwanul Haque, è possibile rimuovere tutti i riferimenti a COPPIA e informazioni; quelli erano metodi sandbox interni per l'ormai defunto tradewave.net
litepresence

21

Per una soluzione pronta per l'uso, consultare https://scipy-cookbook.readthedocs.io/items/SignalSmooth.html . Fornisce media corrente con ilflat tipo di finestra. Si noti che questo è un po 'più sofisticato del semplice metodo convoluto fai-da-te, poiché cerca di gestire i problemi all'inizio e alla fine dei dati riflettendoli (che potrebbero o meno funzionare nel tuo caso. ..).

Per cominciare, puoi provare:

a = np.random.random(100)
plt.plot(a)
b = smooth(a, window='flat')
plt.plot(b)

1
Questo metodo si basa su numpy.convolve, la differenza solo nel modificare la sequenza.
Alleo,

10
Sono sempre infastidito dalla funzione di elaborazione del segnale che restituisce segnali di uscita di forma diversa rispetto ai segnali di ingresso quando sia gli ingressi che le uscite sono della stessa natura (ad esempio, entrambi i segnali temporali). Rompe la corrispondenza con la variabile indipendente correlata (ad es. Tempo, frequenza) rendendo la stampa o il confronto non una questione diretta ... comunque, se condividi il sentimento, potresti voler cambiare le ultime righe della funzione proposta come y = np .convolve (w / w.sum (), s, mode = 'stesso'); return y [window_len-1 :-( window_len-1)]
Christian O'Reilly

@ ChristianO'Reilly, dovresti pubblicarlo come una risposta separata - è esattamente quello che stavo cercando, dato che in effetti ho altri due array che devono corrispondere alla lunghezza dei dati livellati, per la stampa ecc. Vorrei sapere esattamente come l'hai fatto: è wla dimensione della finestra e si dati?
Demis,

@Demis Sono contento che il commento abbia aiutato. Maggiori informazioni sulla funzione di convoluzione numpy qui docs.scipy.org/doc/numpy-1.15.0/reference/generated/… Una funzione di convoluzione ( en.wikipedia.org/wiki/Convolution ) contorta due segnali tra loro. In questo caso, avvolge i tuoi segnali con una finestra normalizzata (cioè area unitaria) (w / w.sum ()).
Christian O'Reilly,

21

Puoi usare scipy.ndimage.filters.uniform_filter1d :

import numpy as np
from scipy.ndimage.filters import uniform_filter1d
N = 1000
x = np.random.random(100000)
y = uniform_filter1d(x, size=N)

uniform_filter1d:

  • dà l'output con la stessa forma intorpidita (cioè il numero di punti)
  • consente più modi di gestire il bordo dove 'reflect'è l'impostazione predefinita, ma nel mio caso, ho preferito'nearest'

È anche piuttosto veloce (quasi 50 volte più veloce di np.convolvee 2-5 volte più veloce dell'approccio cumsum sopra indicato ):

%timeit y1 = np.convolve(x, np.ones((N,))/N, mode='same')
100 loops, best of 3: 9.28 ms per loop

%timeit y2 = uniform_filter1d(x, size=N)
10000 loops, best of 3: 191 µs per loop

ecco 3 funzioni che ti consentono di confrontare errori / velocità di diverse implementazioni:

from __future__ import division
import numpy as np
import scipy.ndimage.filters as ndif
def running_mean_convolve(x, N):
    return np.convolve(x, np.ones(N) / float(N), 'valid')
def running_mean_cumsum(x, N):
    cumsum = np.cumsum(np.insert(x, 0, 0))
    return (cumsum[N:] - cumsum[:-N]) / float(N)
def running_mean_uniform_filter1d(x, N):
    return ndif.uniform_filter1d(x, N, mode='constant', origin=-(N//2))[:-(N-1)]

1
Questa è l'unica risposta che sembra tener conto delle problematiche legate alle frontiere (piuttosto importante, in particolare durante la trama). Grazie!
Gabriel,

1
ho profilato uniform_filter1d, np.convolvecon un rettangolo, e np.cumsumseguito da np.subtract. i miei risultati: (1.) contorto è il più lento. (2.) Il cumulo / sottrazione è circa 20-30 volte più veloce. (3.) uniform_filter1d è circa 2-3 volte più veloce di cumsum / sottrarre. il vincitore è sicuramente uniform_filter1d.
Trevor Boyd Smith,

l'utilizzo uniform_filter1dè più veloce della cumsumsoluzione (di circa 2-5x). e uniform_filter1d non si verifica un grosso errore in virgola mobile come lacumsum soluzione.
Trevor Boyd Smith,

15

So che questa è una vecchia domanda, ma ecco una soluzione che non utilizza alcuna struttura di dati o librerie aggiuntive. È lineare nel numero di elementi dell'elenco di input e non riesco a pensare a nessun altro modo per renderlo più efficiente (in realtà se qualcuno conosce un modo migliore di allocare il risultato, per favore fatemi sapere).

NOTA: questo sarebbe molto più veloce utilizzando un array intorpidito anziché un elenco, ma volevo eliminare tutte le dipendenze. Sarebbe anche possibile migliorare le prestazioni mediante l'esecuzione multi-thread

La funzione presuppone che l'elenco di input sia monodimensionale, quindi fai attenzione.

### Running mean/Moving average
def running_mean(l, N):
    sum = 0
    result = list( 0 for x in l)

    for i in range( 0, N ):
        sum = sum + l[i]
        result[i] = sum / (i+1)

    for i in range( N, len(l) ):
        sum = sum - l[i-N] + l[i]
        result[i] = sum / N

    return result

Esempio

Supponiamo che abbiamo un elenco data = [ 1, 2, 3, 4, 5, 6 ]su cui vogliamo calcolare una media mobile con periodo di 3 e che si desidera anche un elenco di output che abbia le stesse dimensioni di quello di input (il più delle volte è il caso).

Il primo elemento ha indice 0, quindi la media mobile dovrebbe essere calcolata su elementi di indice -2, -1 e 0. Ovviamente non abbiamo dati [-2] e dati [-1] (a meno che tu non voglia usare speciali condizioni al contorno), quindi assumiamo che quegli elementi siano 0. Ciò equivale a zero-padding dell'elenco, tranne per il fatto che non lo pading in realtà, basta tenere traccia degli indici che richiedono il padding (da 0 a N-1).

Quindi, per i primi N elementi continuiamo a sommare gli elementi in un accumulatore.

result[0] = (0 + 0 + 1) / 3  = 0.333    ==   (sum + 1) / 3
result[1] = (0 + 1 + 2) / 3  = 1        ==   (sum + 2) / 3
result[2] = (1 + 2 + 3) / 3  = 2        ==   (sum + 3) / 3

Dagli elementi N + 1 in avanti l'accumulo semplice non funziona. ci aspettiamo result[3] = (2 + 3 + 4)/3 = 3ma questo è diverso da (sum + 4)/3 = 3.333.

Il modo per calcolare il valore corretto è quello di sottrarre data[0] = 1da sum+4, dando così sum + 4 - 1 = 9.

Questo accade perché attualmente sum = data[0] + data[1] + data[2], ma è anche vero per ogni i >= Nperché, prima della sottrazione, lo sumè data[i-N] + ... + data[i-2] + data[i-1].


12

Sento che questo può essere elegantemente risolto usando il collo di bottiglia

Vedi esempio di base di seguito:

import numpy as np
import bottleneck as bn

a = np.random.randint(4, 1000, size=100)
mm = bn.move_mean(a, window=5, min_count=1)
  • "mm" è il mezzo mobile per "a".

  • "finestra" è il numero massimo di voci da considerare per la media mobile.

  • "min_count" è il numero minimo di voci da considerare per lo spostamento della media (ad es. per i primi elementi o se l'array ha valori nan).

La parte buona è che il collo di bottiglia aiuta a gestire i valori nanometrici ed è anche molto efficiente.


Questa lib è davvero veloce. La funzione media mobile pura di Python è lenta. Bootleneck è una libreria PyData, che ritengo stabile e in grado di ottenere supporto continuo dalla community di Python, quindi perché non utilizzarla?
GoingMyWay

6

Non ho ancora verificato quanto sia veloce, ma potresti provare:

from collections import deque

cache = deque() # keep track of seen values
n = 10          # window size
A = xrange(100) # some dummy iterable
cum_sum = 0     # initialize cumulative sum

for t, val in enumerate(A, 1):
    cache.append(val)
    cum_sum += val
    if t < n:
        avg = cum_sum / float(t)
    else:                           # if window is saturated,
        cum_sum -= cache.popleft()  # subtract oldest value
        avg = cum_sum / float(n)

1
Questo è quello che stavo per fare. Qualcuno può per favore criticare perché questo è un brutto modo di andare?
Staggart

1
Questa semplice soluzione Python ha funzionato bene per me senza richiedere intorpidimento. Ho finito per inserirlo in una classe per il riutilizzo.
Matthew Tschiegg,

6

Questa risposta contiene soluzioni che utilizzano la libreria standard Python per tre diversi scenari.


Media corrente con itertools.accumulate

Questa è una soluzione Python 3.2+ efficiente in termini di memoria che calcola la media corrente su un iterabile di valori sfruttando la leva itertools.accumulate.

>>> from itertools import accumulate
>>> values = range(100)

Nota che valuespuò essere qualsiasi iterabile, inclusi generatori o qualsiasi altro oggetto che produce valori al volo.

Innanzitutto, costruisci pigramente la somma cumulativa dei valori.

>>> cumu_sum = accumulate(value_stream)

Successivamente, enumeratela somma cumulativa (a partire da 1) e costruisci un generatore che produce la frazione dei valori accumulati e l'indice di enumerazione corrente.

>>> rolling_avg = (accu/i for i, accu in enumerate(cumu_sum, 1))

È possibile emettere means = list(rolling_avg)se sono necessari tutti i valori in memoria contemporaneamente o chiamare in modo nextincrementale.
(Naturalmente, puoi anche iterare rolling_avgcon un forciclo, che chiamerà nextimplicitamente.)

>>> next(rolling_avg) # 0/1
>>> 0.0
>>> next(rolling_avg) # (0 + 1)/2
>>> 0.5
>>> next(rolling_avg) # (0 + 1 + 2)/3
>>> 1.0

Questa soluzione può essere scritta come una funzione come segue.

from itertools import accumulate

def rolling_avg(iterable):
    cumu_sum = accumulate(iterable)
    yield from (accu/i for i, accu in enumerate(cumu_sum, 1))
    

Un coroutine a cui è possibile inviare valori in qualsiasi momento

Questo coroutine consuma i valori inviati e mantiene una media corrente dei valori visti finora.

È utile quando non si dispone di un iterabile di valori ma si acquisiscono i valori da calcolare uno a uno in momenti diversi durante la vita del programma.

def rolling_avg_coro():
    i = 0
    total = 0.0
    avg = None

    while True:
        next_value = yield avg
        i += 1
        total += next_value
        avg = total/i
        

La coroutine funziona così:

>>> averager = rolling_avg_coro() # instantiate coroutine
>>> next(averager) # get coroutine going (this is called priming)
>>>
>>> averager.send(5) # 5/1
>>> 5.0
>>> averager.send(3) # (5 + 3)/2
>>> 4.0
>>> print('doing something else...')
doing something else...
>>> averager.send(13) # (5 + 3 + 13)/3
>>> 7.0

Calcolo della media su una finestra scorrevole di dimensioni N

Questa funzione generatore prende una dimensione iterabile e una finestra N e produce la media rispetto ai valori correnti all'interno della finestra. Utilizza a deque, che è una struttura di dati simile a un elenco, ma ottimizzata per modifiche rapide ( pop, append) in entrambi gli endpoint .

from collections import deque
from itertools import islice

def sliding_avg(iterable, N):        
    it = iter(iterable)
    window = deque(islice(it, N))        
    num_vals = len(window)

    if num_vals < N:
        msg = 'window size {} exceeds total number of values {}'
        raise ValueError(msg.format(N, num_vals))

    N = float(N) # force floating point division if using Python 2
    s = sum(window)
    
    while True:
        yield s/N
        try:
            nxt = next(it)
        except StopIteration:
            break
        s = s - window.popleft() + nxt
        window.append(nxt)
        

Ecco la funzione in azione:

>>> values = range(100)
>>> N = 5
>>> window_avg = sliding_avg(values, N)
>>> 
>>> next(window_avg) # (0 + 1 + 2 + 3 + 4)/5
>>> 2.0
>>> next(window_avg) # (1 + 2 + 3 + 4 + 5)/5
>>> 3.0
>>> next(window_avg) # (2 + 3 + 4 + 5 + 6)/5
>>> 4.0

5

Un po 'in ritardo per la festa, ma ho realizzato la mia piccola funzione che NON avvolge le estremità o i pad con zero che vengono quindi utilizzati per trovare anche la media. Come ulteriore vantaggio è che ricampiona anche il segnale in punti spaziati linearmente. Personalizza il codice a piacimento per ottenere altre funzionalità.

Il metodo è una semplice moltiplicazione matriciale con un kernel gaussiano normalizzato.

def running_mean(y_in, x_in, N_out=101, sigma=1):
    '''
    Returns running mean as a Bell-curve weighted average at evenly spaced
    points. Does NOT wrap signal around, or pad with zeros.

    Arguments:
    y_in -- y values, the values to be smoothed and re-sampled
    x_in -- x values for array

    Keyword arguments:
    N_out -- NoOf elements in resampled array.
    sigma -- 'Width' of Bell-curve in units of param x .
    '''
    N_in = size(y_in)

    # Gaussian kernel
    x_out = np.linspace(np.min(x_in), np.max(x_in), N_out)
    x_in_mesh, x_out_mesh = np.meshgrid(x_in, x_out)
    gauss_kernel = np.exp(-np.square(x_in_mesh - x_out_mesh) / (2 * sigma**2))
    # Normalize kernel, such that the sum is one along axis 1
    normalization = np.tile(np.reshape(sum(gauss_kernel, axis=1), (N_out, 1)), (1, N_in))
    gauss_kernel_normalized = gauss_kernel / normalization
    # Perform running average as a linear operation
    y_out = gauss_kernel_normalized @ y_in

    return y_out, x_out

Un semplice utilizzo su un segnale sinusoidale con aggiunta di rumore distribuito normale: inserisci qui la descrizione dell'immagine


Questo non funziona per me (python 3.6). 1 Non esiste alcuna funzione denominata sum, usando np.suminvece 2 L' @operatore (non ho idea di cosa sia) genera un errore. Potrei esaminarlo più tardi, ma in questo momento mi manca il tempo
Bastian,

La @è la matrice operatore di moltiplicazione che implementa np.matmul . Controlla se il tuo y_inarray è un array intorpidito, questo potrebbe essere il problema.
xyzzyqed

5

Invece di intorpidire o scipy, consiglierei ai panda di farlo più rapidamente:

df['data'].rolling(3).mean()

Questo richiede la media mobile (MA) di 3 periodi della colonna "dati". Puoi anche calcolare le versioni spostate, ad esempio quella che esclude la cella corrente (spostata indietro) può essere calcolata facilmente come:

df['data'].shift(periods=1).rolling(3).mean()

In che cosa differisce dalla soluzione proposta nel 2016 ?
Mr. T

2
La soluzione proposta nel 2016 utilizza pandas.rolling_meanmentre la mia usa pandas.DataFrame.rolling. Puoi anche calcolare lo spostamento min(), max(), sum()ecc., Nonché mean()con questo metodo facilmente.
Gursel Karacor,

Nel primo è necessario utilizzare un metodo diverso come pandas.rolling_min, pandas.rolling_maxecc. Sono simili ma diversi.
Gursel Karacor,

4

C'è un commento di Mab sepolto in una delle risposte sopra che ha questo metodo. bottleneckha move_meanquale è una media mobile semplice:

import numpy as np
import bottleneck as bn

a = np.arange(10) + np.random.random(10)

mva = bn.move_mean(a, window=2, min_count=1)

min_countè un parametro utile che in pratica porterà la media mobile fino a quel punto dell'array. Se non si imposta min_count, sarà uguale windowe tutto sarà fino ai windowpunti nan.


3

Un altro approccio per trovare la media mobile senza usare intorpidimento, panda

import itertools
sample = [2, 6, 10, 8, 11, 10]
list(itertools.starmap(lambda a,b: b/a, 
               enumerate(itertools.accumulate(sample), 1)))

stamperà [2.0, 4.0, 6.0, 6.5, 7.4, 7.833333333333333]


itertools.accumulate non esiste in Python 2.7, ma in Python 3.4
grayaii

3

Questa domanda è ora ancora più antica di quando NeXuS ne ha scritto il mese scorso, MA mi piace come il suo codice gestisce i casi limite. Tuttavia, poiché si tratta di una "media mobile semplice", i suoi risultati sono in ritardo rispetto ai dati a cui si applicano. Ho pensato che si tratta di casi limite in modo più soddisfacente rispetto alle modalità di NumPy valid, samee fullpotrebbe essere raggiunto mediante l'applicazione di un approccio simile a un convolution()metodo basato.

Il mio contributo utilizza una media corrente centrale per allineare i risultati con i loro dati. Quando ci sono troppi punti disponibili per l'uso della finestra a dimensione intera, le medie correnti vengono calcolate da finestre successivamente più piccole ai bordi dell'array. [In realtà, da finestre successivamente più grandi, ma questo è un dettaglio di implementazione.]

import numpy as np

def running_mean(l, N):
    # Also works for the(strictly invalid) cases when N is even.
    if (N//2)*2 == N:
        N = N - 1
    front = np.zeros(N//2)
    back = np.zeros(N//2)

    for i in range(1, (N//2)*2, 2):
        front[i//2] = np.convolve(l[:i], np.ones((i,))/i, mode = 'valid')
    for i in range(1, (N//2)*2, 2):
        back[i//2] = np.convolve(l[-i:], np.ones((i,))/i, mode = 'valid')
    return np.concatenate([front, np.convolve(l, np.ones((N,))/N, mode = 'valid'), back[::-1]])

È relativamente lento perché lo utilizza convolve(), e probabilmente potrebbe essere abbastanza elaborato da un vero Pythonista, tuttavia, credo che l'idea sia valida.


3

Ci sono molte risposte sopra sul calcolo di una media corrente. La mia risposta aggiunge due funzionalità extra:

  1. ignora i valori nan
  2. calcola la media per gli N valori vicini NON includendo il valore dell'interesse stesso

Questa seconda funzione è particolarmente utile per determinare quali valori differiscono dall'andamento generale di un certo importo.

Uso numpy.cumsum poiché è il metodo più efficiente in termini di tempo ( vedere la risposta di Alleo sopra ).

N=10 # number of points to test on each side of point of interest, best if even
padded_x = np.insert(np.insert( np.insert(x, len(x), np.empty(int(N/2))*np.nan), 0, np.empty(int(N/2))*np.nan ),0,0)
n_nan = np.cumsum(np.isnan(padded_x))
cumsum = np.nancumsum(padded_x) 
window_sum = cumsum[N+1:] - cumsum[:-(N+1)] - x # subtract value of interest from sum of all values within window
window_n_nan = n_nan[N+1:] - n_nan[:-(N+1)] - np.isnan(x)
window_n_values = (N - window_n_nan)
movavg = (window_sum) / (window_n_values)

Questo codice funziona solo per Ns. Può essere regolato per i numeri dispari modificando np.insert di padded_x e n_nan.

Esempio di output (raw in nero, movavg in blu): dati non elaborati (nero) e media mobile (blu) di 10 punti attorno a ciascun valore, escluso tale valore.  i valori di nan vengono ignorati.

Questo codice può essere facilmente adattato per rimuovere tutti i valori della media mobile calcolati da meno di cutoff = 3 valori non-nan.

window_n_values = (N - window_n_nan).astype(float) # dtype must be float to set some values to nan
cutoff = 3
window_n_values[window_n_values<cutoff] = np.nan
movavg = (window_sum) / (window_n_values)

dati non elaborati (nero) e media mobile (blu) ignorando qualsiasi finestra con meno di 3 valori non nan


2

Usa solo libreria standard Python (efficiente in termini di memoria)

Basta dare un'altra versione dell'uso della dequesola libreria standard . È una sorpresa per me che la maggior parte delle risposte stia utilizzando pandaso numpy.

def moving_average(iterable, n=3):
    d = deque(maxlen=n)
    for i in iterable:
        d.append(i)
        if len(d) == n:
            yield sum(d)/n

r = moving_average([40, 30, 50, 46, 39, 44])
assert list(r) == [40.0, 42.0, 45.0, 43.0]

In realtà ho trovato un'altra implementazione nei documenti di Python

def moving_average(iterable, n=3):
    # moving_average([40, 30, 50, 46, 39, 44]) --> 40.0 42.0 45.0 43.0
    # http://en.wikipedia.org/wiki/Moving_average
    it = iter(iterable)
    d = deque(itertools.islice(it, n-1))
    d.appendleft(0)
    s = sum(d)
    for elem in it:
        s += elem - d.popleft()
        d.append(elem)
        yield s / n

Tuttavia l'implementazione mi sembra un po 'più complessa di quanto dovrebbe essere. Ma deve essere nei documenti standard di Python per una ragione, qualcuno potrebbe commentare l'implementazione del mio e del documento standard?


2
Una grande differenza è che continui a sommare i membri della finestra ogni iterazione e aggiornano efficacemente la somma (rimuovi un membro e aggiungine un altro). in termini di complessità stai facendo O(n*d) calcoli ( dessendo la dimensione della finestra, ndimensione dell'iterabile) e loro stanno facendoO(n)
Iftah

@Iftah, gentile, grazie per la spiegazione, hai ragione.
MaThMaX,

2

Con le variabili di @ Aikude, ho scritto una riga.

import numpy as np

mylist = [1, 2, 3, 4, 5, 6, 7]
N = 3

mean = [np.mean(mylist[x:x+N]) for x in range(len(mylist)-N+1)]
print(mean)

>>> [2.0, 3.0, 4.0, 5.0, 6.0]

1

Sebbene ci siano soluzioni per questa domanda qui, dai un'occhiata alla mia soluzione. È molto semplice e funziona bene.

import numpy as np
dataset = np.asarray([1, 2, 3, 4, 5, 6, 7])
ma = list()
window = 3
for t in range(0, len(dataset)):
    if t+window <= len(dataset):
        indices = range(t, t+window)
        ma.append(np.average(np.take(dataset, indices)))
else:
    ma = np.asarray(ma)

1

Leggendo le altre risposte, non penso che questo sia ciò che la domanda ha posto, ma sono arrivato qui con la necessità di mantenere una media corrente di un elenco di valori che stava crescendo di dimensioni.

Quindi, se vuoi mantenere un elenco di valori che stai acquisendo da qualche parte (un sito, un dispositivo di misurazione, ecc.) E la media degli ultimi nvalori aggiornati, puoi usare il codice qui sotto, che minimizza lo sforzo di aggiungere nuovi elementi:

class Running_Average(object):
    def __init__(self, buffer_size=10):
        """
        Create a new Running_Average object.

        This object allows the efficient calculation of the average of the last
        `buffer_size` numbers added to it.

        Examples
        --------
        >>> a = Running_Average(2)
        >>> a.add(1)
        >>> a.get()
        1.0
        >>> a.add(1)  # there are two 1 in buffer
        >>> a.get()
        1.0
        >>> a.add(2)  # there's a 1 and a 2 in the buffer
        >>> a.get()
        1.5
        >>> a.add(2)
        >>> a.get()  # now there's only two 2 in the buffer
        2.0
        """
        self._buffer_size = int(buffer_size)  # make sure it's an int
        self.reset()

    def add(self, new):
        """
        Add a new number to the buffer, or replaces the oldest one there.
        """
        new = float(new)  # make sure it's a float
        n = len(self._buffer)
        if n < self.buffer_size:  # still have to had numbers to the buffer.
            self._buffer.append(new)
            if self._average != self._average:  # ~ if isNaN().
                self._average = new  # no previous numbers, so it's new.
            else:
                self._average *= n  # so it's only the sum of numbers.
                self._average += new  # add new number.
                self._average /= (n+1)  # divide by new number of numbers.
        else:  # buffer full, replace oldest value.
            old = self._buffer[self._index]  # the previous oldest number.
            self._buffer[self._index] = new  # replace with new one.
            self._index += 1  # update the index and make sure it's...
            self._index %= self.buffer_size  # ... smaller than buffer_size.
            self._average -= old/self.buffer_size  # remove old one...
            self._average += new/self.buffer_size  # ...and add new one...
            # ... weighted by the number of elements.

    def __call__(self):
        """
        Return the moving average value, for the lazy ones who don't want
        to write .get .
        """
        return self._average

    def get(self):
        """
        Return the moving average value.
        """
        return self()

    def reset(self):
        """
        Reset the moving average.

        If for some reason you don't want to just create a new one.
        """
        self._buffer = []  # could use np.empty(self.buffer_size)...
        self._index = 0  # and use this to keep track of how many numbers.
        self._average = float('nan')  # could use np.NaN .

    def get_buffer_size(self):
        """
        Return current buffer_size.
        """
        return self._buffer_size

    def set_buffer_size(self, buffer_size):
        """
        >>> a = Running_Average(10)
        >>> for i in range(15):
        ...     a.add(i)
        ...
        >>> a()
        9.5
        >>> a._buffer  # should not access this!!
        [10.0, 11.0, 12.0, 13.0, 14.0, 5.0, 6.0, 7.0, 8.0, 9.0]

        Decreasing buffer size:
        >>> a.buffer_size = 6
        >>> a._buffer  # should not access this!!
        [9.0, 10.0, 11.0, 12.0, 13.0, 14.0]
        >>> a.buffer_size = 2
        >>> a._buffer
        [13.0, 14.0]

        Increasing buffer size:
        >>> a.buffer_size = 5
        Warning: no older data available!
        >>> a._buffer
        [13.0, 14.0]

        Keeping buffer size:
        >>> a = Running_Average(10)
        >>> for i in range(15):
        ...     a.add(i)
        ...
        >>> a()
        9.5
        >>> a._buffer  # should not access this!!
        [10.0, 11.0, 12.0, 13.0, 14.0, 5.0, 6.0, 7.0, 8.0, 9.0]
        >>> a.buffer_size = 10  # reorders buffer!
        >>> a._buffer
        [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0]
        """
        buffer_size = int(buffer_size)
        # order the buffer so index is zero again:
        new_buffer = self._buffer[self._index:]
        new_buffer.extend(self._buffer[:self._index])
        self._index = 0
        if self._buffer_size < buffer_size:
            print('Warning: no older data available!')  # should use Warnings!
        else:
            diff = self._buffer_size - buffer_size
            print(diff)
            new_buffer = new_buffer[diff:]
        self._buffer_size = buffer_size
        self._buffer = new_buffer

    buffer_size = property(get_buffer_size, set_buffer_size)

E puoi provarlo con, ad esempio:

def graph_test(N=200):
    import matplotlib.pyplot as plt
    values = list(range(N))
    values_average_calculator = Running_Average(N/2)
    values_averages = []
    for value in values:
        values_average_calculator.add(value)
        values_averages.append(values_average_calculator())
    fig, ax = plt.subplots(1, 1)
    ax.plot(values, label='values')
    ax.plot(values_averages, label='averages')
    ax.grid()
    ax.set_xlim(0, N)
    ax.set_ylim(0, N)
    fig.show()

Che dà:

Valori e loro media in funzione dei valori #


1

Un'altra soluzione che utilizza solo una libreria standard e un deque:

from collections import deque
import itertools

def moving_average(iterable, n=3):
    # http://en.wikipedia.org/wiki/Moving_average
    it = iter(iterable) 
    # create an iterable object from input argument
    d = deque(itertools.islice(it, n-1))  
    # create deque object by slicing iterable
    d.appendleft(0)
    s = sum(d)
    for elem in it:
        s += elem - d.popleft()
        d.append(elem)
        yield s / n

# example on how to use it
for i in  moving_average([40, 30, 50, 46, 39, 44]):
    print(i)

# 40.0
# 42.0
# 45.0
# 43.0

1

A scopo educativo, vorrei aggiungere altre due soluzioni Numpy (che sono più lente della soluzione cumsum):

import numpy as np
from numpy.lib.stride_tricks import as_strided

def ra_strides(arr, window):
    ''' Running average using as_strided'''
    n = arr.shape[0] - window + 1
    arr_strided = as_strided(arr, shape=[n, window], strides=2*arr.strides)
    return arr_strided.mean(axis=1)

def ra_add(arr, window):
    ''' Running average using add.reduceat'''
    n = arr.shape[0] - window + 1
    indices = np.array([0, window]*n) + np.repeat(np.arange(n), 2)
    arr = np.append(arr, 0)
    return np.add.reduceat(arr, indices )[::2]/window

Funzioni utilizzate: as_strided , add.reduceat


1

Tutte le soluzioni di cui sopra sono scarse perché mancano

  • velocità dovuta a un pitone nativo invece di un'implementazione vettorializzata numpy,
  • stabilità numerica a causa del cattivo uso di numpy.cumsum, o
  • velocità dovuta a O(len(x) * w)implementazioni come convoluzioni.

Dato

import numpy
m = 10000
x = numpy.random.rand(m)
w = 1000

Si noti che è x_[:w].sum()uguale x[:w-1].sum(). Quindi, per la prima media, numpy.cumsum(...)aggiunge x[w] / w(via x_[w+1] / w) e sottrae 0(da x_[0] / w). Questo risulta inx[0:w].mean()

Tramite cumsum, aggiornerai la seconda media aggiungendo x[w+1] / we sottraendo ulteriormente x[0] / w, risultando x[1:w+1].mean().

Questo continua fino a quando non x[-w:].mean()viene raggiunto.

x_ = numpy.insert(x, 0, 0)
sliding_average = x_[:w].sum() / w + numpy.cumsum(x_[w:] - x_[:-w]) / w

Questa soluzione è vettoriale O(m), leggibile e numericamente stabile.


1

Che ne dici di un filtro a media mobile ? È anche un liner e ha il vantaggio di poter facilmente manipolare il tipo di finestra se hai bisogno di qualcosa di diverso dal rettangolo, cioè. una media mobile semplice lunga N di un array a:

lfilter(np.ones(N)/N, [1], a)[N:]

E con la finestra triangolare applicata:

lfilter(np.ones(N)*scipy.signal.triang(N)/N, [1], a)[N:]

Nota: di solito scarto i primi N campioni come fasulli quindi [N:]alla fine, ma non è necessario e si tratta solo di una scelta personale.


-7

Se scegli di creare il tuo, anziché utilizzare una libreria esistente, sii consapevole dell'errore in virgola mobile e prova a minimizzare i suoi effetti:

class SumAccumulator:
    def __init__(self):
        self.values = [0]
        self.count = 0

    def add( self, val ):
        self.values.append( val )
        self.count = self.count + 1
        i = self.count
        while i & 0x01:
            i = i >> 1
            v0 = self.values.pop()
            v1 = self.values.pop()
            self.values.append( v0 + v1 )

    def get_total(self):
        return sum( reversed(self.values) )

    def get_size( self ):
        return self.count

Se tutti i tuoi valori sono all'incirca allo stesso ordine di grandezza, ciò contribuirà a preservare la precisione aggiungendo sempre valori di grandezza approssimativamente simili.


15
Questa è una risposta terribilmente poco chiara, almeno qualche commento nel codice o una spiegazione del perché questo aiuta l'errore in virgola mobile sarebbe bello.
Gabe,

Nella mia ultima frase stavo cercando di indicare perché aiuta l'errore in virgola mobile. Se due valori hanno approssimativamente lo stesso ordine di grandezza, aggiungendoli si perde meno precisione rispetto a quando si aggiunge un numero molto grande a uno molto piccolo. Il codice combina valori "adiacenti" in modo tale che anche somme intermedie debbano essere sempre ragionevolmente vicine in grandezza, per ridurre al minimo l'errore in virgola mobile. Niente è insensato, ma questo metodo ha salvato un paio di progetti realizzati in modo molto scadente nella produzione.
Mayur Patel,

1. applicato al problema originale, questo sarebbe terribilmente lento (media di calcolo), quindi questo è irrilevante 2. per soffrire del problema della precisione dei numeri a 64 bit, si deve riassumere >> 2 ^ 30 di quasi numeri uguali.
Alleo,

@Alleo: invece di fare un'aggiunta per valore, ne farai due. La dimostrazione è la stessa del problema del bit-flipping. Tuttavia, il punto di questa risposta non è necessariamente la prestazione, ma la precisione. L'utilizzo della memoria per la media dei valori a 64 bit non supererebbe i 64 elementi nella cache, quindi è semplice anche nell'uso della memoria.
Mayur Patel,

Sì, hai ragione sul fatto che questo richiede il doppio delle operazioni rispetto alla semplice somma, ma il problema originale è calcolare la media , non solo la somma. Il che può essere fatto in O (n), ma la tua risposta richiede O (mn), dove m è la dimensione della finestra.
Alleo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.