Média móvel ou média em execução


192

Existe uma função SciPy ou a função ou módulo NumPy para Python que calcula a média de execução de uma matriz 1D, dada uma janela específica?

Respostas:


24

Para uma solução curta e rápida que faz a coisa toda em um loop, sem dependências, o código abaixo funciona muito bem.

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)

44
Rápido?! Essa solução é de ordem de magnitude mais lenta que as soluções com o Numpy.
Bart Bart

3
Embora essa solução nativa seja legal, o OP solicitou uma função numpy / scipy - presumivelmente essas serão consideravelmente mais rápidas.
Demis

254

UPD: soluções mais eficientes foram propostas por Alleo e jasaarim .


Você pode usar np.convolvepara isso:

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

Explicação

A média corrente é um caso da operação matemática da convolução . Para a média em execução, você desliza uma janela ao longo da entrada e calcula a média do conteúdo da janela. Para sinais 1D discretos, convolução é a mesma coisa, exceto que, em vez da média, você calcula uma combinação linear arbitrária, ou seja, multiplica cada elemento por um coeficiente correspondente e soma os resultados. Esses coeficientes, um para cada posição na janela, às vezes são chamados de kernel de convolução . Agora, a média aritmética de N valores é (x_1 + x_2 + ... + x_N) / N, então o kernel correspondente é (1/N, 1/N, ..., 1/N), e é exatamente isso que obtemos usando np.ones((N,))/N.

Arestas

O modeargumento de np.convolveespecifica como lidar com as arestas. Eu escolhi o validmodo aqui porque acho que é assim que a maioria das pessoas espera que a corrida funcione, mas você pode ter outras prioridades. Aqui está um gráfico que ilustra a diferença entre os modos:

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

Executando os modos de convolução média


4
Eu gosto desta solução porque é limpa (uma linha) e relativamente eficiente (trabalho realizado dentro de um numpy). Mas o uso da "solução eficiente" da Alleo numpy.cumsumtem uma complexidade melhor.
Ulrich Stern

2
@denfromufa, acredito que a documentação cobre a implementação bem o suficiente e também tem links para a Wikipedia, que explica a matemática. Considerando o foco da pergunta, você acha que essa resposta precisa copiá-las?
lapis

@lapis, o uso de convolve para média móvel é bastante incomum e não óbvio. Aqui é a melhor explicação visual que eu encontrei: matlabtricks.com/post-11/moving-average-by-convolution
denfromufa

Para plotagem e tarefas relacionadas, seria útil preenchê-lo com valores Nenhum. Minha (não tão bonita, mas curta) sugestão: `` `def moving_average (x, N, fill = True): retorna np.concatenate ([x for x em [[None] * (N // 2 + N% 2) * fill, np.convolve (x, np.ones ((N)) / N, mode = 'válido'), [None] * (N // 2) * fill,] se len (x)]) ` `` O código parece tão feio nos comentários do SO xD que eu não queria adicionar outra resposta, pois havia muitas, mas você pode simplesmente copiá-lo e colá-lo no seu IDE.
Chaoste 28/01/19

143

Solução eficiente

A convolução é muito melhor do que a abordagem direta, mas (eu acho) ela usa FFT e, portanto, bastante lenta. No entanto, especialmente para calcular a corrida, a seguinte abordagem funciona bem

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

O código para verificar

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

Note-se que numpy.allclose(result1, result2)é True, dois métodos são equivalentes. Quanto maior o N, maior a diferença no tempo.

aviso: embora o cumsum seja mais rápido, haverá um aumento no erro de ponto flutuante que pode fazer com que seus resultados sejam inválidos / incorretos / inaceitáveis

os comentários apontaram esse problema de erro de ponto flutuante aqui, mas estou tornando mais óbvio aqui na resposta. .

