web-dev-qa-db-fra.com

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

La mutation peut-elle être utilisée 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 paquet dplyr (et oui je sais que ce n'est pas du code qui devrait fonctionner, mais j'imagine que cela explique clairement le but) 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 cherche 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 at-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 que 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 moyens plus efficaces de gérer ce problème?

153
rdatasculptor

Utilisez 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, une fonction if_else est définie. Une alternative serait donc de remplacer ifelse par if_else; notez cependant que, puisque if_else est plus strict que ifelse (les deux branches de la condition doivent avoir le même type), le NA doit alors ê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_)))

Added - case_when Depuis que cette question a été postée, dplyr a ajouté case_when. Une 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_))
189
G. Grothendieck

Puisque vous demandez d’autres moyens de mieux 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 g correctement. Il n'y a pas de copie de g faite, même pendant la deuxième affectation - elle est remplacée sur place .

Sur des données plus volumineuses, cela donnerait de meilleures performances que d'utiliser imbriqué if-else, comme il peut évaluer à la fois "oui" et "non" 'cas , et l'imbrication peut devenir plus difficile à lire/maintenir à mon humble avis.


Voici un point de repère sur des données relativement plus 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

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

52
Arun

dplyr a maintenant une fonction case_when qui offre un if vectorisé. La syntaxe est un peu étrange comparée à mosaic:::derivedFactor car vous ne pouvez pas accéder aux variables de la manière standard dplyr et devez déclarer le mode de NA, mais elle 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 dplyr::case_when() d’avant la version 0.7.0 du paquet, vous devez faire précéder les noms de variables par '.$' (par exemple, write .$a == 1 dans case_when).

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

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

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

microbenchmark(
  DT_fun(DT),
  DPLYR_fun(DF),
  DPLYR_case_when(DF),
  mosa_fun(DF),
  times=20
)

Cela donne:

            expr        min         lq       mean     median         uq        max neval
         DT_fun(DT)   1.503589   1.626971   2.054825   1.755860   2.292157   3.426192    20
      DPLYR_fun(DF)   2.420798   2.596476   3.617092   3.484567   4.184260   6.235367    20
DPLYR_case_when(DF)   2.153481   2.252134   6.124249   2.365763   3.119575  72.344114    20
       mosa_fun(DF) 396.344113 407.649356 413.743179 412.412634 416.515742 459.974969    20
34
Matifou

La fonction derivedFactor de mosaic 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 voulez que le résultat soit numérique au lieu d'un facteur, vous pouvez envelopper derivedFactor dans un appel as.numeric.)

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

13
Jake Fisher

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

Utiliser dplyr 0.7.4

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

11
Rasmus Larsen