Як застосувати ту саму функцію до кожного вказаного стовпця в data.table


85

У мене є таблиця даних. За допомогою якої я хотів би виконати ту саму операцію з певними стовпцями. Назви цих стовпців подані у векторному символі. У цьому конкретному прикладі я хотів би помножити всі ці стовпці на -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?

Відповіді:


150

Здається, це працює:

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(...).

EDIT : Ось ще один спосіб, який, мабуть, швидший, як згадував @Arun:

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

21
інший спосіб - використовувати 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 (збіги ("Sepal")), list (times_two = ~. * 2)), і він додасть "_times_two" до нових імен.
kennyB

1
Я не думаю, що це можливо, але насправді не впевнений у цьому.
hannes101,

це додало б стовпці з іменами out_cols, залишаючи colsна місці. Отже, вам потрібно буде їх усунути, явно 1) просячи лише log.a та log.b: прив’язати a [,.(outcols)]до кінця та повторно зберегти dtчерез <-. 2) зняти старі колони з ланцюжком [,c(cols):=NULL]. dt[,c(cols):=...]setnames(dt, cols, newcols)
Розв’язуючий ланцюг

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

11

ОНОВЛЕННЯ: Наступне - це чудовий спосіб зробити це без циклу for

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

як показано на діаграмі нижче

Діаграма_порівняння_продукції

Моя попередня відповідь: Наступне також працює

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

По суті, це те саме, що відповідь Френка з півтора року тому.
Дін Макгрегор,

1
Дякую, у відповіді Френка було використання набору. Коли я працюю з великими даними. Таблиці з мільйонами рядків, я бачу: = оператор перевершує функції
Орхан Челік

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
Fyi, "кожен зазначений стовпець" у заголовку означав, що запитувач зацікавлений у застосуванні його до підмножини стовпців (можливо, не до всіх).
Френк

1
@Frank впевнений! У цьому випадку ОП може виконувати dt [, c ("a", "b")] * (- 1).
amonk

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

здається, що необхідним новим синтаксисом є dt [, cols] <- dt [, ..cols] * (-1)
Артур Іп,

0

dplyrфункції працюють на data.tables, тож ось dplyrрішення, яке також "уникає for-loop" :)

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()

Створено 2020-10-16 пакетом reprex (v0.3.0)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.