dplyr altera / substitui várias colunas em um subconjunto de linhas

86

Estou tentando um fluxo de trabalho baseado em dplyr (em vez de usar principalmente data.table, ao qual estou acostumado) e me deparei com um problema para o qual não consigo encontrar uma solução dplyr equivalente para . Normalmente, encontro o cenário em que preciso atualizar / substituir condicionalmente várias colunas com base em uma única condição. Aqui está um exemplo de código, com minha solução data.table:

library(data.table)

# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                               replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

# Replace the values of several columns for rows where measure is "exit"
dt <- dt[measure == 'exit', 
         `:=`(qty.exit = qty,
              cf = 0,
              delta.watts = 13)]

Existe uma solução dplyr simples para este mesmo problema? Gostaria de evitar o uso de ifelse porque não quero ter que digitar a condição várias vezes - este é um exemplo simplificado, mas às vezes há muitas atribuições com base em uma única condição.

Obrigado antecipadamente pela ajuda!

Chris Newton
fonte

Respostas:

83

Essas soluções (1) mantêm o pipeline, (2) não substituem a entrada e (3) exigem apenas que a condição seja especificada uma vez:

1a) mutate_cond Crie uma função simples para frames de dados ou tabelas de dados que podem ser incorporados em pipelines. Esta função é semelhante, mutatemas age apenas nas linhas que satisfazem a condição:

mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
  condition <- eval(substitute(condition), .data, envir)
  .data[condition, ] <- .data[condition, ] %>% mutate(...)
  .data
}

DF %>% mutate_cond(measure == 'exit', qty.exit = qty, cf = 0, delta.watts = 13)

1b) mutate_last Esta é uma função alternativa para quadros de dados ou tabelas de dados que são semelhantes, mutatemas são usados ​​apenas dentro group_by(como no exemplo abaixo) e só opera no último grupo ao invés de todos os grupos. Observe que TRUE> FALSE, portanto, se group_byespecifica uma condição, mutate_lastsó operará em linhas que satisfaçam essa condição.

mutate_last <- function(.data, ...) {
  n <- n_groups(.data)
  indices <- attr(.data, "indices")[[n]] + 1
  .data[indices, ] <- .data[indices, ] %>% mutate(...)
  .data
}


DF %>% 
   group_by(is.exit = measure == 'exit') %>%
   mutate_last(qty.exit = qty, cf = 0, delta.watts = 13) %>%
   ungroup() %>%
   select(-is.exit)

2) fatorar a condição Fatorar a condição tornando-a uma coluna extra que é posteriormente removida. Em seguida ifelse, use replaceou aritmética com lógica, conforme ilustrado. Isso também funciona para tabelas de dados.

library(dplyr)

DF %>% mutate(is.exit = measure == 'exit',
              qty.exit = ifelse(is.exit, qty, qty.exit),
              cf = (!is.exit) * cf,
              delta.watts = replace(delta.watts, is.exit, 13)) %>%
       select(-is.exit)

3) sqldf Poderíamos usar SQL updatepor meio do pacote sqldf no pipeline para quadros de dados (mas não tabelas de dados, a menos que os convertamos - isso pode representar um bug no dplyr. Veja o problema 1579 do dplyr ). Pode parecer que estamos modificando indesejavelmente a entrada neste código devido à existência do, updatemas na verdade o updateestá agindo em uma cópia da entrada no banco de dados gerado temporariamente e não na entrada real.

library(sqldf)

DF %>% 
   do(sqldf(c("update '.' 
                 set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13 
                 where measure = 'exit'", 
              "select * from '.'")))

4) row_case_when Verifique também row_case_whendefinido em Retornando uma tabela: como vetorizar com case_when? . Ele usa uma sintaxe semelhante a, case_whenmas se aplica a linhas.

library(dplyr)

DF %>%
  row_case_when(
    measure == "exit" ~ data.frame(qty.exit = qty, cf = 0, delta.watts = 13),
    TRUE ~ data.frame(qty.exit, cf, delta.watts)
  )

Nota 1: Usamos isso comoDF

