Скользящее среднее или скользящее среднее


195

Есть ли функция PyiPy или функция или модуль NumPy для Python, которая вычисляет среднее значение для одномерного массива в конкретном окне?

Ответы:


24

Для короткого, быстрого решения, которое делает все в одном цикле, без зависимостей, приведенный ниже код прекрасно работает.

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
Быстро?! Это решение на несколько порядков медленнее, чем решения с Numpy.
Барт

3
Несмотря на то, что это нативное решение классное, OP попросил использовать функцию numpy / scipy - вероятно, они будут значительно быстрее.
Демис

256

UPD: более эффективные решения были предложены Alleo и jasaarim .


Вы можете использовать np.convolveдля этого:

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

объяснение

Скользящее среднее - это случай математической операции свертки . Для скользящего среднего вы перемещаете окно вдоль ввода и вычисляете среднее значение содержимого окна. Для дискретных одномерных сигналов свертка - это то же самое, за исключением того, что вместо среднего вы вычисляете произвольную линейную комбинацию, то есть умножаете каждый элемент на соответствующий коэффициент и суммируете результаты. Эти коэффициенты, по одному для каждой позиции в окне, иногда называют ядром свертки . Теперь среднее арифметическое значений N равно (x_1 + x_2 + ... + x_N) / N, так что соответствующее ядро ​​есть (1/N, 1/N, ..., 1/N), и это именно то, что мы получаем, используя np.ones((N,))/N.

Ребра

modeАргумент np.convolveопределяет , как обрабатывать края. Я выбрал validрежим здесь, потому что я думаю, что именно так большинство людей ожидает, что сработает среднее значение, но у вас могут быть другие приоритеты. Вот график, который иллюстрирует разницу между режимами:

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

Ходовые средние режимы свертки


5
Мне нравится это решение, потому что оно чистое (одна строка) и относительно эффективное (работа выполняется внутри numpy). Но использование «Эффективного решения» Alleo numpy.cumsumимеет большую сложность.
Ульрих Стерн

2
@denfromufa, я считаю, что документация достаточно хорошо описывает реализацию, а также ссылки на Википедию, которая объясняет математику. Учитывая фокус вопроса, вы думаете, этот ответ должен скопировать их?
ляпис

@lapis использование convolve для скользящего среднего довольно необычно и неочевидно. Вот лучшее визуальное объяснение, которое я нашел: matlabtricks.com/post-11/moving-average-by-convolution
denfromufa

Для построения графиков и связанных задач было бы полезно заполнить их значениями None. Мое (не очень красивое, но короткое) предложение: `` `def moving_average (x, N, fill = True): вернуть np.concatenate ([x для x в [[None] * (N // 2 + N% 2)) * fill, np.convolve (x, np.ones ((N,)) / N, mode = 'valid'), [None] * (N // 2) * fill,] if len (x)]) ` `` Код выглядит так ужасно в SO комментариях xD Я не хотел добавлять другой ответ, так как их было так много, но вы могли бы просто скопировать и вставить его в свою IDE.
Chaoste

146

Эффективное решение

Свертка намного лучше, чем простой подход, но (я думаю) она использует БПФ и, следовательно, довольно медленно. Тем не менее, специально для вычисления среднего значения работает следующий подход

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

Код для проверки

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

Обратите внимание , что numpy.allclose(result1, result2)есть Trueдва способа эквивалентны. Чем больше N, тем больше разница во времени.

предупреждение: хотя cumsum быстрее, будет увеличиваться ошибка с плавающей запятой, которая может привести к тому, что ваши результаты будут недействительными / неправильными / неприемлемыми

комментарии указывают на эту проблему с плавающей запятой здесь, но я делаю это более очевидным здесь, в ответе. ,

# 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)
  • чем больше очков вы накапливаете, тем больше ошибка с плавающей запятой (поэтому 1e5 очков заметны, 1e6 очков значительнее, чем 1e6, и вы можете сбросить аккумуляторы)
  • Вы можете обмануть, используя, np.longdoubleно ваша ошибка с плавающей запятой все еще станет значительной для относительно большого количества точек (около> 1e5, но зависит от ваших данных)
  • Вы можете отобразить ошибку и увидеть, как она увеличивается относительно быстро
  • Сверточное решение медленнее, но не имеет этой потери точности с плавающей запятой
  • решениеiform_filter1d быстрее, чем это решение cumsum И не имеет этой потери точности с плавающей запятой

