So wenden Sie dieselbe Funktion auf jede angegebene Spalte in einer data.table an

85

Ich habe eine data.table, mit der ich dieselbe Operation für bestimmte Spalten ausführen möchte. Die Namen dieser Spalten werden in einem Zeichenvektor angegeben. In diesem Beispiel möchte ich alle diese Spalten mit -1 multiplizieren.

Einige Spielzeugdaten und ein Vektor, der relevante Spalten angibt:

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

Im Moment mache ich es so und durchlaufe den Zeichenvektor:

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

Gibt es eine Möglichkeit, dies direkt ohne die for-Schleife zu tun?

Dean MacGregor
quelle

Antworten:

150

Das scheint zu funktionieren:

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

Das Ergebnis ist

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

Hier gibt es ein paar Tricks:

  • Da Klammern enthalten sind (cols) :=, wird das Ergebnis den in angegebenen Spalten zugewiesen colsund nicht einer neuen Variablen mit dem Namen "cols".
  • .SDcolsteilt dem Aufruf mit, dass wir nur diese Spalten betrachten, und ermöglicht es uns .SD, die STeilmenge der Data zu verwenden , die diesen Spalten zugeordnet sind.
  • lapply(.SD, ...)arbeitet mit .SD, das ist eine Liste von Spalten (wie alle data.frames und data.tables). lapplygibt eine Liste zurück, jsieht also am Ende so aus cols := list(...).

EDIT : Hier ist ein anderer Weg, der wahrscheinlich schneller ist, wie @Arun erwähnt hat:

for (j in cols) set(dt, j = j, value = -dt[[j]])
Frank
quelle
21
Ein anderer Weg ist die Verwendung setmit a for-loop. Ich vermute, es wird schneller gehen.
Arun
3
@Arun Ich habe eine Bearbeitung vorgenommen. Ist es das, was du meintest? Ich habe noch nie benutzt set.
Frank
8
+1 Tolle Antwort. Ja, ich bevorzuge auch für solche Fälle eine forSchleife mit set.
Matt Dowle
2
Ja, die Verwendung set()scheint schneller zu sein, ~ 4-mal schneller für meinen Datensatz! Tolle.
Konstantinos
2
Danke, @JamesHirschorn. Ich bin mir nicht sicher, aber ich vermute, dass das Unterteilen von Spalten auf diese Weise mehr Aufwand bedeutet, als .SD zu verwenden, was ohnehin die Standardsprache ist und in der Intro-Vignette github.com/Rdatatable/data.table/wiki/Getting-started erscheint Ich denke, ein Teil des Grundes für die Redewendung besteht darin, den Tabellennamen nicht zweimal einzugeben.
Frank
20

Ich möchte eine Antwort hinzufügen, wenn Sie auch den Namen der Spalten ändern möchten. Dies ist sehr praktisch, wenn Sie den Logarithmus mehrerer Spalten berechnen möchten, was in empirischen Arbeiten häufig der Fall ist.

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
quelle
1
Gibt es eine Möglichkeit, die Namen basierend auf einer Regel zu ändern? In dplyr können Sie beispielsweise iris%>% mutate_at (vars (Übereinstimmungen ("Sepal")), list (times_two = ~. * 2)) ausführen und "_times_two" an die neuen Namen anhängen.
kennyB
1
Ich denke nicht, dass das möglich ist, bin mir aber nicht sicher.
Hannes101
Dies würde Spalten mit den Namen von hinzufügen out_cols, während sie colsan Ort und Stelle bleiben . Sie müssen diese also eliminieren, indem Sie entweder explizit 1) nur nach log.a und log.b fragen: Verketten Sie a [,.(outcols)]bis zum Ende und speichern Sie es erneut dtüber via <-. 2) Entfernen Sie die alten Säulen mit einer Kette [,c(cols):=NULL]. Auf eine nicht verkettete Lösung 3) dt[,c(cols):=...]folgtsetnames(dt, cols, newcols)
mpag
@mpag, ja das stimmt, aber für meinen Anwendungsfall der empirischen Forschung benötige ich meistens beide Reihen im Datensatz.
Hannes101
11

UPDATE: Das Folgende ist eine gute Möglichkeit, auf eine for-Schleife zu verzichten

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

Dies ist ein guter Weg für eine einfache Lesbarkeit des Codes. Aber was die Leistung betrifft, bleibt sie gemäß dem unten angegebenen Mikrobenchmark-Ergebnis hinter Franks Lösung zurück

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

wie in der folgenden Tabelle gezeigt

Leistungsvergleichsdiagramm

Meine vorherige Antwort: Das Folgende funktioniert auch

for (j in cols)
  dt[,(j):= -1 * dt[,  ..j]]
Orhan Celik
quelle
Dies ist im Wesentlichen dasselbe wie Franks Antwort vor anderthalb Jahren.
Dean MacGregor
1
Danke, Franks Antwort war Set. Wenn ich mit großen Datenmengen mit Millionen von Zeilen arbeite, sehe ich: = Operator übertrifft Funktionen
Orhan Celik
2
Der Grund, warum ich eine Antwort auf eine alte Frage hinzugefügt habe, ist folgender: Ich hatte auch ein ähnliches Problem. Ich bin auf diesen Beitrag mit der Google-Suche gestoßen. Danach habe ich eine Lösung für mein Problem gefunden, und ich sehe, dass dies auch hier gilt. Tatsächlich verwendet mein Vorschlag eine neue Funktion von data.table, die in neuen Versionen der Bibliothek verfügbar ist, die zum Zeitpunkt der Frage nicht vorhanden waren. Ich dachte, es ist eine gute Idee zu teilen, zu denken, dass andere mit ähnlichen Problemen hier mit der Google-Suche enden werden.
Orhan Celik
1
Benchmarking mit dt3 Zeilen?
Uwe
3
Hannes 'Antwort ist eine andere Berechnung und sollte daher nicht mit den anderen verglichen werden, oder?
Frank
2

Keine der oben genannten Lösungen scheint mit der Berechnung nach Gruppen zu funktionieren. Folgendes ist das Beste, was ich bekommen habe:

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

Hinzufügen eines Beispiels zum Erstellen neuer Spalten basierend auf einem Zeichenfolgenvektor von Spalten. Basierend auf der Antwort von 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
quelle
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
ein Mönch
quelle
1
Zu Ihrer Information, die "jede angegebene Spalte" im Titel bedeutete, dass der Fragesteller daran interessiert war, sie auf eine Teilmenge von Spalten anzuwenden (möglicherweise nicht alle).
Frank
1
@ Frank sicher! In diesem Fall könnte das OP dt [, c ("a", "b")] * (- 1) ausführen.
Amonk
1
Nun, lassen Sie uns vollständig sein und sagendt[, cols] <- dt[, cols] * (-1)
Gregor Thomas
Die neue erforderliche Syntax scheint dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip
0

dplyrFunktionen funktionieren auf data.tables, also hier ist eine dplyrLösung, die auch "die for-Schleife vermeidet" :)

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

Ich gebenchmarkt es Orhans Code (Hinzufügen von Zeilen und Spalten) , und Sie werden sehen , dplyr::mutatemit acrossmeist schneller ausführt als die meisten anderen Lösungen und langsamer als die data.table Lösung mit 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()

Erstellt am 16.10.2020 durch das reprex-Paket (v0.3.0)

Arthur Yip
quelle