¿Se puede usar el paquete dplyr para la mutación condicional?

178

¿Se puede usar la mutación cuando la mutación es condicional (dependiendo de los valores de ciertos valores de columna)?

Este ejemplo ayuda a mostrar lo que quiero decir.

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

Esperaba encontrar una solución a mi problema usando el paquete dplyr (y sí, sé que este código no debería funcionar, pero supongo que aclara el propósito) para crear una nueva columna 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})

El resultado del código que estoy buscando debería tener este resultado en este ejemplo particular:

  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

¿Alguien tiene una idea sobre cómo hacer esto en dplyr? Este marco de datos es solo un ejemplo, los marcos de datos con los que estoy tratando son mucho más grandes. Debido a su velocidad, traté de usar dplyr, pero ¿quizás hay otras formas mejores de manejar este problema?

rdatasculptor
fuente
2
Sí, pero dplyr::case_when()es mucho más claro que un ifelse,
smci

Respuestas:

216

Utilizar 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)))

Añadido - if_else: tenga en cuenta que en dplyr 0.5 hay una if_elsefunción definida, por lo que una alternativa sería reemplazar ifelsecon if_else; sin embargo, tenga en cuenta que dado que if_elsees más estricto que ifelse(ambas partes de la condición deben tener el mismo tipo), entonces NAen ese caso tendría que ser reemplazado 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_)))

Agregado - case_when Desde que se publicó esta pregunta, dplyr ha agregado, case_whenpor lo que otra alternativa sería:

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_))

Agregado - aritmética / na_if Si los valores son numéricos y las condiciones (excepto el valor predeterminado de NA al final) son mutuamente excluyentes, como es el caso en la pregunta, entonces podemos usar una expresión aritmética de manera que cada término se multiplique por el resultado deseado usando na_ifal final para reemplazar 0 con 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
fuente
3
¿Cuál es la lógica si, en lugar de NA, quiero que las filas que no cumplen con las condiciones permanezcan igual?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck
11
case_when es muuuy hermoso, y me llevó mucho tiempo darme cuenta de que realmente estaba allí. Creo que esto debería estar en los tutoriales más simples de dplyr, es muy común tener la necesidad de calcular cosas para subconjuntos de datos, pero aún así quiero mantener los datos completos.
Javier Fajardo
55

Dado que solicita otras formas mejores de manejar el problema, aquí hay otra forma 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]

Tenga en cuenta que el orden de las declaraciones condicionales se invierte para obtener gcorrectamente. No hay copia de ghecho, incluso durante la segunda asignación, se reemplaza en el lugar .

En datos más grandes, esto tendría un mejor rendimiento que el uso anidado if-else , ya que puede evaluar los casos 'sí' y 'no' , y la anidación puede ser más difícil de leer / mantener en mi humilde opinión.


Aquí hay un punto de referencia en datos relativamente más grandes:

# 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

No estoy seguro de si esta es una alternativa que había pedido, pero espero que ayude.

Arun
fuente
44
Bonito pedazo de código! La respuesta de G. Grotendieck funciona y es breve, así que elegí esa como respuesta a mi pregunta, pero le agradezco su solución. Seguro que lo intentaré de esta manera también.
rdatasculptor
Dado que DT_funestá modificando su entrada en el lugar, el punto de referencia podría no ser del todo justo, además de no recibir la misma entrada desde la segunda iteración hacia adelante (¿qué podría afectar el tiempo ya DT$gque ya está asignado?), El resultado también se propaga de regreso ans1y, por lo tanto, podría ( Si optimizador de R estime necesario? No estoy seguro de esto ...) evitar otra copia que DPLYR_funy BASE_funes necesario hacer?
Ken Williams
Sin embargo, para ser claros, creo que esta data.tablesolución es excelente, y la uso data.tabledonde sea que realmente necesite velocidad para las operaciones en tablas y no quiero llegar hasta C ++. ¡Sin embargo, requiere tener mucho cuidado con las modificaciones en el lugar!
Ken Williams
Estoy tratando de acostumbrarme a más cosas ordenadas de data.table, y este es uno de esos ejemplos de un caso de uso bastante común en el que data.table es más fácil de leer y más eficiente. Mi razón principal para querer desarrollar más tidyverse en mi vocabulario es la legibilidad para mí y para otros, pero en este caso parece que data.table triunfa.
Paul McMurdie
38

dplyr ahora tiene una función case_whenque ofrece un if vectorizado. La sintaxis es un poco extraña en comparación con el hecho de mosaic:::derivedFactorque no puede acceder a las variables en la forma estándar dplyr, y necesita declarar el modo de NA, pero es considerablemente más 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)))

EDITAR: Si está utilizando dplyr::case_when()desde la versión 0.7.0 anterior del paquete, entonces necesita preceder los nombres de las variables con ' .$' (por ejemplo, escribir .$a == 1dentro case_when).

Punto de referencia : para el punto de referencia (reutilizando funciones de la publicación de Arun) y reduciendo el tamaño de la muestra:

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
)

Esto da:

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
fuente
case_whentambién podría escribirse 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
¿Es este punto de referencia en microsegundos / milisegundos / días, qué? Este punto de referencia no tiene sentido sin la unidad de medida proporcionada. Además, la evaluación comparativa en un conjunto de datos más pequeño que 1e6 tampoco tiene sentido ya que no escala.
David Arenburg
3
Modifique su respuesta, ya no la necesita .$en la nueva versión de dplyr
Amit Kohli
14

La derivedFactorfunción del mosaicpaquete parece estar diseñada para manejar esto. Usando este ejemplo, se vería así:

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
     ))

(Si desea que el resultado sea numérico en lugar de un factor, puede completar derivedFactoruna as.numericllamada).

derivedFactor también se puede usar para un número arbitrario de condicionales.

Jake Fisher
fuente
44
@hadley debería hacer que esta sea la sintaxis predeterminada para dplyr. Necesidad de instrucciones anidadas "ifelse" es el peor parte del paquete, que es principalmente el caso porque las otras funciones son tan buenos
rsoren
También puede evitar que el resultado sea un factor usando la .asFactor = Fopción o usando la función (similar) derivedVariableen el mismo paquete.
Jake Fisher
Parece que recodedesde dplyr 0.5 hará esto. Sin embargo, aún no lo he investigado. Ver blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when ahora es una implementación bastante limpia del caso de estilo SQL cuando:

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

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

Rasmus Larsen
fuente