Элегантное назначение нескольких столбцов в data.table с помощью lapply()

Я пытаюсь найти элегантный способ использования назначения := для замены многих столбцов сразу в 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)]

Ответ 1

Да, вы здесь под вопросом:

Я понимаю, что более эффективно перебирать вектор имен столбцов с помощью := для назначения:

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 было непросто прочитать, относится ли это к LHS или RHS :=. Отойдите в сторону.

Как вы знаете, это эффективно, потому что каждый столбец один за другим, поэтому рабочая память нужна только для одного столбца за раз. Это может сделать разницу между работой и ошибкой с ошибкой из-за недостатка памяти.

Проблема с lapply в RHS := заключается в том, что сначала оценивается RHS (lapply); то есть результат для 80 столбцов. Это 80 столбцов новой памяти, которая должна быть распределена и заполнена. Таким образом, для достижения этой цели вам понадобится 80-разрядная оперативная память. Это использование ОЗУ доминирует против мгновенной операции назначения (наложения) этих 80 новых столбцов в слоты указателя столбца data.table.

Как отметил @Frank, если у вас много столбцов (например, 10 000 или более), тогда небольшие накладные расходы на отправку методу [.data.table начинают складываться). Чтобы устранить эти служебные данные, которые есть data.table::set, которые под ?set описываются как "закодированные" :=. Я использую цикл for для этого типа операции. Это самый быстрый способ и довольно легко писать и читать.

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

Хотя всего 80 столбцов, это вряд ли имеет значение. (Обратите внимание, что это может быть более распространено для цикла set для большого количества строк, чем большое количество столбцов.) Однако looped set не решает проблему повторной ссылки на имя символа dt, которое вы упомянули в вопросе:

Мне это не нравится, потому что мне не нравится ссылаться на data.table в выражении j.

Согласен. Поэтому лучше всего я вернусь к вашему циклу :=, но вместо этого использую get.

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

Однако я опасаюсь, что использование get в j может быть неэффективным. Для этого необходим бенчмаркинг, # 1380. Кроме того, возможно, смущает использование get() в RHS, но не на LHS. Чтобы решить, что мы можем сахара LHS и разрешить get(), # 1381:

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

Кроме того, возможно, value of set может выполняться в пределах dt, # 1382.

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

Ответ 2

Они должны работать, если вы хотите ссылаться на столбцы по имени строки:

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

или

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

Ответ 3

Это то, что вы ищете?

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

Я слышал, что использование .SD не так эффективно, потому что оно делает копию таблицы заранее, но если ваша таблица не огромна (очевидно, что относительная в зависимости от ваших системных спецификаций), я сомневаюсь, что она сделает много разницы.