Скользящее окно формы M-by-N numpy.ndarray

У меня есть массив формы Numpy (6,2):

[[ 0, 1],
 [10,11],
 [20,21],
 [30,31],
 [40,41],
 [50,51]]

Мне нужно скользящее окно с размером шага 1 и размером окна 3 вот так:

[[ 0, 1,10,11,20,21],
 [10,11,20,21,30,31],
 [20,21,30,31,40,41],
 [30,31,40,41,50,51]]

Я ищу решение Numpy. Если бы ваше решение могло параметризовать форму исходного массива, а также размер окна и размер шага, это было бы здорово.


Я нашел этот связанный ответ Использование шагов для эффективного фильтра скользящего среднего но я не вижу, как указать там размер шага и как свернуть окно из 3d в непрерывный 2d-массив. Также этот итератор скользящего или скользящего окна? но это в Python, и я не уверен, насколько это эффективно. Кроме того, он поддерживает элементы, но не объединяет их в конце, если каждый элемент имеет несколько функций.


person siamii    schedule 30.03.2013    source источник
comment
gist.github.com/seberg/3866040 Многомерное прокручивающееся окно для numpy   -  person wyx    schedule 31.05.2017
comment
я изменил заголовок, чтобы было ясно, что это не дубликат stackoverflow.com/q/13728392/52074   -  person Trevor Boyd Smith    schedule 24.04.2019


Ответы (8)


In [1]: import numpy as np

In [2]: a = np.array([[00,01], [10,11], [20,21], [30,31], [40,41], [50,51]])

In [3]: w = np.hstack((a[:-2],a[1:-1],a[2:]))

In [4]: w
Out[4]: 
array([[ 0,  1, 10, 11, 20, 21],
       [10, 11, 20, 21, 30, 31],
       [20, 21, 30, 31, 40, 41],
       [30, 31, 40, 41, 50, 51]])

Вы можете написать это как функцию так:

def window_stack(a, stepsize=1, width=3):
    n = a.shape[0]
    return np.hstack( a[i:1+n+i-width:stepsize] for i in range(0,width) )

На самом деле это не зависит от формы исходного массива, пока a.ndim = 2. Обратите внимание, что я никогда не использую ни одну из длин в интерактивной версии. Второе измерение формы не имеет значения; каждая строка может быть сколь угодно длинной. Благодаря предложению @Jaime вы можете сделать это, вообще не проверяя форму:

def window_stack(a, stepsize=1, width=3):
    return np.hstack( a[i:1+i-width or None:stepsize] for i in range(0,width) )
person askewchan    schedule 30.03.2013
comment
Починил это. У меня там был +1, но я удалил его в другом редактировании. Добавлен комментарий, связанный с этим. - person askewchan; 30.03.2013
comment
Это не работает с размером шага › 1. В любом случае, большинству людей нужен только размер шага 1, так что этого достаточно. Я просто удаляю это как параметр - person siamii; 30.03.2013
comment
Для [:-i] неработающей вещи я видел [:-i or None] подержанную. - person Jaime; 31.03.2013
comment
что если a.ndim = 1? есть общий подход? - person leoschet; 04.09.2018
comment
@leoschet, он должен работать как есть, но он будет интерпретировать a как одну строку, и я полагаю, вы хотите, чтобы он вел себя как один столбец. Быстрое решение — сделать его столбцом с a[:, None] или a.reshape(-1, 1). Но на самом деле лучшим решением является ответ индексатора. - person askewchan; 11.09.2018
comment
точно, мое решение состояло в том, чтобы переключиться между hstack и vstack, я проверю ваше решение! - person leoschet; 12.09.2018
comment
@askewchan любую версию без использования np? - person loretoparisi; 17.05.2019
comment
@loretoparisi, это должно работать без особых изменений: начните с замены вызова на np.hstack( ... ) и понимания списка: [ ... ]. Вам может понадобиться zip, если вам нужно его транспонировать. - person askewchan; 24.06.2019
comment
Теперь этот код выдает FutureWarning: arrays to stack must be passed as a "sequence" type such as list or tuple. Support for non-sequence iterables such as generators is deprecated as of NumPy 1.16 and will raise an error in the future. Аргумент np.hstack следует заключить в квадратные скобки. - person Björn Lindqvist; 06.09.2019

