Pandas: 带步长的滚动窗口

创建于 2017-02-09  ·  38评论  ·  资料来源: pandas-dev/pandas

只是一个建议 - 扩展rolling以支持具有步长的滚动窗口,例如 R 的rollapply(by=X)

代码示例

Pandas - 低效的解决方案(将函数应用于每个窗口,然后切片以获得每秒的结果)

import pandas
ts = pandas.Series(range(0, 40, 2))
ts.rolling(5).apply(max).dropna()[::2]

建议:

ts = pandas.Series(range(0, 40, 2))
ts.rolling(window=5, step=2).apply(max).dropna()

受 R 启发(参见rollapply文档):

require(zoo)
TS <- zoo(seq(0, 40, 2))
rollapply(TS, 5, FUN=max, by=2)

8 12 16 20 24 28 32 36 40

Enhancement Needs Discussion Numeric Window

最有用的评论

“这可以做到,但我希望看到一个重要的用例。”

无论我使用 Pandas 从事什么项目,我几乎总是错过这个功能,每当您需要偶尔计算一次应用但仍然需要每个窗口内的良好分辨率时,它都很有用。

所有38条评论

如果您使用“标准”函数,则这些函数是矢量化的,因此速度很快( ts.rolling(5).max().dropna()[::2] )。

IIUC 此处的节省将来自仅在一小部分时间(例如,每 n 个值)应用该函数。 但是有没有一种情况会产生实际差异?

这可以做到,但我想看到一个用例,这很重要。 这也会破坏“返回与输入相同大小”的 API。 虽然我认为这实际上并不难实现(尽管在实现中会涉及许多更改)。 我们使用边际窗口(IOW,计算窗口,并在您前进时放下离开的点并添加您获得的点)。 所以仍然需要计算一切,但你只是不会输出它。

感谢您的回复!

IIUC 此处的节省将来自仅在一小部分时间(例如,每 n 个值)应用该函数。 但是有没有一种情况会产生实际差异?

我的用例是在一些大型时间序列数据帧上运行聚合函数(不仅仅是最大值)——400 列,5-25Hz 的数小时数据。 过去我也做过类似的事情(传感器数据的特征工程),数据高达 20kHz。 以 5 秒的步长运行 30 秒的窗口可以节省大量的处理工作——例如,在 25Hz 的 5 秒步长下,它是工作的 1/125,这使得它在 1 分钟或 2 小时内运行之间存在差异。

我显然可以回退到 numpy,但是如果有更高级别的 API 来执行此操作会很好。 我只是认为值得提出建议,以防其他人也觉得它有用 - 我不希望你为我构建一个功能!

您可以先尝试重新采样到更高的频率间隔然后滚动

就像是

df = df.resample('30s')
df.rolling(..).max() (或任何函数)

@jreback ,感谢您的建议。

如果我只是在我的数据上运行max ,这会起作用(重新采样需要一个缩减函数,否则它默认为mean ,对吧?):

df.resample('1s').max().rolling(30).max()

但是我想在 30 秒的数据上运行我的缩减函数,然后向前移动 1 秒,然后在接下来的 30 秒的数据上运行它,等等。上面的方法对 1 秒的数据应用一个函数,然后另一个对第一个函数的 30 个结果进行函数处理。

这是一个快速示例 - 运行峰峰值计算不能运行两次(显然):

# 10 minutes of data at 5Hz
n = 5 * 60 * 10
rng = pandas.date_range('1/1/2017', periods=n, freq='200ms')
np.random.seed(0)
d = np.cumsum(np.random.randn(n), axis=0)
s = pandas.Series(d, index=rng)

# Peak to peak
def p2p(d):
    return d.max() - d.min()

def p2p_arr(d):
    return d.max(axis=1) - d.min(axis=1)

def rolling_with_step(s, window, step, func):
    # See https://ga7g08.github.io/2015/01/30/Applying-python-functions-in-moving-windows/
    vert_idx_list = np.arange(0, s.size - window, step)
    hori_idx_list = np.arange(window)
    A, B = np.meshgrid(hori_idx_list, vert_idx_list)
    idx_array = A + B
    x_array = s.values[idx_array]
    idx = s.index[vert_idx_list + int(window/2.)]
    d = func(x_array)
    return pandas.Series(d, index=idx)

# Plot data
ax = s.plot(figsize=(12, 8), legend=True, label='Data')