3
Отличное решение! Я догадываюсь, numpy.convolveчто O (мн); его документы упоминают, что scipy.signal.fftconvolveиспользует БПФ.
Ульрих Стерн

3
Этот метод не имеет дело с краями массива, не так ли?
JoVe

6
Хорошее решение, но учтите, что оно может страдать от числовых ошибок для больших массивов, так как к концу массива вы можете вычесть два больших числа, чтобы получить небольшой результат.
Бас Суинкельс

1
Это использует целочисленное деление вместо деления с плавающей запятой: running_mean([1,2,3], 2)дает array([1, 2]). Замена xпо [float(value) for value in x]делает трюк.
ChrisW

4
Численная стабильность этого решения может стать проблемой, если xсодержит поплавки. Пример: running_mean(np.arange(int(1e7))[::-1] + 0.2, 1)[-1] - 0.2возврат в 0.003125ожидании 0.0. Дополнительная информация: en.wikipedia.org/wiki/Loss_of_significance
Милан,

80

Обновление: в приведенном ниже примере показана старая pandas.rolling_meanфункция, которая была удалена из последних версий панд. Современный эквивалент вызова функции ниже будет

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

Панды больше подходят для этого, чем NumPy или SciPy. Его функция Rolling_mean делает работу удобно. Он также возвращает массив NumPy, когда входные данные являются массивом.

Сложно превзойти rolling_meanпо производительности любую обычную реализацию Python. Вот пример производительности против двух предложенных решений:

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

Есть также хорошие варианты того, как работать со значениями ребер.


6
Pandas roll_mean - хороший инструмент для работы, но он устарел для ndarrays. В будущих выпусках Pandas он будет работать только на серии Pandas. Куда мы сейчас обратимся для данных массива, отличного от Pandas?
Майк

5
@Mike Rolling_mean () устарела, но теперь вы можете использовать Rolling и Mean отдельно: df.rolling(windowsize).mean()теперь работает вместо (очень быстро, я мог бы добавить). для 6000 рядов строк %timeit test1.rolling(20).mean()возвращено 1000 циклов, лучшее из 3: 1,16 мс на цикл
Vlox

5
@Vlox df.rolling()работает достаточно хорошо, проблема в том, что даже эта форма не будет поддерживать ndarrays в будущем. Чтобы использовать его, нам нужно сначала загрузить наши данные в Pandas Dataframe. Я хотел бы видеть эту функцию добавляемой либо numpyили scipy.signal.
Майк

1
@ Майк полностью согласен. В частности, я изо всех сил пытаюсь подобрать скорость панд .ewm (). Mean () для моих собственных массивов (вместо того, чтобы сначала загружать их в df). Я имею в виду, это здорово, что это быстро, но просто чувствует себя немного неуклюже, слишком часто входя и выходя из кадров данных.
Vlox

6
%timeit bottleneck.move_mean(x, N)в 3-15 раз быстрее, чем методы cumsum и pandas на моем компьютере. Взгляните на их тест в README .
МАБ

50

Вы можете рассчитать скользящее среднее с помощью:

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

Но это медленно.

К счастью, numpy включает в себя функцию сверток, которую мы можем использовать для ускорения работы. Промежуточное среднее эквивалентно свертыванию xс Nдлинным вектором , причем все члены равны 1/N. Реализация NumPy свертки включает в себя начальный переходный процесс, поэтому вы должны удалить первые N-1 точек:

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

На моей машине быстрая версия работает в 20-30 раз быстрее, в зависимости от длины входного вектора и размера окна усреднения.

Обратите внимание, что в Convolve есть 'same'режим, который, похоже, должен решать начальную временную проблему, но разделяет его между началом и концом.


Обратите внимание, что удаление первых N-1 точек все еще оставляет граничный эффект в последних точках. Простой способ решить проблему заключается в использовании mode='valid'в convolveкоторых не требует последующей обработки.
ляпис

1
@Psycho - mode='valid'удаляет переходный процесс с обоих концов, верно? Если len(x)=10и N=4для среднего значения я бы хотел 10 результатов, но validвернул бы 7.
mtrw

1
Он удаляет переходный процесс с конца, а у начала его нет. Ну, я полагаю, это вопрос приоритетов, мне не нужно такое же количество результатов за счет достижения наклона к нулю, которого нет в данных. Кстати, вот команда, чтобы показать разницу между режимами: 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')(с импортированным pyplot и numpy).
ляпис

runningMeanПобочный эффект усреднения с нулями, когда вы выходите из массива с x[ctr:(ctr+N)]правой стороны массива.
mrgloom

