使用管道运算符%>%时的R条件评估

138

当使用管道操作符%>% 与诸如dplyrggvisdycharts等包时,我如何有条件地执行一步操作?例如:

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
}

有没有更好的方法,而不需要那么多的冗余?


6
最好提供一个示例来作为参考(就像Ben所提供的)。附带信息:供您参考。 - Frank
6个回答

162
这是一个利用 .ifelse 的快速示例:

以下是代码:

X<-1
Y<-T

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

ifelse中,如果YTRUE,它将添加1;否则,它将返回X的最后一个值。点号.是一个占位符,告诉函数链的上一步输出去哪里,因此我可以在两个分支上使用它。 编辑 正如@BenBolker指出的那样,您可能不想使用ifelse,因此这里提供了一个if版本。
X %>% 
add(1) %>% 
 {if(Y) add(.,1) else .}

感谢@Frank指出我应该在我的if和ifelse语句中使用花括号来继续链式操作。

8
我喜欢修改后的版本。ifelse 对于控制流来说似乎不太自然。 - Frank
8
需要注意的一点是:如果链中后面还有步骤,使用 {}。例如,如果在此处没有它们,则会发生糟糕的事情(由于某种原因仅打印 Y):X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5) - Frank
1
使用 magrittr 别名 add 会使示例更清晰。 - ctbrown
1
为什么它会将数据框转换为只包含第一列的列表?例如:data.frame(a = c(1,2,3), b = c(4,5,6)) %>% { ifelse(T, ., 0) } 返回 [[1]] [1] 1 2 3 - Karol Daniluk
2
{} 条件块中的一个重要事项是,您必须使用点号 (.) 引用 dplyr 管道的前一个参数(也称为 LHS),否则条件块将无法接收 . 参数! - Agile Bean
显示剩余5条评论

47

编辑: purrr::when() 已自 {purrr} 版本 1.0.0 起被弃用。

我认为这是 purrr::when() 的一个案例。如果几个数字的总和低于25,则让我们对它们求和,否则返回0。


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

when 会返回首个有效条件的结果值。将条件放在 ~ 的左侧,将操作放在其右侧即可。在上例中,我们只使用了一个条件(然后是 else 分支),但你也可以有很多条件。

你可以轻松地将其集成到更长的管道中。


2
很好!这也为“switch”提供了更直观的替代方案。 - Steve G. Jones
2
when已被弃用,现在应该使用if:https://purrr.tidyverse.org/reference/when.html?q=when#ref-usage - jjj
@jjj 你是指 base::if 吗? - Julien
1
@Julien 我的意思是 purrr 开发者在链接中提供的示例建议。我认为这是 if 的基础。 - jjj

23

这是对@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管道运算符如何处理点占位符的更多信息,请参见帮助文件%>% 的“使用点进行辅助操作”一节。


使用\if`ifelse`有什么区别?它们的行为是否相同? - Agile Bean
@AgileBean ififelse函数的行为不完全相同。ifelse函数是矢量化的if。如果您向if函数提供逻辑向量,它将打印警告并仅使用该逻辑向量的第一个元素。将\if`(c(T, F), 1:2, 3:4)ifelse(c(T, F), 1:2, 3:4)`进行比较。 - Cameron Bieganek
非常好,感谢澄清!因此,由于上述问题未向量化,您还可以将解决方案编写为 X %>% { ifelse(Y, .+1, .+2) } - Agile Bean

16

对我来说,似乎最简单的方法是稍微离开这些管道一点点(尽管我很想看到其他解决方案),例如:

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

1
只是想指出,当 y 不是 . 时,iff() 返回一个错误。 - mihagazvoda

13

我喜欢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
  }
}

另一方面,函数或公式只有在满足相关条件时才会应用于数据。我这样翻译是因为它准确地传达了原始文本的含义,同时使用了流畅、专业和优雅的语言风格。 - mihagazvoda
所以我只计算我需要计算的内容,但我想知道为什么我没有使用表达式。由于某种原因,似乎我不想使用非标准评估。我认为我在我的自定义函数中有一个修改过的版本,我会在有机会时进行更新。 - moodymudskipper

2
一个可能的解决方案是使用匿名函数。
library(magrittr)
1 %>% 
  (\(.) if (T) . + 1 else .) %>% 
  multiply_by(2)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接