# 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)
  • quanto mais pontos você acumular, maior será o erro de ponto flutuante (então 1e5 pontos é perceptível, 1e6 pontos é mais significativo, mais de 1e6 e convém redefinir os acumuladores)
  • você pode trapacear usando, np.longdoublemas seu erro de ponto flutuante ainda será significativo para um número relativamente grande de pontos (em torno de> 1e5, mas depende dos seus dados)
  • você pode plotar o erro e vê-lo aumentando relativamente rápido
  • a solução convolve é mais lenta, mas não tem essa perda de precisão de ponto flutuante
  • a solução uniform_filter1d é mais rápida que esta solução cumsum E não possui essa perda de precisão de ponto flutuante

3
Ótima solução! Meu palpite é numpy.convolveO (mn); seus documentos mencionam que scipy.signal.fftconvolveusa FFT.
Ulrich Stern

3
Este método não lida com as bordas da matriz, pois não?
Jové

6
Solução agradável, mas observe que pode sofrer erros numéricos para matrizes grandes, pois no final da matriz, você pode subtrair dois números grandes para obter um resultado pequeno.
Bas Swinckels

1
Isso usa divisão inteira em vez de divisão flutuante: running_mean([1,2,3], 2)gives array([1, 2]). Substituir xpor [float(value) for value in x]faz o truque.
ChrisW

4
A estabilidade numérica desta solução pode se tornar um problema se xcontiver flutuadores. Exemplo: running_mean(np.arange(int(1e7))[::-1] + 0.2, 1)[-1] - 0.2retorna 0.003125enquanto se espera 0.0. Mais informações: en.wikipedia.org/wiki/Loss_of_significance
Milan

79

Atualização: O exemplo abaixo mostra a pandas.rolling_meanfunção antiga que foi removida nas versões recentes do pandas. Um equivalente moderno da chamada de função abaixo seria

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

pandas é mais adequado para isso do que o NumPy ou SciPy. Sua função rolling_mean faz o trabalho de maneira conveniente. Ele também retorna uma matriz NumPy quando a entrada é uma matriz.

É difícil superar o rolling_meandesempenho com qualquer implementação Python pura e personalizada. Aqui está um exemplo de desempenho em relação a duas das soluções propostas:

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

Também existem boas opções de como lidar com os valores das arestas.


6
O rolling_mean do Pandas é uma boa ferramenta para o trabalho, mas foi preterido por ndarrays. Nos próximos lançamentos do Pandas, ele funcionará apenas na série Pandas. Onde nos voltamos agora para dados de array que não são do Pandas?
25316 Mike

5
@ Mike rolling_mean () está obsoleto, mas agora você pode usar rolling e mean separadamente: df.rolling(windowsize).mean()agora funciona (em vez disso, devo acrescentar muito rapidamente). para 6.000 séries de linhas %timeit test1.rolling(20).mean()retornou 1000 loops, o melhor de 3: 1,16 ms por loop
Vlox

5
O @Vlox df.rolling()funciona bem o suficiente, o problema é que mesmo esse formulário não suportará ndarrays no futuro. Para usá-lo, teremos que carregar nossos dados em um Dataframe do Pandas primeiro. Eu adoraria ver essa função adicionada a um numpyou a outro scipy.signal.
Mike

1
@ Mike concorda totalmente. Estou lutando, em particular, para combinar a velocidade dos pandas .ewm (). Mean () para minhas próprias matrizes (em vez de precisar carregá-las em um df primeiro). Quero dizer, é ótimo que seja rápido, mas parece um pouco complicado entrar e sair dos quadros de dados com muita frequência.
Vlox 3/17/17

6
%timeit bottleneck.move_mean(x, N)é de 3 a 15 vezes mais rápido que os métodos cumsum e pandas no meu pc. Dê uma olhada em sua referência no README do repo .
mab

50

Você pode calcular uma média de execução com:

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

Mas é lento.

Felizmente, o numpy inclui uma função convolve que podemos usar para acelerar as coisas. A média de execução é equivalente a convolver xcom um vetor que é Nlongo, com todos os membros iguais a 1/N. A implementação numpy do convolve inclui o transiente inicial, portanto, você deve remover os primeiros pontos N-1:

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

