Le package dplyr peut-il être utilisé pour la mutation conditionnelle?

179

Le mutate peut-il être utilisé lorsque la mutation est conditionnelle (en fonction des valeurs de certaines valeurs de colonne)?

Cet exemple aide à montrer ce que je veux dire.

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

J'espérais trouver une solution à mon problème en utilisant le package dplyr (et oui je sais que ce n'est pas du code qui devrait fonctionner, mais je suppose que cela rend le but clair) pour créer une nouvelle colonne 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})

Le résultat du code que je recherche devrait avoir ce résultat dans cet exemple particulier:

  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

Quelqu'un a-t-il une idée sur la façon de faire cela dans dplyr? Cette trame de données n'est qu'un exemple, les trames de données dont je traite sont beaucoup plus grandes. En raison de sa vitesse, j'ai essayé d'utiliser dplyr, mais peut-être existe-t-il d'autres meilleurs moyens de gérer ce problème?

rdatasculptor
la source
2
Oui, mais dplyr::case_when()c'est beaucoup plus clair qu'un ifelse,
smci

Réponses:

216

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

Ajouté - if_else: Notez que dans dplyr 0.5 il y a une if_elsefonction définie donc une alternative serait de remplacer ifelsepar if_else; cependant, notez que puisque if_elseest plus strict que ifelse(les deux branches de la condition doivent avoir le même type) donc NAdans ce cas devrait être remplacé par 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_)))

Ajouté - case_when Depuis que cette question a été publiée, dplyr a ajouté case_whenune autre alternative serait:

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

Ajouté - arithmétique / na_if Si les valeurs sont numériques et les conditions (à l'exception de la valeur par défaut de NA à la fin) sont mutuellement exclusives, comme c'est le cas dans la question, alors nous pouvons utiliser une expression arithmétique telle que chaque terme est multiplié par le résultat souhaité en utilisant na_ifà la fin pour remplacer 0 par 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
la source
3
Quelle est la logique si au lieu de NA, je veux que les lignes qui ne remplissent pas les conditions restent les mêmes?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck
11
case_when est tellement beau, et il m'a fallu tellement de temps pour comprendre que c'était vraiment là. Je pense que cela devrait être dans les didacticiels dplyr les plus simples, il est très courant d'avoir besoin de calculer des éléments pour des sous-ensembles de données, mais en voulant toujours garder les données complètes.
Javier Fajardo
55

Puisque vous demandez d'autres meilleures façons de gérer le problème, voici une autre façon d'utiliser 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]

Notez que l'ordre des instructions conditionnelles est inversé pour obtenir gcorrectement. Il n'y a pas de copie de gmade, même pendant la deuxième mission - elle est remplacée sur place .

Sur des données plus volumineuses, cela aurait de meilleures performances que l'utilisation imbriquée if-else , car il peut évaluer à la fois les cas «oui» et «non» , et l'imbrication peut devenir plus difficile à lire / maintenir à mon humble avis.


Voici un benchmark sur des données relativement plus volumineuses:

# 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

Je ne sais pas si c'est une alternative que vous aviez demandée, mais j'espère que cela vous aidera.

Arun
la source
4
Beau morceau de code! La réponse de G. Grotendieck fonctionne et est courte, j'ai donc choisi celle-là comme réponse à ma question, mais je vous remercie pour votre solution. Je vais certainement essayer de cette façon aussi.
rdatasculptor
Puisqu'il DT_funmodifie son entrée en place, le benchmark peut ne pas être tout à fait juste - en plus de ne pas recevoir la même entrée de la 2ème itération vers l'avant (ce qui pourrait affecter le timing car DT$gest déjà alloué?), Le résultat se propage également vers ans1et pourrait donc ( si l'optimiseur de R le juge nécessaire? Pas sûr à ce sujet ...) éviter une autre copie DPLYR_funet que vous BASE_fundevez faire?
Ken Williams
Pour être clair cependant, je pense que cette data.tablesolution est excellente, et je l'utilise data.tablepartout où j'ai vraiment besoin de vitesse pour les opérations sur les tables et je ne veux pas aller jusqu'au C ++. Il faut cependant faire très attention aux modifications en place!
Ken Williams
J'essaie de m'habituer à des trucs plus tidyverse de data.table, et c'est l'un de ces exemples d'un cas d'utilisation assez courant où data.table est à la fois plus facile à lire et plus efficace. Ma principale raison de vouloir développer plus de tidyverse dans mon vocabulaire est la lisibilité pour moi et les autres, mais dans ce cas, il semble que data.table gagne.
Paul McMurdie
38

dplyr a maintenant une fonction case_whenqui offre un if vectorisé. La syntaxe est un peu étrange comparée à mosaic:::derivedFactorcar vous ne pouvez pas accéder aux variables de la manière standard de dplyr, et devez déclarer le mode de NA, mais c'est considérablement plus rapide 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: Si vous utilisez une dplyr::case_when()version antérieure à 0.7.0 du paquet, vous devez alors faire précéder les noms de variables avec ' .$' (par exemple écrire à l' .$a == 1intérieur case_when).

Benchmark : Pour le benchmark (réutilisation des fonctions du poste d'Arun) et réduction de la taille de l'échantillon:

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
)

Cela donne:

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
la source
case_whenpourrait aussi s'écrire: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
Cette référence est-elle en microsecondes / millisecondes / jours, quoi? Cette référence n'a pas de sens sans l'unité de mesure fournie. De plus, le benchmarking sur un ensemble de données inférieur à 1e6 n'a pas de sens car il ne s'adapte pas.
David Arenburg
3
Veuillez modifier votre réponse, vous n'en avez plus besoin .$dans la nouvelle version de dplyr
Amit Kohli
14

La derivedFactorfonction de mosaicpackage semble être conçue pour gérer cela. En utilisant cet exemple, cela ressemblerait à:

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 vous souhaitez que le résultat soit numérique au lieu d'un facteur, vous pouvez encapsuler derivedFactorun as.numericappel.)

derivedFactor peut également être utilisé pour un nombre arbitraire de conditions.

Jake Fisher
la source
4
@hadley devrait en faire la syntaxe par défaut de dplyr. Le besoin d'instructions "ifelse" imbriquées est la pire partie du paquet, ce qui est principalement le cas parce que les autres fonctions sont si bonnes
rsoren
Vous pouvez également empêcher le résultat d'être un facteur en utilisant l' .asFactor = Foption ou en utilisant la fonction (similaire) derivedVariabledans le même package.
Jake Fisher
Il semble que recodedplyr 0.5 fera cela. Cependant, je n'ai pas encore enquêté dessus. Voir blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when est maintenant une implémentation assez propre du cas de style SQL lorsque:

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

Utilisation de dplyr 0.7.4

Le manuel: http://dplyr.tidyverse.org/reference/case_when.html

Rasmus Larsen
la source