set.seed(1)
DF <- data.frame(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                               replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

Nota 2: O problema de como especificar facilmente a atualização de um subconjunto de linhas também é discutido nas questões de dplyr 134 , 631 , 1518 e 1573, com 631 sendo o thread principal e 1573 sendo uma revisão das respostas aqui.

G. Grothendieck
fonte
1
Excelente resposta, obrigado! Seu mutate_cond e @Kevin Ushey's mutate_when são boas soluções para este problema. Acho que tenho uma ligeira preferência pela legibilidade / flexibilidade de mutate_when, mas vou dar a esta resposta a "verificação" de precisão.
Chris Newton
Eu realmente gosto da abordagem mutate_cond. Também me parece que esta função ou algo muito próximo a ela merece inclusão no dplyr e seria uma solução melhor do que VectorizedSwitch (que é discutido em github.com/hadley/dplyr/issues/1573 ) para o caso de uso que as pessoas estão pensando sobre aqui ...
Magnus
Eu amo mutate_cond. As várias opções deveriam ser respostas separadas.
Holger Brandl
Já se passaram alguns anos e os problemas do github parecem fechados e bloqueados. Existe uma solução oficial para este problema?
static_rtti
27

Você pode fazer isso com magrittro tubo bidirecional de %<>%:

library(dplyr)
library(magrittr)

dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                    cf = 0,  
                                    delta.watts = 13)

Isso reduz a quantidade de digitação, mas ainda é muito mais lento do que data.table.

eipi10
fonte
Na verdade, agora que tive a chance de testar isso, prefiro uma solução que evite a necessidade de subconjuntos usando a notação dt [dt $ measure == 'exit',], uma vez que pode ficar difícil com mais nomes dt.
Chris Newton
Apenas um FYI, mas esta solução só funcionará se data.frame/ tibblejá contiver a coluna definida por mutate. Não funcionará se você estiver tentando adicionar uma nova coluna, por exemplo, pela primeira vez executando um loop e modificando a data.frame.
Ursus Frost
@UrsusFrost adicionar uma nova coluna que é apenas um subconjunto do conjunto de dados parece estranho para mim. Você adiciona NA a linhas que não são subdivididas?
Baraliuh
@Baraliuh Sim, posso apreciar isso. É parte de um loop no qual incremento e acrescento dados a uma lista de datas. As primeiras datas devem ser tratadas de maneira diferente das datas subsequentes, pois estão replicando processos de negócios do mundo real. Em outras iterações, dependendo das condições das datas, os dados são calculados de forma diferente. Devido à condicionalidade, não quero alterar inadvertidamente datas anteriores no data.frame. FWIW, acabei de voltar a usar em data.tablevez de dplyrporque sua iexpressão lida com isso facilmente - além disso, o loop geral é executado muito mais rápido.
Ursus Frost
19

Aqui está uma solução de que gosto:

mutate_when <- function(data, ...) {
  dots <- eval(substitute(alist(...)))
  for (i in seq(1, length(dots), by = 2)) {
    condition <- eval(dots[[i]], envir = data)
    mutations <- eval(dots[[i + 1]], envir = data[condition, , drop = FALSE])
    data[condition, names(mutations)] <- mutations
  }
  data
}

Ele permite que você escreva coisas como, por exemplo

mtcars %>% mutate_when(
  mpg > 22,    list(cyl = 100),
  disp == 160, list(cyl = 200)
)

que é bastante legível - embora possa não ter o desempenho que poderia ser.

Kevin Ushey
fonte
14

Como mostra o eipi10 acima, não há uma maneira simples de fazer uma substituição de subconjunto em dplyr porque o DT usa semântica de passagem por referência vs dplyr usando passagem por valor. dplyr requer o uso de ifelse()em todo o vetor, enquanto o DT fará o subconjunto e atualizará por referência (retornando o DT inteiro). Portanto, para este exercício, o DT será substancialmente mais rápido.

Você pode alternativamente primeiro subconjunto, a seguir atualizar e finalmente recombinar:

dt.sub <- dt[dt$measure == "exit",] %>%
  mutate(qty.exit= qty, cf= 0, delta.watts= 13)

dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])

Mas o DT será substancialmente mais rápido: (editado para usar a nova resposta do eipi10)

