将表达式传递给data.table中嵌套分组的方法

3

我有一个类似于这个的data.table对象

library(data.table)

c <- data.table(CO = c(10000,10000,10000,20000,20000,20000,20000),
                SH = c(1427,1333,1333,1000,1000,300,350),
                PRC = c(6.5,6.125,6.2,0.75,0.5,3,3.5),
                DAT = c(0.5,-0.5,0,-0.1,NA_real_,0.2,0.5),
                MM = c("A","A","A","A","A","B","B"))

我正在尝试使用嵌套分组进行计算,将表达式作为参数传递。以下是我的简化版本:

setkey(c,MM)

mycalc <- quote({nobscc <- length(DAT[complete.cases(DAT)]); 
                 list(MKTCAP = tail(SH,n=1)*tail(PRC,n=1),
                      SQSUM = ifelse(nobscc>=2, sum(DAT^2,na.rm=TRUE), NA_real_),
                      COVCOMP = ifelse(nobscc >= 2, head(DAT,n=1), NA_real_),
                      NOBS = nobscc)}) 


myresults <- c[,.SD[,{setkey=CO; eval(mycalc)},by=CO],by=MM]

它会产生

     MM    CO MKTCAP SQSUM COVCOMP NOBS
[1,]  A 10000 8264.6  0.50     0.5    3
[2,]  A 20000  500.0    NA      NA    1
[3,]  B 20000 1225.0  0.29     0.2    2

在上面的示例中,我有两个列表元素使用了ifelse结构(实际代码中有3个),所有的测试都相同:如果观察次数大于2,则需要执行某个计算(每个元素的计算不同,可以编写为函数),否则我希望这些元素的值为NA。这些元素共同的另一点是它们使用我的data.table的一个列:名为DAT的列。
所以我的问题是:是否有任何方法可以仅对ifelse测试进行一次测试,如果结果为FALSE,则将值NA传递给列表的各个元素,如果结果为TRUE,则针对列表的每个元素评估不同的表达式?
注意:我的目标是减少system.time(系统和经过的时间)。如果此修改不会减少时间和计算量,考虑到我有7200万个观察值,那么这是可以接受的答案。我也欢迎建议改变代码的其他部分。
编辑:summaryRprof()的结果
$by.total
                          total.time total.pct self.time self.pct
"system.time"                  18.94     99.79      0.00     0.00
".Call"                        18.92     99.68      0.10     0.53
"["                            18.92     99.68      0.04     0.21
"[.data.table"                 18.92     99.68      0.02     0.11
"eval"                         18.80     99.05      0.24     1.26
"ifelse"                       18.30     96.42      0.46     2.42
"lm"                           17.70     93.26      0.58     3.06
"sapply"                        8.06     42.47      0.36     1.90
"model.frame"                   7.74     40.78      0.16     0.84
"model.frame.default"           7.58     39.94      0.98     5.16
"lapply"                        6.62     34.88      0.70     3.69
"FUN"                           4.24     22.34      1.10     5.80
"model.matrix"                  4.04     21.29      0.02     0.11
"model.matrix.default"          4.02     21.18      0.26     1.37
"match"                         3.66     19.28      0.86     4.53
".getXlevels"                   3.12     16.44      0.12     0.63
"na.omit"                       2.40     12.64      0.24     1.26
"%in%"                          2.30     12.12      0.34     1.79
"simplify2array"                2.24     11.80      0.12     0.63
"na.omit.data.frame"            2.16     11.38      0.14     0.74
"[.data.frame"                  2.12     11.17      1.18     6.22
"deparse"                       1.80      9.48      0.66     3.48
"unique"                        1.80      9.48      0.54     2.85
"[["                            1.52      8.01      0.12     0.63
"[[.data.frame"                 1.40      7.38      0.54     2.85
".deparseOpts"                  1.34      7.06      0.96     5.06
"paste"                         1.32      6.95      0.16     0.84
"lm.fit"                        1.20      6.32      0.64     3.37
"mode"                          1.14      6.01      0.14     0.74
"unlist"                        1.12      5.90      0.56     2.95

1
我不明白你使用双重 by 的结果,但是我用这个更简单的查询可以得到完全相同的结果:c[,eval(mycalc),by=list(MM,CO)]。问题描述是否完全正确?例如,我不明白 setkey=CO 是做什么的。 - Matt Dowle
既然问题是要减少时间,请发布Rprof()的结果。 - Matt Dowle
@MatthewDowle 我不确定Rprof是什么,但我会查一下并发布它。关于by=list(MM,CO),我曾尝试过将by放在一起,但由于某些MM中并非所有CO都存在,因此出现了错误。但我确信这是因为缺少某些东西,比如列表(keys)。 - Vivi
我为嵌套分组感到非常自豪... - Vivi
1个回答

4

不要像这样形成和操作数据子集:

setkey(c,MM)
myresults <- c[, .SD[,{setkey=CO; eval(mycalc)},by=CO], by=MM]

你可以尝试这样做:
setkeyv(c, c("MM", "CO"))
myresults <- c[, eval(mycalc), by=key(c)]

这应该可以加快您的代码,因为它避免了所有嵌套的 .SD 对象子集,每个对象都需要自己调用 [.data.table]。
关于您最初的问题,我怀疑 ifelse 评估并没有花费太多时间,但如果您想避免它们,您可以将它们从 mycalc 中删除,并使用 := 用 NA 覆盖所需的值:
mycalc <- quote(list(MKTCAP = tail(SH,n=1)*tail(PRC,n=1),
                      SQSUM = sum(DAT^2,na.rm=TRUE),
                      COVCOMP = head(DAT,n=1),
                      NOBS = length(DAT[complete.cases(DAT)]))) 
setkeyv(c, c("MM", "CO"))
myresults <- c[, eval(mycalc), by=key(c)]


myresults[NOBS<2, c("SQSUM", "COVCOMP"):=NA_real_]
## Or, alternatively
# myresults[NOBS<2, SQSUM:=NA_real_]
# myresults[NOBS<2, COVCOMP:=NA_real_]

刚在我的评论后看到这个。你看到 setkey=CO 是干什么用的了吗? - Matt Dowle
答案的第一部分对system.time没有任何影响。我使用我的数据子集(50年观测中的一年)进行测试,使用我的版本得到了27.416 0.016 27.431,使用您的版本得到了26.786 0.018 26.803。但我更喜欢您的方法(我曾尝试过类似的方法,但失败了,最终采用了嵌套分组)。 - Vivi
第二部分应该增加时间,对吧?因为它会强制 R 每次都计算东西,而在某些情况下,它会跳过计算并直接转到 NA。此外,测试是必要的,因为当观察值少于一定数量时,尝试计算某些值时会出现错误(例如,covcomp 包括 RET 的滞后值。如果我只有一个观察值,则滞后值不存在。我可以绕过它,但这会使问题更加复杂)。 - Vivi
@MatthewDowle -- 我认为这是早期尝试让 by=CO 部分工作的残留物。顺便问一下,c("SQSUM","COVCMP"):=NA_REAL_ 这部分是否比两个单独的 := 调用对速度更快? 快两倍?此外,如果可能的话,避免引用 .SD 是快速 data.table 代码的一般原则吗? - Josh O'Brien
1
@Vivi 最后一点。鉴于对 lm() 的调用占用了93%的时间,看起来我给你的第一部分实际上可以将与 data.table 相关的部分加速35%或更多。 - Josh O'Brien
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接