runningMeanFastтакже есть эта проблема пограничного эффекта.
mrgloom

22

или модуль для Python, который рассчитывает

в моих тестах на Tradewave.net TA-lib всегда побеждает:

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

полученные результаты:

[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

введите описание изображения здесь


NameError: name 'info' is not defined, Я получаю эту ошибку, сэр.
Г-жа Резванул Хак

1
Похоже, у вас временные ряды сдвинуты после сглаживания, это желаемый эффект?
mrgloom

@mrgloom да, для целей визуализации; иначе они будут отображаться в виде одной линии на графике; Md. Rezwanul Haque вы можете удалить все ссылки на PAIR и информацию; это были внутренние методы песочницы для уже не существующего tradewave.net
litepresence

21

Готовое решение см. По адресу https://scipy-cookbook.readthedocs.io/items/SignalSmooth.html . Это обеспечивает скользящее среднее с flatтипом окна. Обратите внимание, что это немного сложнее, чем простой метод «сделай сам», поскольку он пытается обработать проблемы в начале и конце данных, отражая их (что может или не может работать в вашем случае. ..).

Для начала вы можете попробовать:

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

1
Этот метод основан на numpy.convolveразнице только в изменении последовательности.
Alleo

10
Меня всегда раздражает функция обработки сигналов, которая возвращает выходные сигналы другой формы, чем входные сигналы, когда оба входа и выхода имеют одинаковую природу (например, оба временных сигнала). Это нарушает соответствие связанной независимой переменной (например, время, частота), делая построение или сравнение не прямым делом ... в любом случае, если вы разделяете это чувство, вы можете изменить последние строки предложенной функции как y = np .convolve (ж / w.sum (), S, режим = 'равно'); return y [window_len-1 :-( window_len-1)]
Кристиан О'Рейли

@ ChristianO'Reilly, вы должны опубликовать это как отдельный ответ - это именно то, что я искал, поскольку у меня действительно есть два других массива, которые должны соответствовать длинам сглаженных данных, для построения графиков и т. Д. Я хотел бы знать как именно вы это сделали - wразмер окна и sданные?
Демис

@ Демис Рад, что комментарий помог. Более подробную информацию о функции NumPy Convolve можно найти здесь docs.scipy.org/doc/numpy-1.15.0/reference/generated/… Функция свертки ( en.wikipedia.org/wiki/Convolution ) сворачивает два сигнала друг с другом. В этом случае он сворачивает ваш сигнал (и) с нормализованным окном (т. Е. Унитарной областью) (w / w.sum ()).
Кристиан О'Рейли

21

Вы можете использовать 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:

  • дает результат с той же формой NumPy (т.е. количество точек)
  • позволяет несколько способов обрабатывать границу, где 'reflect'по умолчанию, но в моем случае, я скорее хотел'nearest'

Это также довольно быстро (почти в 50 раз быстрее np.convolveи в 2-5 раз быстрее, чем описанный выше подход к суммированию ):

%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

Вот 3 функции, которые позволяют сравнивать ошибки / скорости различных реализаций:

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
Это единственный ответ, который, кажется, учитывает пограничные проблемы (довольно важный, особенно при построении графика). Спасибо!
Габриэль

1
я профилирован uniform_filter1d, np.convolveс прямоугольником, а np.cumsumзатем np.subtract. мои результаты: (1.) Свернуть это самый медленный. (2.) сумма / вычитание примерно в 20-30 раз быстрее. (3. )iform_filter1d примерно в 2-3 раза быстрее, чем сумма / вычитание. Победитель определенно --iform_filter1d.
Тревор Бойд Смит

используя uniform_filter1dэто быстрее , чем cumsumраствор (примерно 2-5x). и uniform_filter1d не получает массивную ошибку с плавающей запятой, какcumsum решение.
Тревор Бойд Смит

15

Я знаю, что это старый вопрос, но вот решение, которое не использует никаких дополнительных структур данных или библиотек. Он линейен по количеству элементов входного списка, и я не могу придумать какой-либо другой способ сделать его более эффективным (на самом деле, если кто-то знает, как лучше распределить результат, пожалуйста, дайте мне знать).

ПРИМЕЧАНИЕ: это было бы намного быстрее, если бы использовать пустой массив вместо списка, но я хотел устранить все зависимости. Также было бы возможно улучшить производительность многопоточным выполнением

Функция предполагает, что входной список является одномерным, поэтому будьте осторожны.

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

пример

Предположим, у нас есть список, data = [ 1, 2, 3, 4, 5, 6 ]по которому мы хотим вычислить скользящее среднее с периодом 3, и что вам также нужен выходной список того же размера, что и входной (это чаще всего так).

Первый элемент имеет индекс 0, поэтому скользящее среднее следует вычислять для элементов с индексами -2, -1 и 0. Очевидно, у нас нет данных [-2] и данных [-1] (если вы не хотите использовать специальные граничные условия), поэтому мы предполагаем, что эти элементы равны 0. Это эквивалентно заполнению нулями списка, за исключением того, что мы фактически не дополняем его, просто следим за индексами, которые требуют заполнения (от 0 до N-1).

Итак, для первых N элементов мы просто продолжаем складывать элементы в аккумуляторе.

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

Из элементов N + 1 вперёд простое накопление не работает. мы ожидаем, result[3] = (2 + 3 + 4)/3 = 3но это отличается от (sum + 4)/3 = 3.333.

Способ вычислить правильное значение вычесть data[0] = 1из sum+4, таким образом давая sum + 4 - 1 = 9.

Это происходит потому sum = data[0] + data[1] + data[2], что в настоящее время , но это также верно для каждого, i >= Nпотому что, перед вычитанием, sumесть data[i-N] + ... + data[i-2] + data[i-1].


12

Я чувствую, что это может быть элегантно решено с помощью узкого места

См основной образец ниже:

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)
  • «мм» - это скользящее среднее для «а».

  • «Окно» - это максимальное количество записей, которые нужно учитывать для скользящего среднего.

  • «min_count» - это минимальное количество записей, которое нужно учитывать для скользящего среднего (например, для первых нескольких элементов или если массив имеет значения nan).