library(data.table)
library(dplyr)
library(microbenchmark)
microbenchmark(dt= {dt <- dt[measure == 'exit', 
                            `:=`(qty.exit = qty,
                                 cf = 0,
                                 delta.watts = 13)]},
               eipi10= {dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                cf = 0,  
                                delta.watts = 13)},
               alex= {dt.sub <- dt[dt$measure == "exit",] %>%
                 mutate(qty.exit= qty, cf= 0, delta.watts= 13)

               dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])})


Unit: microseconds
expr      min        lq      mean   median       uq      max neval cld
     dt  591.480  672.2565  747.0771  743.341  780.973 1837.539   100  a 
 eipi10 3481.212 3677.1685 4008.0314 3796.909 3936.796 6857.509   100   b
   alex 3412.029 3637.6350 3867.0649 3726.204 3936.985 5424.427   100   b
Alex W
fonte
10

Acabei mutate_cond()de descobrir isso e realmente gostei do @G. Grothendieck, mas achou que poderia ser útil também para lidar com novas variáveis. Portanto, abaixo tem duas adições:

Não relacionado: a penúltima linha foi um pouco mais dplyrcomplicada usandofilter()

Três novas linhas no início obtêm nomes de variáveis ​​para uso em mutate()e inicializam quaisquer novas variáveis ​​no quadro de dados antes de mutate()ocorrer. Novas variáveis ​​são inicializadas para o restante do data.frameuso new_init, que é definido como missing ( NA) como padrão.

mutate_cond <- function(.data, condition, ..., new_init = NA, envir = parent.frame()) {
  # Initialize any new variables as new_init
  new_vars <- substitute(list(...))[-1]
  new_vars %<>% sapply(deparse) %>% names %>% setdiff(names(.data))
  .data[, new_vars] <- new_init

  condition <- eval(substitute(condition), .data, envir)
  .data[condition, ] <- .data %>% filter(condition) %>% mutate(...)
  .data
}

Aqui estão alguns exemplos usando os dados da íris:

Mude Petal.Lengthpara 88 onde Species == "setosa". Isso funcionará na função original, bem como nesta nova versão.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)

O mesmo que acima, mas também cria uma nova variável x( NAem linhas não incluídas na condição). Não era possível antes.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)

O mesmo que acima, mas as linhas não incluídas na condição para xsão definidas como FALSE.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)

Este exemplo mostra como new_initpode ser definido como a listpara inicializar várias novas variáveis ​​com valores diferentes. Aqui, duas novas variáveis ​​são criadas com linhas excluídas sendo inicializadas usando valores diferentes ( xinicializado como FALSE, yas NA)

iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
                  x = TRUE, y = Sepal.Length ^ 2,
                  new_init = list(FALSE, NA))
Simon Jackson
fonte
Sua mutate_condfunção mostra um erro em meu conjunto de dados, e a função de Grothendiecks não. Error: incorrect length (4700), expecting: 168Parece estar relacionado à função de filtro.
RHA
Você colocou isso em uma biblioteca ou formalizou-o como uma função? Parece um acéfalo, especialmente com todas as melhorias.
Nettle
1
Não. Acho que a melhor abordagem com dplyr no momento é combinar mutate com if_elseou case_when.
Simon Jackson
Você pode fornecer um exemplo (ou link) para essa abordagem?
Nettle
6

mutate_cond é uma ótima função, mas dá um erro se houver um NA na (s) coluna (s) usada (s) para criar a condição. Acho que uma mutação condicional deve simplesmente deixar essas linhas de lado. Isso corresponde ao comportamento de filter (), que retorna linhas quando a condição é TRUE, mas omite ambas as linhas com FALSE e NA.

Com esta pequena mudança, a função funciona perfeitamente:

mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
    condition <- eval(substitute(condition), .data, envir)
    condition[is.na(condition)] = FALSE
    .data[condition, ] <- .data[condition, ] %>% mutate(...)
    .data
}
Magnus
fonte
Obrigado Magnus! Estou usando isso para atualizar uma tabela contendo ações e tempos para todos os objetos que compõem uma animação. Eu achei o problema de NA porque os dados são tão variados que algumas ações não fazem sentido para alguns objetos, então eu tenho NAs nessas células. O outro mutate_cond acima travou, mas sua solução funcionou perfeitamente.
Phil van Kleur,
Se isso for útil para você, esta função está disponível em um pequeno pacote que escrevi, "zulutils". Não está no CRAN, mas você pode instalá-lo usando remotes :: install_github ("torfason / zulutils")
Magnus
4

