【问题标题】:Row operations in data.table using `by = .I`使用 `by = .I` 在 data.table 中进行行操作
【发布时间】:2016-06-06 21:43:10
【问题描述】:

这是关于row operations in 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 ]

【问题讨论】:

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

标签: r data.table


【解决方案1】:

注意:此答案的第 (3) 部分已于 2019 年 4 月更新,因为随着时间的推移 data.table 发生了许多变化,原始版本已过时。此外,从 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) 虽然我们已经在第 1 部分中看到,在 rowSums 的情况下,它已经有效地逐行循环,但还有比创建 rowpos 列更快的方法。但是,如果我们没有快速的逐行函数,那么循环呢?

by = rowposby = 1:NROW(dt) 版本与for 循环与set() 进行基准比较在这里提供了丰富的信息。我们发现在 for 循环中循环 set 比使用 data.table 的 by 参数进行循环的任何一种方法都慢。但是,创建附加列的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)已经按行操作的情况下,也有使用 rowpos 列的替代方法,虽然速度不快,不需要创建冗余列。

【讨论】:

  • 很好的答案!我很想知道你为什么使用NROW 而不是nrow?我认为它们在这种情况下是等价的,但不确定我是否遗漏了一些微妙之处。
  • @Lyngbakr 没有真正的微妙之处-两者都可以。我更喜欢NROW,因为它也可以推广到向量。我过去曾见过使用列名或位置向量选择列的情况,如果只指定了一个列,则可能会无意中返回向量而不是 data.frame 或 data.table。 NROW 捕获这些边缘情况,但 nrow 没有。所以我倾向于坚持使用NROW 作为一般政策,除非我特别想要 nrow 行为。
  • 很高兴知道。感谢您的见解!
  • 无论如何,by=-I 将被包括在内:github.com/Rdatatable/data.table/pull/5235
  • 感谢@skan 提供的信息 - 一旦它进入发布版本,我将更新此答案并附上有关更改的说明。
猜你喜欢
  • 2020-06-10
  • 2017-02-15
  • 1970-01-01
  • 2014-03-06
  • 2011-12-14
  • 1970-01-01
  • 2014-11-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多