O pacote dplyr pode ser usado para mutação condicional?

178

O mutate pode ser usado quando a mutação é condicional (dependendo dos valores de certos valores da coluna)?

Este exemplo ajuda a mostrar o que quero dizer.

structure(list(a = c(1, 3, 4, 6, 3, 2, 5, 1), b = c(1, 3, 4, 
2, 6, 7, 2, 6), c = c(6, 3, 6, 5, 3, 6, 5, 3), d = c(6, 2, 4, 
5, 3, 7, 2, 6), e = c(1, 2, 4, 5, 6, 7, 6, 3), f = c(2, 3, 4, 
2, 2, 7, 5, 2)), .Names = c("a", "b", "c", "d", "e", "f"), row.names = c(NA, 
8L), class = "data.frame")

  a b c d e f
1 1 1 6 6 1 2
2 3 3 3 2 2 3
3 4 4 6 4 4 4
4 6 2 5 5 5 2
5 3 6 3 3 6 2
6 2 7 6 7 7 7
7 5 2 5 2 6 5
8 1 6 3 6 3 2

Eu esperava encontrar uma solução para o meu problema usando o pacote dplyr (e sim, eu sei que esse código não deve funcionar, mas acho que isso esclarece o propósito) para criar uma nova coluna g:

 library(dplyr)
 df <- mutate(df,
         if (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)){g = 2},
         if (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4) {g = 3})

O resultado do código que estou procurando deve ter esse resultado neste exemplo específico:

  a b c d e f  g
1 1 1 6 6 1 2  3
2 3 3 3 2 2 3  3
3 4 4 6 4 4 4  3
4 6 2 5 5 5 2 NA
5 3 6 3 3 6 2 NA
6 2 7 6 7 7 7  2
7 5 2 5 2 6 5  2
8 1 6 3 6 3 2  3

Alguém tem uma idéia sobre como fazer isso no dplyr? Esse quadro de dados é apenas um exemplo, os quadros de dados com os quais estou lidando são muito maiores. Por causa de sua velocidade, tentei usar o dplyr, mas talvez haja outras maneiras melhores de lidar com esse problema?

rdatasculptor
fonte
2
Sim, mas dplyr::case_when()é muito mais claro que um ifelse,
smci

Respostas:

216

Usar ifelse

df %>%
  mutate(g = ifelse(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4), 2,
               ifelse(a == 0 | a == 1 | a == 4 | a == 3 |  c == 4, 3, NA)))

Added - if_else: Observe que no dplyr 0.5 existe uma if_elsefunção definida; portanto, uma alternativa seria substituir ifelsepor if_else; no entanto, observe que, uma vez que if_elseé mais rígido do que ifelse(ambas as pernas da condição devem ter o mesmo tipo), portanto NA, nesse caso, teria que ser substituído por NA_real_.

df %>%
  mutate(g = if_else(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4), 2,
               if_else(a == 0 | a == 1 | a == 4 | a == 3 |  c == 4, 3, NA_real_)))

Added - case_when Desde que esta pergunta foi publicada, o dplyr adicionou, case_whenentão outra alternativa seria:

df %>% mutate(g = case_when(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4) ~ 2,
                            a == 0 | a == 1 | a == 4 | a == 3 |  c == 4 ~ 3,
                            TRUE ~ NA_real_))

Added - arithmetic / na_if Se os valores forem numéricos e as condições (exceto o valor padrão de NA no final) forem mutuamente exclusivas, como é o caso da pergunta, podemos usar uma expressão aritmética para que cada termo seja multiplicado pelo resultado desejado usando na_ifno final para substituir 0 por NA.

df %>%
  mutate(g = 2 * (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)) +
             3 * (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
         g = na_if(g, 0))
G. Grothendieck
fonte
3
Qual é a lógica se, em vez de NA, quero que as linhas que não atendem às condições permaneçam as mesmas?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck 30/03
11
case_when é tããããão lindo e demorei tanto para descobrir que ele realmente estava lá. Eu acho que isso deveria estar nos tutoriais mais simples do dplyr, é muito comum ter a necessidade de calcular coisas para subconjuntos de dados, mas ainda querendo manter os dados completos.
21418 Javier Fajardo
55

