Jak zastosować tę samą funkcję do każdej określonej kolumny w data.table

85

Mam tabelę danych, z którą chciałbym wykonać tę samą operację na niektórych kolumnach. Nazwy tych kolumn są podane w wektorze znakowym. W tym konkretnym przykładzie chciałbym pomnożyć wszystkie te kolumny przez -1.

Niektóre dane zabawki i wektor określający odpowiednie kolumny:

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

W tej chwili robię to w ten sposób, zapętlając wektor znaków:

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

Czy można to zrobić bezpośrednio, bez pętli for?

Dean MacGregor
źródło

Odpowiedzi:

150

To wydaje się działać:

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

Wynik to

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

Jest tutaj kilka sztuczek:

  • Ponieważ w programie znajdują się nawiasy (cols) :=, wynik jest przypisywany do kolumn określonych w colszamiast do jakiejś nowej zmiennej o nazwie „cols”.
  • .SDcolsopowiada połączenie, które patrzymy tylko na tych kolumn, a także pozwala nam wykorzystać .SD, tym Subset z DATA związanego z tymi kolumnami.
  • lapply(.SD, ...)działa na .SD, która jest listą kolumn (podobnie jak wszystkie data.frames i data.tables). lapplyzwraca listę, więc na końcu jwygląda jak cols := list(...).

EDYCJA : Oto inny sposób, który jest prawdopodobnie szybszy, jak wspomniał @Arun:

for (j in cols) set(dt, j = j, value = -dt[[j]])
Szczery
źródło
21
innym sposobem jest użycie setz rozszerzeniem for-loop. Podejrzewam, że będzie szybciej.
Arun
3
@Arun Dokonałem edycji. Czy to miałeś na myśli? Nie używałem setwcześniej.
Frank,
8
+1 Świetna odpowiedź. Tak, wolę też forpętlę z settakimi przypadkami.
Matt Dowle
2
Tak, używanie set()wydaje się szybsze, ~ 4 razy szybsze w przypadku mojego zbioru danych! Niesamowity.
Konstantinos,
2
Dzięki, @JamesHirschorn. Nie jestem pewien, ale podejrzewam, że jest więcej narzutów związanych z podziałem kolumn w ten sposób, zamiast używania .SD, który i tak jest standardowym idiomem, pojawiającym się w winiecie intro github.com/Rdatatable/data.table/wiki/Getting-started Myślę, że jednym z powodów tego idiomu jest unikanie dwukrotnego wpisywania nazwy tabeli.
Frank
20

Chciałbym dodać odpowiedź, gdy chciałbyś zmienić również nazwy kolumn. Jest to bardzo przydatne, jeśli chcesz obliczyć logarytm wielu kolumn, co często ma miejsce w pracach empirycznych.

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]
hannes101
źródło
1
Czy istnieje sposób na zmianę nazw na podstawie reguły? Na przykład w dplyr możesz wykonać iris%>% mutate_at (vars (match ("Sepal")), list (times_two = ~. * 2)) i doda on "_times_two" do nowych nazw.
kennyB
1
Nie sądzę, żeby to było możliwe, ale nie jestem tego pewien.
hannes101
spowodowałoby to dodanie kolumn z nazwami out_cols, pozostawiając nadal colsna miejscu. Tak więc, musisz je wyeliminować, albo jawnie 1) prosząc o podanie tylko log.a i log.b: łańcuch a [,.(outcols)]do końca i ponownie zapisz na dtvia <-. 2) usuń stare kolumny za pomocą łańcucha [,c(cols):=NULL]. Po rozwiązaniu bez łańcucha 3) dt[,c(cols):=...]następujesetnames(dt, cols, newcols)
mpag
@mpag, tak, to prawda, ale w moim przypadku badań empirycznych przez większość czasu potrzebuję obu serii w zestawie danych.
hannes101
11

AKTUALIZACJA: Następujące jest zgrabnym sposobem na zrobienie tego bez pętli for

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

Jest to zgrabny sposób na łatwą czytelność kodu. Ale jeśli chodzi o wydajność, pozostaje w tyle za rozwiązaniem Franka, zgodnie z poniższym wynikiem microbenchmark

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

jak pokazano na poniższym wykresie

performance_comparison_chart

Moja poprzednia odpowiedź: Działa również następująca

for (j in cols)
  dt[,(j):= -1 * dt[,  ..j]]
Orhan Celik
źródło
To w zasadzie to samo, co odpowiedź Franka sprzed półtora roku.
Dean MacGregor
1
Dzięki, odpowiedzią Franka było użycie zestawu. Kiedy pracuję z dużymi danymi.table z milionami wierszy, widzę: = operator przewyższa funkcje
Orhan Celik
2
Powód, dla którego dodałem odpowiedź na stare pytanie, jest następujący: ja też miałem podobny problem, natknąłem się na ten post z wyszukiwarką google. Później znalazłem rozwiązanie mojego problemu i widzę, że dotyczy to również tutaj. Właściwie moja sugestia wykorzystuje nową funkcję data.table, która jest dostępna w nowych wersjach biblioteki, która nie istniała w czasie pytania. Pomyślałem, że to dobry pomysł, aby się podzielić, myśląc, że inni z podobnymi problemami trafią tutaj z wyszukiwarką Google.
Orhan Celik
1
Czy przeprowadzasz testy porównawcze z dt3 rzędami?
Uwe
3
Odpowiedzią Hannesa jest wykonanie innego obliczenia, więc nie należy jej porównywać z innymi, prawda?
Frank
2

Żadne z powyższych rozwiązań nie wydaje się działać w przypadku obliczeń według grup. Oto najlepsze, jakie mam:

for(col in cols)
{
    DT[, (col) := scale(.SD[[col]], center = TRUE, scale = TRUE), g]
}
Jfly
źródło
1

Aby dodać przykład tworzenia nowych kolumn na podstawie wektora łańcuchowego kolumn. Na podstawie odpowiedzi 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)]
Dorian Grv
źródło
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
amonk
źródło
1
Fyi, „każda określona kolumna” w tytule oznaczała, że ​​pytający był zainteresowany zastosowaniem go do podzbioru kolumn (może nie do wszystkich).
Frank
1
@Frank pewnie! W takim przypadku PO może wykonać dt [, c ("a", "b")] * (- 1).
amonk
1
Cóż, bądźmy kompletni i powiedzmydt[, cols] <- dt[, cols] * (-1)
Gregor Thomas
wygląda na to, że wymagana nowa składnia to dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip
0

dplyrfunkcje działają na data.tables, więc oto dplyrrozwiązanie, które również „unika pętli for” :)

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

Przeprowadziłem test porównawczy przy użyciu kodu Orhana (dodając wiersze i kolumny) i zobaczysz, że dplyr::mutatew acrosswiększości wykonuje się szybciej niż większość innych rozwiązań i wolniej niż rozwiązanie data.table przy użyciu 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()

Utworzono 16.10.2020 r. Przez pakiet reprex (v0.3.0)

Arthur Yip
źródło