Kann das dplyr-Paket für die bedingte Mutation verwendet werden?

178

Kann die Mutation verwendet werden, wenn die Mutation bedingt ist (abhängig von den Werten bestimmter Spaltenwerte)?

Dieses Beispiel zeigt, was ich meine.

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

Ich hatte gehofft, mit dem Paket dplyr eine Lösung für mein Problem zu finden (und ja, ich weiß, dass dieser Code nicht funktionieren sollte, aber ich denke, er macht den Zweck klar), um eine neue Spalte g zu erstellen:

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

Das Ergebnis des gesuchten Codes sollte dieses Ergebnis in diesem speziellen Beispiel haben:

  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

Hat jemand eine Idee, wie man das in dplyr macht? Dieser Datenrahmen ist nur ein Beispiel, die Datenrahmen, mit denen ich es zu tun habe, sind viel größer. Aufgrund seiner Geschwindigkeit habe ich versucht, dplyr zu verwenden, aber vielleicht gibt es andere, bessere Möglichkeiten, um dieses Problem zu lösen?

rdatasculptor
quelle
2
Ja, aber es dplyr::case_when()ist viel klarer als ein ifelse,
smci

Antworten:

216

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

Hinzugefügt - if_else: Beachten Sie, dass in dplyr 0,5 gibt es eine ist if_elseFunktion definiert , um eine Alternative zu ersetzen wäre ifelsemit if_else; Beachten Sie jedoch, dass da if_elsestrenger ist als ifelse(beide Beine der Bedingung müssen den gleichen Typ haben), so dass das NAin diesem Fall durch ersetzt werden müsste 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_)))

Hinzugefügt - case_when Da diese Frage gestellt wurde, hat dplyr hinzugefügt, case_whensodass eine andere Alternative wäre:

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

Hinzugefügt - arithmetic / na_if Wenn die Werte numerisch sind und sich die Bedingungen (mit Ausnahme des Standardwerts von NA am Ende) gegenseitig ausschließen, wie dies in der Frage der Fall ist, können wir einen arithmetischen Ausdruck verwenden, sodass jeder Term multipliziert wird durch das gewünschte Ergebnis mitna_if am Ende verwenden, um 0 durch NA zu ersetzen.

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
quelle
3
Was ist die Logik, wenn stattdessen NAdie Zeilen, die die Bedingungen nicht erfüllen, einfach gleich bleiben sollen?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck
11
case_when ist sooooo schön und ich habe soooo lange gebraucht, um herauszufinden, dass es tatsächlich da war. Ich denke, dies sollte in den einfachsten dplyr-Tutorials sein. Es ist sehr häufig, dass für Teilmengen der Daten Daten berechnet werden müssen, die Daten aber dennoch vollständig bleiben sollen.
Javier Fajardo
55

Da Sie nach anderen besseren Möglichkeiten zur Lösung des Problems fragen, verwenden Sie eine andere Möglichkeit 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]

Beachten Sie, dass die Reihenfolge der bedingten Anweisungen umgekehrt ist, um gkorrekt zu sein. gSelbst während der zweiten Aufgabe wird keine Kopie erstellt - sie wird an Ort und Stelle ersetzt .

Bei größeren Daten hat dies eine bessere Leistung als die Verwendung von verschachtelten Daten if-else, da sowohl Ja- als auch Nein-Fälle ausgewertet werden können und das Verschachteln IMHO schwieriger zu lesen / zu warten ist.


Hier ist ein Benchmark für relativ große Datenmengen:

# 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

Ich bin mir nicht sicher, ob dies eine Alternative ist, nach der Sie gefragt haben, aber ich hoffe, es hilft.

