Как применить одну и ту же функцию к каждому указанному столбцу в таблице data.table


86

У меня есть таблица данных, с которой я хотел бы выполнить ту же операцию с определенными столбцами. Имена этих столбцов даны в векторе символов. В этом конкретном примере я хотел бы умножить все эти столбцы на -1.

Некоторые данные игрушки и вектор, определяющий соответствующие столбцы:

library(data.table)
dt <- data.table(a = 1:3, b = 1:3, d = 1:3)
cols <- c("a", "b")

Прямо сейчас я делаю это так, перебирая вектор символов:

for (col in 1:length(cols)) {
   dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
}

Есть ли способ сделать это напрямую без цикла for?

Ответы:


151

Кажется, это работает:

dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols]

Результат

    a  b d
1: -1 -1 1
2: -2 -2 2
3: -3 -3 3

Здесь есть несколько хитростей:

  • Поскольку в скобках (cols) :=заключены круглые скобки , результат присваивается столбцам, указанным в cols, а не какой-то новой переменной с именем «cols».
  • .SDcolsговорит вызов , который мы только глядя на эти колонны, и позволяет нам использовать .SD, тем Subset из Dата , связанных с этими столбцами.
  • lapply(.SD, ...)работает на .SD, который представляет собой список столбцов (как и все data.frames и data.tables). lapplyвозвращает список, так что в итоге jвыглядит так cols := list(...).

РЕДАКТИРОВАТЬ : Вот еще один способ, который, вероятно, быстрее, как упоминал @Arun:

for (j in cols) set(dt, j = j, value = -dt[[j]])

22
другой способ - использовать setс for-loop. Я подозреваю, что это будет быстрее.
Арун

3
@Arun Я отредактировал. Вы это имели в виду? Раньше не использовал set.
Фрэнк

8
+1 Отличный ответ. Да, я тоже предпочитаю forцикл с setтакими случаями.
Мэтт Доул

2
Да, использование set()кажется быстрее, ~ в 4 раза быстрее для моего набора данных! Удивительный.
Константинос

2
Спасибо, @JamesHirschorn. Я не уверен, но подозреваю, что при таком способе разбиения столбцов на подмножества накладных расходов больше, чем при использовании .SD, который в любом случае является стандартной идиомой, появляющейся во вступительной виньетке github.com/Rdatatable/data.table/wiki/Getting-started Я думаю, что отчасти причина этой идиомы в том, чтобы не вводить имя таблицы дважды.
Фрэнк

20

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

cols <- c("a", "b")
out_cols = paste("log", cols, sep = ".")
dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols]

1
Есть ли способ изменить имена на основе правила? В dplyr, например, вы можете сделать iris%>% mutate_at (vars (match ("Sepal")), list (times_two = ~. * 2)), и он будет добавлять "_times_two" к новым именам.
kennyB

1
Я не думаю, что это возможно, но не совсем уверен в этом.
hannes101

это добавит столбцы с именами out_cols, оставаясь при этом colsна месте. Итак, вам нужно будет устранить их, либо явно 1) запросив только log.a и log.b: связать a [,.(outcols)]до конца и повторно сохранить в dtvia <-. 2) удалить старые колонны с цепочкой [,c(cols):=NULL]. За решением без цепочки 3) dt[,c(cols):=...]следуетsetnames(dt, cols, newcols)
mpag

@mpag, да, это правда, но для моего случая использования эмпирических исследований в большинстве случаев мне нужны оба ряда в наборе данных.
hannes101

11

ОБНОВЛЕНИЕ: Ниже приведен изящный способ сделать это без цикла.

dt[,(cols):= - dt[,..cols]]

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

mbm = microbenchmark(
  base = for (col in 1:length(cols)) {
    dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
  },
  franks_solution1 = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
  franks_solution2 =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
  hannes_solution = dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols],
  orhans_solution = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
  orhans_solution2 = dt[,(cols):= - dt[,..cols]],
  times=1000
)
mbm

Unit: microseconds
expr                  min        lq      mean    median       uq       max neval
base_solution    3874.048 4184.4070 5205.8782 4452.5090 5127.586 69641.789  1000  
franks_solution1  313.846  349.1285  448.4770  379.8970  447.384  5654.149  1000    
franks_solution2 1500.306 1667.6910 2041.6134 1774.3580 1961.229  9723.070  1000    
hannes_solution   326.154  405.5385  561.8263  495.1795  576.000 12432.400  1000
orhans_solution  3747.690 4008.8175 5029.8333 4299.4840 4933.739 35025.202  1000  
orhans_solution2  752.000  831.5900 1061.6974  897.6405 1026.872  9913.018  1000

как показано в таблице ниже

performance_comparison_chart

Мой предыдущий ответ: Следующее также работает

for (j in cols)
  dt[,(j):= -1 * dt[,  ..j]]

По сути, это то же самое, что и ответ Фрэнка полтора года назад.
Дин МакГрегор