Na minha máquina, a versão rápida é 20 a 30 vezes mais rápida, dependendo do comprimento do vetor de entrada e do tamanho da janela de média.

Observe que o convolve inclui um 'same'modo que parece que deveria resolver o problema transitório inicial, mas o divide entre o começo e o fim.


Observe que a remoção dos primeiros pontos N-1 ainda deixa um efeito de contorno nos últimos pontos. Uma maneira mais fácil de resolver o problema é usar mode='valid'em convolveque não requer qualquer pós-processamento.
lapis

1
@ Psycho - mode='valid'remove o transitório de ambas as extremidades, certo? Se len(x)=10e N=4, para uma corrida significa que eu gostaria 10 resultados, mas validretorna 7.
MTRW

1
Ele remove o transitório do final e o começo não possui um. Bem, acho que é uma questão de prioridades, não preciso do mesmo número de resultados à custa de obter uma inclinação em direção a zero que não existe nos dados. BTW, aqui está um comando para mostrar a diferença entre os modos: 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')(com pyplot e numpy importados).
lapis

runningMeanTenho o efeito colateral da média com zeros, quando você sai da matriz com x[ctr:(ctr+N)]o lado direito da matriz.
Mrgloom 2/11

runningMeanFasttambém tem esse problema de efeito de fronteira.
precisa saber é

21

ou módulo para python que calcula

nos meus testes no Tradewave.net, o TA-lib sempre vence:

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

resultados:

