Como aplicar a mesma função a cada coluna especificada em uma data.table


86

Eu tenho um data.table com o qual gostaria de realizar a mesma operação em certas colunas. Os nomes dessas colunas são fornecidos em um vetor de caracteres. Neste exemplo específico, gostaria de multiplicar todas essas colunas por -1.

Alguns dados de brinquedo e um vetor especificando colunas relevantes:

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

No momento, estou fazendo isso desta maneira, repetindo o vetor de caracteres:

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

Existe uma maneira de fazer isso diretamente sem o loop for?

Respostas:


151

Isso parece funcionar:

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

O resultado é

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

Existem alguns truques aqui:

  • Como existem parênteses (cols) :=, o resultado é atribuído às colunas especificadas em cols, em vez de a alguma nova variável chamada "cols".
  • .SDcolsdiz à chamada que estamos apenas olhando para essas colunas e nos permite usar .SDo Subset do Data associado a essas colunas.
  • lapply(.SD, ...)opera em .SD, que é uma lista de colunas (como todos os data.frames e data.tables). lapplyretorna uma lista, então no final jparece cols := list(...).

EDITAR : Esta é outra maneira que provavelmente é mais rápida, como @Arun mencionou:

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

22
outra maneira é usar setcom a for-loop. Eu suspeito que será mais rápido.
Arun

3
@Arun eu fiz uma edição. É isso que você quis dizer? Eu não usei setantes.
Frank

8
+1 Ótima resposta. Sim, também prefiro um forloop com setpara casos como este.
Matt Dowle

2
Sim, o uso set()parece mais rápido, ~ 4 vezes mais rápido para meu conjunto de dados! Surpreendente.
Konstantinos

2
Obrigado, @JamesHirschorn. Não tenho certeza, mas suspeito que haja mais sobrecarga para definir colunas dessa maneira, em vez de usar .SD, que é o idioma padrão de qualquer maneira, aparecendo na vinheta de introdução github.com/Rdatatable/data.table/wiki/Getting-started Parte da razão para o idioma, eu acho, é evitar digitar o nome da tabela duas vezes.
Frank

20

Eu gostaria de adicionar uma resposta, quando você quiser alterar o nome das colunas também. Isso é muito útil se você deseja calcular o logaritmo de várias colunas, o que costuma ser o caso em trabalhos empíricos.

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
Existe uma maneira de alterar os nomes com base em uma regra? No dplyr, por exemplo, você pode fazer iris%>% mutate_at (vars (coincide ("Sepal")), list (times_two = ~. * 2)) e anexará "_times_two" aos novos nomes.
kennyB

1
Eu não acho que isso seja possível, mas não tenho certeza sobre isso.
hannes101

isso adicionaria colunas com os nomes de out_cols, embora continuasse colsno lugar. Portanto, você precisaria eliminá-los explicitamente 1) pedindo apenas log.a e log.b: encadeie a [,.(outcols)]até o final e re-armazene em dtvia <-. 2) remova as colunas antigas com uma corrente [,c(cols):=NULL]. Uma solução não dt[,c(cols):=...]setnames(dt, cols, newcols)
encadeada

@mpag, sim, é verdade, mas para meu caso de uso de pesquisa empírica, na maioria das vezes preciso de ambas as séries no conjunto de dados.
hannes101

11

ATUALIZAÇÃO: a seguir está uma maneira legal de fazer isso sem o loop for

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

É uma maneira elegante de facilitar a leitura do código. Mas quanto ao desempenho, fica atrás da solução de Frank de acordo com o resultado abaixo do 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

como mostrado no gráfico abaixo

performance_comparison_chart

Minha resposta anterior: O seguinte também funciona

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

Esta é essencialmente a mesma coisa que a resposta de Frank de um ano e meio atrás.
Dean MacGregor,

1
Obrigado, a resposta de Frank foi usando set. Quando trabalho com data.table grande com milhões de linhas, vejo: = operator supera funções
Orhan Celik

2
A razão pela qual adicionei uma resposta a uma pergunta antiga é a seguinte: Eu também tive um problema semelhante, encontrei esta postagem com a pesquisa do Google. Depois, encontrei uma solução para o meu problema e vejo que também se aplica aqui. Na verdade, minha sugestão usa uma nova função data.table que está disponível em novas versões da biblioteca, que não existiam na época da pergunta. Achei uma boa ideia compartilhar, pensando que outras pessoas com problema semelhante vão acabar aqui com a pesquisa do google.
Orhan Celik,

1
Você está fazendo benchmarking com dtconsistindo de 3 linhas?
Uwe

3
A resposta de Hannes é fazer um cálculo diferente e, portanto, não deve ser comparada com as outras, certo?
Frank

2

Nenhuma das soluções acima parece funcionar com cálculo por grupo. A seguir está o melhor que eu tenho:

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

1

Para adicionar exemplo para criar novas colunas com base em um vetor string de colunas. Com base na resposta do 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, o "cada coluna especificada" no título significava que o autor da pergunta estava interessado em aplicá-lo a um subconjunto de colunas (talvez não todas).
Frank

1
@Frank com certeza! Nesse caso, o OP poderia executar dt [, c ("a", "b")] * (- 1).
amonk

1
Bem, vamos ser completos e dizerdt[, cols] <- dt[, cols] * (-1)
Gregor Thomas

parece que a nova sintaxe necessária é dt [, cols] <- dt [, ..cols] * (-1)
Arthur Yip

0

dplyrfunções funcionam em data.tables, então aqui está uma dplyrsolução que também "evita o loop for" :)

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

Eu fiz o benchmarking usando o código de orhan (adicionando linhas e colunas) e você verá que dplyr::mutatea acrossmaioria executa mais rápido do que a maioria das outras soluções e mais lenta do que a solução 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()

Criado em 2020-10-16 pelo pacote reprex (v0.3.0)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.