Il pacchetto dplyr può essere utilizzato per il muting condizionale?


179

La mutazione può essere utilizzata quando la mutazione è condizionata (a seconda dei valori di determinati valori di colonna)?

Questo esempio aiuta a mostrare cosa intendo.

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

Speravo di trovare una soluzione al mio problema usando il pacchetto dplyr (e sì, so che non è un codice che dovrebbe funzionare, ma suppongo che chiarisca lo scopo) per creare una nuova colonna 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})

Il risultato del codice che sto cercando dovrebbe avere questo risultato in questo esempio particolare:

  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

Qualcuno ha un'idea di come farlo in dplyr? Questo frame di dati è solo un esempio, i frame di dati di cui mi occupo sono molto più grandi. A causa della sua velocità ho provato ad usare dplyr, ma forse ci sono altri modi migliori per gestire questo problema?


2
Sì, ma dplyr::case_when()è molto più chiaro di un ifelse,
smci

Risposte:


216

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

Aggiunto - if_else: nota che in dplyr 0.5 è stata if_elsedefinita una funzione, quindi un'alternativa sarebbe sostituirla ifelsecon if_else; tuttavia, si noti che poiché if_elseè più rigoroso di ifelse(entrambe le gambe della condizione devono avere lo stesso tipo), NAin tal caso si dovrebbe sostituire con 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_)))

Aggiunto - case_when Da quando questa domanda è stata pubblicata, dplyr ha aggiunto case_whenquindi un'altra alternativa sarebbe:

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

Aggiunto - arithmetic / na_if Se i valori sono numerici e le condizioni (ad eccezione del valore predefinito di NA alla fine) si escludono a vicenda, come nel caso della domanda, allora possiamo usare un'espressione aritmetica tale che ogni termine sia moltiplicato dal risultato desiderato usando na_ifalla fine per sostituire 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))

3
Qual è la logica se invece di NA, voglio che le righe che non soddisfano le condizioni rimangano le stesse?
Nazer,

10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck,

11
case_when è così bello e mi ci è voluto moltissimo tempo per capire che in realtà era lì. Penso che questo dovrebbe essere nei tutorial dplyr più semplici, è molto comune avere la necessità di calcolare roba per sottoinsiemi di dati, ma voler comunque mantenere i dati completi.
Javier Fajardo,

55

Dato che chiedi altri modi migliori per gestire il problema, ecco un altro modo di usare 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]

Si noti che l'ordine delle istruzioni condizionali è invertito per ottenere gcorrettamente. Non c'è alcuna copia di gmade, anche durante il secondo incarico: viene sostituito sul posto .

Su dati più grandi ciò avrebbe prestazioni migliori rispetto all'utilizzo nidificato if-else , in quanto può valutare sia i casi 'sì' che 'no' e l'annidamento può diventare più difficile da leggere / mantenere IMHO.


Ecco un benchmark su dati relativamente più grandi:

# 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

Non sono sicuro che si tratti di un'alternativa che avresti chiesto, ma spero che sia di aiuto.


4
Bel pezzo di codice! La risposta di G. Grotendieck funziona ed è breve, quindi l'ho scelta come risposta alla mia domanda, ma ti ringrazio per la tua soluzione. Di sicuro lo proverò anche in questo modo.
rdatasculptor,

Poiché DT_funsta modificando il proprio input sul posto, il benchmark potrebbe non essere abbastanza equo - oltre a non ricevere lo stesso input dal 2 ° iteration forward (che potrebbe influire sui tempi poiché DT$gè già allocato?), Il risultato si propaga anche a ans1e quindi potrebbe ( se l'ottimizzatore di R lo ritiene necessario? Non sei sicuro su questo ...) evita un'altra copia che DPLYR_fune BASE_fundevi fare?
Ken Williams,

Solo per essere chiari, penso che questa data.tablesoluzione sia eccezionale e uso data.tableovunque abbia davvero bisogno di velocità per le operazioni sui tavoli e non voglio andare fino in C ++. Tuttavia richiede molta attenzione alle modifiche in atto!
Ken Williams,

Sto cercando di abituarmi a cose più ordinate da data.table, e questo è uno di quegli esempi di un caso d'uso piuttosto comune in cui data.table è sia più facile da leggere che più efficiente. La mia ragione principale per voler sviluppare un ordine più ordinato nel mio vocabolario è la leggibilità per me stesso e gli altri, ma in questo caso sembra che vince data.table.
Paul McMurdie,

38

dplyr ora ha una funzione case_whenche offre un if vettoriale. La sintassi è un po 'strana rispetto a mosaic:::derivedFactorcome non puoi accedere alle variabili nel modo dplyr standard e devi dichiarare la modalità di NA, ma è considerevolmente più veloce di 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)))

MODIFICA: se stai utilizzando una dplyr::case_when()versione precedente alla 0.7.0 del pacchetto, devi precedere i nomi delle variabili con ' .$' (ad esempio, scrivi .$a == 1all'interno case_when).

Benchmark : per il benchmark (riutilizzo delle funzioni del post di Arun) e riduzione della dimensione del campione:

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
)

Questo 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

case_whenpotrebbe anche essere scritto come: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 il

3
Questo benchmark è in microsecondi / millisecondi / giorni, cosa? Questo benchmark non ha senso senza l'unità di misura fornita. Inoltre, il benchmarking su un set di dati inferiore a 1e6 è anche privo di significato in quanto non viene ridimensionato.
David Arenburg,

3
Per favore modifica la tua risposta, non hai più bisogno .$della nuova versione di dplyr
Amit Kohli

14

La derivedFactorfunzione dal mosaicpacchetto sembra essere progettata per gestire questo. Usando questo esempio, sembrerebbe:

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 si desidera che il risultato sia numerico anziché un fattore, è possibile concludere derivedFactoruna as.numericchiamata.)

derivedFactor può essere utilizzato anche per un numero arbitrario di condizionali.


4
@hadley dovrebbe rendere questa la sintassi predefinita per dplyr. La necessità di istruzioni "ifelse" nidificate è la parte peggiore del pacchetto, che è principalmente il caso perché le altre funzioni sono così buone
rsoren,

Puoi anche evitare che il risultato sia un fattore usando l' .asFactor = Fopzione o usando la funzione (simile) derivedVariablenello stesso pacchetto.
Jake Fisher,

Sembra che recodeda dplyr 0.5 lo farà. Non l'ho ancora investigato però. Vedi blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher

12

case_when è ora un'implementazione abbastanza chiara del caso in stile 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

Il manuale: http://dplyr.tidyverse.org/reference/case_when.html

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.