Data.table: 滚动函数、滚动聚合、滑动窗口、移动平均

创建于 2018-04-21  ·  39评论  ·  资料来源: Rdatatable/data.table

在一个地方收集需求并刷新大约 4 年前的讨论,创建此问题以涵盖_滚动函数_功能(也称为_滚动聚合_、_滑动窗口_或_移动平均值_/_移动聚合_)。

滚动功能

  • [x] 滚动平均值
  • [x] 总和
  • [ ] 滚动
  • [ ] 最大滚轮
  • [ ] 滚动中值
  • [ ] 卷尺
  • [ ] 卷
  • [ ] 滚动变量
  • [ ] 滚动排名
  • [x] rollapply(用户提供的乐趣)
  • [ ] 滚动回归(需求量很大)
  • [] 滚动?
  • [ ] 滚动冠状病毒?

特征

  • [x] 一次多列
  • [x] 一次多个窗口
  • [x] 一次多列和多个窗口
  • [x] 原子向量输入和单窗口返回原子向量
  • [x] 各种长度的向量列表
  • [x] 对齐:左/中/右
  • [x] 处理 NA
  • [x] 填充常数
  • [x] 长向量支持
  • [ ] 部分窗口支持(如果需要,可以在 ea766f2499034cedf6abf872440268b78979147c 中找到)
  • [x] 自适应窗口支持
  • [x] 使用openmp并行计算多列/窗口
  • [x] 舍入误差修正
  • [x] 并行区域中详细模式下的计时(被 ~#3422~, #3423 阻止)
feature request

最有用的评论

@mattdowl回答公关问题

为什么我们在 data.table 中这样做? 为什么我们要集成它而不是为现有包做出贡献并从 data.table 使用它们?

  1. 创建了 3 个不同的问题,要求在 data.table 中使用该功能。 还有多个 SO 问题标记为 data.table。 用户希望它在 data.table 的范围内。
  2. data.table 非常适合时间序列数据,滚动聚合是非常有用的统计数据。

我的猜测是它归结为语法(功能只有内置到 data.table 中才可能或方便;例如在 [...] 内部并优化)并将 data.table 内部构建到 C 级别的滚动函数中; 例如 froll* 应该知道并使用 data.table 索引和键。 如果是这样,则需要更多细节; 例如一个简单的简短示例。

对我个人而言,这是关于速度和缺乏依赖链,现在不容易实现。
键/索引可能对 frollmin/frollmax 有用,但用户不太可能在度量变量上创建索引。 用户不太可能对度量变量进行索引,而且我们还没有对 min/max 进行此优化。 我认为 GForce 优化没有多大意义,因为分配的内存不会在 roll* 调用后释放,而是作为答案返回(与非滚动均值、总和等相反)。

如果没有令人信服的集成论据,那么我们应该为其他包做出贡献。

我在上面列出了一些,如果您不相信我建议您向 data.table 用户填写问题,在 twitter 上提问等以检查回复。 长期以来,许多用户都要求此功能。 如果回复不能说服您,那么您可以关闭此问题。

所有39条评论

建议的rollmean实现,简化。

x = data.table(v1=1:5, v2=1:5)
k = c(2, 3)
i - single column
j - single window
m - int referring to single row
w - current row's sum of rolling window
r - answer for each i, j



md5-be70673ef4a3bb883d4f334bd8fadec9



for i in x
  for j in k
  r = NA_real_
  w = 0
    for m in 1:length(i)
      w = w + i[m]
      w = w - i[m-j]
      r[m] = w / j

是的,还有更多的滚动函数遵循相同的基本思想(包括
滚动标准偏差/任何基于期望的矩,以及任何函数
像 rollproduct 使用 invertible * 而不是 + 来聚合
窗户

我一直将滚动窗口功能设想为数据集

DT[i, j,
   by = roll(width=5, align="center")]

然后,如果j包含,比如说, mean(A) ,我们可以在内部用rollmean(A)替换它——就像我们现在用gmean()做的那样。 或者j可以包含任意复杂的功能(例如,为每个窗口运行回归),在这种情况下,我们将提供.SD data.table 给它——就像我们对组所做的一样马上。

这样就不需要引入 10 多个新功能,只需一个。 它在精神上也感觉 data.table-y。

是的,同意

2018 年 4 月 21 日星期六下午 3:38 Pasha Stetsenko通知@github.com
写道:

我一直设想滚动窗口功能作为分组
将数据集分成多个重叠组(窗口)。 然后 API 会看起来
像这样:

DT[i, j,
by = roll(width=5, align="center")]

然后如果 j 包含,比如说,mean(A),我们可以在内部用
rollmean(A)——就像我们现在对 gmean() 所做的一样。 或者 j 可以
包含任意复杂的功能(例如,运行回归
每个窗口),在这种情况下,我们将提供 .SD data.table 给它——正是
就像我们现在对团体所做的那样。

这样就不需要引入 10 多个新功能,只需一个。 而它
在精神上也感觉 data.table-y 。


您收到此消息是因为您发表了评论。
直接回复本邮件,在GitHub上查看
https://github.com/Rdatatable/data.table/issues/2778#issuecomment-383275134
或静音线程
https://github.com/notifications/unsubscribe-auth/AHQQdbADiE4aAI1qPxPnFXUM5gR-0w2Tks5tquH8gaJpZM4TeTQf
.

@st-pasha 有趣的想法,看起来像 data.table-y 精神,但它会施加很多限制,并不真正适合这类功能。


  • 如何按组执行rollmean
DT[, rollmean(V1, 3), by=V2]
  • 如何计算不同列的不同窗口大小
DT[, .(rollmean(V1, 3), rollmean(V2, 100))]
  • 如何计算[.data.table之外的 rollmean,因为我们现在允许移位
rollmean(rnorm(10), 3)
  • 如何支持查询,如
DT[, .(rollmean(list(V1, V2), c(5, 20)), rollmean(list(V2, V3), c(10, 30)))]
  • 如何在同一个j调用中调用meanrollmean
DT[, .(rollmean(V1, 3), mean(V1)), by=V2]

通常在使用by我们将数据聚合到较少的行数,而滚动函数总是返回与输入长度相同的向量。 SQL 中的此类函数具有以下样式的 API:

SELECT AVG(value) OVER (ROWS BETWEEN 99 PRECEDING AND CURRENT ROW)
FROM tablename;

您仍然可以将其与 GROUP BY 组合,如下所示:

SELECT AVG(value) OVER (ROWS BETWEEN 99 PRECEDING AND CURRENT ROW)
FROM tablename
GROUP BY group_columns;

所以在 SQL 中,这些函数保留在SELECT ,它指的是 DT 中的j
在 DT 中,我们可以通过以下方式实现:

DT[, rollmean(value, 100)]
DT[, rollmean(value, 100), group_columns]

滚动函数适合与shift相同的函数类别,它也返回与输入相同的行数。
SQL 中的移位看起来像:

SELECT LAG(value, 1) OVER ()
FROM tablename;

meanrollmean不仅仅是不同的函数,它们是不同类别的函数。 一种是按组聚合,另一种是根本不聚合。 这在 SQL 中很容易看到,我们不使用GROUP BY作为滚动函数类型,但我们确实需要将GROUP BY用于像mean这样的聚合(最终在分组时获得总授权)条款不存在)。
我没有看到尝试应用与我们对mean相同的优化规则的强烈理由,特别是当它不适合用例时,所有这些只是为了 data.table-y精神。 当前的提议也是 data.table-y 精神,它可以很容易地与:= ,与shift 。 它只是添加了一组新函数,目前在 data.table 中不可用。

@jangorecki谢谢,这些都是有效的考虑因素。 当然,不同的人有不同的经历,对于什么应该被认为是“自然的”有不同的看法。

可以按组执行 rollmean:这只是一个 2 级分组: DT[, mean(V1), by=.(V2, roll(3))] 。 但是我没有看到如何使用我的语法在不同的列上制作不同的窗口大小......

我必须承认我以前从未见过用于滚动连接的 SQL 语法。 有趣的是,他们使用标准聚合器(例如AVG但对其应用了窗口规范。 查看Transact-SQL 文档,那里有一些有趣的想法,例如逻辑/物理行选择之间的区别。 它们确实允许在不同的列上使用不同的“OVER”运算符,但是在它们给出的所有示例中,它是重复多次的相同 OVER 子句。 所以它表明这个用例更常见,因此使用单个roll()组会导致更少的重复。

此外,这个 SO question提供了一个有趣的见解,为什么在 SQL 中引入了 OVER 语法:

您可以使用 GROUP BY SalesOrderID。 不同之处在于,使用 GROUP BY,您只能拥有未包含在 GROUP BY 中的列的聚合值。 相反,使用窗口聚合函数而不是 GROUP BY,您可以检索聚合值和非聚合值。 也就是说,虽然您在示例查询中没有这样做,但您可以检索单个 OrderQty 值及其总和、计数、平均值等。

因此,语法似乎旨在规避标准 SQL 的限制,其中分组结果不能与未聚合的值组合(即在同一表达式中同时选择Amean(A) )。 但是data.table没有这样的限制,所以在语法的选择上有更大的自由度。


现在,如果我们想真正走在前面,我们需要从更广阔的角度思考:什么是“滚动”函数,它们的用途是什么,如何扩展它们等等。这是我的看法,即将到来从统计学家的角度来看:

“滚动均值”功能用于平滑一些嘈杂的输入。 比如说,如果你有一段时间的观察,并且你想有一些“平均数量”的概念,尽管很慢,但它会随着时间的推移而变化。 在这种情况下,可以考虑“过去 100 次观察的滚动平均值”或“所有先前观察的滚动平均值”。 同样,如果您在输入范围内观察到某个数量,您可以通过应用“超过 ±50 个观察值的滚动平均值”来平滑它。

  • 因此,第一个扩展是查看“平滑窗口”:想象过去观察的平均值,其中过去的观察越远,其贡献越小。 或者对高斯核的附近观测值的平均值。
  • 其次是自适应窗口。 例如,如果您在区间 [0, 1] 上定义了噪声输入,则使用 ±N 窗口对其进行平滑会在边缘附近产生有偏差的结果。 无偏估计器将根据与边缘的距离来调整窗口的形状。
  • 重新采样平滑:产生与源数据中一样多的观察的限制太有限了。 如果您将数据视为对某些基础函数的噪声观察,那么要求在比原始输入更粗/更细的网格上计算该函数的平滑值是完全合理的。 或者,原始数据的间距可能不规则,您想将其重新采样到规则网格上。
  • Jackknife:对于每个观察,您要计算除当前观察之外的所有观察的平均值/回归。
  • K 折拆分:将数据视为多个组,其中每个组仅排除一小部分观察结果。

所有这些都可以作为扩展分组运算符来实现,滚动窗口只是此列表中的元素之一。 话虽如此,我不明白为什么我们不能同时拥有它。

我必须承认我以前从未见过用于滚动连接的 SQL 语法。

我假设您的意思是滚动函数,问题与滚动连接无关。

它们确实允许在不同的列上使用不同的“OVER”运算符,但是在它们给出的所有示例中,它是重复多次的相同 OVER 子句。 所以它表明这个用例更常见,因此使用单个 roll() 组会导致更少的重复。

这只是用例问题,如果您多次调用相同的 OVER() ,您可能会发现使用GROUP BY 、构建查找表并在其他查询中重用性能更高。 无论有什么例子,都需要重复 OVER() 以保留所提供的每个度量的局部性特征。 我来自数据仓库的用例并不像 Microsoft 文档中的用例那么简单。

相反,使用窗口聚合函数而不是 GROUP BY,您可以检索聚合值和非聚合值。

在 data.table 中,我们在一个查询中执行:=by来实现它。

因此,语法似乎旨在规避标准 SQL 的限制,其中分组结果无法与未聚合的值组合(即在同一表达式中同时选择 A 和 mean(A))。 但是data.table没有这样的限制,所以在语法的选择上有更大的自由度。

SQL 没有太多限制,只是 GROUP BY 的设计,它会聚合,就像我们的by聚合一样。 需要新的 API 来覆盖新的窗口功能。 可以使用FUN() OVER (PARTITION BY ...)为每个函数调用提供 SQL 窗口函数的分组,其中 _partition by_ 类似于单个度量的本地分组。 因此,为了实现 SQL 的灵活性,我们需要使用j = mean(V1, roll=5)j = over(mean(V1), roll=5)将该 API 保留在j 。 这种方法仍然不允许支持上述所有用例。

您可以通过应用“超过 ±50 个观测值的滚动平均值”来平滑它。

这就是align参数的用途。

因此,第一个扩展是查看“平滑窗口”:想象过去观察的平均值,其中过去的观察越远,其贡献越小。 或者对高斯核的附近观测值的平均值。

移动平均线有许多变体(几乎无限数量),最常见的平滑窗口函数(rollmean/SMA 除外)是指数移动平均线 (EMA)。 哪些应该包括,哪些不包括在内,决定并不容易,实际上最好根据来自用户的功能请求做出决定,到目前为止还没有这样的请求。

所有这些都可以作为扩展分组运算符来实现,滚动窗口只是此列表中的元素之一。

他们当然可以,但是如果您查看 SO 以及在我们的 repo 中创建的问题,您会发现这里的少数滚动功能负责 95+% 的用户请求。 我很高兴在 EMA 和其他 MA 上工作(虽然我不确定 data.table 是否是最适合这些的地方),但作为一个单独的问题。 包括我在内的一些用户已经等待 data.table 中的简单移动平均线 4 年了。

这是我的看法,来自统计学家的观点

我的观点来自数据仓库(我使用窗口函数,每周至少一次)和价格趋势分析(我使用数十种不同的移动平均线)。

rollmean草稿被推送到roll分支。 我发现大多数实现滚动均值的其他软件包无法很好地处理输入中存在的na.rm=FALSE和 NA。 此实现始终将 NA 处理为mean ,这会由于ISNAN调用而产生一些额外的开销。 如果用户确定输入中没有 NA,我们可以允许 API 更快但安全性较低。
公关在#2795

@mattdowl回答公关问题

为什么我们在 data.table 中这样做? 为什么我们要集成它而不是为现有包做出贡献并从 data.table 使用它们?

  1. 创建了 3 个不同的问题,要求在 data.table 中使用该功能。 还有多个 SO 问题标记为 data.table。 用户希望它在 data.table 的范围内。
  2. data.table 非常适合时间序列数据,滚动聚合是非常有用的统计数据。

我的猜测是它归结为语法(功能只有内置到 data.table 中才可能或方便;例如在 [...] 内部并优化)并将 data.table 内部构建到 C 级别的滚动函数中; 例如 froll* 应该知道并使用 data.table 索引和键。 如果是这样,则需要更多细节; 例如一个简单的简短示例。

对我个人而言,这是关于速度和缺乏依赖链,现在不容易实现。
键/索引可能对 frollmin/frollmax 有用,但用户不太可能在度量变量上创建索引。 用户不太可能对度量变量进行索引,而且我们还没有对 min/max 进行此优化。 我认为 GForce 优化没有多大意义,因为分配的内存不会在 roll* 调用后释放,而是作为答案返回(与非滚动均值、总和等相反)。

如果没有令人信服的集成论据,那么我们应该为其他包做出贡献。

我在上面列出了一些,如果您不相信我建议您向 data.table 用户填写问题,在 twitter 上提问等以检查回复。 长期以来,许多用户都要求此功能。 如果回复不能说服您,那么您可以关闭此问题。

我发现sparklyr可以在非常大规模的数据集中很好地支持滚动函数。

@harryprince可以通过提供您如何在 sparklyr 中执行此操作的示例代码来增加一些亮点?
根据“窗口函数”dplyr 小插图

滚动聚合在固定宽度的窗口中运行。 您不会在基础 R 或 dplyr 中找到它们,但在其他包中有许多实现,例如 RcppRoll。

AFAIU 您通过未实现 dplyr 接口的 sparklyr 使用自定义 spark API,对吗?

这个问题是关于滚动聚合,其他“类型”的窗口函数已经在data.table中很久了。

提供一些示例以便我们可以比较(内存中)性能与sparklyr / SparkR也将有所帮助。

我突然想到这个问题:

如何计算不同列的不同窗口大小?

实际上具有更广泛的范围,并且不仅仅适用于滚动功能。

例如,询问如何按日期、然后按周、然后按周+类别选择平均产品价格似乎是完全合理的——所有这些都在同一个查询中。 如果我们要实现这样的功能,它的自然语法可能是

DT[, .( mean(price, by=date), 
        mean(price, by=week), 
        mean(price, by=c(week, category)) )]

现在,如果此功能已经实现,那么从那里到滚动方式将是一个简单的飞跃:

DT[, .( mean(price, roll=5), 
        mean(price, roll=20), 
        mean(price, roll=100) )]

并不是说这绝对比rollmean(price, 5)更好——只是考虑一些替代方案......

@st-pasha

如何按日期,然后按周,然后按周+类别选择平均产品价格——所有这些都在同一个查询中。

AFAIU 这已经可以使用?groupingsets ,但还没有连接到[.data.table

@jangorecki 、@st-pasha 和 Co. -- 感谢您为此所做的所有工作! 我很好奇为什么从范围中删除了部分窗口支持,该功能是否有可能使其重新回到路线图上? 有时对我来说会派上用场,并填补一个功能空白,据我所知, zooRcppRoll都没有填补。

这个堆栈溢出问题是滚动应用程序的一个很好的例子,它可以从partial = TRUE参数中受益。

@msummersgill感谢您的反馈。 在第一篇文章中,我明确链接了 commit sha,其中可以找到部分窗口功能代码。 后来删除了那里的实现以降低代码的复杂性。 即使不使用该功能,它也会产生很小的性能成本。 此功能可以(并且可能应该)以另一种方式实现,首先按原样完成,然后使用1:window_size额外循环填充缺失的部分窗口。 因此,该功能的开销只有在您使用它时才会明显。 尽管如此,我们确实通过adaptive参数提供了该功能,其中partial功能只是adaptive滚动平均值的特例。 在?froll手册中有如何使用adaptive实现partial示例。 贴在这里:

d = as.data.table(list(1:6/2, 3:8/4))
an = function(n, len) c(seq.int(n), rep(n, len-n))
n = an(3, nrow(d))
frollmean(d, n, adaptive=TRUE)

当然,它不会像使用额外循环来填充部分窗口的非自适应滚动函数那样有效。
AFAIK zoo具有partial功能。

你们有没有计划将滚动回归函数添加到 data.table 中?

@waynelapierre如果有需求,那么是的。 你有我的+1

谢谢,这很棒。 不过就一个问题。 我只看到简单的滚动聚合,比如滚动平均值或滚动中位数。 您是否还在实施更精细的滚动功能,例如滚动 DT 数据帧? 比如说,使用最后 10 个 obs 创建一个滚动 DT 并对其运行lm回归。

谢谢!

@randomgabit我会说它超出了范围,除非对此有很高的需求。 仅仅通过在 C 中处理嵌套循环来做到比基本 R/zoo 更快并不是很难。但是我们应该尝试使用“在线”算法来实现它,以避免嵌套循环。 这更棘手,我们最终可以对任何统计数据进行处理,因此我们必须在某个时候切断这些统计数据。

@jangorecki有趣的谢谢。 这意味着我将继续使用tsibble将... DATA.TABLES嵌入到tibble ! 脑洞大开 :D

试图用frollmean来计算非参数“逻辑曲线”,其示出了P[y | x]二进制y使用的最近的邻居x 。 很惊讶y存储为logical没有自动转换为integer

DT = data.table(x = rnorm(1000), y = runif(1000) > .5)
DT[order(x), .(x, p_y = frollmean(y, 50L))]

froll 中的错误(fun = "mean", x = x, n = n, fill = fill, algo = algo, align = align, :
x 必须是数字类型

向量化x / n参数如何影响性能的示例。
https://github.com/AdrianAntico/RemixAutoML/commit/d8370712591323be01d0c66f34a70040e2867636#r34784427
更少的循环,更容易阅读的代码,更快(10x-36x 加速)。

frollapply 准备好: https :

    ### fun             mean     sum  median
    # rollfun          8.815   5.151  60.175
    # zoo::rollapply  34.373  27.837  88.552
    # zoo::roll[fun]   0.215   0.185      NA
    # frollapply       5.404   1.419  56.475
    # froll[fun]       0.003   0.002      NA

嗨,伙计们,将传递给 frollapply 的 FUN(用户定义)更改为返回 R 对象或 data.frame(data.table),传递给 frollapply 的 x 可能是 data.table 的字符而不是强制为数字,然后 FUN 可以做标签和 frollapply 返回一个列表? 然后我们可以进行滚动回归或滚动测试,例如对标签进行 Benford 的测试或摘要。

提供可重现的示例总是有用的。 为了澄清......在这种情况下,您希望frollapply(dt, 3, FUN)返回长度nrow(dt)列表,其中每个列表元素将是data.table返回的FUN(dt[window]) ?
frollapply(x=dt, n=3, fun=FUN)[[3]]等于FUN(dt[1:3])
frollapply(x=dt, n=3, FUN=FUN)[[4]]等于FUN(dt[2:4])
那是对的吗? @jerryfuyu0104

目前我们支持传递给第一个参数的多列,但我们单独处理它们,循环。 我们可能需要一些额外的参数multi.var=FALSE ,当设置为 true 时,它​​不会遍历x (就像现在: list(FUN(x[[1]]),FUN(x[[2]])) )而是传递所有列FUN(x)

有什么更新吗?

我支持之前的请求。

此外,是否有可能支持“部分”参数以允许部分窗口?

@eliocamp你能详细说明partial窗口是什么吗?

@eliocamp有可能支持“部分”论点。 您可能已经知道,但是已经支持此功能,使用adaptive=TRUE参数,请参阅示例了解详细信息。

这意味着从头到尾计算函数,而不是形成半窗口点。
例如,对于宽度为 11 的滚动平均值,返回的第一个元素将是元素 1 到 6 的平均值。第二个元素是第 1 到第 7 个元素的平均值,依此类推。

@jangorecki哦,谢谢,我不知道! 我会检查一下。

同意,我们需要部分论证,不仅是为了方便,也是为了速度。 adaptive=TRUE增加了开销。
是的,我们还需要滚动回归,因此提供多个变量并一次滚动它们,而不是每个变量都单独滚动。
没有关于这些状态的更新。

我很乐意提供帮助,但我的 C++ 技能完全不存在。 :sweat: 你认为它可能适合完全的新手吗?

我们不使用 C++ 编写代码,而是使用 C。是的,这是一个很好的起点。 我在 frollmean 上就是这样做的。

我看着代码,似乎令人生畏。 但无论如何我都会更新你。

但是现在,对于另一个请求: frollmean(.SD) 应该保留名称。 更一般地,如果输入是带有名称的列表,则 froll* 应该保留名称。

作为 data.table 的常客,我发现拥有“时间感知”功能非常有用,如目前在tsibble包中提供的功能。 不幸的是,这个包是围绕dplyr 。 我想知道 data.table 实现是否可能。 本期提出的窗函数是这些特征的一个子集。

@ywhcuhk感谢反馈,我实际上认为这个问题已经要求太多了。 其中大部分都被速度非常快的轻质包装很好地覆盖了。 至于其他功能,我建议为您感兴趣的每个功能创建新问题,因此可以分别讨论我们是否要实现/维护。 仅从 tstibble 的自述文件来看,我没有看到它提供的任何新内容......
它的标题是“Tidy Temporal Data Frames”,但它似乎甚至不提供时间连接。

谢谢@jangorecki的回复。 也许这是一个上下文相关的问题。 我最常处理的数据结构称为“面板数据”,带有 ID 和时间。 如果程序“意识到”了这个数据特征,很多操作,尤其是时序操作,就会变得非常容易。 对STATA了解的人来说,就是基于tssetxtset ,比如lead、lag、fill gap等,我觉得data.table中的“索引”可以增强以某种方式启用此类操作。

当然,这些操作可以在shiftby等 data.table 函数中完成。 我只是认为 data.table 中的index有很大的潜力可以探索。 我同意这应该属于一个不同的问题。 但我不知道如何在不失去上述讨论的情况下移动它......

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