Хорошая часть заключается в том, что «Узкое место» помогает справиться со значениями наночастиц, а также очень эффективно.


Эта библиотека действительно быстрая. Чистая функция скользящего среднего Python работает медленно. Bootleneck - это библиотека PyData, которая, на мой взгляд, стабильна и может получить постоянную поддержку сообщества Python, так почему бы не использовать ее?
GoingMyWay

6

Я еще не проверил, насколько это быстро, но вы можете попробовать:

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
Это то, что я собирался сделать. Может ли кто-нибудь критиковать, почему это плохой путь?
staggart

1
Это простое решение на Python хорошо сработало, не требуя ничего. Я закончил тем, что свернул это в класс для повторного использования.
Мэтью Чиегг

6

Этот ответ содержит решения, использующие стандартную библиотеку Python для трех различных сценариев.


Скользящее среднее с itertools.accumulate

Это решение Python 3.2+ с эффективным использованием памяти, вычисляющее скользящее среднее по многократным значениям путем использования itertools.accumulate.

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

Обратите внимание, что values может быть любая итерация, включая генераторы или любой другой объект, который создает значения на лету.

Сначала лениво построим накопленную сумму значений.

>>> cumu_sum = accumulate(value_stream)

Далее enumerateкумулятивную сумму (начиная с 1) и построение генератора, который выдает долю накопленных значений и текущий индекс перечисления.

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

Вы можете выдавать, means = list(rolling_avg)если вам нужны все значения в памяти одновременно или вызывать nextпошагово.
(Конечно, вы можете также перебирать rolling_avgс forпетлей, которая будет вызывать nextнеявно.)

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

Это решение можно записать в виде функции следующим образом.

from itertools import accumulate

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

Сопрограммный , к которому вы можете отправить значение в любое время

Эта сопрограмма использует значения, которые вы ей отправляете, и хранит скользящее среднее от значений, которые вы видели до сих пор.

Это полезно, когда у вас нет итерируемых значений, но вы хотите получить значения, которые будут усредняться по одному в разное время на протяжении всей жизни вашей программы.

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
        

Сопрограмма работает так:

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

Вычисление среднего по скользящему окну размера N

Эта функция-генератор принимает итеративный размер окна N и выдает среднее значение по текущим значениям внутри окна. Он использует структуру данных deque, похожую на список, но оптимизированную для быстрых изменений ( pop, append) на обеих конечных точках .

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)
        

Вот функция в действии:

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

Немного опоздал на вечеринку, но я сделал свою маленькую функцию, которая НЕ обматывает концы или площадки нулями, которые затем используются для определения среднего значения. Еще одно преимущество заключается в том, что он также повторно дискретизирует сигнал в линейно разнесенных точках. Настройте код по желанию, чтобы получить другие функции.