[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

insira a descrição da imagem aqui


NameError: name 'info' is not defined. Estou recebendo esse erro, senhor.
Md. Rezwanul Haque 08/08/19

1
Parece que as séries temporais são alteradas após a suavização, é o efeito desejado?
Mrgloom 2/11

@mrgloom sim, para fins de visualização; caso contrário, eles apareceriam como uma linha no gráfico; Md. Rezwanul Haque, você pode remover todas as referências ao PAR e informações; esses eram métodos internos em área restrita para o tradewave.net agora extinto
litepresence

21

Para uma solução pronta para uso, consulte https://scipy-cookbook.readthedocs.io/items/SignalSmooth.html . Ele fornece média de execução com o flattipo de janela. Observe que isso é um pouco mais sofisticado do que o método simples de convolução do tipo faça você mesmo, pois ele tenta lidar com os problemas no início e no final dos dados refletindo-os (o que pode ou não funcionar no seu caso). ..)

Para começar, você pode tentar:

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

1
Este método depende numpy.convolve, a diferença apenas na alteração da sequência.
Alleo

10
Fico sempre aborrecido com a função de processamento de sinal que retorna sinais de saída de forma diferente dos sinais de entrada quando as entradas e saídas são da mesma natureza (por exemplo, ambos os sinais temporais). Ele quebra a correspondência com a variável independente relacionada (por exemplo, tempo, frequência), tornando a plotagem ou a comparação não um assunto direto ... de qualquer maneira, se você compartilhar o sentimento, poderá alterar as últimas linhas da função proposta como y = np .convolve (w / w.sum (), s, mode = 'igual'); return y [window_len-1 :-( window_len-1)]
Christian O'Reilly

@ ChristianO'Reilly, você deve postar isso como uma resposta separada - é exatamente isso que eu estava procurando, pois eu realmente tenho duas outras matrizes que precisam coincidir com os comprimentos dos dados suavizados, para plotagem etc. Eu gostaria de saber exatamente como você fez isso - é wo tamanho da janela e sos dados?
Demis 8/10

@ Demis Fico feliz que o comentário ajudou. Mais informações sobre a função numpy convolve aqui docs.scipy.org/doc/numpy-1.15.0/reference/generated/… Uma função de convolução ( en.wikipedia.org/wiki/Convolution ) envolve dois sinais entre si. Nesse caso, ele envolve seu (s) sinal (es) com uma janela normalizada (por exemplo, área unitária) (w / w.sum ()).
Christian O'Reilly

18

Você pode usar 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:

  • fornece a saída com a mesma forma numpy (ou seja, número de pontos)
  • permite várias maneiras de lidar com a borda onde 'reflect'está o padrão, mas no meu caso, eu queria'nearest'

Também é bastante rápido (quase 50 vezes mais rápido np.convolvee 2-5 vezes mais rápido que a abordagem de cumsum fornecida acima ):

%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

aqui estão três funções que permitem comparar erros / velocidades de diferentes implementações:

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
Esta é a única resposta que parece levar em consideração as questões de fronteira (bastante importante, principalmente ao tramar). Obrigado!
6608 Gabriel

1
eu perfilado uniform_filter1d, np.convolvecom um retângulo, e np.cumsumseguido por np.subtract. meus resultados: (1.) convolve é o mais lento. (2.) cumsum / subtrair é cerca de 20 a 30 vezes mais rápido. (3.) uniform_filter1d é cerca de 2-3 vezes mais rápido que cumsum / subtrair. vencedor é definitivamente uniform_filter1d.
Trevor Boyd Smith

o uso uniform_filter1dé mais rápido que a cumsumsolução (em cerca de 2-5x). e uniform_filter1d não obtém erro maciço de ponto flutuante como acumsum solução.
Trevor Boyd Smith

15

Sei que essa é uma pergunta antiga, mas aqui está uma solução que não usa nenhuma estrutura ou biblioteca de dados extra. É linear no número de elementos da lista de entrada e não consigo pensar em outra maneira de torná-la mais eficiente (na verdade, se alguém souber uma maneira melhor de alocar o resultado, informe-me).

NOTA: isso seria muito mais rápido usando uma matriz numpy em vez de uma lista, mas eu queria eliminar todas as dependências. Também seria possível melhorar o desempenho através da execução multithread

A função assume que a lista de entrada é unidimensional, portanto, tenha cuidado.

### 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

Exemplo

Suponha que temos uma lista data = [ 1, 2, 3, 4, 5, 6 ] na qual queremos calcular uma média móvel com período de 3 e que você também deseja uma lista de saída com o mesmo tamanho da entrada (na maioria das vezes).

O primeiro elemento possui o índice 0, portanto a média móvel deve ser calculada nos elementos do índice -2, -1 e 0. Obviamente, não temos dados [-2] e dados [-1] (a menos que você queira usar métodos especiais). condições de contorno), então assumimos que esses elementos são 0. Isso equivale a preencher com zero a lista, exceto que na verdade não a preenchemos, basta acompanhar os índices que exigem preenchimento (de 0 a N-1).

Assim, para os primeiros N elementos, continuamos somando os elementos em um acumulador.

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

A partir dos elementos N + 1, a acumulação simples não funciona. esperamos, result[3] = (2 + 3 + 4)/3 = 3mas isso é diferente de (sum + 4)/3 = 3.333.

A maneira de calcular o valor correto é subtrair data[0] = 1de sum+4, dando assim sum + 4 - 1 = 9.

Isso acontece porque atualmente sum = data[0] + data[1] + data[2], mas também é verdade para todos i >= Nporque, antes da subtração, sumé data[i-N] + ... + data[i-2] + data[i-1].


12

Eu sinto que isso pode ser resolvido de forma elegante usando gargalo

Veja a amostra básica abaixo:

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" é a média móvel de "a".

  • "window" é o número máximo de entradas a serem consideradas para a média móvel.

  • "min_count" é o número mínimo de entradas a serem consideradas para a média móvel (por exemplo, para os primeiros elementos ou se a matriz possui valores nan).

A parte boa é que o gargalo ajuda a lidar com os valores nan e também é muito eficiente.


