将Python序列(时间序列/数组)拆分为具有重叠的子序列

Mar*_*cia 8 python performance numpy time-series pandas

我需要提取给定窗口的时间序列/数组的所有子序列.例如:

>>> 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]])
Run Code Online (Sandbox Code Playgroud)

迭代序列的朴素方法当然是昂贵的,例如:

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)
Run Code Online (Sandbox Code Playgroud)

我找到了一种更好的方法,通过复制序列,将其移动一个不同的值,直到窗口被覆盖,然后用不同的序列分割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))
Run Code Online (Sandbox Code Playgroud)

我已经看到pandas在pandas.stats.moment模块中包含了几个滚动函数,我猜他们所做的事情在某种程度上类似于子序列问题.该模块中的任何地方,或者熊猫中的其他任何地方都可以提高效率吗?

谢谢!

更新(解决方案):

根据@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)
Run Code Online (Sandbox Code Playgroud)

给定1-D numpy数组,我们首先计算结果数组的形状.我们将在数组的每个位置开始一行,只有最后几个元素的例外,在这些元素的启动它们旁边没有足够的元素来完成窗口.

请参阅本说明中的第一个示例,我们开始的最后一个数字是6,因为从7开始,我们无法创建三个元素的窗口.因此,行数是大小减去窗口加一.列数就是窗口.

接下来,棘手的部分是告诉我们如何用我们刚刚定义的形状填充结果数组.

要做到这一点,我们认为第一个元素将是第一个元素.然后我们需要指定两个值(在两个整数的元组中作为参数的参数strides).这些值指定了我们需要在原始数组中执行的步骤(1-D one)以填充第二个(2-D one).

考虑一个不同的例子,我们想要实现这个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])]
Run Code Online (Sandbox Code Playgroud)

那么,reshape我们的两个维度的步骤将是(1, 3).对于我们的情况,它存在重叠,实际上更简单.当我们向右移动以填充结果数组时,我们从1-D数组中的下一个位置开始,当我们向右移动时,我们再次获得下一个元素,即1-D数组中的1步.所以,步骤将是(1, 1).

最后要注意的是最后一件事.该strides参数不接受,我们使用的"台阶",而是在内存中的字节数.要了解它们,我们可以使用stridesnumpy数组的方法.它返回一个带有步幅的元组(以字节为单位的步骤),每个维度有一个元素.在我们的例子中,我们得到一个1元素元组,我们想要它两次,所以我们有* 2.

np.lib.stride_tricks.as_strided函数使用所描述的方法执行填充而不复制数据,这使得它非常有效.

最后,请注意,此处发布的函数假定为1-D输入数组(与2-D数组不同,其中1个元素为行或列).见输入数组的形状的方法,你应该得到的东西一样(N, ),不(N, 1).这种方法会对后者失败.请注意,@ elyase发布的方法处理二维输入数组(这就是为什么这个版本稍微简单一些).

ely*_*ase 10

这比我的机器中的快速版本快34倍:

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]])
Run Code Online (Sandbox Code Playgroud)

幸得埃里克Rigtorp.