【问题标题】:Complicated summary function -- is it possible to solve with R data.table package?复杂的汇总功能——可以用 R data.table 包解决吗?
【发布时间】:2014-01-26 03:16:52
【问题描述】:

我正在重新编写一些分析大量数据(约 1700 万行)的 R 脚本,我想我会尝试通过使用 data.table 包来提高它的内存效率(我只是在学习! )。

代码的一部分让我有点困惑。我无法发布我的原始解决方案,因为 (1) 它是垃圾(慢!),并且 (2) 它在数据方面非常细微,只会使这个问题复杂化。

相反,我做了这个玩具示例(它确实是一个玩具示例):

ds <- data.table(ID=c(1,1,1,1,2,2,2,3,3,3),
Obs=c(1.5,2.5,0.0,1.25,1.45,1.5,2.5,0.0,1.25,1.45), 
Pos=c(1,3,5,6,2,3,5,2,3,4))

看起来像这样:

    ID  Obs Pos
 1:  1 1.50   1
 2:  1 2.50   3
 3:  1 0.00   5
 4:  1 1.25   6
 5:  2 1.45   2
 6:  2 1.50   3
 7:  2 2.50   5
 8:  3 0.00   2
 9:  3 1.25   3
10:  3 1.45   4

为了便于解释,我假设我们正在观察火车(每辆火车都有自己的ID),在一条线性单向轨道上移动,观察(一些价值,而不是导入问题)关于火车在沿轨道的设定位置(pos,此处为 1-6)。预计一列火车不会将其延伸到轨道的整个长度(可能在到达位置 6 之前就爆炸了),有时观察者会错过观察......位置是连续的(因此,如果我们错过了观察火车在 4 号位置,但我们在 5 号位置观察到它,我们知道它必须经过 4 号位置。

从上面的data.table,我需要生成一个这样的表:

   Pos Count
1:   1     3
2:   2     3
3:   3     3
4:   4     3
5:   5     2
6:   6     1

对于我的 data.table ds 中的每个唯一 Pos,我计算了到达轨道上该位置(或更远)的火车数量,无论观察是否是在赛道上的那个位置制作的。

如果有人对如何解决此问题有任何想法或建议,将不胜感激。不幸的是,我对 data.table 不够熟悉,不知道这是否可以做到!或者它可能是非常简单的问题要解决,我只是很慢:)

