Разбить последовательность Python (временные ряды / массив) на подпоследовательности с перекрытием

мне нужно извлечь все подпоследовательности временного ряда / массива данного окна. Например:

>>> ts = pd.Series([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> window = 3
>>> subsequences(ts, window)
array([[0, 1, 2],
       [1, 2, 3],
       [2, 3, 4],
       [3, 4, 5],
       [4, 5, 6],
       [5, 6, 7],
       [5, 7, 8],
       [6, 8, 9]])

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

def subsequences(ts, window):
    res = []
    for i in range(ts.size - window + 1):
        subts = ts[i:i+window]
        subts.reset_index(drop=True, inplace=True)
        subts.name = None
        res.append(subts)
    return pd.DataFrame(res)

я нашел лучший способ, скопировав последовательность, сдвинув ее на другое значение, пока окно не будет закрыто, и разделив разные последовательности с reshape. Производительность примерно в 100 раз лучше, потому что цикл for повторяет размер окна и не размер последовательности:

def subsequences(ts, window):
    res = []
    for i in range(window):
        subts = ts.shift(-i)[:-(ts.size%window)].reshape((ts.size // window, window))
        res.append(subts)
    return pd.DataFrame(np.concatenate(res, axis=0))

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

спасибо!

ОБНОВЛЕНИЕ (РЕШЕНИЕ):

основываясь на ответе @elyase, для этого конкретного случая есть немного проще реализация, позвольте мне записать ее здесь и объяснить, что она делает:

def subsequences(ts, window):
    shape = (ts.size - window + 1, window)
    strides = ts.strides * 2
    return np.lib.stride_tricks.as_strided(ts, shape=shape, strides=strides)

учитывая 1-D массив numpy, мы сначала вычисляем форму результирующего массива. У нас будет строка, начинающаяся с каждой позиции массива, за исключением последних нескольких элементов, при запуске которых не будет достаточно элементов для завершения окна.

см. на первом примере в этом описании, как последнее число мы начинаем с 6, потому что начиная с 7, мы не можем создать окно из трех элементов. Таким образом, количество строк-это размер минус окно плюс один. Количество столбцов-это просто окно.

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

сделать мы считаем, что первый элемент будет первым. Затем нам нужно указать два значения (в кортеж из двух целых чисел в качестве аргумента к параметру strides). Значения определяют шаги, которые мы должны сделать в исходном массиве (1-D один) заполнить второй (2-D один).

рассмотрим другой пример, где мы хотим использовать тег np.reshape функция, от массива 9 элементов 1-D, к массиву 3x3. Первый элемент заполняет первую позицию, а затем, справа от него, будет следующим в 1-D массиве, поэтому мы перемещаем 1 шаг. Затем, хитрая часть, чтобы заполнить первый элемент второй строки, мы должны сделать 3 шага, от 0 до 4, см.:

>>> original = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>> new = array([[0, 1, 2],
                 [3, 4, 5],
                 [6, 7, 8])]

Итак,reshape, наши шаги для двух измерений были бы (1, 3). Для нашего случая, где он существует, это на самом деле проще. Когда мы перемещаемся вправо, чтобы заполнить результирующий массив, мы начинаем со следующей позиции в 1-D массиве, а когда мы перемещаемся вправо, снова получаем следующий элемент, поэтому 1 шаг, в 1-D массиве. Итак, шаги будут (1, 1).

есть только одна последняя вещь, чтобы отметить. The strides аргумент не принимает" шаги", которые мы использовали, но вместо этого байты в памяти. Чтобы узнать их, мы можем использовать strides метод массивов numpy. Он возвращает кортеж с шагами (шагами в байтах), с одним элементом для каждого измерения. В нашем случае мы получаем кортеж из 1 элемента, и мы хотим его дважды, поэтому у нас есть * 2.

на np.lib.stride_tricks.as_strided функция выполняет заполнение с помощью описанного метода без копирования данных, что делает его довольно эффективным.

наконец, обратите внимание, что функция опубликована здесь предполагается 1 - D входной массив (который отличается от 2-D массива с 1 элементом в виде строки или столбца). См. метод shape входного массива, и вы должны получить что-то вроде (N, ), а не (N, 1). В последнем случае этот метод потерпел бы неудачу. Обратите внимание, что метод, размещенный @elyase, обрабатывает входной массив двух измерений (поэтому эта версия немного проще).

2 ответов


Это 34x быстрее, чем ваша быстрая версия в моей машине:

def rolling_window(a, window):
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

>>> rolling_window(ts.values, 3)
array([[0, 1, 2],
      [1, 2, 3],
      [2, 3, 4],
      [3, 4, 5],
      [4, 5, 6],
      [5, 6, 7],
      [6, 7, 8],
      [7, 8, 9]])

заслуга Ерик Rigtorp.


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

l = np.asarray([1,2,3,4,5,6,7,8,9])
_ = rolling_window(l, 3)
print(_)
array([[1, 2, 3],
   [2, 3, 4],
   [3, 4, 5],
   [4, 5, 6],
   [5, 6, 7],
   [6, 7, 8],
   [7, 8, 9]])

_[0,1] = 1000
print(_)
array([[   1, 1000,    3],
   [1000,    3,    4],
   [   3,    4,    5],
   [   4,    5,    6],
   [   5,    6,    7],
   [   6,    7,    8],
   [   7,    8,    9]])

# create new matrix from original array
xx = pd.DataFrame(rolling_window(l, 3))
# the updated values are still updated
print(xx)
      0     1  2
0     1  1000  3
1  1000     3  4
2     3     4  5
3     4     5  6
4     5     6  7
5     6     7  8
6     7     8  9

# change values in xx changes values in _ and l
xx.loc[0,1] = 100
print(_)
print(l)
[[  1 100   3]
 [100   3   4]
 [  3   4   5]
 [  4   5   6]
 [  5   6   7]
 [  6   7   8]
 [  7   8   9]]
[  1 100   3   4   5   6   7   8   9]

# make a dataframe copy to avoid unintended side effects
new = xx.copy()
# changing values in new won't affect l, _, or xx

любые значения, которые изменяются в xx или _ или l показать в других переменных, потому что все они являются одним и тем же объектом в памяти.

см. документы numpy для получения более подробной информации:numpy.Либ.stride_tricks.as_strided