Na verdade, não vejo nenhuma mudança dplyrque torne isso muito mais fácil. case_whené ótimo para quando há várias condições e resultados diferentes para uma coluna, mas não ajuda neste caso em que você deseja alterar várias colunas com base em uma condição. Da mesma forma, recodeeconomiza digitação se você estiver substituindo vários valores diferentes em uma coluna, mas não ajuda a fazer isso em várias colunas de uma vez. Finalmente, mutate_atetc. aplicam apenas condições aos nomes das colunas, não às linhas do dataframe. Você poderia potencialmente escrever uma função para mutate_at que faria isso, mas não consigo descobrir como você faria com que ela se comportasse de maneira diferente para colunas diferentes.

Dito isso, é como eu abordaria isso usando o nestformulário tidyre a mappartir de purrr.

library(data.table)
library(dplyr)
library(tidyr)
library(purrr)

# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                                  replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

dt2 <- dt %>% 
  nest(-measure) %>% 
  mutate(data = if_else(
    measure == "exit", 
    map(data, function(x) mutate(x, qty.exit = qty, cf = 0, delta.watts = 13)),
    data
  )) %>%
  unnest()
ver 24
fonte
1
A única coisa que eu sugiro é usar nest(-measure)para evitar ogroup_by
Dave Gruenewald
Editado para refletir a sugestão de
@DaveGruenewald
4

Uma solução concisa seria fazer a mutação no subconjunto filtrado e, em seguida, adicionar de volta as linhas de não saída da tabela:

library(dplyr)

dt %>% 
    filter(measure == 'exit') %>%
    mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
    rbind(dt %>% filter(measure != 'exit'))
Bob Zimmermann
fonte
3

Com a criação de rlang, uma versão ligeiramente modificada do exemplo 1a de Grothendieck é possível, eliminando a necessidade do envirargumento, pois enquo()captura o ambiente que .pé criado automaticamente.

mutate_rows <- function(.data, .p, ...) {
  .p <- rlang::enquo(.p)
  .p_lgl <- rlang::eval_tidy(.p, .data)
  .data[.p_lgl, ] <- .data[.p_lgl, ] %>% mutate(...)
  .data
}

dt %>% mutate_rows(measure == "exit", qty.exit = qty, cf = 0, delta.watts = 13)
Davis Vaughan
fonte
2

Você pode dividir o conjunto de dados e fazer uma chamada mutate regular na TRUEparte.

O dplyr 0.8 apresenta a função group_splitque divide por grupos (e os grupos podem ser definidos diretamente na chamada), então vamos usá-la aqui, mas também base::splitfunciona.

library(tidyverse)
df1 %>%
  group_split(measure == "exit", keep=FALSE) %>% # or `split(.$measure == "exit")`
  modify_at(2,~mutate(.,qty.exit = qty, cf = 0, delta.watts = 13)) %>%
  bind_rows()