Arun
quelle
4
Schöner Code! Die Antwort von G. Grotendieck funktioniert und ist kurz, daher habe ich diese als Antwort auf meine Frage ausgewählt, aber ich danke Ihnen für Ihre Lösung. Ich werde es sicher auch so versuchen.
rdatasculptor
Da DT_fundie Eingabe an Ort und Stelle geändert wird, ist der Benchmark möglicherweise nicht ganz fair. Zusätzlich zum Erhalt derselben Eingabe ab der 2. Iteration vorwärts (was sich möglicherweise auf das Timing auswirkt, da DT$gbereits zugewiesen?), Wird das Ergebnis auch zurück zu ans1und daher möglicherweise ( wenn R Optimierer dies für erforderlich hält? Nicht sicher , auf diese ...) vermeiden eine weitere Kopie , dass DPLYR_funund BASE_funNotwendigkeit zu machen?
Ken Williams
Um ganz klar zu sein, ich finde diese data.tableLösung großartig und verwende sie data.tableüberall dort , wo ich wirklich Geschwindigkeit für Operationen an Tabellen benötige und nicht bis zu C ++ gehen möchte. Es ist jedoch sehr vorsichtig, wenn Änderungen vorgenommen werden!
Ken Williams
Ich versuche mich daran zu gewöhnen, die Daten von data.table besser zu ordnen, und dies ist eines der Beispiele für einen ziemlich häufigen Anwendungsfall, bei dem data.table sowohl einfacher zu lesen als auch effizienter ist. Mein Hauptgrund dafür, dass ich mehr Ordnung in meinem Wortschatz entwickeln möchte, ist die Lesbarkeit für mich und andere, aber in diesem Fall scheint data.table zu gewinnen.
Paul McMurdie
38

dplyr hat jetzt eine Funktion case_when, die ein vektorisiertes if bietet. Die Syntax ist etwas seltsam im Vergleich zu, mosaic:::derivedFactorda Sie nicht auf Standard-Dplyr-Weise auf Variablen zugreifen können und den Modus von NA deklarieren müssen, aber sie ist erheblich schneller als 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)))

BEARBEITEN: Wenn Sie dplyr::case_when()vor Version 0.7.0 des Pakets verwenden, müssen Sie Variablennamen mit ' .$' voranstellen (z. B. .$a == 1innen schreiben)case_when . ).

Benchmark : Für den Benchmark (Wiederverwendung von Funktionen aus Aruns Beitrag) und Reduzierung der Stichprobengröße:

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
)

Das gibt:

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
quelle
case_whenkönnte auch geschrieben werden als: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
Ist dieser Benchmark in Mikrosekunden / Millisekunden / Tagen, was? Dieser Benchmark ist ohne die mitgelieferte Maßeinheit bedeutungslos. Auch das Benchmarking eines Datensatzes, der kleiner als 1e6 ist, ist bedeutungslos, da es nicht skaliert.
David Arenburg
3
Bitte ändern Sie Ihre Antwort, Sie brauchen die .$nicht mehr in der neuen Version von dplyr
Amit Kohli
14

Die derivedFactorFunktion aus dem mosaicPaket scheint darauf ausgelegt zu sein. In diesem Beispiel würde es so aussehen:

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

(Wenn das Ergebnis anstelle eines Faktors numerisch sein soll, können Sie derivedFactoreinen as.numericAnruf einschließen.)

derivedFactor kann auch für eine beliebige Anzahl von Bedingungen verwendet werden.

Jake Fisher
quelle
4
@hadley sollte dies zur Standardsyntax für dplyr machen. Die Notwendigkeit verschachtelter "ifelse" -Anweisungen ist der schlechteste Teil des Pakets, was hauptsächlich der Fall ist, weil die anderen Funktionen so gut sind
rsoren
Sie können auch verhindern, dass das Ergebnis ein Faktor ist, indem Sie die .asFactor = FOption oder die (ähnliche) derivedVariableFunktion im selben Paket verwenden.
Jake Fisher
Es sieht so recodeaus, als würde ab dplyr 0.5 dies geschehen. Ich habe es aber noch nicht untersucht. Siehe blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when ist jetzt eine ziemlich saubere Implementierung des SQL-Falls, wenn:

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

Verwenden von dplyr 0.7.4

Das Handbuch: http://dplyr.tidyverse.org/reference/case_when.html

Rasmus Larsen
quelle