【问题标题】:How to solve gaps and island problems in R and performance vs SQL?如何解决 R 和性能与 SQL 中的差距和孤岛问题?
【发布时间】:2015-06-05 01:08:45
【问题描述】:

我想知道是否可以在 R 中有效地解决孤岛和间隙问题,类似于 SQL。如果我们检查一个 ID,我有以下可用数据:

ID StartDate  StartTime EndDate      EndTime 
1  19-05-2014 19:00     19-05-2014   20:00
1  19-05-2014 19:30     19-05-2014   23:30
1  19-05-2014 16:00     19-05-2014   18:00
1  20-05-2014 20:00     20-05-2014   20:30

注意前两行重叠,我想做的是合并重叠的行,结果:

ID StartDate  StartTime EndDate      EndTime 
1  19-05-2014 19:00     19-05-2014   23:30
1  19-05-2014 16:00     19-05-2014   18:00
1  20-05-2014 20:00     20-05-2014   20:30

有没有办法在 R 中做到这一点?

我很清楚这是在 SQL 中完成的,但由于我的数据已经在 R 中,我更喜欢在 R 中执行此操作。其次,我对查找间隙和孤岛的性能有一些疑问,我知道 SQL 是这样做的速度非常快,但我想知道由于所有数据都在内存中,R 是否更快。

我想使用data.table 来做这个,但我不知道怎么做。

更新 - 对阿伦的回应

我创建了以下测试用例,其中包含所有可能的区间方向。

dat <- structure(
  list(ID = c(1L, 1L, 1L, 1L, 1L, 1L), 
       stime = structure(c(as.POSIXct("2014-01-15 08:00:00"),
                           as.POSIXct("2014-01-15 10:00:00"),
                           as.POSIXct("2014-01-15 08:30:00"),
                           as.POSIXct("2014-01-15 09:00:00"),
                           as.POSIXct("2014-01-15 11:30:00"),
                           as.POSIXct("2014-01-15 12:00:00")),
                         class = c("POSIXct", "POSIXt"), tzone = ""),
       etime = structure(c(as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 11:00:00"),
                           as.POSIXct("2014-01-15 10:00:00"), 
                           as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 12:30:00"),
                           as.POSIXct("2014-01-15 13:00:00")), 
                         class = c("POSIXct", "POSIXt"), tzone = "")
  ),
  .Names = c("ID", "stime", "etime"),
  sorted = c("ID", "stime", "etime"),
  class = c("data.table", "data.frame"),
  row.names = c(NA,-6L)
)

我希望从 8:30 到 10:00 的时间间隔将“粘合”到 10:00 到 11:00,但事实并非如此。结果是:

   idx ID               stime               etime
1:   4  1 2014-01-15 08:00:00 2014-01-15 10:00:00
2:   3  1 2014-01-15 10:00:00 2014-01-15 11:00:00
3:   6  1 2014-01-15 11:30:00 2014-01-15 13:00:00

以下数据集提供了更彻底的测试:

# The numbers represent seconds from 1970-01-01 01:00:01
dat <- structure(
  list(ID = c(1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L), 
       stime = structure(c(as.POSIXct("2014-01-15 08:00:00"),
                           as.POSIXct("2014-01-15 10:00:00"),
                           as.POSIXct("2014-01-15 08:30:00"),
                           as.POSIXct("2014-01-15 09:00:00"),
                           as.POSIXct("2014-01-15 11:30:00"),
                           as.POSIXct("2014-01-15 12:00:00"),
                           as.POSIXct("2014-01-15 07:30:00"),
                           as.POSIXct("2014-01-15 08:00:00"),
                           as.POSIXct("2014-01-15 08:30:00"),
                           as.POSIXct("2014-01-15 09:00:00"),
                           as.POSIXct("2014-01-15 09:00:00"),
                           as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 10:00:00")
                           ),
                         class = c("POSIXct", "POSIXt"), tzone = ""),
       etime = structure(c(as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 11:00:00"),
                           as.POSIXct("2014-01-15 10:00:00"), 
                           as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 12:30:00"),
                           as.POSIXct("2014-01-15 13:00:00"),
                           as.POSIXct("2014-01-15 08:30:00"),
                           as.POSIXct("2014-01-15 09:00:00"),
                           as.POSIXct("2014-01-15 09:30:00"),
                           as.POSIXct("2014-01-15 10:00:00"),
                           as.POSIXct("2014-01-15 10:00:00"),
                           as.POSIXct("2014-01-15 10:30:00"),
                           as.POSIXct("2014-01-15 11:00:00")
                           ), 
                         class = c("POSIXct", "POSIXt"), tzone = "")
  ),
  .Names = c("ID", "stime", "etime"),
  sorted = c("ID", "stime", "etime"),
  class = c("data.table", "data.frame"),
  row.names = c(NA,-6L)
)