#    site space measure qty qty.exit delta.watts          cf
# 1     1     4     led   1        0        73.5 0.246240409
# 2     2     3     cfl  25        0        56.5 0.360315879
# 3     5     4     cfl   3        0        38.5 0.279966850
# 4     5     3  linear  19        0        40.5 0.281439486
# 5     2     3  linear  18        0        82.5 0.007898384
# 6     5     1  linear  29        0        33.5 0.392412729
# 7     5     3  linear   6        0        46.5 0.970848817
# 8     4     1     led  10        0        89.5 0.404447182
# 9     4     1     led  18        0        96.5 0.115594622
# 10    6     3  linear  18        0        15.5 0.017919745
# 11    4     3     led  22        0        54.5 0.901829577
# 12    3     3     led  17        0        79.5 0.063949974
# 13    1     3     led  16        0        86.5 0.551321441
# 14    6     4     cfl   5        0        65.5 0.256845013
# 15    4     2     led  12        0        29.5 0.340603733
# 16    5     3  linear  27        0        63.5 0.895166931
# 17    1     4     led   0        0        47.5 0.173088800
# 18    5     3  linear  20        0        89.5 0.438504370
# 19    2     4     cfl  18        0        45.5 0.031725246
# 20    2     3     led  24        0        94.5 0.456653397
# 21    3     3     cfl  24        0        73.5 0.161274319
# 22    5     3     led   9        0        62.5 0.252212124
# 23    5     1     led  15        0        40.5 0.115608182
# 24    3     3     cfl   3        0        89.5 0.066147321
# 25    6     4     cfl   2        0        35.5 0.007888337
# 26    5     1  linear   7        0        51.5 0.835458916
# 27    2     3  linear  28        0        36.5 0.691483644
# 28    5     4     led   6        0        43.5 0.604847889
# 29    6     1  linear  12        0        59.5 0.918838163
# 30    3     3  linear   7        0        73.5 0.471644760
# 31    4     2     led   5        0        34.5 0.972078100
# 32    1     3     cfl  17        0        80.5 0.457241602
# 33    5     4  linear   3        0        16.5 0.492500255
# 34    3     2     cfl  12        0        44.5 0.804236607
# 35    2     2     cfl  21        0        50.5 0.845094268
# 36    3     2  linear  10        0        23.5 0.637194873
# 37    4     3     led   6        0        69.5 0.161431896
# 38    3     2    exit  19       19        13.0 0.000000000
# 39    6     3    exit   7        7        13.0 0.000000000
# 40    6     2    exit  20       20        13.0 0.000000000
# 41    3     2    exit   1        1        13.0 0.000000000
# 42    2     4    exit  19       19        13.0 0.000000000
# 43    3     1    exit  24       24        13.0 0.000000000
# 44    3     3    exit  16       16        13.0 0.000000000
# 45    5     3    exit   9        9        13.0 0.000000000
# 46    2     3    exit   6        6        13.0 0.000000000
# 47    4     1    exit   1        1        13.0 0.000000000
# 48    1     1    exit  14       14        13.0 0.000000000
# 49    6     3    exit   7        7        13.0 0.000000000
# 50    2     4    exit   3        3        13.0 0.000000000

Se a ordem das linhas for importante, use tibble::rowid_to_columnprimeiro, depois dplyr::arrangeon rowide selecione-o no final.

dados

df1 <- data.frame(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                                  replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50),
                 stringsAsFactors = F)
Moody_Mudskipper
fonte
2

Acho que essa resposta não foi mencionada antes. Funciona quase tão rápido quanto o 'padrão'data.table solução .

Usar base::replace()

df %>% mutate( qty.exit = replace( qty.exit, measure == 'exit', qty[ measure == 'exit'] ),
                          cf = replace( cf, measure == 'exit', 0 ),
                          delta.watts = replace( delta.watts, measure == 'exit', 13 ) )

substituir recicla o valor de substituição, então quando você quiser que os valores das colunas sejam qtyinseridos nas colunas qty.exit, você deve subconjuntoqty ... portanto, qty[ measure == 'exit']na primeira substituição ..

agora, você provavelmente não desejará redigitar o measure == 'exit'o tempo todo ... portanto, você pode criar um vetor de índice contendo essa seleção e usá-lo nas funções acima.

#build an index-vector matching the condition
index.v <- which( df$measure == 'exit' )

df %>% mutate( qty.exit = replace( qty.exit, index.v, qty[ index.v] ),
               cf = replace( cf, index.v, 0 ),
               delta.watts = replace( delta.watts, index.v, 13 ) )

benchmarks

# Unit: milliseconds
#         expr      min       lq     mean   median       uq      max neval
# data.table   1.005018 1.053370 1.137456 1.112871 1.186228 1.690996   100
# wimpel       1.061052 1.079128 1.218183 1.105037 1.137272 7.390613   100
# wimpel.index 1.043881 1.064818 1.131675 1.085304 1.108502 4.192995   100
Wimpel
fonte
1

À custa de quebrar a sintaxe dplyr usual, você pode usar withindo básico:

dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
              delta.watts[measure == 'exit'] <- 13)

Parece se integrar bem com o tubo e você pode fazer praticamente tudo o que quiser dentro dele.

Jan Hlavacek
fonte
Isso não funciona conforme está escrito porque a segunda tarefa não acontece de fato. Mas se você fizer dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })isso, ele funcionará
veja