Метод представляет собой простое матричное умножение с нормализованным ядром Гаусса.

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

Простое использование синусоидального сигнала с добавлением нормального распределенного шума: введите описание изображения здесь


Это не работает для меня (Python 3.6). 1 Там нет функции с именем sum, используя np.sumвместо 2 The @оператора (понятия не имею , что это такое) выдает сообщение об ошибке. Я могу рассмотреть это позже, но мне не хватает времени прямо сейчас
Бастиан

Оператор @умножения матриц, который реализует np.matmul . Проверьте, является ли ваш y_inмассив пустым, это может быть проблемой.
xyzzyqed

5

Вместо того, чтобы болтать или скупиться, я бы порекомендовал пандам сделать это быстрее:

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

Для этого берется скользящее среднее (MA) из 3 периодов столбца «данные». Вы также можете рассчитать смещенные версии, например, ту, которая исключает текущую ячейку (смещенную на одну назад), можно легко вычислить как:

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


2
Решение, предложенное в 2016 году, использует pandas.rolling_meanпока мое pandas.DataFrame.rolling. Вы также можете легко рассчитать перемещение min(), max(), sum()и т. Д., А также mean()с этим методом.
Gursel Karacor

В первом случае вам нужно использовать другой метод, например, pandas.rolling_min, pandas.rolling_maxи т. Д. Они похожи, но отличаются.
Gursel Karacor

4

В одном из ответов выше есть комментарий от mab, в котором есть этот метод. имеет простое скользящее среднее:bottleneckmove_mean

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это удобный параметр, который в основном поднимает скользящее среднее до этой точки в вашем массиве. Если вы не установите min_count, он будет равен window, и все до windowочков будет nan.


3

Еще один подход к поиску скользящей средней без использования numpy, панда

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

напечатает [2.0, 4.0, 6.0, 6.5, 7.4, 7.833333333333333]


itertools.accumulate не существует в python 2.7, но существует в python 3.4
grayaii

3

Этот вопрос теперь даже старше, чем когда NeXuS писал об этом в прошлом месяце, НО мне нравится, как его код обрабатывает крайние случаи. Однако, поскольку это «простая скользящая средняя», его результаты отстают от данных, к которым они применяются. Я думал, что работа с крайними случаями более удовлетворительна, чем режимы NumPy valid, sameи fullможет быть достигнута путем применения аналогичного подхода кconvolution() основанному методу.

Мой вклад использует центральное скользящее среднее, чтобы привести результаты в соответствие с их данными. Если для полноразмерного окна доступно слишком мало точек, средние значения вычисляются из последовательно меньших окон по краям массива. [На самом деле, из последовательно увеличивающихся окон, но это деталь реализации.]

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

Он сравнительно медленный, потому что он использует convolve(), и, вероятно, может быть подкреплен истинным Pythonista, однако, я считаю, что идея стоит.


3

Есть много ответов выше о вычислении среднего значения. Мой ответ добавляет две дополнительные функции:

  1. игнорирует значения нан
  2. вычисляет среднее значение для N соседних значений, НЕ включая значение самого интереса

Эта вторая особенность особенно полезна для определения того, какие значения отличаются от общей тенденции на определенную величину.

Я использую numpy.cumsum, так как это наиболее эффективный по времени метод ( см. Ответ Аллео выше ).

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)

Этот код работает только для четных Ns. Его можно настроить для нечетных чисел, изменив np.insert из padded_x и n_nan.

Пример вывода (raw в черном, movavg в синем): необработанные данные (черные) и скользящая средняя (синие) из 10 точек вокруг каждого значения, не включая это значение.  значения nan игнорируются.

Этот код может быть легко адаптирован для удаления всех значений скользящих средних, рассчитанных из меньшего, чем значения отсечки = 3 не-нанона.

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)

необработанные данные (черный цвет) и скользящее среднее (синий цвет) при игнорировании любого окна с менее чем 3 значениями, отличными от нан


2

Использовать только стандартную библиотеку Python (эффективная память)

Просто дайте другую версию использования только стандартной библиотеки deque. Для меня довольно удивительно, что большинство ответов используют pandasили 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]

На самом деле я нашел другую реализацию в документации 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

Однако реализация кажется мне более сложной, чем должна быть. Но по какой-то причине это должно быть в стандартных документах на Python, может кто-нибудь прокомментировать реализацию моей и стандартной документации?


