Cómo aplicar la misma función a cada columna especificada en un data.table

85

Tengo un data.table con el que me gustaría realizar la misma operación en ciertas columnas. Los nombres de estas columnas se dan en un vector de caracteres. En este ejemplo en particular, me gustaría multiplicar todas estas columnas por -1.

Algunos datos de juguetes y un vector que especifica columnas relevantes:

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

Ahora mismo lo estoy haciendo de esta manera, recorriendo el vector de caracteres:

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

¿Hay alguna forma de hacer esto directamente sin el bucle for?

Dean MacGregor
fuente

Respuestas:

150

Esto parece funcionar:

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

El resultado es

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

Aquí hay algunos trucos:

  • Debido a que hay paréntesis (cols) :=, el resultado se asigna a las columnas especificadas en cols, en lugar de a una nueva variable denominada "cols".
  • .SDcolsle dice a la llamada que solo estamos mirando esas columnas, y nos permite usar .SDel Subset del Data asociado con esas columnas.
  • lapply(.SD, ...)opera .SD, que es una lista de columnas (como todos los data.frames y data.tables). lapplydevuelve una lista, por lo que al final jparece cols := list(...).

EDITAR : Aquí hay otra forma que probablemente sea más rápida, como mencionó @Arun:

for (j in cols) set(dt, j = j, value = -dt[[j]])
Franco
fuente
21
otra forma es usar setcon un for-loop. Sospecho que será más rápido.
Arun
3
@Arun He hecho una edición. ¿Es eso lo que querías decir? No lo he usado setantes.
Frank
8
+1 Gran respuesta. Sí, también prefiero un forbucle con setpara casos como este.
Matt Dowle
2
Sí, el uso set()parece más rápido, ¡~ 4 veces más rápido para mi conjunto de datos! Asombroso.
Konstantinos
2
Gracias, @JamesHirschorn. No estoy seguro, pero sospecho que hay más gastos generales para subconjuntos de columnas de esa manera en lugar de usar .SD, que es el idioma estándar de todos modos, que aparece en la viñeta de introducción github.com/Rdatatable/data.table/wiki/Getting-started Creo que parte del motivo del modismo es evitar escribir dos veces el nombre de la tabla.
Frank
20

Me gustaría agregar una respuesta, cuando también desee cambiar el nombre de las columnas. Esto resulta bastante útil si desea calcular el logaritmo de varias columnas, que suele ser el caso en el trabajo empírico.

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
fuente
1
¿Hay alguna forma de cambiar los nombres según una regla? En dplyr, por ejemplo, puede hacer iris%>% mutate_at (vars (coincide con ("Sepal")), list (times_two = ~. * 2)) y agregará "_times_two" a los nuevos nombres.
kennyB
1
No creo que sea posible, pero no estoy seguro de ello.
hannes101
esto agregaría columnas con los nombres de out_cols, mientras se deja colsen su lugar. Por lo tanto, necesitaría eliminarlos explícitamente 1) solicitando solo log.a y log.b: encadene a [,.(outcols)]hasta el final y vuelva a almacenar en dtvia <-. 2) quitar las columnas viejas con un encadenado [,c(cols):=NULL]. Una solución no encadenada 3) es dt[,c(cols):=...]seguida porsetnames(dt, cols, newcols)
mpag
@mpag, sí, eso es cierto, pero para mi caso de uso de investigación empírica, la mayoría de las veces necesito ambas series en el conjunto de datos.
hannes101
11

ACTUALIZACIÓN: Lo siguiente es una forma ordenada de hacerlo sin el bucle for

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

Es una forma ordenada para facilitar la lectura del código. Pero en cuanto al rendimiento, se mantiene detrás de la solución de Frank de acuerdo con el resultado de microbenchmark a continuación

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

como se muestra en la siguiente tabla

performance_comparison_chart

Mi respuesta anterior: lo siguiente también funciona

for (j in cols)
  dt[,(j):= -1 * dt[,  ..j]]
Orhan Celik
fuente
Esto es esencialmente lo mismo que la respuesta de Frank de hace un año y medio.
Dean MacGregor
1
Gracias, la respuesta de Frank estaba usando set. Cuando trabajo con tablas de datos grandes con millones de filas, veo: = el operador supera a las funciones
Orhan Celik
2
La razón por la que agregué una respuesta a una pregunta anterior es la siguiente: también tuve un problema similar, encontré esta publicación con la búsqueda de Google. Luego encontré una solución a mi problema y veo que también se aplica aquí. En realidad, mi sugerencia usa una nueva función de data.table que está disponible en nuevas versiones de la biblioteca, que no existía en el momento de la pregunta. Pensé que es una buena idea compartir, pensando que otros con problemas similares terminarán aquí con la búsqueda de Google.
Orhan Celik
1
¿Está comparando con que dtconsta de 3 filas?
Uwe
3
La respuesta de Hannes es hacer un cálculo diferente y, por lo tanto, no debería compararse con las demás, ¿verdad?
Frank
2

Ninguna de las soluciones anteriores parece funcionar con el cálculo por grupo. Lo siguiente es lo mejor que obtuve:

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

Para agregar un ejemplo para crear nuevas columnas basadas en un vector de cadena de columnas. Basado en la respuesta de 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
fuente
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
un monje
fuente
1
Para su información, "cada columna especificada" en el título significaba que el autor de la pregunta estaba interesado en aplicarlo a un subconjunto de columnas (tal vez no a todas).
Frank
1
@Frank seguro! En ese caso, el OP podría realizar dt [, c ("a", "b")] * (- 1).
amonk
1
Bueno, seamos completos y digamosdt[, cols] <- dt[, cols] * (-1)
Gregor Thomas
parece que la nueva sintaxis requerida es dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip
0

dplyrlas funciones funcionan en data.tables, así que aquí hay una dplyrsolución que también "evita el bucle for" :)

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

Lo comparé usando el código de orhan (agregando filas y columnas) y verá que dplyr::mutateen acrosssu mayoría se ejecuta más rápido que la mayoría de las otras soluciones y más lento que la solución data.table usando 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()

Creado el 16/10/2020 por el paquete reprex (v0.3.0)

Arthur Yip
fuente