所以我们的结果是:

   idx ID               stime               etime
1:   4  1 2014-01-15 08:00:00 2014-01-15 10:00:00
2:   3  1 2014-01-15 10:00:00 2014-01-15 11:00:00
3:   6  1 2014-01-15 11:30:00 2014-01-15 13:00:00
4:  12  2 2014-01-15 07:30:00 2014-01-15 09:30:00
5:  13  2 2014-01-15 09:00:00 2014-01-15 11:00:00

现在对于 ID=2 的受访者,我们看到区间是重叠的,但没有报告为一个区间。正确的解决方案是:

   idx ID               stime               etime
1:   ?  1 2014-01-15 08:00:00 2014-01-15 11:00:00
3:   ?  1 2014-01-15 11:30:00 2014-01-15 13:00:00
4:  ??  2 2014-01-15 07:30:00 2014-01-15 11:00:00

更新 - 基准测试和大型数据集

我有以下数据集,其中包含大约 1000 个用户,每个用户有 500 个持续时间,提供 50 万行。您可以通过我的Google Drive 下载数据集,包括 Google Drive 中的解决方案。

使用 Itzik Ben-Gan 在 SQL 中提供的解决方案,在 8GB RAM、64 位、i5-4210U CPU @ 1.70Ghz - 2.39Ghz 的笔记本电脑上运行 SQL Server 2014 大约需要 5 秒。 5 秒不包括创建函数的过程。此外,不会为任何表创建任何索引。

PS:我用library(lubridate);

【问题讨论】:

  • 什么是 SQL 解决方案?
  • 亲爱的 Eddi,以下是该问题的一些示例:sqlmag.com/blog/…。 Itzik Ben-Gan 能够在令人印象深刻的 2 秒内做到这一点。
  • @alexis_laz,是的,它可以在这里工作,但 POSIXct 也可以有毫秒,这会失败(因为 IRanges::reduce 会隐式将其转换为整数范围)..

标签: r data.table gaps-and-islands


【解决方案1】:

这是一个非常简单的想法。按开始时间排序,然后找到结束时间的累积最大值。完成此操作后,重叠组就是下一次开始时间仍小于或等于当前累积最大结束时间的那些重叠组(全部由 ID 完成):

setorder(dat, ID, stime) # ordering by ID is unnecessary, it's just prettier

dat[, etime.max := as.POSIXct(cummax(as.numeric(etime)), origin = '1970-01-01'), by = ID]

# find the grouping of intervals (1:.N hack is to avoid warnings when .N=1)
dat[, grp := cumsum(c(FALSE, stime[2:.N] > etime.max[1:(.N-1)]))[1:.N], by = ID]

dat[, .(stime = min(stime), etime = max(etime)), by = .(ID, grp)][, grp := NULL][]
#   ID               stime               etime
#1:  1 2014-01-15 08:00:00 2014-01-15 11:00:00
#2:  1 2014-01-15 11:30:00 2014-01-15 13:00:00
#3:  2 2014-01-15 07:30:00 2014-01-15 11:00:00

由于不需要找到所有可能的重叠,因此速度非常快。在与 OP 的描述大致匹配的模拟数据集上,它对我来说是瞬时的(

【讨论】:

  • 这听起来可能很愚蠢,但是您能否详细说明一下您的累计最大结束时间是什么意思? (我对 data.table 还不是很好,但我很想知道你的代码的第 3 行是如何工作的)。
  • 我的意思是到当前点的最大结束时间。以?cummax 为例。至于第 3 行,检查每个部分分别计算的内容(对于单个 ID),然后它就会有意义。将etime.max 转换为POSIXct 也可能有助于可视化(实际上,我只是为此进行了编辑,使其更易于阅读)。
  • 谢谢eddi,我现在明白你的代码了,但我想知道“.(stuff)”是什么。我认为它应该在列表中,但我找不到关于点括号符号的任何内容。或者也许我忽略了它。这似乎是一个非常优雅的解决方案,可能会奏效。我将使用我用于测试和报告的完整 500 000 行来检查它:)。
  • 点相当于写list,imo 可读性更强。我想说它是最近推出的,因为我最近开始这样做,但我不记得了。酷,我期待测试:)
  • 感谢 Eddi,它比 SQL 快得多。我得到了 1 秒的测试数据。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-20
  • 1970-01-01
  • 2022-01-06
相关资源
最近更新 更多