Como você solicita outras maneiras melhores de lidar com o problema, aqui está outra maneira de usar data.table:

require(data.table) ## 1.9.2+
setDT(df)
df[a %in% c(0,1,3,4) | c == 4, g := 3L]
df[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]

Observe que a ordem das instruções condicionais é revertida para obter gcorretamente. Não há cópia gfeita, mesmo durante a segunda tarefa - ela é substituída no local .

Em dados maiores, isso teria melhor desempenho do que o uso de aninhados if-else , pois pode avaliar os casos 'sim' e 'não' , e o aninhamento pode ficar mais difícil de ler / manter o IMHO.


Aqui está uma referência em dados relativamente maiores:

# R version 3.1.0
require(data.table) ## 1.9.2
require(dplyr)
DT <- setDT(lapply(1:6, function(x) sample(7, 1e7, TRUE)))
setnames(DT, letters[1:6])
# > dim(DT) 
# [1] 10000000        6
DF <- as.data.frame(DT)

DT_fun <- function(DT) {
    DT[(a %in% c(0,1,3,4) | c == 4), g := 3L]
    DT[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]
}

DPLYR_fun <- function(DF) {
    mutate(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
            ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

BASE_fun <- function(DF) { # R v3.1.0
    transform(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
            ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

system.time(ans1 <- DT_fun(DT))
#   user  system elapsed 
#  2.659   0.420   3.107 

system.time(ans2 <- DPLYR_fun(DF))
#   user  system elapsed 
# 11.822   1.075  12.976 

system.time(ans3 <- BASE_fun(DF))
#   user  system elapsed 
# 11.676   1.530  13.319 

identical(as.data.frame(ans1), as.data.frame(ans2))
# [1] TRUE

identical(as.data.frame(ans1), as.data.frame(ans3))
# [1] TRUE

Não tenho certeza se essa é uma alternativa que você pediu, mas espero que ajude.

Uma corrida
fonte
4
Bom pedaço de código! A resposta de G. Grotendieck funciona e é curta, por isso escolhi essa como resposta à minha pergunta, mas agradeço a sua solução. Eu com certeza vou tentar dessa maneira também.
Rdatasculptor
Como DT_funestá modificando sua entrada no local, o benchmark pode não ser bastante justo - além de não receber a mesma entrada da 2ª iteração para frente (o que pode afetar o tempo, uma vez que DT$gjá está alocado?), O resultado também se propaga de volta ans1e, portanto, pode ( se Deems otimizador de R necessário? Não tenho certeza sobre isso ...) evitar outra cópia que DPLYR_fune BASE_funnecessidade de fazer?
Ken Williams
Só para esclarecer, acho que essa data.tablesolução é ótima e uso data.tablesempre que realmente preciso de velocidade para operações em tabelas e não quero ir até o C ++. No entanto, é preciso ter muito cuidado com as modificações implementadas!
Ken Williams
Estou tentando me acostumar com coisas mais organizadas do data.table, e este é um daqueles exemplos de um caso de uso bastante comum em que o data.table é mais fácil de ler e mais eficiente. Meu principal motivo para querer desenvolver um vocabulário mais ordenado é a legibilidade para mim e para os outros, mas, neste caso, parece que o data.table vence.
Paul McMurdie
38

O dplyr agora tem uma função case_whenque oferece um vetorizado if. A sintaxe é um pouco estranha se comparada com a de mosaic:::derivedFactorque você não pode acessar variáveis ​​da maneira dplyr padrão e precisa declarar o modo de NA, mas é consideravelmente mais rápido que mosaic:::derivedFactor.

df %>%
mutate(g = case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, 
                     a %in% c(0,1,3,4) | c == 4 ~ 3L, 
                     TRUE~as.integer(NA)))

EDIT: Se você estiver usando a dplyr::case_when()versão anterior à 0.7.0 do pacote, precisará preceder os nomes das variáveis ​​com ' .$' (por exemplo, escreva .$a == 1dentro case_when).

Referência : para a referência (reutilizando funções do post de Arun) e reduzindo o tamanho da amostra:

require(data.table) 
require(mosaic) 
require(dplyr)
require(microbenchmark)

set.seed(42) # To recreate the dataframe
DT <- setDT(lapply(1:6, function(x) sample(7, 10000, TRUE)))
setnames(DT, letters[1:6])
DF <- as.data.frame(DT)

DPLYR_case_when <- function(DF) {
  DF %>%
  mutate(g = case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, 
                       a %in% c(0,1,3,4) | c==4 ~ 3L, 
                       TRUE~as.integer(NA)))
}

DT_fun <- function(DT) {
  DT[(a %in% c(0,1,3,4) | c == 4), g := 3L]
  DT[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]
}

DPLYR_fun <- function(DF) {
  mutate(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
                    ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

mosa_fun <- function(DF) {
  mutate(DF, g = derivedFactor(
    "2" = (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)),
    "3" = (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
    .method = "first",
    .default = NA
  ))
}

perf_results <- microbenchmark(
  dt_fun <- DT_fun(copy(DT)),
  dplyr_ifelse <- DPLYR_fun(copy(DF)),
  dplyr_case_when <- DPLYR_case_when(copy(DF)),
  mosa <- mosa_fun(copy(DF)),
  times = 100L
)

Isto dá:

print(perf_results)
Unit: milliseconds
           expr        min         lq       mean     median         uq        max neval
         dt_fun   1.391402    1.560751   1.658337   1.651201   1.716851   2.383801   100
   dplyr_ifelse   1.172601    1.230351   1.331538   1.294851   1.390351   1.995701   100
dplyr_case_when   1.648201    1.768002   1.860968   1.844101   1.958801   2.207001   100
           mosa 255.591301  281.158350 291.391586 286.549802 292.101601 545.880702   100
Matifou
fonte
case_whentambém poderia ser escrito como:df %>% mutate(g = with(., case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, a %in% c(0,1,3,4) | c==4 ~ 3L, TRUE ~ NA_integer_)))
G. Grothendieck
3
Essa referência é em microssegundos / milissegundos / dias? Esta referência não faz sentido sem a unidade de medida fornecida. Além disso, a marcação de banco de dados em um conjunto de dados menor que 1e6 também não faz sentido, pois não é dimensionada.
22717 David Arenburg
3
Pls modificar a sua resposta, você não precisa o .$mais na nova versão do dplyr
Amit Kohli
14

A derivedFactorfunção do mosaicpacote parece ter sido projetada para lidar com isso. Usando este exemplo, seria semelhante a:

library(dplyr)
library(mosaic)
df <- mutate(df, g = derivedFactor(
     "2" = (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)),
     "3" = (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
     .method = "first",
     .default = NA
     ))

(Se você deseja que o resultado seja numérico em vez de um fator, pode encerrar derivedFactoruma as.numericchamada.)

derivedFactor também pode ser usado para um número arbitrário de condicionais.

Jake Fisher
fonte
4
@hadley deve fazer desta a sintaxe padrão para o dplyr. Precisando de instruções aninhadas "ifelse" é o único pior parte do pacote, que é principalmente o caso porque as outras funções são tão bons
rsoren
Você também pode impedir que o resultado seja um fator usando a .asFactor = Fopção ou usando a derivedVariablefunção (similar) no mesmo pacote.
Jake Fisher
Parece que a recodepartir do dplyr 0.5 fará isso. Ainda não investiguei isso. Veja blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when agora é uma implementação bastante limpa do caso do estilo SQL quando:

structure(list(a = c(1, 3, 4, 6, 3, 2, 5, 1), b = c(1, 3, 4, 
2, 6, 7, 2, 6), c = c(6, 3, 6, 5, 3, 6, 5, 3), d = c(6, 2, 4, 
5, 3, 7, 2, 6), e = c(1, 2, 4, 5, 6, 7, 6, 3), f = c(2, 3, 4, 
2, 2, 7, 5, 2)), .Names = c("a", "b", "c", "d", "e", "f"), row.names = c(NA, 
8L), class = "data.frame") -> df


df %>% 
    mutate( g = case_when(
                a == 2 | a == 5 | a == 7 | (a == 1 & b == 4 )     ~   2,
                a == 0 | a == 1 | a == 4 |  a == 3 | c == 4       ~   3
))

Usando dplyr 0.7.4

O manual: http://dplyr.tidyverse.org/reference/case_when.html

Rasmus Larsen
fonte