以最有效的方式获取间隔期间的事件数
Posted
技术标签:
【中文标题】以最有效的方式获取间隔期间的事件数【英文标题】:Get number of events during interval the most efficient way 【发布时间】:2020-01-21 10:50:06 【问题描述】:样本数据
我有一个包含事件的 data.table (dt
),以及一个包含特定时间段内所有分钟数的 data.table (dt.minutes
)。
dt <- data.table( id = 1:3,
start = c("2019-01-01 18:00:00", "2019-01-01 19:00:00", "2019-01-01 20:00:00"),
end = c("2019-01-01 21:00:00", "2019-01-01 20:15:00", "2019-01-01 20:30:00") )
dt[, c("start", "end") := lapply( .SD,
as.POSIXct,
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
.SDcols = c("start", "end")]
dt.minutes <- data.table( from = seq( from = as.POSIXct( "2019-01-01 00:00:00",
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
to = as.POSIXct( "2019-01-05 00:00:00",
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
by = "1 min") )
dt.minutes[, to := from + 59 ][]
setkey( dt, start, end)
setkey( dt.minutes, from, to )
看起来像这样
> dt
id start end
1: 1 2019-01-01 18:00:00 2019-01-01 21:00:00
2: 2 2019-01-01 19:00:00 2019-01-01 20:15:00
3: 3 2019-01-01 20:00:00 2019-01-01 20:30:00
> dt.minutes
from to
1: 2019-01-01 00:00:00 2019-01-01 00:00:59
2: 2019-01-01 00:01:00 2019-01-01 00:01:59
3: 2019-01-01 00:02:00 2019-01-01 00:02:59
4: 2019-01-01 00:03:00 2019-01-01 00:03:59
5: 2019-01-01 00:04:00 2019-01-01 00:04:59
---
5757: 2019-01-04 23:56:00 2019-01-04 23:56:59
5758: 2019-01-04 23:57:00 2019-01-04 23:57:59
5759: 2019-01-04 23:58:00 2019-01-04 23:58:59
5760: 2019-01-04 23:59:00 2019-01-04 23:59:59
5761: 2019-01-05 00:00:00 2019-01-05 00:00:59
问题
对于dt.minutes
中的每一行(=分钟),我想知道在这一分钟内发生了多少来自dt
的事件。
我可以提出两种可能的 data.table 解决方案:
setkey( dt, start, end)
setkey( dt.minutes, from, to )
#method 1: non-equi join
ans1 <- dt.minutes[ dt.minutes, N :=
num = dt[ start <= i.to & end >= i.from ]
list( nrow(num) )
, by = .EACHI ][]
#method 2: use foverlaps, summarise on `from` and then update-join
ans2 <- dt.minutes[, N:=0L][ foverlaps( dt, copy(dt.minutes) )[, .(N =.N), by = .(from)], N := i.N, on = .(from)]
这两种方法都有效并提供了我需要的答案
all.equal( ans1, ans2 )
# [1] TRUE
但是当我查看基准时,foverlaps()
以压倒性优势获胜..
# Unit: milliseconds
# expr min lq mean median uq max neval
# non_equi_join 2074.0594 2097.3363 2111.87762 2100.1306 2116.6965 2171.1653 5
# foverlaps 10.5716 10.8999 10.93622 10.9011 10.9479 11.3606 5
#
microbenchmark::microbenchmark(
non_equi_join =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT2, from, to )
DT2[ DT2, N :=
num = DT[ start <= i.to & end >= i.from ]
list( nrow(num) )
, by = .EACHI ][]
,
foverlaps =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT, start, end)
setkey( DT2, from, to )
DT2[, N := 0L][ foverlaps( DT, copy(DT2) )[, .( N = .N ), by = .(from)], N := i.N, on = .(from)]
, times = 5L
)
问题
本着更好地理解 data.table 连接的精神,我正在寻找与 foverlaps()
(ans2) 相比,我的连接 (ans1) 花费这么长时间(慢 200 倍)的原因。
有没有办法提高连接的性能?或者foverlaps()
只是这项工作的优化工具?
或者有更快的方法来实现我的目标吗?
【问题讨论】:
【参考方案1】:首先,我不确定foverlaps
的默认type
是否是您想要的。
举个例子:
> foverlaps(dt.minutes, dt)[1368]
id start end from to
1: 1 2019-01-01 18:00:00 2019-01-01 21:00:00 2019-01-01 21:00:00 2019-01-01 21:00:59
这确实像文档指定的那样,
但这似乎不是你所追求的
(id
应该是 NA
)。
你可能需要type = "within"
。
我不熟悉data.table
的内部结构,
所以以下是有根据的猜测。
使用by = .EACHI
时加入时的总结是为了优化内存使用,而不是速度。
如果连接中的每个结果组都很大,
每次只实现其中的一部分可能是值得的,
但是您传递给j
的任何代码都是R 代码
(通常,请参见下面的 cmets),
即未编译的代码。
加入的基本代码可能完全在 C 中评估,
但如果你使用by = .EACHI
,
查找连接的匹配行可能很快,
但是评估j
本质上变成了R中跨组的循环,
如果有很多小组,相关的时间开销就会增加
(就像你的问题一样)。
我想出了另外 2 个替代方案 (并稍微修改了设置), 我系统中的基准如下所示:
library(data.table)
dt <- data.table( id = 1:3,
start = c("2019-01-01 18:00:00", "2019-01-01 19:00:00", "2019-01-01 20:00:00"),
end = c("2019-01-01 21:00:00", "2019-01-01 20:15:00", "2019-01-01 20:30:00") )
dt[, c("start", "end") := lapply( .SD,
as.POSIXct,
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
.SDcols = c("start", "end")]
dt.minutes <- data.table( from = seq( from = as.POSIXct( "2019-01-01 00:00:00",
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
to = as.POSIXct( "2019-01-05 00:00:00",
format = "%Y-%m-%d %H:%M:%S",
tz = "Europe/Amsterdam"),
by = "1 min") )
dt.minutes[, to := from + 59 ]
library(microbenchmark)
microbenchmark::microbenchmark(
times = 5L,
non_equi_join =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT, start, end)
setkey( DT2, from, to )
DT2[ DT2, N :=
num = DT[ start <= i.to & end >= i.from ]
list( nrow(num) )
, by = .EACHI ]
,
foverlaps =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT, start, end)
setkey( DT2, from, to )
DT2[, N := 0L][ foverlaps( DT, copy(DT2) )[, .( N = .N ), by = .(from)], N := i.N, on = .(from)]
,
nej =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT, start, end)
setkey( DT2, from, to )
DT2[, N := DT[.SD, .(id, start), on = .(start <= from, end >= to), allow.cartesian = TRUE
][, sum(!is.na(id)), by = "start"]$V1]
,
fo =
DT <- copy(dt)
DT2 <- copy(dt.minutes)
setkey( DT, start, end)
setkey( DT2, from, to )
DT2[, N := foverlaps(DT2, DT, type="within", which=TRUE)[, sum(!is.na(yid)), by="xid"]$V1]
)
Unit: milliseconds
expr min lq mean median uq max neval
non_equi_join 2506.3448 2535.3132 2597.71440 2565.4727 2647.7538 2733.6875 5
foverlaps 13.8878 14.3945 14.66726 14.9400 15.0491 15.0649 5
nej 11.6391 12.0179 13.89408 13.2644 13.3602 19.1888 5
fo 11.4082 12.7889 13.77820 12.9216 13.0430 18.7293 5
*由于我在开头提到的type
,我的版本的结果与你的不匹配。
我们可以看到它们并没有比你的快多少,
但值得注意的是nej
版本。
还使用了非等连接,
但没有by = .EACHI
。
连接的整个结果首先被物化,
只有在我们聚合结果之后,
在这种情况下更快。
不幸的是我不能告诉你确切的原因
(再次,不熟悉内部),
但一般的经验法则应该是 by = .EACHI
应该只在你期望结果中有几个大组时才使用,
或者j
中的代码是否可以通过data.table
进行优化。
顺便说一句,在fo
版本中,我使用which = TRUE
来避免从连接返回所有列,
仅返回索引。
由于条目的数量很重要,
返回具有匹配项的索引的工作方式类似。
在这种情况下并没有太大的不同。
*请注意foverlaps
'文档中提到通常应在x
中提供较大的表。
编辑:弗兰克的版本似乎是最快的:
dt.minutes[, n := dt[.SD, on=.(start <= from, end >= to), allow.cartesian=TRUE, .N, by=.EACHI]$N]
【讨论】:
我不认为 by=.EACHI 是问题,在这里运行了一个基准测试,在 nej 上有另一个变体:chat.***.com/transcript/message/47363955#47363955 @Frank 我想知道.N
是否算作“j
中的 R 代码”。由于它是data.table
的特殊符号之一,因此可能会对其进行优化。如果您在 OP 的 non_equi_join
和您的 nej2
(内框)中设置 verbose = TRUE
,则可以看出采用了不同的路径,后者显示“检测到 j 使用这些列:DT[...]
中有很多解析表达式来优化特定于data.table 的调用。根据详细输出,DT[x == 1]
转换为“bmerge”,一些j
表达式被“GForce”优化,等等。所以从这个意义上说,.N
是语法上有效的 R 代码,但只在 data.table 中有意义我猜是方言。以上是关于以最有效的方式获取间隔期间的事件数的主要内容,如果未能解决你的问题,请参考以下文章