样本数据
我有一个包含事件的 data.table (dt
),以及一个包含特定时间段内所有分钟数 (dt.minutes
) 的 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 ][]
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()
相比我的连接 (ans1) 花费这么长时间(慢 200 倍)的原因(答案 2).
有没有办法提高连接的性能?或者 foverlaps()
只是针对这项工作的优化工具?
或者是否有更快的方法来实现我的目标?
最佳答案
首先,我不确定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 代码
(通常,请参阅下面的评论),
即未编译代码。
加入的基本代码可能完全用 C 语言评估,
但是如果你使用 by = .EACHI
,
找到连接的匹配行可能很快,
但是评估 j
本质上变成了 R 中跨组的循环,
如果有很多小组,相关的时间开销会加起来
(就像你的问题)。
我想到了另外两个选择 (并稍微修改了设置), 我系统中的基准如下所示:
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
中提供较大的表格。
编辑:Frank 的版本似乎是最快的:
dt.minutes[, n := dt[.SD, on=.(start <= from, end >= to), allow.cartesian=TRUE, .N, by=.EACHI]$N]
关于r - 以最有效的方式获取间隔期间的事件数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58027333/