Czy pakiet dplyr może być użyty do warunkowej mutacji?

178

Czy mutatu można użyć, gdy mutacja jest warunkowa (w zależności od wartości określonych wartości kolumn)?

Ten przykład pomaga pokazać, o co mi chodzi.

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

Miałem nadzieję, że znajdę rozwiązanie mojego problemu za pomocą pakietu dplyr (i tak, wiem, że to nie kod, który powinien działać, ale wydaje mi się, że wyjaśnia cel) do utworzenia nowej kolumny 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})

Wynik szukanego kodu powinien mieć następujący wynik w tym konkretnym przykładzie:

  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

Czy ktoś ma pomysł jak to zrobić w dplyr? Ta ramka danych to tylko przykład, ramki danych, z którymi mam do czynienia, są znacznie większe. Ze względu na jego szybkość próbowałem użyć dplyr, ale może są inne, lepsze sposoby rozwiązania tego problemu?

rdatasculptor
źródło
2
Tak, ale dplyr::case_when()jest znacznie jaśniejsze niż ifelse,
smci

Odpowiedzi:

216

Posługiwać się 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)))

Dodanej - if_else: Zauważ, że w dplyr 0.5 istnieje if_elsefunkcja zdefiniowana tak alternatywą byłoby wymienić ifelsez if_else; należy jednak pamiętać, że ponieważ if_elsejest bardziej rygorystyczne niż ifelse(obie nogi warunku muszą mieć ten sam typ), więc NAw tym przypadku należałoby zastąpić 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_)))

Dodano - case_when Od czasu wysłania tego pytania dplyr dodał, case_whenwięc inną alternatywą byłoby:

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

Dodane - arithmetic / na_if Jeśli wartości są liczbowe, a warunki (z wyjątkiem domyślnej wartości NA na końcu) wzajemnie się wykluczają, tak jak ma to miejsce w pytaniu, możemy użyć wyrażenia arytmetycznego, w którym każdy termin jest pomnożony przez pożądany wynik, używając na_ifna końcu, aby zamienić 0 na 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
źródło
3
Jaka jest logika, jeśli zamiast tego NAchcę, aby wiersze, które nie spełniają warunków, pozostały takie same?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck
11
case_when jest baaaardzo piękna i zajęło mi bardzo dużo czasu, zanim zorientowałem się, że faktycznie tam jest. Myślę, że powinno to być w najprostszych samouczkach dplyr, bardzo często występuje potrzeba obliczania elementów dla podzbiorów danych, ale nadal chce się, aby dane były kompletne.
Javier Fajardo
55

Ponieważ pytasz o inne lepsze sposoby rozwiązania problemu, oto inny sposób 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]

Zauważ, że kolejność instrukcji warunkowych jest odwrócona, aby uzyskać gpoprawne wyniki. Nie ma kopii g, nawet podczas drugiego zadania - jest wymieniana na miejscu .

W przypadku większych danych miałoby to lepszą wydajność niż użycie zagnieżdżonych if-else , ponieważ może oceniać przypadki „tak” i „nie” , a zagnieżdżanie może być trudniejsze do odczytania / utrzymania IMHO.


Oto test porównawczy dla stosunkowo większych danych:

# 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

Nie jestem pewien, czy jest to alternatywa, o którą prosiłeś, ale mam nadzieję, że pomoże.

Bieg
źródło
4
Niezły kawałek kodu! Odpowiedź G. Grotendiecka działa i jest krótka, więc wybrałem ją jako odpowiedź na moje pytanie, ale dziękuję za rozwiązanie. Z pewnością spróbuję tego również w ten sposób.
rdatasculptor
Ponieważ DT_funmodyfikuje swoje dane wejściowe w miejscu, benchmark może nie być całkiem sprawiedliwy - oprócz tego, że nie otrzymuje tych samych danych wejściowych z drugiej iteracji do przodu (co może wpływać na czas, ponieważ DT$gjest już przydzielony?), Wynik również propaguje się z powrotem do, ans1a zatem może ( jeśli uzna to za optymalizatora R jest konieczne? Nie wiesz na ten temat ...) uniknąć kolejną kopię, która DPLYR_funi BASE_funtrzeba dokonać?
Ken Williams
Żeby było jasne, myślę, że to data.tablerozwiązanie jest świetne i używam data.tablewszędzie tam, gdzie naprawdę potrzebuję szybkości do operacji na tabelach i nie chcę przechodzić aż do C ++. Wymaga to jednak dużej ostrożności podczas wprowadzania modyfikacji!
Ken Williams,
Próbuję przyzwyczaić się do bardziej uporządkowanych rzeczy z data.table i jest to jeden z przykładów dość powszechnego przypadku użycia, że ​​data.table jest zarówno łatwiejsza do odczytania, jak i bardziej wydajna. Moim głównym powodem, dla którego chcę rozwinąć bardziej uporządkowaną wersję w swoim słownictwie, jest czytelność dla siebie i innych, ale w tym przypadku wygląda na to, że data.table wygrywa.
Paul McMurdie
38

dplyr ma teraz funkcję, case_whenktóra oferuje wektoryzację if. Składnia jest trochę dziwna w porównaniu z tym, mosaic:::derivedFactorże nie możesz uzyskać dostępu do zmiennych w standardowy sposób dplyr i musisz zadeklarować tryb NA, ale jest znacznie szybszy niż 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)))

EDYCJA: Jeśli używasz dplyr::case_when()pakietu sprzed wersji 0.7.0, musisz poprzedzić nazwy zmiennych znakiem „ .$” (np. Wpisz .$a == 1wewnątrz case_when).

Benchmark : W przypadku testu porównawczego (ponowne wykorzystanie funkcji z postu Arun) i zmniejszenie wielkości próby:

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
)

To daje:

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
źródło
case_whenmożna też zapisać jako: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
Czy ten test jest w mikrosekundach / milisekundach / dni, co? Ten wzorzec jest bez znaczenia bez dostarczonej jednostki miary. Również benchmarking na zestawie danych mniejszym niż 1e6 również jest bez znaczenia, ponieważ nie jest skalowany.
David Arenburg
3
Proszę zmodyfikuj swoją odpowiedź, nie potrzebujesz .$już jej w nowej wersji dplyr
Amit Kohli
14

derivedFactorFunkcji z mosaicpakietu wydaje się być tak zaprojektowane, aby sobie z tym poradzić. Korzystając z tego przykładu, wyglądałoby to tak:

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

(Jeśli chcesz być wynik numeryczny zamiast czynnika, można owinąć derivedFactorw as.numericzaproszeniu).

derivedFactor może być również użyty do dowolnej liczby warunków.

Jake Fisher
źródło
4
@hadley powinno uczynić tę składnię domyślną dla dplyr. Potrzeba zagnieżdżonych instrukcji „ifelse” jest najgorszą częścią pakietu, co jest spowodowane głównie tym, że inne funkcje są tak dobre
rsoren
Możesz także zapobiec byciu czynnikiem, używając .asFactor = Fopcji lub używając (podobnej) derivedVariablefunkcji w tym samym pakiecie.
Jake Fisher
Wygląda na to, że recodedplyr 0.5 to zrobi. Jednak jeszcze tego nie zbadałem. Zobacz blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when jest teraz całkiem czystą implementacją przypadku w stylu SQL, gdy:

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

Korzystanie z dplyr 0.7.4

Podręcznik: http://dplyr.tidyverse.org/reference/case_when.html

Rasmus Larsen
źródło