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?


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.


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ą.


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”.


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

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

„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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.