Essa biblioteca é realmente rápida. A função média móvel pura do Python é lenta. Bootleneck é uma biblioteca PyData, que eu acho estável e pode obter suporte contínuo da comunidade Python, então por que não usá-la?
GoingMyWay 20/01

6

Ainda não verifiquei o quão rápido isso é, mas você pode tentar:

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
Era isso que eu ia fazer. Alguém pode criticar por que este é um mau caminho a percorrer?
staggart

1
Essa solução simples de python funcionou bem para mim sem a necessidade de dormência. Acabei rolando para uma aula para reutilização.
Matthew Tschiegg

6

Esta resposta contém soluções usando a biblioteca padrão do Python para três cenários diferentes.


Média de execução com itertools.accumulate

Esta é uma solução Python 3.2+ com eficiência de memória que calcula a média de execução em uma quantidade iterável de valores ao aproveitar itertools.accumulate.

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

Observe que valuespode ser iterável, incluindo geradores ou qualquer outro objeto que produza valores em tempo real.

Primeiro, construa preguiçosamente a soma cumulativa dos valores.

>>> cumu_sum = accumulate(value_stream)

Em seguida, enumeratea soma cumulativa (começando em 1) e construa um gerador que produz a fração dos valores acumulados e o índice de enumeração atual.

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

Você pode emitir means = list(rolling_avg)se precisar de todos os valores na memória de uma vez ou ligar de forma nextincremental.
(Obviamente, você também pode iterar rolling_avgcom um forloop, que será chamado 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

Esta solução pode ser escrita como uma função da seguinte maneira.

from itertools import accumulate

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

Uma corrotina para a qual você pode enviar valores a qualquer momento

Essa rotina consome os valores que você envia e mantém uma média contínua dos valores vistos até o momento.

É útil quando você não possui valores iteráveis, mas adquire os valores a serem calculados em média um a um em momentos diferentes ao longo da vida do programa.

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
        

A corotina funciona assim:

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

Computando a média em uma janela deslizante de tamanho N

Essa função de gerador pega um iterável e um tamanho de janela N e gera a média sobre os valores atuais dentro da janela. Ele usa a deque, que é uma estrutura de dados semelhante a uma lista, mas otimizada para modificações rápidas ( pop, append) nos dois pontos de extremidade .

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)
        

Aqui está a função em ação:

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

Um pouco atrasado para a festa, mas criei minha própria função que NÃO envolve as extremidades ou os zeros com zeros que são usados ​​para encontrar a média também. Como um tratamento adicional, é que ele também faz nova amostragem do sinal em pontos espaçados linearmente. Personalize o código à vontade para obter outros recursos.

O método é uma multiplicação simples de matriz com um kernel Gaussiano normalizado.

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

Um uso simples em um sinal sinusoidal com ruído distribuído normal adicionado: insira a descrição da imagem aqui


Isso não funciona para mim (python 3.6). 1 Não há nenhuma função nomeada sum, usando em np.sumvez disso 2 O @operador (não faz ideia do que é isso) gera um erro. Eu posso olhar para ele mais tarde, mas eu estou faltando o tempo agora
Bastian

O @é o operador de multiplicação de matrizes que implementa np.matmul . Verifique se sua y_inmatriz é uma matriz numpy, esse pode ser o problema.
xyzzyqed

5

Em vez de entorpecido ou covarde, eu recomendaria que os pandas fizessem isso mais rapidamente:

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

Isso leva a média móvel (MA) de 3 períodos da coluna "dados". Você também pode calcular as versões deslocadas, por exemplo, a que exclui a célula atual (deslocada uma para trás) pode ser calculada facilmente como:

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

Como isso difere da solução proposta em 2016 ?
Mr. T

2
A solução proposta em 2016 usa pandas.rolling_meanenquanto a mina usa pandas.DataFrame.rolling. Você também pode calcular o movimento, min(), max(), sum()etc., bem como mean()com esse método facilmente.
Gursel Karacor

