以最有效的方式获取间隔期间的事件数

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 中有意义我猜是方言。

以上是关于以最有效的方式获取间隔期间的事件数的主要内容,如果未能解决你的问题,请参考以下文章

如何以最有效的方式将两个未排序的数组合并成一个排序的数组?

JavaScript计时事件

JavaScript 计时事件

C#问题...Form2怎么获取Form1的按钮事件!..

vue小技能:组件间的数据传递

如何在lua中设置事件之间的时间间隔