Вы можете сделать векторизованное скользящее окно в numpy, используя причудливую индексацию.

>>> import numpy as np

>>> a = np.array([[00,01], [10,11], [20,21], [30,31], [40,41], [50,51]])

>>> a
array([[ 0,  1],
       [10, 11],
       [20, 21],                      #define our 2d numpy array
       [30, 31],
       [40, 41],
       [50, 51]])

>>> a = a.flatten()

>>> a
array([ 0,  1, 10, 11, 20, 21, 30, 31, 40, 41, 50, 51])    #flattened numpy array

>>> indexer = np.arange(6)[None, :] + 2*np.arange(4)[:, None]

>>> indexer
array([[ 0,  1,  2,  3,  4,  5],
       [ 2,  3,  4,  5,  6,  7],            #sliding window indices
       [ 4,  5,  6,  7,  8,  9],
       [ 6,  7,  8,  9, 10, 11]])

>>> a[indexer]
array([[ 0,  1, 10, 11, 20, 21],
       [10, 11, 20, 21, 30, 31],            #values of a over sliding window
       [20, 21, 30, 31, 40, 41],
       [30, 31, 40, 41, 50, 51]])

>>> np.sum(a[indexer], axis=1)
array([ 63, 123, 183, 243])         #sum of values in 'a' under the sliding window.

Объяснение того, что делает этот код.

np.arange(6)[None, :] создает вектор-строку от 0 до 6, а np.arange(4)[:, None] создает вектор-столбец от 0 до 4. Это приводит к матрице 4x6, где каждая строка (шесть из них) представляет окно, а количество строк (четыре из них) представляет собой количество окон. Число, кратное 2, заставляет скользящее окно перемещаться на 2 единицы за раз, что необходимо для перемещения по каждому кортежу. Используя нарезку массива numpy, вы можете передать скользящее окно в сглаженный массив numpy и выполнять на них агрегаты, такие как сумма.

person user42541    schedule 15.02.2017
comment
Это должен быть правильный ответ. Я хотел бы дать вам больше голосов. - person Mad Physicist; 18.08.2017
comment
Можно также написать indexer = np.arange(6).reshape(1, -1) + 2 * np.arange(4).reshape(-1, 1) ... Я нашел это более знакомым, чем обозначение [None, :]. - person Elias Strehle; 16.05.2018

Одно решение

np.lib.stride_tricks.as_strided(a, shape=(4,6), strides=(8,4)).

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

Метод as_strided() имеет 3 аргумента.

  1. данные
  2. форма
  3. успехи

data — это массив, с которым мы будем работать.

Чтобы использовать as_strided() для реализации функций скользящего окна, мы должны заранее вычислить форму вывода. В вопросе (4,6) - это форма вывода. Если размеры неверны, мы в конечном итоге читаем мусорные значения. Это потому, что мы обращаемся к данным, перемещая указатель на пару байтов (в зависимости от типа данных).

Определение правильного значения strides необходимо для получения ожидаемых результатов. Перед вычислением шагов узнайте, сколько памяти занимает каждый элемент, используя arr.strides[-1]. В этом примере память, занимаемая одним элементом, составляет 4 байта. Массивы Numpy создаются в основном в виде строк. Первый элемент следующей строки находится рядом с последним элементом текущей строки.

Ex:

0 , 1 | 10, 11 | ...

10 рядом с 1.

Представьте себе двумерный массив, преобразованный в одномерный (это допустимо, поскольку данные хранятся в формате строк). Первый элемент каждой строки в выходных данных является нечетным индексированным элементом в массиве 1D.