【问题讨论】:

    标签: r data.table


    【解决方案1】:

    一些时间供参考:

    计时码:

    library(data.table)
    set.seed(0L)
    nr <- 2e7
    nid <- 1e6
    npos <- 20
    ds <- unique(data.table(ID=sample(nid, nr, TRUE), Pos=sample(npos, nr, TRUE)))
    # ds <- data.table(ID=c(1,1,1,1,2,2,2,3,3,3),
    #     Obs=c(1.5,2.5,0.0,1.25,1.45,1.5,2.5,0.0,1.25,1.45),
    #     Pos=c(1,3,5,6,2,3,5,2,3,4))
    setkey(ds, ID, Pos)
    
    ids = ds[, sort(unique(ID))]   # or from the data: unique(ds$ID)
    pos = ds[, sort(unique(Pos))]   # or from the data: unique(ds$Pos)
    
    mtd0 <- function() ds[CJ(ids, pos), roll=-Inf, nomatch=0][, .N, by=Pos]
    mtd1 <- function() ds[,max(Pos),by=ID][,rev(cumsum(rev(tabulate(V1))))]
    mtd2 <- function() ds[, .(Pos=1:Pos[.N]), ID][, .N, by=Pos]
    bench::mark(mtd0(), mtd1(), mtd2(), check=FALSE)
    
    identical(mtd0()$N, mtd2()$N)
    #[1] TRUE
    
    identical(mtd1(), mtd2()$N)
    #[1] TRUE
    

    时间安排:

    # A tibble: 3 x 13
      expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result            memory               time     gc              
      <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>            <list>               <list>   <list>          
    1 mtd0()        2.14s    2.14s     0.468    1.26GB     1.40     1     3      2.14s <df[,2] [20 x 2]> <df[,3] [41 x 3]>    <bch:tm> <tibble [1 x 3]>
    2 mtd1()     281.54ms 284.89ms     3.51   209.24MB     1.76     2     1   569.78ms <int [20]>        <df[,3] [24 x 3]>    <bch:tm> <tibble [2 x 3]>
    3 mtd2()        1.63s    1.63s     0.613  785.65MB     7.35     1    12      1.63s <df[,2] [20 x 2]> <df[,3] [9,111 x 3]> <bch:tm> <tibble [1 x 3]>
    

    【讨论】:

      【解决方案2】:

      好问题!!示例数据的构造和解释都特别好。

      首先我会展示这个答案,然后我会逐步解释它。

      > ids = 1:3   # or from the data: unique(ds$ID)
      > pos = 1:6   # or from the data: unique(ds$Pos)
      > setkey(ds,ID,Pos)
      
      > ds[CJ(ids,pos), roll=-Inf, nomatch=0][, .N, by=Pos]
         Pos N
      1:   1 3
      2:   2 3
      3:   3 3
      4:   4 3
      5:   5 2
      6:   6 1
      > 
      

      这对您的大数据也应该非常有效。

      一步一步

      首先我尝试了交叉连接(CJ);即,对于每个位置的每列火车。

      > ds[CJ(ids,pos)]
          ID Pos  Obs
       1:  1   1 1.50
       2:  1   2   NA
       3:  1   3 2.50
       4:  1   4   NA
       5:  1   5 0.00
       6:  1   6 1.25
       7:  2   1   NA
       8:  2   2 1.45
       9:  2   3 1.50
      10:  2   4   NA
      11:  2   5 2.50
      12:  2   6   NA
      13:  3   1   NA
      14:  3   2 0.00
      15:  3   3 1.25
      16:  3   4 1.45
      17:  3   5   NA
      18:  3   6   NA
      

      我看到每列火车有 6 排。我看到3列火车。正如我所料,我有 18 行。我看到NA 没有看到那列火车。好的。查看。交叉连接似乎正在工作。现在让我们构建查询。

      您写道,如果在位置 n 观察到一列火车,它一定已经通过了之前的位置。我马上想到roll。让我们试试吧。

      ds[CJ(ids,pos), roll=TRUE]
          ID Pos  Obs
       1:  1   1 1.50
       2:  1   2 1.50
       3:  1   3 2.50
       4:  1   4 2.50
       5:  1   5 0.00
       6:  1   6 1.25
       7:  2   1   NA
       8:  2   2 1.45
       9:  2   3 1.50
      10:  2   4 1.50
      11:  2   5 2.50
      12:  2   6 2.50
      13:  3   1   NA
      14:  3   2 0.00
      15:  3   3 1.25
      16:  3   4 1.45
      17:  3   5 1.45
      18:  3   6 1.45
      

      嗯。这将每列火车的观察结果向前滚动。它为 2 号和 3 号列车在位置 1 留下了一些 NA,但是您说如果在 2 号位置观察到一列火车,它一定已经通过了 1 号位置。它还将 2 号和 3 号列车的最后一次观察结果向前滚动到 6 号位置,但是你说火车可能会爆炸。所以,我们要向后滚动!那是roll=-Inf。这是一个复杂的-Inf,因为您还可以控制向后滚动多远,但对于这个问题,我们不需要它;我们只想无限期地向后滚动。让我们试试roll=-Inf 看看会发生什么。

      > ds[CJ(ids,pos), roll=-Inf]
          ID Pos  Obs
       1:  1   1 1.50
       2:  1   2 2.50
       3:  1   3 2.50
       4:  1   4 0.00
       5:  1   5 0.00
       6:  1   6 1.25
       7:  2   1 1.45
       8:  2   2 1.45
       9:  2   3 1.50
      10:  2   4 2.50
      11:  2   5 2.50
      12:  2   6   NA
      13:  3   1 0.00
      14:  3   2 0.00
      15:  3   3 1.25
      16:  3   4 1.45
      17:  3   5   NA
      18:  3   6   NA
      

      这样更好。差不多了。我们现在需要做的就是数数。但是,那些讨厌的NA 是在 2 号和 3 号列车爆炸后出现的。让我们删除它们。

      > ds[CJ(ids,pos), roll=-Inf, nomatch=0]
          ID Pos  Obs
       1:  1   1 1.50
       2:  1   2 2.50
       3:  1   3 2.50
       4:  1   4 0.00
       5:  1   5 0.00
       6:  1   6 1.25
       7:  2   1 1.45
       8:  2   2 1.45
       9:  2   3 1.50
      10:  2   4 2.50
      11:  2   5 2.50
      12:  3   1 0.00
      13:  3   2 0.00
      14:  3   3 1.25
      15:  3   4 1.45
      

      顺便说一句,data.table 尽可能地喜欢在单个DT[...] 中,因为这就是它优化查询的方式。在内部,它不会创建 NA 然后删除它们;它从一开始就不会创建NA。这个概念对效率很重要。

      最后,我们要做的就是数数。我们可以将其作为复合查询添加到最后。

      > ds[CJ(ids,pos), roll=-Inf, nomatch=0][, .N, by=Pos]
         Pos N
      1:   1 3
      2:   2 3
      3:   3 3
      4:   4 3
      5:   5 2
      6:   6 1
      

      【讨论】:

      • +1 非常好的解决方案,甚至更好的解释。您能说一下随着数据变大,您希望这与ds[ , list( Pos = 1:Pos[.N] ) , by = ID ][ , .N , by = Pos ] 相比如何吗?
      • @SimonO'Hanlon 不错的选择。 Pos[.N] 将是一个新的长度为 1 的向量,传递给 : 函数以创建一个新的 1:Pos[.N] 向量。我希望所有这些小向量都会阻塞内存并导致更多垃圾收集。随着火车数量的增加,可能会比职位数量的增加咬更多(更多组)。如果你测试它,我对结果很感兴趣!
      • 我不太了解 data.table 的语法,但是 CJ 看起来很贵(从概念上讲,如果不是真的?);有没有像我这样的解决方案,其中 data.table 通过 ID 标识最大 Pos,即让我们快速到达nAtMax?也许这就是@SimonO'Hanlon 正在做的事情?
      • @MartinMorgan 是的,好点。也许:ds[,max(Pos),by=ID][,rev(cumsum(rev(tabulate(V1))))]。还为您的答案添加了更长的评论。
      • @MattDowle 很好的例子和很好的解释!帮助我更好地理解 data.table 的工作原理。很想在小插图中看到这一点。谢谢。
      【解决方案3】:

      data.table 听起来是一个很好的解决方案。从数据的排序方式可以找到每列火车的最大值

      maxPos = ds$Pos[!duplicated(ds$ID, fromLast=TRUE)]
      

      然后将到达该位置的火车制成表格

      nAtMax = tabulate(maxPos)
      

      并计算每个位置的列车累计总和,从末端开始计数

      rev(cumsum(rev(nAtMax)))
      ## [1] 3 3 3 3 2 1
      

      我认为这对于大数据来说会非常快,尽管内存效率不完全。

      【讨论】:

      • +1 我从问题和标题中得到的印象是,由于 Meep 解释说他的示例数据和任务非常精简,因此要求进行 data.table 演示。 rev(cumsum(rev(tabulate()))) 按照要求完成了确切的任务,但是如果火车从不同的点开始,观察值变得有趣,火车不再爆炸,或者还有卡车(2 列 ID)怎么办?这些是对 data.table 查询的简单更改(切换),而在基础上可能会有一些令人头疼的问题?
      • 感谢您的解决方案,它仍然比我的解决方案好得多! :) Matt 正确地暗示数据可能更复杂,这就是我接受他的回答的原因。如果你很好奇,我正在研究的实际上是 DNA 测序追踪数据,与火车完全没有关系 :)
      【解决方案4】:

      您可以尝试如下。为了更好地理解,我有目的地将其拆分为多步解决方案。您也可以通过链接[] 将所有这些组合为一个步骤。

      这里的逻辑是首先我们找到每个 ID 的最终位置。然后我们汇总数据以查找每个最终位置的 ID 计数。由于最终位置 6 的所有 ID 也应计入最终位置 5,因此我们使用 cumsum 将所有较高的 ID 计数添加到它们各自的较低 ID 中。

      ds2 <- ds[, list(FinalPos=max(Pos)), by=ID]
      
      ds2 
      ##    ID FinalPos
      ## 1:  1        6
      ## 2:  2        5
      ## 3:  3        4
      
      ds3 <- ds2[ , list(Count = length(ID)), by = FinalPos][order(FinalPos, decreasing=TRUE), list(FinalPos, Count = cumsum(Count))]
      
      ds3
      ##    FinalPos Count
      ## 1:        4     3
      ## 2:        5     2
      ## 3:        6     1
      
      setkey(ds3, FinalPos)
      
      ds3[J(c(1:6)), roll = 'nearest']
      
      ##    FinalPos Count
      ## 1:        1     3
      ## 2:        2     3
      ## 3:        3     3
      ## 4:        4     3
      ## 5:        5     2
      ## 6:        6     1
      

      【讨论】:

      • +1,很好地使用roll="nearest"。我不认为ds3 是必要的? - setkey(ds[, list(N=max(Pos)), keyby=ID], N)[J(1:6), roll="nearest"]
      • 稍微考虑一下,roll="nearest" 会给出错误的结果,例如,如果数据中根本不存在“6”并且您从 1:6 进行连接,不是它(将给出“2”而不是 NA 或 0)?
      猜你喜欢
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 2015-04-08
      • 1970-01-01
      • 2021-01-29
      • 1970-01-01
      • 2013-05-07
      相关资源
      最近更新 更多