1
Спасибо, Фрэнк ответил с помощью набора. Когда я работаю с большими таблицами data.table с миллионами строк, я вижу: оператор = превосходит функции
Орхан Челик

2
Причина, по которой я добавил ответ на старый вопрос, заключается в следующем: у меня тоже была похожая проблема, я наткнулся на этот пост с поиском в Google. Впоследствии я нашел решение своей проблемы, и я вижу, что оно применимо и здесь. На самом деле в моем предложении используется новая функция data.table, которая доступна в новых версиях библиотеки, которой не существовало на момент вопроса. Я подумал, что это хорошая идея, потому что другие люди, столкнувшиеся с подобной проблемой, попадут сюда с помощью поиска Google.
Орхан Челик

1
Вы проводите сравнительный анализ с dtсостоящим из 3 строк?
Уве

3
Ответ Ханнеса - это другое вычисление, поэтому его не следует сравнивать с другими, верно?
Фрэнк

2

Ни одно из вышеперечисленных решений не работает с расчетом по группе. Вот лучшее, что у меня есть:

for(col in cols)
{
    DT[, (col) := scale(.SD[[col]], center = TRUE, scale = TRUE), g]
}

1

Чтобы добавить пример создания новых столбцов на основе строкового вектора столбцов. На основании ответа Jfly:

dt <- data.table(a = rnorm(1:100), b = rnorm(1:100), c = rnorm(1:100), g = c(rep(1:10, 10)))

col0 <- c("a", "b", "c")
col1 <- paste0("max.", col0)  

for(i in seq_along(col0)) {
  dt[, (col1[i]) := max(get(col0[i])), g]
}

dt[,.N, c("g", col1)]

0
library(data.table)
(dt <- data.table(a = 1:3, b = 1:3, d = 1:3))

Hence:

   a b d
1: 1 1 1
2: 2 2 2
3: 3 3 3

Whereas (dt*(-1)) yields:

    a  b  d
1: -1 -1 -1
2: -2 -2 -2
3: -3 -3 -3

1
К сведению, «каждый указанный столбец» в заголовке означает, что запрашивающий был заинтересован в применении его к подмножеству столбцов (возможно, не ко всем).
Фрэнк

1
@ Фрэнк, конечно! В этом случае OP может выполнить dt [, c («a», «b»)] * (- 1).
amonk

1
Что ж, давайте будем полными и скажемdt[, cols] <- dt[, cols] * (-1)
Грегор Томас

похоже, что новый синтаксис требуется: dt [, cols] <- dt [, ..cols] * (-1)
Артур Йип

0

dplyrфункции работают с data.tables, поэтому вот dplyrрешение, которое также "избегает цикла for" :)

dt %>% mutate(across(all_of(cols), ~ -1 * .))

Я протестированные его с помощью кода Орхана (добавление строк и столбцов) , и вы увидите , dplyr::mutateс acrossосновном выполняется быстрее , чем большинство других решений и медленнее , чем data.table решения с использованием lapply.

library(data.table); library(dplyr)
dt <- data.table(a = 1:100000, b = 1:100000, d = 1:100000) %>% 
  mutate(a2 = a, a3 = a, a4 = a, a5 = a, a6 = a)
cols <- c("a", "b", "a2", "a3", "a4", "a5", "a6")

dt %>% mutate(across(all_of(cols), ~ -1 * .))
#>               a       b      d      a2      a3      a4      a5      a6
#>      1:      -1      -1      1      -1      -1      -1      -1      -1
#>      2:      -2      -2      2      -2      -2      -2      -2      -2
#>      3:      -3      -3      3      -3      -3      -3      -3      -3
#>      4:      -4      -4      4      -4      -4      -4      -4      -4
#>      5:      -5      -5      5      -5      -5      -5      -5      -5
#>     ---                                                               
#>  99996:  -99996  -99996  99996  -99996  -99996  -99996  -99996  -99996
#>  99997:  -99997  -99997  99997  -99997  -99997  -99997  -99997  -99997
#>  99998:  -99998  -99998  99998  -99998  -99998  -99998  -99998  -99998
#>  99999:  -99999  -99999  99999  -99999  -99999  -99999  -99999  -99999
#> 100000: -100000 -100000 100000 -100000 -100000 -100000 -100000 -100000

library(microbenchmark)
mbm = microbenchmark(
  base_with_forloop = for (col in 1:length(cols)) {
    dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
  },
  franks_soln1_w_lapply = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
  franks_soln2_w_forloop =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
  orhans_soln_w_forloop = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
  orhans_soln2 = dt[,(cols):= - dt[,..cols]],
  dplyr_soln = (dt %>% mutate(across(all_of(cols), ~ -1 * .))),
  times=1000
)

library(ggplot2)
ggplot(mbm) +
  geom_violin(aes(x = expr, y = time)) +
  coord_flip()

Создано 16.10.2020 пакетом REPEX (v0.3.0)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.