使用lapply()优雅地在data.table中分配多个列

时间:2013-06-05 15:29:19

标签: r data.table

我试图通过应用共享函数,找出一种使用:=赋值在data.table中一次替换多列的优雅方法。这种情况的典型用法可能是将字符串函数(例如gsub)应用于表中的所有字符列。将data.frame这样做的方式扩展到data.table并不困难,但我正在寻找一种符合data.table做事方式的方法。

例如:

library(data.table)

m <- matrix(runif(10000), nrow = 100)
df <- df1 <- df2 <- df3 <- as.data.frame(m)
dt <- as.data.table(df)
head(names(df))
head(names(dt))

## replace V20-V100 with sqrt

# data.frame approach
# by column numbers
df1[20:100] <- lapply(df1[20:100], sqrt)
# by reference to column numbers
v <- 20:100
df2[v] <- lapply(df2[v], sqrt)
# by reference to column names
n <- paste0("V", 20:100)
df3[n] <- lapply(df3[n], sqrt)

# data.table approach
# by reference to column names
n <- paste0("V", 20:100)
dt[, n] <- lapply(dt[, n, with = FALSE], sqrt)

我知道使用:=循环遍历列名称向量来分配更有效:

for (col in paste0("V", 20:100)) dt[, col := sqrt(dt[[col]]), with = FALSE]

我不喜欢这样,因为我不喜欢在data.table表达式中引用j。我也知道,如果我知道列名,我可以使用:=分配lapply

dt[, c("V20", "V30", "V40", "V50", "V60") := lapply(list(V20, V30, V40, V50, V60), sqrt)]

(您可以通过构建具有未知列名称的表达式来扩展它。)

以下是我在此尝试的想法,但我无法让它们发挥作用。我犯了一个错误,还是我还缺少另一种方法?

# possible data.table approaches?
# by reference to column names; assignment works, but not lapply
n <- paste0("V", 20:100)
dt[, n := lapply(n, sqrt), with = FALSE]
# by (smaller for example) list; lapply works, but not assignment
dt[, list(list(V20, V30, V40, V50, V60)) := lapply(list(V20, V30, V40, V50, V60), sqrt)]
# by reference to list; neither assignment nor lapply work
l <- parse(text = paste("list(", paste(paste0("V", 20:100), collapse = ", "), ")"))
dt[, eval(l) := lapply(eval(l), sqrt)]

3 个答案:

答案 0 :(得分:35)

是的,你在这里有问题:

  

我知道使用:=循环遍历列名称向量来分配更有效:

     

for (col in paste0("V", 20:100)) dt[, col := sqrt(dt[[col]]), with = FALSE]

除此之外:请注意,新方法是:

for (col in paste0("V", 20:100))
  dt[ , (col) := sqrt(dt[[col]])]

因为with = FALSE无论是指{L}还是:=的RHS,都不容易阅读。放在一边。

如您所知,这是有效的,因为每列都会逐一进行,因此一次只需要一列工作内存。这可以在它工作之间产生影响,并且由于可怕的内存不足错误而失败。

lapply的RHS :=的问题是首先评估RHS(lapply);即,创建了80列的结果。这个80列的新内存值必须分配和填充。因此,您需要80列的空闲RAM才能成功完成该操作。 RAM使用占主导地位的是后续即将将这80个新列分配( plonking )到data.table的列指针槽中的操作。

正如@Frank指出的那样,如果你有很多列(比如10,000或更多),那么派遣到[.data.table方法的小开销就会开始累加起来。为了消除data.table::set ?set下的:=被描述为&#34; loopable&#34; for。我使用for (col in paste0("V", 20:100)) set(dt, j = col, value = sqrt(dt[[col]])) 循环进行此类操作。它是最快的方式,并且相当容易编写和阅读。

set

虽然只有80列,但它不太重要。 (注意,对于大量行而不是大量列循环set可能更常见。)但是,循环dt并不能解决重复引用的问题。您在问题中提到的:=符号名称:

  

我不喜欢这样,因为我不喜欢在j表达式中引用data.table。

同意。因此,我能做的最好的事情是恢复为get的循环,但改为使用for (col in paste0("V", 20:100)) dt[, (col) := sqrt(get(col))]

get

但是,我担心在j中使用get()会产生开销。 #1380中的基准测试。另外,在RHS上使用get()可能会令人困惑,但在LHS上却没有。为了解决这个问题,我们可以为LHS加糖并允许for (col in paste0("V", 20:100)) dt[, get(col) := sqrt(get(col))] #1381

value

此外,set的{​​{1}}可能会在DT#1382的范围内运行。

for (col in paste0("V", 20:100))
  set(dt, j = col, value = sqrt(get(col))

答案 1 :(得分:14)

如果要按字符串名称引用列,则这些应该有效:

n = paste0("V", 20:100)
dt[, (n) := lapply(n, function(x) {sqrt(get(x))})]

dt[, (n) := lapply(n, function(x) {sqrt(dt[[x]])})]

答案 2 :(得分:7)

这是你在找什么?

dt[ , names(dt)[20:100] :=lapply(.SD, function(x) sqrt(x) ) , .SDcols=20:100]

我听说过使用.SD并不是那么有效,因为它预先复制了表格,但是如果你的表格不是巨大的(显然这是相对的,取决于你的系统规格)我怀疑它会产生很大的不同。