# Plot resample then rolling (obviously does not work)
s.resample('1s').apply(p2p).rolling(window=30, center=True).apply(p2p).plot(ax=ax, label='1s p2p, roll 30 p2p', legend=True)

# Plot rolling window with step
rolling_with_step(s, window=30 * 5, step=5, func=p2p_arr).plot(ax=ax, label='Roll 30, step 1s', legend=True)

rolling window

@alexlouden从您的原始描述中我认为类似

df.resample('5s').max().rolling('30s').mean() (或其他任何减少)更符合您的需求

IOW,取出 5s 垃圾箱中的任何东西,然后将其减少到一个点,然后翻转这些垃圾箱。 这个总的想法是你有很多可以在短时间内汇总的数据,但你实际上希望在更高的层次上滚动。

@jreback ,我实际上想每 5 秒运行一个超过 30 秒数据的函数。 请参阅我之前示例中的 rolling_with_step 函数。 max/mean 的附加步骤不适用于我的用例。

@jreback ,真正需要本次讨论中尚未提出的 step 函数。 我支持@alexlouden描述的所有内容,但我想添加更多用例。

假设我们正在使用大约 3 到 10 毫秒采样的输入数据进行时间序列分析。 我们对频域特征感兴趣。 构建它们的第一步是找出奈奎斯特频率。 假设根据领域知识我们知道是 10 Hz(每 100 毫秒一次)。 这意味着,如果特征应该很好地捕获输入信号,我们需要数据的频率至少为 20 Hz(每 50 ms 一次)。 我们不能重新采样到比这更低的频率。 最终这里是我们做的计算:

df.resample('50ms').mean().rolling(window=32).aggregate(power_spectrum_coeff)

这里我们选择了 8 的倍数的窗口大小,选择 32 使窗口大小为 1.6 秒。 聚合函数返回单边频域系数,没有第一个平均分量(fft 函数是对称的,平均值在第 0 个元素)。 以下是示例聚合函数:

def power_spectrum_coeff():
    def power_spectrum_coeff_(x):
        return np.fft.fft(x)[1 : int(len(x) / 2 + 1)]

    power_spectrum_coeff_.__name__ = 'power_spectrum_coeff'
    return power_spectrum_coeff_

现在,我们想在一个滑动窗口中重复这个,比如每 0.4 秒或每 0.8 秒。 没有必要浪费计算并每 50 毫秒计算一次 FFT,然后再进行切片。 此外,重新采样到 400 ms 也不是一种选择,因为 400 ms 只是 2.5 Hz,远低于奈奎斯特频率,这样做会导致特征中的所有信息丢失。

这是频域特征,它在许多与时间序列相关的科学实验中都有应用。 然而,即使是更简单的时域聚合函数,如标准差,也无法通过重采样得到有效支持。

虽然我认为这实际上并不难实现(尽管在实现中会涉及许多更改)。 我们使用边际窗口(IOW,计算窗口,并随着您前进,减少离开的点并添加您获得的点)。 所以仍然需要计算一切,但你只是不会输出它。

拥有 'step' 参数并能够通过使用它来减少实际计算必须是 Pandas 的未来目标。 如果 step 参数只返回较少的点,则不值得这样做,因为无论如何我们都可以对输出进行切片。 也许考虑到这样做所涉及的工作,我们可能只是推荐所有有这些需求的项目使用 Numpy。

@Murmuria欢迎您提交拉取请求来执行此操作。 其实没那么难。

当我第二次为请求的step参数rolling() ,我想指出的是,有可能获得与期望的结果base参数resample()如果步长是窗口大小的整数部分。 使用@alexlouden的例子:

pandas.concat([
    s.resample('30s', label='left', loffset=pandas.Timedelta(15, unit='s'), base=i).agg(p2p) 
    for i in range(30)
]).sort_index().plot(ax=ax, label='Solution with resample()', legend=True, style='k:')

我们得到相同的结果(请注意,该线在两侧都延长了 30 秒):
rolling_with_step_using_resample

这仍然有点浪费,具体取决于聚合的类型。 对于@alexlouden示例中的峰峰值计算的特殊情况, p2p_arr()速度几乎快了 200 倍,因为它将系列重新排列为二维矩阵,然后使用对max()的单个调用min()

滚动中的 step 参数也允许在没有日期时间索引的情况下使用此功能。 有人已经在研究了吗?

上面的@alexlouden 是这样说的:

我显然可以回退到 numpy,但是如果有更高级别的 API 来执行此操作会很好。

