R Bedingte Auswertung bei Verwendung des Rohrbetreibers%>%

90

Wenn das Rohr Operator %>%mit Paketen wie dplyr, ggvis, dychartsusw., wie soll ich tun bedingt einen Schritt? Beispielsweise;

step_1 %>%
step_2 %>%

if(condition)
step_3

Diese Ansätze scheinen nicht zu funktionieren:

step_1 %>%
step_2 
if(condition) %>% step_3

step_1 %>%
step_2 %>%
if(condition) step_3

Es ist ein langer Weg:

if(condition)
{
step_1 %>%
step_2 
}else{
step_1 %>%
step_2 %>%
step_3
}

Gibt es einen besseren Weg ohne all die Redundanz?

rmf
quelle
4
Ein Beispiel für die Arbeit (wie von Ben angegeben) wäre vorzuziehen, fyi.
Frank

Antworten:

99

Hier ist ein kurzes Beispiel, das das .und nutzt ifelse:

X<-1
Y<-T

X %>% add(1) %>% { ifelse(Y ,add(.,1), . ) }

In der ifelse, wenn Yist , TRUEwenn wird 1 hinzufügen, sonst wird es nur den letzten Wert von zurückzukehren X. Das .ist ein Ersatz, der der Funktion mitteilt, wohin die Ausgabe des vorherigen Schritts der Kette geht, sodass ich sie für beide Zweige verwenden kann.

Bearbeiten Wie @BenBolker betonte, möchten Sie möglicherweise nicht ifelse, daher hier eine ifVersion.

X %>% 
add(1) %>% 
 {if(Y) add(.,1) else .}

