R Условная оценка при использовании оператора вертикальной черты%>%

93

При использовании оператора трубы %>%с пакетами , такими как dplyr, ggvis, dychartsи т.д., как я делаю шаг условно? Например;

step_1 %>%
step_2 %>%

if(condition)
step_3

Эти подходы, похоже, не работают:

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

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

Это долгий путь:

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

Есть ли лучший способ без избыточности?

rmf
источник
4
Пример для работы (как предоставил Бен) был бы предпочтительнее, fyi.
Фрэнк

Ответы:

104

Вот краткий пример, в котором используются преимущества .и ifelse:

X<-1
Y<-T

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

В ifelseif Yis TRUEif добавляется 1, в противном случае возвращается только последнее значение X. Это .подстановка, которая сообщает функции, куда идет вывод из предыдущего шага цепочки, поэтому я могу использовать его в обеих ветвях.

Изменить Как отметил @BenBolker, возможно, вы не захотите ifelse, так что вот ifверсия.

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

Благодаря @Frank за указание на то , что я должен использовать {фигурные скобки вокруг моих ifи ifelseзаявлений продолжать цепочку.

Джон Пол
источник
4
Мне нравится версия после редактирования. ifelseкажется неестественным для потока управления.
Фрэнк
7
Одно замечание: если в цепочке есть более поздний шаг, используйте {}. Например, если у вас их здесь нет, случаются неприятности (просто печать Yпо какой-то причине): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Фрэнк
Использование псевдонима magrittr addсделало бы пример более ясным.
ctbrown
В терминах игры в гольф этот конкретный пример можно было бы записать так, X %>% add(1*Y)но он, конечно, не отвечает на исходный вопрос
talat
1
Одна важная вещь в условном блоке между ними {}заключается в том, что вы должны ссылаться на предыдущий аргумент канала dplyr (также называемый LHS) с точкой (.) - в противном случае условный блок не получит. аргумент!
Agile Bean
32

Я думаю, это случай purrr::when. Суммируем несколько чисел, если их сумма меньше 25, иначе вернем 0.


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

whenвозвращает значение, полученное в результате действия первого допустимого условия. Поместите условие слева от него ~, а действие - справа от него. Выше мы использовали только одно условие (а затем еще случай), но у вас может быть много условий.

Вы можете легко встроить это в более длинную трубу.

Лоренц Вальтерт
источник
2
отлично! Это также обеспечивает более интуитивно понятную альтернативу «переключению».
Steve G. Jones
16

Вот вариант ответа, предоставленного @JohnPaul. Этот вариант использует `if`функцию вместо составного if ... else ...оператора.

library(magrittr)

X <- 1
Y <- TRUE

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

Обратите внимание, что в этом случае фигурные скобки не нужны `if`ни вокруг функции, ни вокруг ifelseфункции - только вокруг if ... else ...оператора. Однако, если точка-заполнитель появляется только во вложенном вызове функции, то magrittr по умолчанию будет передавать левую часть в первый аргумент правой части. Это поведение отменяется заключением выражения в фигурные скобки. Обратите внимание на разницу между этими двумя цепочками:

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

Заполнитель точки вложен в вызов функции оба раза, когда он появляется в `if`функции, поскольку . + 1и . + 2интерпретируются как `+`(., 1)и `+`(., 2)соответственно. Итак, первое выражение возвращает результат `if`(1, TRUE, 1 + 1, 1 + 2)(как ни странно, `if`не жалуется на лишние неиспользованные аргументы), а второе выражение возвращает результат `if`(TRUE, 1 + 1, 1 + 2), что в данном случае является желаемым поведением.

Для получения дополнительной информации о том , как magrittr труба оператор обрабатывает точку заполнители, увидеть файл справки для %>%, в частности , в раздел «Использование точки для вторичных целей».

Кэмерон Биганек
источник
в чем разница между использованием `ìf`и ifelse? они идентичны по поведению?
Agile Bean
@AgileBean Поведение ifи ifelseфункций не идентичны. ifelseФункция является векторизация if. Если вы предоставите ifфункции с логическим вектором, она напечатает предупреждение и будет использовать только первый элемент этого логического вектора. Сравните `if`(c(T, F), 1:2, 3:4)с ifelse(c(T, F), 1:2, 3:4).
Кэмерон Биганек 02
отлично, спасибо за разъяснения! Так как X %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Мне показалось бы, что проще всего немного отступить от труб (хотя мне было бы интересно увидеть другие решения), например:

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

Это небольшая модификация ответа @JohnPaul (возможно, вам этого не нужно ifelse, поскольку он оценивает оба своих аргумента и векторизуется). Было бы неплохо изменить это, чтобы он возвращался .автоматически, если условие ложно ... ( осторожно : я думаю, что это работает, но на самом деле не тестировал / не думал об этом слишком много ...)

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

Мне нравятся purrr::whenи другие базовые решения, представленные здесь, все великолепны, но я хотел что-то более компактное и гибкое, поэтому я разработал функцию pif(pipe if), см. Код и документацию в конце ответа.

Аргументы могут быть выражениями функций (поддерживается обозначение формул), а входные данные по умолчанию возвращаются без изменений, если условие равно FALSE.

Используется в примерах из других ответов:

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

Другие примеры:

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

Функция

#' 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
источник
«С другой стороны, функции или формулы будут применяться к данным только при соблюдении соответствующих условий». Вы можете объяснить, почему вы так решили?
mihagazvoda
Поэтому я вычисляю только то, что мне нужно для вычисления, но мне интересно, почему я не сделал это с помощью выражений. Почему-то кажется, что я не хотел использовать нестандартную оценку. Я думаю, что у меня есть измененная версия моих пользовательских функций, я обновлю, когда у меня будет возможность.
Moody_Mudskipper
Пожалуйста, дайте мне знать, когда вы обновите его. Спасибо!
mihagazvoda