@alexlouden或其他任何知道的人能否分享一些有关如何使用 numpy 执行此操作的见解? 从我迄今为止的研究来看,在 numpy.xml 中执行此操作似乎并非易事。 事实上,这里有一个悬而未决的问题https://github.com/numpy/numpy/issues/7753

谢谢

@tsando - 我上面使用的函数rolling_with_step对你不起作用吗?

@alexlouden谢谢,刚刚检查了该函数,它似乎仍然依赖于https://github.com/numpy/numpy/issues/7753线程中,他们提出了一个使用 numpy strides 的函数,但它们很难理解并转换为窗口和步进输入。

@tsando这是我上面链接的博客文章他的网站。 (我只是在本地运行它以将其转换为 PDF)。

我上面的功能是我只是将他的最后一个示例转换为与 Pandas 一起使用 - 如果您想直接使用 numpy,您可以执行以下操作: https :

希望这可以帮助!

@alexlouden谢谢! 我只是在(13, 1313)形状的数组上尝试过,但它给了我这个错误:

image

“这可以做到,但我希望看到一个重要的用例。”

无论我使用 Pandas 从事什么项目,我几乎总是错过这个功能,每当您需要偶尔计算一次应用但仍然需要每个窗口内的良好分辨率时,它都很有用。

我也同意并支持这个功能

在处理时间序列时几乎每次都需要它,该功能可以更好地控制生成用于可视化和分析的时间序列特征。 强烈支持这个想法!

同意并支持此功能

这对于减少计算时间仍然保持良好的窗口分辨率非常有帮助。

我提供了一个解决方案代码,可以根据您的特定目标进一步调整。

def average_smoothing(signal, kernel_size, stride):
    sample = []
    start = 0
    end = kernel_size
    while end <= len(signal):
        start = start + stride
        end = end + stride
        sample.append(np.mean(signal[start:end]))
    return np.array(sample)

我同意并支持此功能。 我看到现在正在停止运动。

当您有 TB 的数据时,计算然后下采样不是一种选择。

这对我的工作也很有帮助。 我有 TB 的数据,我需要非重叠窗口的各种统计数据来了解当地情况。 我目前的“修复”是创建一个生成器来对数据帧进行切片并生成统计数据。 拥有此功能将非常有帮助。

当涉及时间序列时,这个功能确实是必须的!

同意,当然需要添加此功能。尝试在股票价格之间运行窗口相关性,并且必须为其创建我自己的函数

不敢相信这样的基本功能还不存在!
这个问题什么时候能解决?
谢谢

为“进一步讨论”做出贡献:
我的用例是每小时为一个月的数据计算一个最小/最大/中值,分辨率为 1 秒。 这是能源使用数据,并且有 1-2 秒的峰值,我会因重新采样而丢失。 除此之外,重新采样到例如 5 秒/1 分钟不会改变这样一个事实,即我每天仍然必须计算需要丢弃的 4k/1k 窗口,而不仅仅是能够每天计算所需的 24 个窗口.

可以通过使用 groupby aso 来解决这个问题,但这似乎既不直观,也不像滚动实现那么快(250 万小时的排序窗口为 2 秒)。 它令人印象深刻的快速和有用,但我们真的需要一个 stride 参数来充分利用它的力量。

我看了一下问题。 这是相对微不足道的,但是代码的实现方式,粗略地看,我认为这需要有人手动编辑所有滚动例程。 它们都不尊重索引器类给出的窗口边界。 如果他们这样做了,这个请求和#11704 都会很容易解决。 无论如何,我认为对于任何想要花一些时间整理东西的人来说都是可以管理的。 我发起了一个半生不熟的 PR(预计会被拒绝,只是为了一个 MVP)来展示我将如何解决这个问题。

跑步:

import numpy as np
import pandas as pd

data = pd.Series(
    np.arange(100),
    index=pd.date_range('2020/05/12 12:00:00', '2020/05/12 12:00:10', periods=100))

print('1s rolling window every 2s')
print(data.rolling('1s', step='2s').apply(np.mean))

data.sort_index(ascending=False, inplace=True)

print('1s rolling window every 500ms (and reversed)')
print(data.rolling('1s', step='500ms').apply(np.mean))

产量