0, 10, 20, 30, ..

Следовательно, количество шагов в памяти, которое нам нужно сделать, чтобы перейти от 0 к 10, от 10 к 20 и так далее, равно 2 * mem size of element. Каждая строка имеет шаг 2 * 4bytes = 8. Для данной строки в выходных данных все элементы находятся рядом друг с другом в нашем воображаемом одномерном массиве. Чтобы получить следующий элемент в строке, просто сделайте один шаг, равный размеру элемента. Значение шага столбца составляет 4 байта.

Следовательно, strides=(8,4)

Альтернативное объяснение: вывод имеет вид (4,6). Шаг столбца 4. Таким образом, элементы первой строки начинаются с индекса 0 и состоят из 6 элементов, отстоящих друг от друга на 4 байта. После того, как первая строка собрана, вторая строка начинается в 8 байтах от начала текущей строки. Третья строка начинается в 8 байтах от начальной точки второй строки и так далее.

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

person pbskumar    schedule 13.09.2017
comment
Обратите внимание, что если вы опустите 3-й аргумент, то значение strides будет взято из массива, который вы передаете в качестве первого аргумента. Это избавит вас от необходимости разбираться в этом самостоятельно. - person Martijn Pieters; 11.12.2018

Понимание короткого списка возможно с помощью more_itertools.windowed1:

Дано

import numpy as np
import more_itertools as mit


a = [["00","01"],
     ["10","11"],
     ["20","21"],
     ["30","31"],
     ["40","41"],
     ["50","51"]]

b = np.array(a)

Код

np.array([list(mit.flatten(w)) for w in mit.windowed(a, n=3)])

or

np.array([[i for item in w for i in item] for w in mit.windowed(a, n=3)])

or

np.array(list(mit.windowed(b.ravel(), n=6)))

Вывод

array([['00', '01', '10', '11', '20', '21'],
       ['10', '11', '20', '21', '30', '31'],
       ['20', '21', '30', '31', '40', '41'],
       ['30', '31', '40', '41', '50', '51']], 
      dtype='<U2')

Скользящие окна размером n=3 создаются и сглаживаются. Обратите внимание, что размер шага по умолчанию равен more_itertools.windowed(..., step=1).


Производительность

В качестве массива принятый ответ является самым быстрым.

%timeit np.hstack((a[:-2], a[1:-1], a[2:]))
# 37.5 µs ± 1.88 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit np.hstack((b[:-2], b[1:-1], b[2:]))
# 12.9 µs ± 166 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit np.array([list(mit.flatten(w)) for w in mit.windowed(a, n=3)])
# 23.2 µs ± 1.73 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit np.array([[i for item in w for i in item] for w in mit.windowed(a, n=3)])
# 21.2 µs ± 999 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit np.array(list(mit.windowed(b.ravel(), n=6)))
# 43.4 µs ± 374 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Сторонняя библиотека, реализующая рецепты itertool и множество полезных инструментов.

person pylang    schedule 12.10.2017

Начиная с Numpy 1.20, используя новый sliding_window_view, чтобы скользить/прокручивать окна элементов и основываться на той же идее, что и user42541 answer, мы можем сделать:

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

# values = np.array([[0,1], [10,11], [20,21], [30,31], [40,41], [50,51]])
sliding_window_view(values.flatten(), window_shape = 2*3)[::2]
# array([[ 0,  1, 10, 11, 20, 21],
#        [10, 11, 20, 21, 30, 31],
#        [20, 21, 30, 31, 40, 41],
#        [30, 31, 40, 41, 50, 51]])

где 2 — размер подмассивов, а 3 — размер окна.


Детали промежуточных шагов:

# values = np.array([[0,1], [10,11], [20,21], [30,31], [40,41], [50,51]])

# Flatten the array (concatenate sub-arrays):
values.flatten()
# array([ 0,  1, 10, 11, 20, 21, 30, 31, 40, 41, 50, 51])