Vielen Dank an @Frank für den Hinweis, dass ich {Klammern um meine ifund -Anweisungen verwenden sollte, um ifelsedie Kette fortzusetzen.

Johannes Paul
quelle
4
Ich mag die Post-Edit-Version. ifelsescheint für den Kontrollfluss unnatürlich.
Frank
7
Eines ist zu beachten: Wenn es einen späteren Schritt in der Kette gibt, verwenden Sie {}. Wenn Sie sie beispielsweise nicht hier haben, passieren schlimme Dinge (nur Drucken Yaus irgendeinem Grund): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Frank
Die Verwendung des Magrittr-Alias addwürde das Beispiel klarer machen.
ctbrown
In Bezug auf Code-Golf könnte dieses spezielle Beispiel so geschrieben werden X %>% add(1*Y), dass es natürlich nicht die ursprüngliche Frage beantwortet
Talat
1
Eine wichtige Sache innerhalb des bedingten Blocks dazwischen {}ist, dass Sie das vorhergehende Argument der dplyr-Pipe (auch LHS genannt) mit dem Punkt (.) Verweisen müssen - andernfalls empfängt der bedingte Block das nicht. Streit!
Agile Bean
31

Ich denke, das ist ein Fall für purrr::when. Fassen wir einige Zahlen zusammen, wenn ihre Summe unter 25 liegt, andernfalls geben wir 0 zurück.


library("magrittr")
1:3 %>% 
  purrr::when(sum(.) < 25 ~ sum(.), 
              ~0
  )
#> [1] 6

whenGibt den Wert zurück, der sich aus der Aktion der ersten gültigen Bedingung ergibt. Stellen Sie die Bedingung links davon ~und die Aktion rechts davon. Oben haben wir nur eine Bedingung (und dann einen anderen Fall) verwendet, aber Sie können viele Bedingungen haben.

Sie können das einfach in ein längeres Rohr integrieren.

Lorenz Walthert
quelle
2
nett! Dies bietet auch eine intuitivere Alternative zum "Wechseln".
Steve G. Jones
15

Hier ist eine Variation der Antwort von @JohnPaul. Diese Variante verwendet die `if`Funktion anstelle einer zusammengesetzten if ... else ...Anweisung.

library(magrittr)

X <- 1
Y <- TRUE

X %>% `if`(Y, . + 1, .) %>% multiply_by(2)
# [1] 4

Beachten Sie, dass in diesem Fall die geschweiften Klammern weder um die `if`Funktion noch um eine ifelseFunktion benötigt werden - nur um die if ... else ...Anweisung. Wenn der Punktplatzhalter jedoch nur in einem verschachtelten Funktionsaufruf angezeigt wird , leitet magrittr standardmäßig die linke Seite in das erste Argument der rechten Seite weiter. Dieses Verhalten wird überschrieben, indem der Ausdruck in geschweiften Klammern eingeschlossen wird. Beachten Sie den Unterschied zwischen diesen beiden Ketten:

X %>% `if`(Y, . + 1, . + 2)
# [1] TRUE
X %>% {`if`(Y, . + 1, . + 2)}
# [1] 4

Der Punktplatzhalter ist in einem Funktionsaufruf beide Male verschachtelt `if`, wenn er in der Funktion angezeigt wird , da . + 1und . + 2als `+`(., 1)bzw. interpretiert `+`(., 2)werden. Der erste Ausdruck gibt also das Ergebnis von zurück `if`(1, TRUE, 1 + 1, 1 + 2)(seltsamerweise `if`beschwert er sich nicht über zusätzliche nicht verwendete Argumente), und der zweite Ausdruck gibt das Ergebnis von zurück `if`(TRUE, 1 + 1, 1 + 2), was in diesem Fall das gewünschte Verhalten ist.

Weitere Informationen darüber , wie die magrittr Rohr Operator behandelt den Punkt Platzhalter finden Sie in der Hilfedatei für %>%, insbesondere den Abschnitt „Mit dem Punkt für sekundäre Zwecke“.

Cameron Bieganek
quelle
Was ist der Unterschied zwischen `ìf`und ifelse? Sind sie im Verhalten identisch?
Agile Bean
@AgileBean Das Verhalten der Funktionen ifund ifelseist nicht identisch. Die ifelseFunktion ist vektorisiert if. Wenn Sie der ifFunktion einen logischen Vektor zur Verfügung stellen, wird eine Warnung ausgegeben und nur das erste Element dieses logischen Vektors verwendet. Vergleiche `if`(c(T, F), 1:2, 3:4)mit ifelse(c(T, F), 1:2, 3:4).
Cameron Bieganek
super, danke für die Klarstellung! Da das obige Problem nicht vektorisiert ist, hätten Sie Ihre Lösung auch alsX %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Es scheint mir am einfachsten, mich ein wenig von den Rohren zurückzuziehen (obwohl ich daran interessiert wäre, andere Lösungen zu sehen), z.

library("dplyr")
z <- data.frame(a=1:2)
z %>% mutate(b=a^2) -> z2
if (z2$b[1]>1) {
    z2 %>% mutate(b=b^2) -> z2
}
z2 %>% mutate(b=b^2) -> z3

Dies ist eine geringfügige Änderung der Antwort von @ JohnPaul (möglicherweise möchten Sie nicht wirklich ifelse, da beide Argumente ausgewertet und vektorisiert werden). Es wäre schön, dies so zu ändern, dass es .automatisch zurückkehrt, wenn die Bedingung falsch ist ... ( Achtung : Ich denke, das funktioniert, aber ich habe nicht wirklich viel darüber getestet / nachgedacht ...)

iff <- function(cond,x,y) {
    if(cond) return(x) else return(y)
}

z %>% mutate(b=a^2) %>%
    iff(cond=z2$b[1]>1,mutate(.,b=b^2),.) %>%
 mutate(b=b^2) -> z4
Ben Bolker
quelle
8

Ich mag purrr::whenund die anderen hier bereitgestellten Basislösungen sind alle großartig, aber ich wollte etwas kompakteres und flexibleres, also habe ich die Funktion pif(Pipe if) entworfen, siehe Code und Dokument am Ende der Antwort.

Argumente können entweder Ausdrücke von Funktionen sein (Formelnotation wird unterstützt), und die Eingabe wird standardmäßig unverändert zurückgegeben, wenn die Bedingung erfüllt ist FALSE.

Verwendet für Beispiele aus anderen Antworten:

## from Ben Bolker
data.frame(a=1:2) %>% 
  mutate(b=a^2) %>%
  pif(~b[1]>1, ~mutate(.,b=b^2)) %>%
  mutate(b=b^2)
#   a  b
# 1 1  1
# 2 2 16

## from Lorenz Walthert
1:3 %>% pif(sum(.) < 25,sum,0)
# [1] 6

## from clbieganek 
1 %>% pif(TRUE,~. + 1) %>% `*`(2)
# [1] 4

# from theforestecologist
1 %>% `+`(1) %>% pif(TRUE ,~ .+1)
# [1] 3

Andere Beispiele:

## using functions
iris %>% pif(is.data.frame, dim, nrow)
# [1] 150   5

## using formulas
iris %>% pif(~is.numeric(Species), 
             ~"numeric :)",
             ~paste(class(Species)[1],":("))
# [1] "factor :("

## using expressions
iris %>% pif(nrow(.) > 2, head(.,2))
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa

## careful with expressions
iris %>% pif(TRUE, dim,  warning("this will be evaluated"))
# [1] 150   5
# Warning message:
# In inherits(false, "formula") : this will be evaluated
iris %>% pif(TRUE, dim, ~warning("this won't be evaluated"))
# [1] 150   5

Funktion

#' Pipe friendly conditional operation
#'
#' Apply a transformation on the data only if a condition is met, 
#' by default if condition is not met the input is returned unchanged.
#' 
#' The use of formula or functions is recommended over the use of expressions
#' for the following reasons :
#' 
#' \itemize{
#'   \item If \code{true} and/or \code{false} are provided as expressions they 
#'   will be evaluated wether the condition is \code{TRUE} or \code{FALSE}.
#'   Functions or formulas on the other hand will be applied on the data only if
#'   the relevant condition is met
#'   \item Formulas support calling directly a column of the data by its name 
#'   without \code{x$foo} notation.
#'   \item Dot notation will work in expressions only if `pif` is used in a pipe
#'   chain
#' }
#' 
#' @param x An object
#' @param p A predicate function, a formula describing such a predicate function, or an expression.
#' @param true,false Functions to apply to the data, formulas describing such functions, or expressions.
#'
#' @return The output of \code{true} or \code{false}, either as expressions or applied on data as functions
#' @export
#'
#' @examples
#'# using functions
#'pif(iris, is.data.frame, dim, nrow)
#'# using formulas
#'pif(iris, ~is.numeric(Species), ~"numeric :)",~paste(class(Species)[1],":("))
#'# using expressions
#'pif(iris, nrow(iris) > 2, head(iris,2))
#'# careful with expressions
#'pif(iris, TRUE, dim,  warning("this will be evaluated"))
#'pif(iris, TRUE, dim, ~warning("this won't be evaluated"))
pif <- function(x, p, true, false = identity){
  if(!requireNamespace("purrr")) 
    stop("Package 'purrr' needs to be installed to use function 'pif'")

  if(inherits(p,     "formula"))
    p     <- purrr::as_mapper(
      if(!is.list(x)) p else update(p,~with(...,.)))
  if(inherits(true,  "formula"))
    true  <- purrr::as_mapper(
      if(!is.list(x)) true else update(true,~with(...,.)))
  if(inherits(false, "formula"))
    false <- purrr::as_mapper(
      if(!is.list(x)) false else update(false,~with(...,.)))

  if ( (is.function(p) && p(x)) || (!is.function(p) && p)){
    if(is.function(true)) true(x) else true
  }  else {
    if(is.function(false)) false(x) else false
  }
}
Moody_Mudskipper
quelle