使用`by = .I`进行数据表中的行操作

18

这里是一个关于data.table中行操作的好的SO解释:(链接)

我想到了一个替代方案,就是为每一行使用唯一的id,然后使用by参数应用函数。像这样:

library(data.table)

dt <- data.table(V0 =LETTERS[c(1,1,2,2,3)],
                 V1=1:5,
                 V2=3:7,
                 V3=5:1)

# create a column with row positions
dt[, rowpos := .I]

# calculate standard deviation by row
dt[ ,  sdd := sd(.SD[, -1, with=FALSE]), by = rowpos ] 

问题:

  1. 是否有不使用此方法的充分理由?或许还有其他更高效的选择吗?

  2. 为什么使用 by = .I 不能达到相同的效果?

    dt[ , sdd := sd(.SD[, -1, with=FALSE]), by = .I ]


2
对于这种情况,您可以使用Reduce("+", dt[, 2:4, with = FALSE])来(1)不按行循环,(2)不转换为“矩阵”。对于其他按行操作,您可以考虑类似于Reduce操作,以避免将函数应用到每一行上,或者-也许-将数据存储为“矩阵”,并使用特定/高效的“矩阵”函数。 - alexis_laz
2
通过查看这里这里,使用sd的一个选项似乎是sqrt(rowSums((dt[, 2:4, with = FALSE] - Reduce("+", dt[, 2:4, with = FALSE]) / 3) ^ 2) / (3 - 1)) - alexis_laz
我不知道为什么 by=.I 不会出错,但它与 1:nrow(dt) 不等价 - 如果我是你,我会提交一个错误报告。 - eddi
感谢@eddi,我刚刚已经提交了它。https://github.com/Rdatatable/data.table/issues/1732 - rafa.pereira
1个回答

26

更新:

自data.table 1.4.3版本以后,已经实现了按行分组by=.I,可以按照OP的预期工作。请注意,使用by=.I将在数据表中创建一个名为I的新列,其中包含行号。然后可以根据需要保留或删除行号列。

本答案的以下部分记录了适用于旧版本data.table的早期版本。如果有人仍在使用旧版本,则可以保留它作为参考。


注意: 由于data.table随时间的许多变化使原始版本过时,因此本答案第(3)部分已于2019年4月进行更新。 此外,已从所有data.table实例中删除了with=参数的使用,因为它已被弃用。

1) 首先,至少对于rowsums的示例而言,不使用它的一个原因是性能和创建不必要的列。与以下选项f2相比,后者几乎快4倍,而且不需要rowpos列 (请注意,原始问题使用rowSums作为示例函数,这部分回答针对该函数。 OP之后编辑了问题,为此第3部分更为相关):

dt <- data.table(V0 =LETTERS[c(1,1,2,2,3)], V1=1:5, V2=3:7, V3=5:1)
f1 <- function(dt){
  dt[, rowpos := .I] 
  dt[ ,  sdd := rowSums(.SD[, 2:4]), by = rowpos ] }
f2 <- function(dt) dt[, sdd := rowSums(.SD), .SDcols= 2:4]

library(microbenchmark)
microbenchmark(f1(dt),f2(dt))
# Unit: milliseconds
#   expr      min       lq     mean   median       uq      max neval cld
# f1(dt) 3.669049 3.732434 4.013946 3.793352 3.972714 5.834608   100   b
# f2(dt) 1.052702 1.085857 1.154132 1.105301 1.138658 2.825464   100  a 

2) 对于您的第二个问题,虽然dt[, sdd := sum(.SD[, 2:4]), by = .I]无法工作,但dt[, sdd := sum(.SD[, 2:4]), by = 1:NROW(dt)]可以完美地工作。根据?data.table 中的说明,“.I 是一个整数向量,等于 seq_len(nrow(x))”,人们可能会认为它们是等效的。然而,区别在于.I用于j中,而不是用于by中。请注意,.I的值是在data.table内部计算的,因此无法提前作为参数值传递,例如by=.I

可能还期望by = .I应该只会引发错误。但这并不会发生,因为加载data.table包会在data.table命名空间中创建一个对象.I,可从全局环境访问,其值为NULL。您可以通过在命令提示符处键入.I来测试此内容。(请注意,相同的原理适用于.SD.EACHI.N.GRP.BY

.I
# Error: object '.I' not found
library(data.table)
.I
# NULL
data.table::.I
# NULL

这意味着 by = .I 的行为等同于 by = NULL

3) 尽管我们已经在第一部分中看到,在rowSums的情况下,由于其已经有效地循环遍历行,因此有比创建rowpos列更快的方法。但是当我们没有快速的逐行函数时该怎么办?

对比使用by=rowposby=1:NROW(dt) 的版本与使用set()for循环可以了解到性能差异。我们发现,在使用data.tableby参数进行循环的两种方法中,循环setfor 循环速度较慢。然而,在创建附加列的by循环和使用seq_len(NROW(dt))的循环之间的时间差异微乎其微。在没有性能差异的情况下,似乎f.nrow可能更可取,但仅基于更简洁且不创建不必要的列。

dt <- data.table(V0 = rep(LETTERS[c(1,1,2,2,3)], 1e3), V1=1:5, V2=3:7, V3=5:1)

f.rowpos <- function() {
  dt[, rowpos := .I] 
  dt[,  sdd := sum(.SD[, 2:4]), by = rowpos ] 
}

f.nrow <- function() {
  dt[, sdd := sum(.SD[, 2:4]), by = seq_len(NROW(dt)) ]
}

f.forset<- function() {
  for (i in seq_len(NROW(dt))) set(dt, i, 'sdd', sum(dt[i, 2:4]))
}

microbenchmark(f.rowpos(),f.nrow(), f.forset(), times = 5)
# Unit: milliseconds
#       expr       min        lq      mean    median        uq       max neval
# f.rowpos()  559.1115  575.3162  580.2853  578.6865  588.5532  599.7591     5
#   f.nrow()  558.4327  582.4434  584.6893  587.1732  588.6689  606.7282     5
# f.forset() 1172.6560 1178.8399 1298.4842 1255.4375 1292.7393 1592.7486     5

总之,即使在没有像rowSums这样已经按行操作的优化函数的情况下,也有使用不需要创建多余列的替代方法,虽然速度不一定更快。


很棒的回答!我很好奇你为什么使用NROW而不是nrow?在这种情况下,我认为它们是等价的,但我不确定是否有我忽略的微妙之处。 - Dan
1
@Lyngbakr 没有真正的微妙之处 - 两者都可以。我倾向于使用 NROW,因为它也适用于向量。过去我曾看到这样的情况:如果只指定了一个列,则使用列名或位置的向量选择列可能会无意中返回一个向量而不是 data.frame 或 data.table。NROW 可以捕获这些边缘情况,但 nrow 不行。因此,除非我特别想要 nrow 的行为,否则我通常会坚持使用 NROW 作为一般策略。 - dww
好的,知道了。谢谢你的见解! - Dan
无论如何,by=-I将被包含在内: https://github.com/Rdatatable/data.table/pull/5235 - skan
感谢@skan提供的信息 - 一旦它被纳入发布版本,我会更新这个答案并注明更改。 - dww

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