Max в скользящем окне в массиве NumPy

Я хочу создать массив, который содержит все max()es окна, перемещающегося через заданный массив numpy. Простите, если это звучит странно. Приведу пример. Ввод:

[ 6,4,8,7,1,4,3,5,7,2,4,6,2,1,3,5,6,3,4,7,1,9,4,3,2 ]

мой выход с шириной окна 5 должен быть следующим:

[     8,8,8,7,7,7,7,7,7,6,6,6,6,6,6,7,7,9,9,9,9     ]

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

[ 6,4,8,7,1,4,3,5,7,2,4,6,2,1,3,5,6,3,4,7,1,9,4,3,2 ]
         /                        /
        /                        /
       /                        /
      /                        /
[     8,8,8,7,7,7,7,7,7,6,6,6,6,6,6,7,7,9,9,9,9     ]

Я не нашел функцию вне коробки в numpy, которая сделала бы это (но я не удивлюсь, если был один; я не всегда думаю в терминах, которые думали разработчики numpy). Я рассмотрел возможность создания смещенной 2D-версии моего ввода:

[ [ 6,4,8,7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1 ]
  [ 4,8,7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9 ]
  [ 8,7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9,4 ]
  [ 7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9,4,3 ]
  [ 1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9,4,3,2 ] ]

тогда я мог бы подать заявку np.max(input, 0) на этом и получите мои результаты. Но это не кажется эффективным в моем случае, потому что и мой массив, и моя ширина окна могут быть большими (>1000000 записей и >100000 ширина окна). Данные будут увеличены более или менее в зависимости от ширины окна.

Я также рассмотрел использование np.convolve() в некотором роде, но не мог понять, как достичь своей цели с его помощью.

есть идеи, как это сделать эффективно?

4 ответов


Pandas имеет метод прокатки для серий и кадров данных, и это может быть полезно здесь:

import pandas as pd

lst = [6,4,8,7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9,4,3,2]
lst1 = pd.Series(lst).rolling(5).max().dropna().tolist()

# [8.0, 8.0, 8.0, 7.0, 7.0, 8.0, 8.0, 8.0, 8.0, 8.0, 6.0, 6.0, 6.0, 6.0, 6.0, 7.0, 7.0, 9.0, 9.0, 9.0, 9.0]

для согласованности вы можете принудить каждый элемент lst1 to int:

[int(x) for x in lst1]

# [8, 8, 8, 7, 7, 8, 8, 8, 8, 8, 6, 6, 6, 6, 6, 7, 7, 9, 9, 9, 9]

подход #1 : вы могли бы использовать 1D максимальный фильтр от Scipy -

from scipy.ndimage.filters import maximum_filter1d

def max_filter1d_valid(a, W):
    hW = (W-1)//2 # Half window size
    return maximum_filter1d(a,size=W)[hW:-hW]

подход #2 : вот еще один подход с strides : strided_app создать 2D сдвинутая версия как вид в массив довольно эффективно, и это должно позволить нам использовать любую пользовательскую операцию сокращения вдоль второй оси после этого -

def max_filter1d_valid_strided(a, W):
    return strided_app(a, W, S=1).max(axis=1)

испытаний

In [55]: a = np.random.randint(0,10,(10000))

# @Abdou's solution using pandas rolling
In [56]: %timeit pd.Series(a).rolling(5).max().dropna().tolist()
1000 loops, best of 3: 999 µs per loop

In [57]: %timeit max_filter1d_valid(a, W=5)
    ...: %timeit max_filter1d_valid_strided(a, W=5)
    ...: 
10000 loops, best of 3: 90.5 µs per loop
10000 loops, best of 3: 87.9 µs per loop

прежде всего, я думаю, что в вашем объяснении есть ошибка, потому что 10-й элемент вашего начального массива вменений в начале вашего объяснения равен 8, а ниже, где вы применяете окно, это 2.

после исправления этого, я думаю, что код, который делает то, что вы хотите, является следующее:

import numpy as np
a=np.array([ 6,4,8,7,1,4,3,5,7,8,4,6,2,1,3,5,6,3,4,7,1,9,4,3,2 ])
window=5
for i in range(0,len(a)-window,1): 
    b[i] = np.amax(a[i:i+window])

Я думаю, что этот способ лучше, чем создание сдвинутой 2D-версии вашего imput, потому что при создании такой версии вам нужно использовать гораздо больше память, чем использование исходного массива imput, поэтому у вас может закончиться память, если вход большой.


я попробовал несколько вариантов сейчас и объявил бы версию панды победителем этой гонки производительности. Я попробовал несколько вариантов, даже используя двоичное дерево (реализованное в чистом Python) для быстрого вычисления Максов произвольных поддиапазонов. (Источник доступный по требованию). Лучшим алгоритмом, который я придумал сам, было простое скользящее окно с использованием ringbuffer; максимум этого нужно было полностью пересчитать, если текущее максимальное значение было сброшено с него в этой итерации; в противном случае он останется или увеличится до следующего нового значения. По сравнению со старыми библиотеками, эта реализация pure-Python была быстрее, чем остальные.

В конце концов я обнаружил, что версия рассматриваемых библиотек была очень актуальной. Довольно старые версии, которые я в основном все еще использовал, были намного медленнее, чем современные версии. Вот цифры для 1M чисел, rollingmax'ED с окном размером 100k:

         old (slow HW)           new (better HW)
scipy:   0.9.0:  21.2987391949   0.13.3:  11.5804400444
pandas:  0.7.0:  13.5896410942   0.18.1:   0.0551438331604
numpy:   1.6.1:   1.17417216301  1.8.2:    0.537392139435

вот реализация чисто numpy версия с помощью ringbuffer:

def rollingMax(a, window):
  def eachValue():
    w = a[:window].copy()
    m = w.max()
    yield m
    i = 0
    j = window
    while j < len(a):
      oldValue = w[i]
      newValue = w[i] = a[j]
      if newValue > m:
        m = newValue
      elif oldValue == m:
        m = w.max()
      yield m
      i = (i + 1) % window
      j += 1
  return np.array(list(eachValue()))

для меня это отлично работает, потому что я веду аудио данных с большим количеством пиков во всех направлениях. Если вы помещаете в него постоянно уменьшающийся сигнал (e. г. -np.arange(10000000)), то вы испытаете наихудший случай (и, возможно, вам следует отменить вход и выход в таких случаях).

Я просто включаю это в случае, если кто-то хочет выполнить эту задачу на машине со старыми библиотеками.