【问题标题】:Split Python sequence (time series/array) into subsequences with overlap将 Python 序列(时间序列/数组)拆分为重叠的子序列
【发布时间】:2015-03-07 06:55:57
【问题描述】:

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

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

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

谢谢!

更新(解决方案):

基于@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)

给定一维 numpy 数组,我们首先计算结果数组的形状。我们将从数组的每个位置开始有一行,除了最后几个元素,从这些元素开始,接下来没有足够的元素来完成窗口。

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

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

为此,我们认为第一个元素将是第一个。然后我们需要指定两个值(在两个整数的元组中作为参数strides 的参数)。这些值指定了我们需要在原始数组(一维数组)中执行的步骤以填充第二个数组(二维数组)。

考虑一个不同的示例,我们要在其中实现 np.reshape 函数,从 9 个元素的一维数组到 3x3 数组。第一个元素填充第一个位置,然后,它右边的元素将成为一维数组中的下一个元素,因此我们移动 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 步。所以,步骤是(1, 1)

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

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

最后,请注意,此处发布的函数假定一个一维输入数组(这与一个具有 1 个元素作为行或列的二维数组不同)。查看输入数组的 shape 方法,您应该得到类似 (N, ) 而不是 (N, 1) 的内容。这种方法在后者上会失败。注意@elyase 发布的方法处理二维输入数组(这就是为什么这个版本稍微简单一些)。

【问题讨论】:

  • 当你说天真的方法很昂贵时,我假设你实际上已经分析了你的程序,这确实是一个瓶颈?
  • 是的,因为我需要遍历整个序列,所以计算中没有优化,而且速度很慢。对于 4719 个元素的序列和 5 个窗口,大约需要 700 毫秒。对于相同的数据,第二种方法大约需要 8 毫秒。问题是 pandas(或 numpy)是否可以在完全不需要迭代的情况下做到这一点,这应该会更快。
  • 您可能在 codereview.stackexchange.com 上运气更好,我也会将您的时间信息放在问题中

标签: python performance numpy pandas time-series


【解决方案1】:

这比我机器上的快速版本快 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]])

归功于Erik Rigtorp

【讨论】:

  • 非常感谢 elyase!您的解决方案在我的机器上也更快,但看起来大部分收益是因为计算是在 numpy 而不是 pandas 中执行的。如果在您的解决方案中,我将返回的 numpy 数组转换为 pandas DataFrame,则增益约为 10%,与 34 倍相差甚远,但这很好。如果我将我的解决方案转换为 numpy,您的解决方案的性能仍然会更好,但只是稍微好一点。让我让这个问题仍然悬而未决,看看是否还有更快的解决方案。谢谢!
  • 是否可以将其更改为通过N 观察值向前移动,而不是1(如您的答案中所实施)?我玩了一下,但无法让它工作。
  • 嗨@Rhubarb,我玩弄了代码并创建了gist 以反映对上述函数的更改
  • @elyase 请如何使重叠为 50%,我的意思是使步幅等于序列 /2 的长度
【解决方案2】:

值得注意的是,在处理转换后的数组时,步幅技巧可能会产生意想不到的后果。它是高效的,因为它修改了内存指针而不创建原始数组的副本。如果您更新返回数组中的任何值,则会更改原始数组中的值,反之亦然。

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.lib.stride_tricks.as_strided

【讨论】:

    【解决方案3】:

    我想指出,PyTorch 为这个问题提供了一个函数,它在使用 Torch 张量时与当前最佳解决方案一样具有内存效率,但更简单和更通用(即在使用多个维度时) :

    # Import packages
    import torch
    import pandas as pd
    # Create array and set window size
    ts = pd.Series([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    window = 3
    # Create subsequences with converting to/from Tensor
    ts_torch = torch.from_numpy(ts.values)  # convert to torch Tensor
    ss_torch = ts_torch.unfold(0, window, 1) # create subsequences in-memory
    ss_numpy = ss_torch.numpy() # convert Tensor back to numpy (obviously now needs more memory)
    # Or just in a single line:
    ss_numpy = torch.from_numpy(ts.values).unfold(0, window, 1).numpy()
    

    重点是unfold函数,详细解释见PyTorch docs。如果您可以直接使用 PyTorch 张量,则可能不需要转换回 numpy - 在这种情况下,该解决方案与内存效率一样高。在我的用例中,我发现首先使用 Torch 张量创建子序列(并进行其他预处理)更容易,然后在这些张量上使用 .numpy() 在需要时转换为 numpy。

    【讨论】:

      猜你喜欢
      • 2020-05-15
      • 1970-01-01
      • 2021-10-25
      • 1970-01-01
      • 2015-01-08
      • 1970-01-01
      • 1970-01-01
      • 2021-10-16
      相关资源
      最近更新 更多