No primeiro, você precisa usar um método diferente, como pandas.rolling_min, pandas.rolling_maxetc. Eles são semelhantes, mas diferentes.
Gursel Karacor

4

Há um comentário de mab enterrado em uma das respostas acima, que possui esse método. bottlenecktem move_meanuma média móvel simples:

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é um parâmetro útil que basicamente levará a média móvel até esse ponto em sua matriz. Se você não definir min_count, ele irá igualar window, e tudo até windowpontos será nan.


3

Outra abordagem para encontrar a média móvel sem usar o numpy, o panda

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

imprimirá [2.0, 4.0, 6.0, 6.5, 7.4, 7.833333333333333]


itertools.accumulate não existe no Python 2.7, mas faz em python 3.4
grayaii

3

Agora, essa pergunta é ainda mais antiga do que quando o NeXuS escreveu sobre isso no mês passado, mas eu gosto de como o código dele lida com casos extremos. No entanto, por ser uma "média móvel simples", seus resultados ficam aquém dos dados aos quais se aplicam. Pensei que lidar com casos extremos de uma forma mais satisfatória do que os modos de Numpy valid, samee fullpoderia ser alcançado através da aplicação de uma abordagem similar a um convolution()método baseado.

Minha contribuição usa uma média de execução central para alinhar seus resultados com seus dados. Quando há muito poucos pontos disponíveis para a janela de tamanho completo a ser usada, as médias de execução são calculadas a partir de janelas sucessivamente menores nas bordas da matriz. [Na verdade, a partir de janelas sucessivamente maiores, mas esse é um detalhe da implementação.]

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 porque usa convolve(), e provavelmente poderia ser bastante estimulado por um verdadeiro Pythonista, no entanto, acredito que a idéia se mantém.


3

Há muitas respostas acima sobre o cálculo de uma média corrente. Minha resposta adiciona dois recursos extras:

  1. ignora os valores nan
  2. calcula a média dos N valores vizinhos, NÃO incluindo o valor do interesse em si

Esse segundo recurso é particularmente útil para determinar quais valores diferem da tendência geral em um determinado valor.

Uso numpy.cumsum, pois é o método mais econômico em termos de tempo ( consulte a resposta de Alleo acima ).

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)

Esse código funciona apenas para Ns. Pode ser ajustado para números ímpares, alterando o np.insert de padded_x e n_nan.

Exemplo de saída (bruto em preto, movavg em azul): dados brutos (preto) e média móvel (azul) de 10 pontos em torno de cada valor, sem incluir esse valor.  os valores nan são ignorados.

Esse código pode ser facilmente adaptado para remover todos os valores médios móveis calculados com menos que cutoff = 3 valores não 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)

dados brutos (preto) e média móvel (azul), ignorando qualquer janela com menos de 3 valores que não sejam nan


2

Use apenas a biblioteca padrão do Python (memória eficiente)

Apenas dê outra versão do uso dequeapenas da biblioteca padrão . É uma surpresa para mim que a maioria das respostas esteja usando pandasou 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]

Na verdade, eu encontrei outra implementação em documentos 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

No entanto, a implementação me parece um pouco mais complexa do que deveria ser. Mas deve estar nos documentos python padrão por um motivo: alguém poderia comentar sobre a implementação do meu e do documento padrão?


2
Uma grande diferença é que você continua somando os membros da janela a cada iteração, e eles atualizam a soma com eficiência (remova um membro e adicione outro). em termos de complexidade que você está fazendo O(n*d) cálculos ( dsendo o tamanho da janela, ntamanho do iterable) e eles estão fazendoO(n)
Iftah

@ Iftah, bom, obrigado pela explicação, você está certo.
MaThMaX 16/10/19

2

Com as variáveis ​​do @ Aikude, escrevi uma linha.

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

Embora existam soluções para esta pergunta aqui, dê uma olhada na minha solução. É muito simples e está funcionando bem.

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

