R Avaliação condicional ao usar o operador de tubo%>%

96

Ao usar o operador pipe %>%com pacotes tais como dplyr, ggvis, dycharts, etc, como eu faço um passo condicionalmente? Por exemplo;

step_1 %>%
step_2 %>%

if(condition)
step_3

Essas abordagens não parecem funcionar:

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

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

Existe um longo caminho:

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

Existe uma maneira melhor sem toda a redundância?

rmf
fonte
4
Um exemplo para trabalhar (como Ben forneceu) seria preferível, fyi.
Frank

Respostas:

108

Aqui está um exemplo rápido que tira proveito de .e ifelse:

X<-1
Y<-T

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

No ifelse, if Yis TRUEif adicionará 1, caso contrário, apenas retornará o último valor de X. O .é um substituto que informa à função para onde vai a saída da etapa anterior da cadeia, para que eu possa usá-la nos dois ramos.

Editar Como @BenBolker apontou, você pode não querer ifelse, então aqui está uma ifversão.

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

Agradeço a @Frank por apontar que eu deveria usar {colchetes ao redor de minhas instruções ife ifelsepara continuar a cadeia.

João paulo
fonte
5
Eu gosto da versão pós-edição. ifelseparece não natural para o fluxo de controle.
Frank
7
Uma coisa a ser observada: se houver uma etapa posterior na cadeia, use {}. Por exemplo, se você não os tem aqui, coisas ruins acontecem (apenas imprimindo Ypor algum motivo): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Frank
O uso do alias magrittr addtornaria o exemplo mais claro.
ctbrown
Em termos de código de golfe, este exemplo específico poderia ser escrito como, X %>% add(1*Y)mas é claro que não responde à pergunta original
talat
1
Uma coisa importante dentro do bloco condicional entre {}é que você deve fazer referência ao argumento anterior do tubo dplyr (também chamado LHS) com o ponto (.) - caso contrário, o bloco condicional não receberá o. argumento!
Agile Bean
33

Acho que é um caso para purrr::when. Vamos somar alguns números se a soma deles for inferior a 25, caso contrário, retorne 0.


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

whenretorna o valor resultante da ação da primeira condição válida. Coloque a condição à esquerda de ~e a ação à direita dele. Acima, usamos apenas uma condição (e depois um outro caso), mas você pode ter muitas condições.

Você pode facilmente integrar isso em um tubo mais longo.

Lorenz Walthert
fonte
2
bom! Isso também fornece uma alternativa mais intuitiva para 'alternar'.
Steve G. Jones
16

Aqui está uma variação da resposta fornecida por @JohnPaul. Esta variação usa a `if`função em vez de uma if ... else ...instrução composta .

library(magrittr)

X <- 1
Y <- TRUE

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

Observe que, neste caso, as chaves não são necessárias em torno da `if`função, nem em torno de uma ifelsefunção - apenas em torno da if ... else ...instrução. No entanto, se o marcador de posição de ponto aparecer apenas em uma chamada de função aninhada, então magrittr irá, por padrão, canalizar o lado esquerdo para o primeiro argumento do lado direito. Esse comportamento é anulado colocando a expressão entre chaves. Observe a diferença entre essas duas cadeias:

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

O marcador de posição de ponto está aninhado em uma chamada de função nas duas vezes em que aparece no `if` função, pois . + 1e . + 2são interpretados como `+`(., 1)e `+`(., 2), respectivamente. Portanto, a primeira expressão está retornando o resultado de `if`(1, TRUE, 1 + 1, 1 + 2), (estranhamente, `if`não reclama sobre argumentos extras não utilizados), e a segunda expressão está retornando o resultado de `if`(TRUE, 1 + 1, 1 + 2), que é o comportamento desejado neste caso.

Para obter mais informações sobre como o operador de tubo magrittr trata o marcador de posição de ponto, consulte o arquivo de ajuda para%>% , em particular a seção sobre "Usando o ponto para finalidades secundárias".

Cameron Bieganek
fonte
qual é a diferença entre usar `ìf`e ifelse? eles são idênticos em comportamento?
Agile Bean
@AgileBean O comportamento das funções ife ifelsenão é idêntico. A ifelsefunção é vetorizada if. Se você fornecer à iffunção um vetor lógico, ela imprimirá um aviso e usará apenas o primeiro elemento desse vetor lógico. Compare `if`(c(T, F), 1:2, 3:4)com ifelse(c(T, F), 1:2, 3:4).
Cameron Bieganek
ótimo, obrigado pelo esclarecimento! Portanto, como o problema acima não é vetorizado, você também poderia ter escrito sua solução comoX %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Parece-me mais fácil me afastar um pouco dos canos (embora eu esteja interessado em ver outras soluções), por exemplo:

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

Esta é uma pequena modificação da resposta de @JohnPaul (você pode realmente não querer ifelse, que avalia ambos os argumentos e é vetorizada). Seria bom modificar isso para retornar .automaticamente se a condição for falsa ... ( atenção : acho que isso funciona, mas realmente não testei / pensei muito sobre isso ...)

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
fonte
1
Apenas quero ressaltar que iff()retorna um erro quando yé diferente de ..
mihagazvoda
8

Eu gosto purrr::whene as outras soluções básicas fornecidas aqui são ótimas, mas eu queria algo mais compacto e flexível, então projetei a função pif(pipe if), veja o código e o doc no final da resposta.

Os argumentos podem ser expressões de funções (a notação de fórmula é suportada) e a entrada é retornada inalterada por padrão se a condição for FALSE.

Usado em exemplos de outras respostas:

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

Outros exemplos:

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

Função

#' 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
fonte
"Funções ou fórmulas, por outro lado, serão aplicadas aos dados somente se a condição relevante for atendida." Você pode explicar por que decidiu fazer isso?
mihagazvoda
Portanto, calculo apenas o que preciso calcular, mas me pergunto por que não fiz isso com expressões. Por alguma razão, parece que eu não queria usar uma avaliação fora do padrão. Acho que tenho uma versão modificada em minhas funções personalizadas, vou atualizá-la quando tiver a chance.
Moody_Mudskipper de