Come applicare la stessa funzione a ogni colonna specificata in un data.table


86

Ho un data.table con cui vorrei eseguire la stessa operazione su determinate colonne. I nomi di queste colonne sono forniti in un vettore di caratteri. In questo particolare esempio, vorrei moltiplicare tutte queste colonne per -1.

Alcuni dati sui giocattoli e un vettore che specifica le colonne pertinenti:

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

In questo momento lo sto facendo in questo modo, ripetendo il vettore del carattere:

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

C'è un modo per farlo direttamente senza il ciclo for?

Risposte:


151

Questo sembra funzionare:

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

Il risultato è

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

Ci sono alcuni trucchi qui:

  • Poiché sono presenti parentesi (cols) :=, il risultato viene assegnato alle colonne specificate in cols, invece che a una nuova variabile denominata "cols".
  • .SDcolsdice alla chiamata che stiamo guardando solo quelle colonne e ci permette di usare .SDl' Subset dell'ata Dassociato a quelle colonne.
  • lapply(.SD, ...)opera su .SD, che è un elenco di colonne (come tutti i data.frames e data.tables). lapplyrestituisce un elenco, quindi alla fine jsembra cols := list(...).

EDIT : ecco un altro modo che è probabilmente più veloce, come menzionato da @Arun:

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

22
un altro modo è usare setcon un file for-loop. Ho il sospetto che sarà più veloce.
Arun

3
@ Arun ho apportato una modifica. È questo che intendevi? Non l'ho mai usato setprima.
Frank

8
+1 Ottima risposta. Sì, preferisco anche un forciclo con setcasi come questo.
Matt Dowle

2
Sì, l'utilizzo set()sembra più veloce, ~ 4 volte più veloce per il mio set di dati! Sorprendente.
Konstantinos

2
Grazie, @JamesHirschorn. Non ne sono sicuro, ma sospetto che ci sia più overhead nel sottoinserimento delle colonne in questo modo piuttosto che usare .SD, che è comunque l'idioma standard, che appare nella vignetta introduttiva github.com/Rdatatable/data.table/wiki/Getting-started Parte del motivo dell'idioma, credo, è evitare di digitare due volte il nome della tabella.
Frank

20

Vorrei aggiungere una risposta, quando vorreste cambiare anche il nome delle colonne. Ciò è molto utile se si desidera calcolare il logaritmo di più colonne, come spesso accade nel lavoro empirico.

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
C'è un modo per cambiare i nomi in base a una regola? In dplyr, ad esempio, puoi eseguire iris%>% mutate_at (vars (match ("Sepal")), list (times_two = ~. * 2)) e aggiungerà "_times_two" ai nuovi nomi.
kennyB

1
Non credo sia possibile, ma non ne sono davvero sicuro.
hannes101

questo aggiungerebbe colonne con i nomi di out_cols, pur rimanendo colsal loro posto. Quindi, dovresti eliminarli esplicitamente 1) chiedendo solo log.a e log.b: catena a [,.(outcols)]fino alla fine e re-memorizzare su dtvia <-. 2) rimuovere le vecchie colonne con una catena [,c(cols):=NULL]. Una soluzione non concatenata 3) è dt[,c(cols):=...]seguita dasetnames(dt, cols, newcols)
mpag

@mpag, sì, è vero, ma per il mio caso di utilizzo della ricerca empirica, la maggior parte delle volte ho bisogno di entrambe le serie nel set di dati.
hannes101

11

AGGIORNAMENTO: Di seguito è riportato un modo accurato per farlo senza ciclo for

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

È un modo accurato per una facile leggibilità del codice. Ma per quanto riguarda le prestazioni, rimane dietro la soluzione di Frank in base al risultato del microbenchmark di seguito

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

come mostrato nella tabella sottostante

performance_comparison_chart

La mia risposta precedente: funziona anche quanto segue

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

Questa è essenzialmente la stessa cosa della risposta di Frank di un anno e mezzo fa.
Dean MacGregor

1
Grazie, la risposta di Frank stava usando il set. Quando lavoro con data.table di grandi dimensioni con milioni di righe, vedo: = l'operatore supera le funzioni
Orhan Celik

2
Il motivo per cui ho aggiunto una risposta a una vecchia domanda è il seguente: ho avuto anche un problema simile, mi sono imbattuto in questo post con la ricerca su Google. Successivamente ho trovato una soluzione al mio problema e vedo che si applica anche qui. In realtà il mio suggerimento utilizza una nuova funzione di data.table che è disponibile nelle nuove versioni della libreria, che non esisteva al momento della domanda. Ho pensato che fosse una buona idea condividere, pensando che altri con problemi simili finiranno qui con la ricerca su Google.
Orhan Celik

1
Stai valutando con dtcomposto da 3 righe?
Uwe

3
La risposta di Hannes è fare un calcolo diverso e quindi non dovrebbe essere confrontato con gli altri, giusto?
Frank

2

Nessuna delle soluzioni precedenti sembra funzionare con il calcolo per gruppo. Di seguito è il migliore che ho ottenuto:

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

1

Per aggiungere un esempio per creare nuove colonne in base a un vettore stringa di colonne. Basato sulla risposta di 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
Cordiali saluti, "ogni colonna specificata" nel titolo significava che il richiedente era interessato ad applicarlo a un sottoinsieme di colonne (forse non a tutte).
Frank

1
@Frank sicuro! In quel caso l'OP potrebbe eseguire dt [, c ("a", "b")] * (- 1).
amonk

1
Bene, siamo completi e diciamodt[, cols] <- dt[, cols] * (-1)
Gregor Thomas

sembra che la nuova sintassi richiesta sia dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip

0

dplyrle funzioni funzionano su data.tables, quindi ecco una dplyrsoluzione che "evita anche il ciclo for" :)

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

I benchmark utilizzando il codice di Orhan (l'aggiunta di righe e colonne) e vedrai dplyr::mutatecon acrossla maggior parte esegue più velocemente di quanto la maggior parte delle altre soluzioni e più lento rispetto alla soluzione data.table utilizzando 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()

Creato il 16-10-2020 dal pacchetto reprex (v0.3.0)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.