Ao ler as outras respostas, acho que não é isso que a pergunta pediu, mas cheguei aqui com a necessidade de manter uma média constante de uma lista de valores que cresciam em tamanho.

Portanto, se você quiser manter uma lista dos valores que está adquirindo de algum lugar (um site, um dispositivo de medição etc.) e a média dos últimos nvalores atualizados, use o código abaixo, para minimizar o esforço de adicionar novos elementos:

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 você pode testá-lo com, por exemplo:

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

Que dá:

Valores e sua média em função dos valores #


1

Outra solução usando apenas uma biblioteca padrão e 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

Para fins educacionais, deixe-me adicionar mais duas soluções Numpy (que são mais lentas que a solução 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

Funções usadas: as_strided , add.reduceat


1

Todas as soluções acima mencionadas são pobres porque não possuem

  • velocidade devido a um python nativo em vez de uma implementação vetorizada numpy,
  • estabilidade numérica devido ao mau uso de numpy.cumsum ou
  • velocidade devido a O(len(x) * w)implementações como convoluções.

Dado

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

Note que x_[:w].sum()é igual x[:w-1].sum(). Portanto, para a primeira média, as numpy.cumsum(...)adições x[w] / w(via x_[w+1] / w) e subtrações 0(de x_[0] / w). Isto resulta emx[0:w].mean()

Por meio do cumsum, você atualizará a segunda média adicionando x[w+1] / we subtraindo adicionalmente x[0] / w, resultando emx[1:w+1].mean() .

Isso continua até que x[-w:].mean()seja alcançado.

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

Esta solução é vetorizada O(m), legível e numericamente estável.


1

Que tal um filtro de média móvel ? É também uma linha e tem a vantagem de poder manipular facilmente o tipo de janela se precisar de algo além do retângulo, por exemplo. uma média móvel simples N-longa de uma matriz a:

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

E com a janela triangular aplicada:

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

Nota: Normalmente, descarto as primeiras N amostras como falsas, portanto, [N:]no final, mas não é necessário e é apenas uma questão de escolha pessoal.


-7

Se você optar por criar o seu próprio, em vez de usar uma biblioteca existente, esteja ciente do erro de ponto flutuante e tente minimizar seus efeitos:

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 todos os seus valores forem aproximadamente da mesma ordem de magnitude, isso ajudará a preservar a precisão, sempre adicionando valores de magnitudes aproximadamente semelhantes.


15
Esta é uma resposta terrivelmente incerta, pelo menos algum comentário no código ou explicação de por que isso ajuda o erro de ponto flutuante seria bom.
Gabe

Na minha última frase, eu estava tentando indicar por que isso ajuda no erro de ponto flutuante. Se dois valores são aproximadamente da mesma ordem de magnitude, a adição deles perde menos precisão do que se você adicionasse um número muito grande a um muito pequeno. O código combina valores "adjacentes" de uma maneira que mesmo somas intermediárias devem sempre ser razoavelmente próximas em magnitude, para minimizar o erro de ponto flutuante. Nada é à prova de idiotas, mas esse método salvou alguns projetos muito mal implementados na produção.
Mayur Patel

1. sendo aplicado ao problema original, isso seria terrivelmente lento (média computacional); portanto, isso é irrelevante 2. para sofrer com o problema de precisão dos números de 64 bits, é preciso somar >> 2 ^ 30 de quase números iguais.
Alleo

@ Alleo: Em vez de fazer uma adição por valor, você fará duas. A prova é a mesma do problema de troca de bits. No entanto, o objetivo desta resposta não é necessariamente desempenho, mas precisão. O uso de memória para obter valores médios de 64 bits não excederia 64 elementos no cache, por isso também é amigável no uso de memória.
Mayur Patel

Sim, você está certo que isso exige duas vezes mais operações do que a soma simples, mas o problema original é a computação em execução , e não apenas a soma. O que pode ser feito em O (n), mas sua resposta requer O (mn), onde m é o tamanho da janela.
Alleo
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.