# Slide through windows of size 2*3=6:
sliding_window_view(values.flatten(), 2*3)
# array([[ 0,  1, 10, 11, 20, 21],
#        [ 1, 10, 11, 20, 21, 30],
#        [10, 11, 20, 21, 30, 31],
#        [11, 20, 21, 30, 31, 40],
#        [20, 21, 30, 31, 40, 41],
#        [21, 30, 31, 40, 41, 50],
#        [30, 31, 40, 41, 50, 51]])

# Only keep even rows (1 row in 2 - if sub-arrays have a size of x, then replace 2 with x):
sliding_window_view(values.flatten(), 2*3)[::2]
# array([[ 0,  1, 10, 11, 20, 21],
#        [10, 11, 20, 21, 30, 31],
#        [20, 21, 30, 31, 40, 41],
#        [30, 31, 40, 41, 50, 51]])
person Xavier Guihot    schedule 25.12.2020

Начиная с версии NumPy 1.20.0 это можно сделать с помощью

np.lib.stride_tricks.sliding_window_view(arr, winsize)

Пример:

>>> arr = np.arange(0, 9).reshape((3, 3))
>>> np.lib.stride_tricks.sliding_window_view(arr, (2, 2))

array([[[[0, 1],
         [3, 4]],

        [[1, 2],
         [4, 5]]],


       [[[3, 4],
         [6, 7]],

        [[4, 5],
         [7, 8]]]])

Подробнее об этом можно прочитать здесь.

person Tomergt45    schedule 27.01.2021

Вот однострочный с использованием Numpy ›= v1.17

rowsJoined = 3

splits = np.vstack(np.split(x,np.array([[i, i + rowsJoined] for i in range(x.shape[0] - (rowsJoined - 1))]).reshape(-1))).reshape(-1, rowsJoined * x.shape[1]) 

Контрольная работа

x = np.array([[00,1],
              [10,11],
              [20,21],
              [30,31],
              [40,41],
              [50,51]])

Результат

[[ 0  1 10 11 20 21]
 [10 11 20 21 30 31]
 [20 21 30 31 40 41]
 [30 31 40 41 50 51]]

Проверка производительности на большом массиве

import numpy as np
import time

x = np.array(range(1000)).reshape(-1, 2)
rowsJoined = 3

all_t = 0.
for i in range(1000):
    start_ = time.time()
    np.vstack(
        numpy.split(x,np.array([[i, i + rowsJoined] for i in range(x.shape[0] - (rowsJoined - 1))])
                    .reshape(-1))).reshape(-1, rowsJoined * x.shape[1])
    all_t += time.time() - start_

print('Average Time of 1000 Iterations on Array of Shape '
      '1000 x 2 is: {} Seconds.'.format(all_t/1000.))

Результат производительности

Average Time of 1000 Iterations on Array of Shape 1000 x 2 is: 0.0016909 Seconds.
person Yahya    schedule 03.10.2019

Это чистая реализация Python:

def sliding_window(arr, window=3):
    i = iter(arr)
    a = []
    for e in range(0, window): a.append(next(i))
    yield a
    for e in i:
        a = a[1:] + [e]
        yield a

Пример:

# flatten array
flatten = lambda l: [item for sublist in l for item in sublist]

a = [[0,1], [10,11], [20,21], [30,31], [40,41], [50,51]]
w = sliding_window(a, width=3)
print( list(map(flatten,w)) )

[[0, 1, 10, 11, 20, 21], [10, 11, 20, 21, 30, 31], [20, 21, 30, 31, 40, 41], [30, 31, 40, 41, 50, 51]]

Эталон

import timeit
def benchmark():
  a = [[0,1], [10,11], [20,21], [30,31], [40,41], [50,51]]
  sliding_window(a, width=3)

times = timeit.Timer(benchmark).repeat(3, number=1000)
time_taken = min(times) / 1000
print(time_taken)

1.0944640007437556e-06
person loretoparisi    schedule 17.05.2019