1s rolling window every 2s
2020-05-12 12:00:00.000000000     4.5
2020-05-12 12:00:02.020202020    24.5
2020-05-12 12:00:04.040404040    44.5
2020-05-12 12:00:06.060606060    64.5
2020-05-12 12:00:08.080808080    84.5
dtype: float64
1s rolling window every 500ms (and reversed)
2020-05-12 12:00:10.000000000    94.5
2020-05-12 12:00:09.494949494    89.5
2020-05-12 12:00:08.989898989    84.5
2020-05-12 12:00:08.484848484    79.5
2020-05-12 12:00:07.979797979    74.5
2020-05-12 12:00:07.474747474    69.5
2020-05-12 12:00:06.969696969    64.5
2020-05-12 12:00:06.464646464    59.5
2020-05-12 12:00:05.959595959    54.5
2020-05-12 12:00:05.454545454    49.5
2020-05-12 12:00:04.949494949    44.5
2020-05-12 12:00:04.444444444    39.5
2020-05-12 12:00:03.939393939    34.5
2020-05-12 12:00:03.434343434    29.5
2020-05-12 12:00:02.929292929    24.5
2020-05-12 12:00:02.424242424    19.5
2020-05-12 12:00:01.919191919    14.5
2020-05-12 12:00:01.414141414     9.5
2020-05-12 12:00:00.909090909     4.5
dtype: float64