2
Одно большое отличие заключается в том, что вы продолжаете суммировать элементы окна на каждой итерации, и они эффективно обновляют сумму (удалите один элемент и добавьте другой). с точки зрения сложности вы делаете O(n*d) вычисления ( будь dто размер окна, nразмер повторяемого), и они делаютO(n)
Ифтах

@ Ифта, хорошо, спасибо за объяснение, ты прав.
MaThMaX

2

С переменными @ Aikude я написал однострочник.

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

Хотя здесь есть решения для этого вопроса, пожалуйста, посмотрите на мое решение. Это очень просто и работает хорошо.

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

Прочитав другие ответы, я не думаю, что это то, о чем спрашивал вопрос, но я пришел сюда с необходимостью сохранять скользящее среднее значение списка значений, размер которого увеличивался.

Поэтому, если вы хотите сохранить список значений, которые вы получаете откуда-то (сайт, измерительное устройство и т. Д.) И среднее значение последних nобновленных значений, вы можете использовать приведенный ниже код, который минимизирует усилия по добавлению новых элементы:

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)

И вы можете проверить это, например:

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

Который дает:

Значения и их среднее значение как функция значений #


1

Другое решение, использующее стандартную библиотеку и 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

В образовательных целях позвольте мне добавить еще два решения Numpy (которые работают медленнее, чем решение 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

Используемые функции: as_strided , add.reduceat


1

Все вышеупомянутые решения плохие, потому что им не хватает

  • скорость благодаря родному питону вместо простой векторизованной реализации,
  • численная стабильность из-за плохого использования numpy.cumsum или
  • скорость из-за O(len(x) * w)реализаций в виде сверток.

Дано

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

Обратите внимание, что x_[:w].sum()равно x[:w-1].sum(). Таким образом , в первом среднем numpy.cumsum(...)добавляет x[w] / w(через x_[w+1] / w), и вычитает 0(изx_[0] / w ). Это приводит кx[0:w].mean()

Через cumsum вы обновите второе среднее, дополнительно сложив x[w+1] / wи вычтя x[0] / w, в результате чегоx[1:w+1].mean() .

Это продолжается до тех пор, пока не x[-w:].mean()будет достигнуто.

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

Это решение векторизовано O(m), читабельно и численно устойчиво.


1

Как насчет фильтра скользящей средней ? Он также является однострочным и имеет то преимущество, что вы можете легко манипулировать типом окна, если вам нужно что-то еще, кроме прямоугольника, т.е. N-длинная простая скользящая средняя массива a:

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

И с примененным треугольным окном:

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

Примечание: я обычно отбрасываю первые N сэмплов как фиктивные, следовательно, [N:]в конце, но это не является обязательным и является вопросом личного выбора.


-7

Если вы решили использовать собственную библиотеку, а не использовать существующую библиотеку, помните об ошибке с плавающей запятой и постарайтесь свести к минимуму ее последствия:

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

Если все ваши значения примерно одинакового порядка, это поможет сохранить точность, всегда добавляя значения примерно одинаковых величин.


15
Это ужасно неясный ответ, по крайней мере, некоторые комментарии в коде или объяснение того, почему это помогает с плавающей запятой.
Гейб

В своем последнем предложении я пытался указать, почему это помогает с плавающей запятой. Если два значения примерно одинакового порядка, то их добавление теряет меньшую точность, чем если бы вы добавили очень большое число к очень маленькому. Код объединяет «смежные» значения таким образом, что даже промежуточные суммы всегда должны быть достаточно близки по величине, чтобы минимизировать ошибку с плавающей запятой. Ничто не является надежным, но этот метод спас пару очень плохо реализованных проектов в производстве.
Майур Патель

1. Применительно к исходной задаче это будет ужасно медленно (вычисление среднего значения), так что это просто не имеет значения 2. Чтобы страдать от проблемы точности 64-битных чисел, нужно суммировать >> 2 ^ 30 из почти равные числа.
Alleo

@Alleo: вместо одного сложения на значение, вы будете делать два. Доказательство аналогично проблеме переворачивания битов. Тем не менее, смысл этого ответа не обязательно производительность, но точность. Использование памяти для усреднения 64-битных значений не должно превышать 64 элементов в кеше, поэтому это удобно и для использования памяти.
Mayur Patel

Да, вы правы, что для этого требуется в 2 раза больше операций, чем для простого суммирования, но первоначальная проблема заключается в среднем вычислении , а не просто сумме. Что можно сделать в O (n), но ваш ответ требует O (mn), где m - размер окна.
Alleo
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.