将 Python 序列(时间 series/array)拆分为重叠的子序列
Split Python sequence (time series/array) into subsequences with overlap
我需要提取给定 window 的时间 series/array 的所有子序列。例如:
>>> 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)
我找到了一个更好的方法,复制序列,将它移动一个不同的值直到 window 被覆盖,然后用 reshape
拆分不同的序列。性能提高了大约 100 倍,因为 for 循环遍历 window 大小,而不是序列大小:
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 数组,我们首先计算结果数组的形状。我们将从数组的每个位置开始一行,只有最后几个元素除外,从它们开始,旁边没有足够的元素来完成 window.
参见本说明中的第一个示例,我们的最后一个数字是 6,因为从 7 开始,我们无法创建包含三个元素的 window。因此,行数是大小减去 window 加一。列数就是 window.
接下来,棘手的部分是如何使用我们刚刚定义的形状填充结果数组。
我们认为第一个元素将是第一个。然后我们需要指定两个值(在两个整数的元组中作为参数 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
参数不接受我们使用的 "steps" ,而是内存中的字节。要了解它们,我们可以使用numpy数组的strides
方法。它 returns 一个包含步长(以字节为单位的步长)的元组,每个维度都有一个元素。在我们的例子中,我们得到一个 1 元素的元组,我们想要它两次,所以我们有 * 2
.
np.lib.stride_tricks.as_strided
函数使用描述的方法执行填充,无需复制数据,这使得它非常有效。
最后,请注意,此处发布的函数采用一维输入数组(不同于以 1 个元素作为行或列的二维数组)。查看输入数组的形状方法,你应该得到类似 (N, )
而不是 (N, 1)
的东西。这种方法在后者上会失败。请注意,@elyase 发布的方法处理二维输入数组(这就是为什么这个版本稍微简单一些)。
这比我机器上的快速版本快 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。
值得注意的是,在处理转换后的数组时,步幅技巧可能会产生意想不到的后果。它是高效的,因为它修改了内存指针而不创建原始数组的副本。如果您更新返回数组中的任何值,则会更改原始数组中的值,反之亦然。
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
我想指出,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。
我需要提取给定 window 的时间 series/array 的所有子序列。例如:
>>> 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)
我找到了一个更好的方法,复制序列,将它移动一个不同的值直到 window 被覆盖,然后用 reshape
拆分不同的序列。性能提高了大约 100 倍,因为 for 循环遍历 window 大小,而不是序列大小:
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 数组,我们首先计算结果数组的形状。我们将从数组的每个位置开始一行,只有最后几个元素除外,从它们开始,旁边没有足够的元素来完成 window.
参见本说明中的第一个示例,我们的最后一个数字是 6,因为从 7 开始,我们无法创建包含三个元素的 window。因此,行数是大小减去 window 加一。列数就是 window.
接下来,棘手的部分是如何使用我们刚刚定义的形状填充结果数组。
我们认为第一个元素将是第一个。然后我们需要指定两个值(在两个整数的元组中作为参数 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
参数不接受我们使用的 "steps" ,而是内存中的字节。要了解它们,我们可以使用numpy数组的strides
方法。它 returns 一个包含步长(以字节为单位的步长)的元组,每个维度都有一个元素。在我们的例子中,我们得到一个 1 元素的元组,我们想要它两次,所以我们有 * 2
.
np.lib.stride_tricks.as_strided
函数使用描述的方法执行填充,无需复制数据,这使得它非常有效。
最后,请注意,此处发布的函数采用一维输入数组(不同于以 1 个元素作为行或列的二维数组)。查看输入数组的形状方法,你应该得到类似 (N, )
而不是 (N, 1)
的东西。这种方法在后者上会失败。请注意,@elyase 发布的方法处理二维输入数组(这就是为什么这个版本稍微简单一些)。
这比我机器上的快速版本快 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。
值得注意的是,在处理转换后的数组时,步幅技巧可能会产生意想不到的后果。它是高效的,因为它修改了内存指针而不创建原始数组的副本。如果您更新返回数组中的任何值,则会更改原始数组中的值,反之亦然。
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
我想指出,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。