有关实施细节,请查看 PR(或此处:https://github.com/anthonytw/pandas/tree/rolling-window-step)

虽然我希望花更多的时间来完成它,但不幸的是,我没有时间去处理重新设计所有滚动功能的繁重工作。 我对任何想要解决这个问题的人的建议是强制执行索引器类生成的窗口边界并统一滚动_*_固定/变量函数。 对于开始和结束边界,我看不出它们应该不同的任何原因,除非您有一个函数可以对非均匀采样数据执行某些特殊操作(在这种情况下,该特定函数将能够更好地处理细微差别,所以也许设置一个标志什么的)。

这也适用于使用get_window_bounds()方法的自定义窗口吗?

你好,我也提出建议。 这将是一个非常有用的功能。

如果您使用“标准”函数,则这些函数是矢量化的,因此速度很快( ts.rolling(5).max().dropna()[::2] )。

IIUC 此处的节省将来自仅在一小部分时间(例如,每 n 个值)应用该函数。 但是有没有一种情况会产生实际差异?

我这里有这样一个例子: https :

每 Nth 将是每 365th。 窗口大小在程序的生命周期内是可变的,并且不能保证步长是窗口大小的整数部分。

我基本上需要一个设置的窗口大小,它可以按“它正在查看的一年中的天数”步进,这对于我迄今为止为这个问题找到的每个解决方案都是不可能的。

我也有类似的需求与以下上下文(改编自真实和专业的需求):

  • 我有一个按时间顺序排列的数据框,其中有一个时间戳列和一个值列,代表不规则事件。 就像一只狗从我窗下经过的时间以及它经过了多少秒的时间戳。 我可以在给定的一天有 6 个事件,然后在接下来的 2 天内根本没有事件
  • 我想计算一个指标(比如狗在我窗前花费的平均时间),滚动窗口为 365 天,每 30 天滚动一次

据我了解,dataframe.rolling() API 允许我指定 365 天的持续时间,但不需要跳过 30 天的值(这是一个非常数的行)来计算下一个平均值选择 365 天的值。

显然,我期望生成的数据帧的行数将比初始的“狗事件”数据帧少(很多)。

只是为了通过一个简单的例子更清楚地了解这个请求。

如果我们有这个系列:

In [1]: s = pd.Series(range(5))

In [2]: s
Out[2]:
0    0
1    1
2    2
3    3
4    4
dtype: int64

我们有一个窗口大小2和步长1 。 将评估索引0处的第一个窗口,跳过索引1的窗口,评估索引2处的窗口,等等?

In [3]: s.rolling(2, step=1, min_periods=0).max()

Out[3]:
0    0.0
1    NaN # step over this observation
2    2.0
3    NaN # step over this observation
4    4.0
dtype: float64

同样,如果我们有这个基于时间的系列

In [1]: s = pd.Series(range(5), index=pd.DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06', '2020-01-09']))

In [2]: s
Out[2]:
2020-01-01    0
2020-01-02    1
2020-01-03    2
2020-01-06    3
2020-01-09    4
dtype: int64

我们有一个窗口大小'3D'和步长'3D' 。 这会是正确的结果吗?

In [3]: s.rolling('3D', step='3D', min_periods=0).max()

Out[3]:
2020-01-01    0.0       # evaluate this window
2020-01-02    NaN    # step over this observation (2020-01-01 + 3 days > 2020-01-02)
2020-01-03    NaN    # step over this observation (2020-01-01 + 3 days > 2020-01-03)
2020-01-06    3.0      # evaluate this window ("snap back" to this observation)
2020-01-09    4.0      # evaluate this window (2020-01-06 + 3 days = 2020-01-09)
dtype: float64

@mroeschke 写到第一个例子 ([3]),结果不是我所期望的。 我假设这是一个尾随窗口(例如,在 index=0 处,它将是 -1 和 0 处元素的最大值,所以只是 max([0]),然后它应该向前推进“1”索引,到 index=0 +step=1,接下来的计算将是 max([0,1]),然后是 max([1,2]),等等。看起来你想要的是步长为 2,所以你会从 index=0 移动到 index=0+2=2(跳过索引 1),并继续这样。在这种情况下,它几乎是正确的,但不应该有 NaN。虽然它可能“仅”是其中的两倍情况下,在其他情况下它是可观的。例如,我有大约一个小时的患者 500Hz ECG 数据,即 180 万个样本。如果我想要每两分钟的 5 分钟移动平均值,那将是一个数组180 万个元素,30 次有效计算,略少于 180 万个 NaN。:-)

对于索引,步长 = 1 是当前行为,即使用窗口中的数据计算感兴趣的特征,将窗口移动一个,然后重复。 在这个例子中,我想使用窗口中的数据计算感兴趣的特征,然后移动 60,000 个索引,然后重复。

类似的言论在当时。 在这种情况下,对于实现此类窗口的正确方法可能存在一些分歧,但在我看来,“最佳”(TM) 方法是从时间 t0 开始,找到范围内的所有元素 (t0-window , t0], 计算特征,然后按步长移动。扔掉任何少于最小元素数的窗口(可以配置,默认为 1)。该示例适用于尾随窗口,但您可以修改以适应任何窗口配置。这有在大间隙中浪费时间的缺点,但可以智能地处理间隙,即使你计算天真的方式(因为你像我一样懒惰)我还没有在实践中看到这个问题,因为差距在实际数据中通常不够大。YMMV。

也许这样更清楚? 看看我上面的示例+代码,这可能会更好地解释它。

感谢@anthonytw 的澄清。 确实,看起来我需要将step为“逐步指向”。

至于NaN的,我理解的情绪自动挂断输出结果NaN的,但作为中提到https://github.com/pandas-dev/pandas/issues/15354#issuecomment@jreback -278676420,有API 一致性考虑使输出与输入具有相同的长度。 可能有些用户也想保留 NaN(也许?),并且在rolling(..., step=...).func()操作之后dropna仍然可用。

@mroeschke我认为应该例外。 只要您在文档中添加了明确的注释,并且行为不是默认的,没有人会因不返回一个充满垃圾的向量而受到不利影响。 保持 NaN 失败了一半的目的。 一个目标是限制我们执行昂贵计算的次数。 另一个目标是将功能集最小化到可管理的程度。 我给你的那个例子是真实的,并没有像病人监护应用程序中真正需要处理的数据那么多。 真的有必要分配60000x的必要空间,然后通过数组搜索删除NaN吗? 对于我们想要计算的每个特征?

请注意,一次计算可能会产生一组值。 我想用 ECG 波形做什么? 好吧,当然要计算功率谱! 因此,我需要为 1 个完整的 PSD 向量(150,000 个元素)分配足够的空间 180 万次(2TB 的数据),然后过滤以获取我关心的部分(34MB)。 对于所有系列。 为所有患者。 我想我需要购买更多的内存!

还值得一提的是,对于某些特征,NaN 可能是一个有意义的输出。 在这种情况下,我再也无法区分有意义的 NaN 和填充数据的垃圾 NaN 之间的区别。

虽然我理解维护 API 的愿望,但这不是一个会破坏任何现有代码的功能(因为它是一个以前不存在的新功能),并且鉴于该功能,没有理由任何人会期望它产生一个相同大小的输出。 即使他们这样做了,文档中的步长注释就足够了。 缺点远远超过拥有“一致”API 的任何好处(请注意,对于以前不存在的功能)。 不以这种方式进行会削弱该功能,在这种情况下几乎不值得实施(根据我的经验,空间成本几乎总是更大的因素)。

此页面是否有帮助?
0 / 5 - 0 等级