R Ocena warunkowa przy użyciu operatora potoku%>%

93

Przy użyciu operatora rury %>%z pakietów, takich jak dplyr, ggvis, dychartsitp, jak to zrobić krok warunkowo? Na przykład;

step_1 %>%
step_2 %>%

if(condition)
step_3

Te podejścia wydają się nie działać:

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

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

Droga jest długa:

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

Czy jest lepszy sposób bez całej nadmiarowości?

rmf
źródło
4
Przykład do pracy (jak podał Ben) byłby lepszy, fyi.
Frank

Odpowiedzi:

104

Oto krótki przykład wykorzystujący .i ifelse:

X<-1
Y<-T

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

W elemencie ifelseif Yis TRUEif doda 1, w przeciwnym razie zwróci tylko ostatnią wartość X. .Jest podstawka w której opowiada funkcję gdzie wyjście z poprzedniego etapu łańcucha przechodzi, więc można go używać na obu oddziałów.

Edytuj Jak zauważył @BenBolker, możesz nie chcieć ifelse, więc oto ifwersja.

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

Podziękowania dla @Frank za wskazanie, że powinienem używać {nawiasów klamrowych wokół moich instrukcji ifi ifelse, aby kontynuować łańcuch.

Jan Paweł
źródło
4
Podoba mi się wersja po edycji. ifelsewydaje się nienaturalny dla przepływu kontroli.
Frank
7
Jedna uwaga: jeśli w łańcuchu jest późniejszy krok, użyj {}. Na przykład, jeśli ich tu nie ma, zdarzają się złe rzeczy (po prostu drukowanie Yz jakiegoś powodu): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Frank
Użycie aliasu magrittr adduczyniłoby przykład jaśniejszym.
ctbrown
W kategoriach golfa kodowego ten konkretny przykład można zapisać jako, X %>% add(1*Y)ale oczywiście nie odpowiada to pierwotnemu pytaniu
talat
1
Jedną ważną rzeczą w bloku warunkowym pomiędzy {}jest to, że musisz odwołać się do poprzedniego argumentu potoku dplyr (zwanego również LHS) za pomocą kropki (.) - w przeciwnym razie blok warunkowy nie otrzyma. argument!
Agile Bean
32

Myślę, że to przypadek purrr::when. Zsumujmy kilka liczb, jeśli ich suma jest mniejsza niż 25, w przeciwnym razie zwróć 0.


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

whenzwraca wartość wynikającą z działania pierwszego prawidłowego warunku. Umieść warunek po lewej stronie ~, a działanie po prawej stronie. Powyżej użyliśmy tylko jednego warunku (a następnie innego przypadku), ale możesz mieć wiele warunków.

Możesz to łatwo zintegrować z dłuższą rurą.

Lorenz Walthert
źródło
2
ładny! Zapewnia to również bardziej intuicyjną alternatywę dla „przełącznika”.
Steve G. Jones
16

Oto wariacja na temat odpowiedzi udzielonej przez @JohnPaul. Ta odmiana używa `if`funkcji zamiast if ... else ...instrukcji złożonej .

library(magrittr)

X <- 1
Y <- TRUE

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

Zauważ, że w tym przypadku nawiasy klamrowe nie są potrzebne wokół `if`funkcji ani wokół ifelsefunkcji - tylko wokół if ... else ...instrukcji. Jeśli jednak symbol zastępczy kropki pojawia się tylko w wywołaniu funkcji zagnieżdżonej, to magrittr domyślnie potokuje lewą stronę do pierwszego argumentu po prawej stronie. To zachowanie jest zastępowane przez umieszczenie wyrażenia w nawiasach klamrowych. Zwróć uwagę na różnicę między tymi dwoma łańcuchami:

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

Symbol zastępczy kropki jest zagnieżdżony w wywołaniu funkcji za każdym razem, gdy pojawia się w `if`funkcji, ponieważ . + 1i . + 2są interpretowane odpowiednio jako `+`(., 1)i `+`(., 2). Zatem pierwsze wyrażenie zwraca wynik `if`(1, TRUE, 1 + 1, 1 + 2), (co dziwne, `if`nie narzeka na dodatkowe nieużywane argumenty), a drugie wyrażenie zwraca wynik`if`(TRUE, 1 + 1, 1 + 2) , co jest pożądanym zachowaniem w tym przypadku.

Aby uzyskać więcej informacji na temat sposobu magrittr rur operator traktuje zastępczy kropka, zobacz plik pomocy dla %>%, w szczególności rozdział „Korzystanie z kropką dla celów drugorzędnych”.

Cameron Bieganek
źródło
Jaka jest różnica między using `ìf`i ifelse? czy zachowują się identycznie?
Agile Bean
@AgileBean Zachowanie funkcji ifi ifelsenie jest identyczne. ifelseFunkcja jest wektorowy if. Jeśli nadasz iffunkcji wektor logiczny, wyświetli ostrzeżenie i użyje tylko pierwszego elementu tego wektora logicznego. Porównaj `if`(c(T, F), 1:2, 3:4)z ifelse(c(T, F), 1:2, 3:4).
Cameron Bieganek
świetnie, dzięki za wyjaśnienie! Ponieważ powyższy problem nie jest zwektoryzowany, możesz również napisać swoje rozwiązanie jakoX %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Najłatwiej byłoby mi się trochę odsunąć od rur (chociaż byłbym zainteresowany innymi rozwiązaniami), np .:

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

To jest niewielka modyfikacja odpowiedzi @ JohnPaul (możesz nie chcieć ifelse, która ocenia oba argumenty i jest wektoryzowana). Byłoby miło zmodyfikować to tak, aby zwracało się .automatycznie, jeśli warunek jest fałszywy ... ( uwaga : myślę, że to działa, ale tak naprawdę nie testowałem / nie myślałem o tym za dużo ...)

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
źródło
8

podoba mi się purrr::when a pozostałe przedstawione tutaj podstawowe rozwiązania są świetne, ale chciałem czegoś bardziej kompaktowego i elastycznego, więc zaprojektowałem funkcjępif (potok, jeśli), zobacz kod i dokument na końcu odpowiedzi.

Argumenty mogą być wyrażeniami funkcji (obsługiwana jest notacja formuły), a dane wejściowe są domyślnie zwracane niezmienione, jeśli warunek to FALSE .

Używane na przykładach z innych odpowiedzi:

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

Inne przykłady:

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

Funkcjonować

#' 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
źródło
„Z drugiej strony funkcje lub formuły zostaną zastosowane do danych tylko wtedy, gdy zostanie spełniony odpowiedni warunek”. Czy możesz wyjaśnić, dlaczego zdecydowałeś się to zrobić?
mihagazvoda
Więc obliczam tylko to, czego potrzebuję, ale zastanawiam się, dlaczego nie zrobiłem tego z wyrażeniami. Z jakiegoś powodu wydaje mi się, że nie chciałem używać niestandardowej oceny. Myślę, że mam zmodyfikowaną wersję w moich niestandardowych funkcjach, zaktualizuję, gdy będę miał okazję.
Moody_Mudskipper
Daj mi znać, kiedy go zaktualizujesz. Dziękuję Ci!
mihagazvoda