R Évaluation conditionnelle lors de l'utilisation de l'opérateur de canalisation%>%

93

Lorsque vous utilisez l'opérateur de conduite %>%avec les paquets tels que dplyr, ggvis, dycharts, etc, comment dois - je faire une étape conditionnelle? Par exemple;

step_1 %>%
step_2 %>%

if(condition)
step_3

Ces approches ne semblent pas fonctionner:

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

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

Il y a un long chemin:

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

Y a-t-il un meilleur moyen sans toute la redondance?

rmf
la source
4
Un exemple avec lequel travailler (comme Ben l'a fourni) serait préférable, fyi.
Frank

Réponses:

104

Voici un exemple rapide qui tire parti de .et ifelse:

X<-1
Y<-T

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

Dans le ifelse, if Yest TRUEif ajoutera 1, sinon il retournera simplement la dernière valeur de X. Le .est un stand-in qui indique à la fonction où va la sortie de l'étape précédente de la chaîne, donc je peux l'utiliser sur les deux branches.

Modifier Comme @BenBolker l'a souligné, vous ne voudrez peut-être pas ifelse, voici donc une ifversion.

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

Merci à @Frank d'avoir indiqué que je devrais utiliser des {accolades autour de mes déclarations ifet ifelsepour continuer la chaîne.

John Paul
la source
4
J'aime la version post-édition. ifelsene semble pas naturel pour le flux de contrôle.
Frank
7
Une chose à noter: s'il y a une étape ultérieure dans la chaîne, utilisez {}. Par exemple, si vous ne les avez pas ici, de mauvaises choses se produisent (juste une impression Ypour une raison quelconque): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Frank
Utilisation de l'alias magrittr add rendrait l'exemple plus clair.
ctbrown
En termes de code golfique, cet exemple spécifique pourrait être écrit comme, X %>% add(1*Y)mais bien sûr, cela ne répond pas à la question d'origine
talat
1
Une chose importante dans le bloc conditionnel entre {}est que vous devez référencer l'argument précédent du tube dplyr (également appelé LHS) avec le point (.) - sinon le bloc conditionnel ne reçoit pas le. argument!
Agile Bean
32

Je pense que c'est un cas pour purrr::when. Résumons quelques nombres si leur somme est inférieure à 25, sinon renvoyez 0.


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

whenrenvoie la valeur résultant de l'action de la première condition valide. Placez la condition à gauche ~et l'action à droite. Ci-dessus, nous n'avons utilisé qu'une seule condition (puis un autre cas), mais vous pouvez avoir plusieurs conditions.

Vous pouvez facilement l'intégrer dans un tuyau plus long.

Lorenz Walthert
la source
2
agréable! Cela fournit également une alternative plus intuitive au «commutateur».
Steve
16

Voici une variation de la réponse fournie par @JohnPaul. Cette variante utilise la `if`fonction au lieu d'une if ... else ...instruction composée .

library(magrittr)

X <- 1
Y <- TRUE

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

Notez que dans ce cas, les accolades ne sont pas nécessaires autour du `if` fonction, ni autour d'une ifelsefonction, uniquement autour de l' if ... else ...instruction. Cependant, si l'espace réservé de point n'apparaît que dans un appel de fonction imbriquée, alors magrittr dirigera par défaut le côté gauche dans le premier argument du côté droit. Ce comportement est remplacé en plaçant l'expression entre accolades. Notez la différence entre ces deux chaînes:

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

L'espace réservé point est imbriqué dans un appel de fonction les deux fois qu'il apparaît dans la `if`fonction, car. + 1 et . + 2sont interprétés comme `+`(., 1)et `+`(., 2), respectivement. Ainsi, la première expression renvoie le résultat de `if`(1, TRUE, 1 + 1, 1 + 2), (curieusement, `if`ne se plaint pas d'arguments inutilisés supplémentaires), et la deuxième expression renvoie le résultat`if`(TRUE, 1 + 1, 1 + 2) , qui est le comportement souhaité dans ce cas.

Pour plus d'informations sur la manière dont l' opérateur de tube magrittr traite l'espace réservé de point, consultez le fichier d'aide de %>%, en particulier la section "Utilisation du point à des fins secondaires".

Cameron Bieganek
la source
Quelle est la différence entre utiliser `ìf`et ifelse? sont-ils identiques dans leur comportement?
Agile Bean
@AgileBean Le comportement des fonctions ifet ifelsen'est pas identique. La ifelsefonction est une vectorisée if. Si vous fournissez à la iffonction un vecteur logique, elle affichera un avertissement et n'utilisera que le premier élément de ce vecteur logique. Comparez `if`(c(T, F), 1:2, 3:4)avec ifelse(c(T, F), 1:2, 3:4).
Cameron Bieganek
super, merci pour la clarification! Donc, comme le problème ci-dessus n'est pas vectorisé, vous auriez pu également écrire votre solution commeX %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Il me semblerait plus facile de m'éloigner un peu des tuyaux (même si je serais intéressé à voir d'autres solutions), par exemple:

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

Il s'agit d'une légère modification de la réponse de @ JohnPaul (vous ne voudrez peut-être pas vraiment ifelse, qui évalue ses deux arguments et est vectorisée). Ce serait bien de modifier ceci pour revenir .automatiquement si la condition est fausse ... ( attention : je pense que cela fonctionne mais je n'ai pas vraiment testé / trop réfléchi ...)

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
la source
8

J'aime purrr::whenet les autres solutions de base fournies ici sont toutes excellentes mais je voulais quelque chose de plus compact et flexible alors j'ai conçu la fonction pif(pipe if), voir le code et la doc à la fin de la réponse.

Les arguments peuvent être soit des expressions de fonctions (la notation de formule est prise en charge), et l'entrée est renvoyée inchangée par défaut si la condition est FALSE.

Utilisé sur des exemples d'autres réponses:

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

Autres exemples:

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

Fonction

#' 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
la source
"Les fonctions ou formules en revanche ne seront appliquées aux données que si la condition pertinente est remplie." Pouvez-vous expliquer pourquoi vous avez décidé de le faire?
mihagazvoda le
Je ne calcule donc que ce dont j'ai besoin pour calculer, mais je me demande pourquoi je ne l'ai pas fait avec des expressions. Pour une raison quelconque, il semble que je ne voulais pas utiliser une évaluation non standard. Je pense avoir une version modifiée de mes fonctions personnalisées, je la mettrai à jour quand j'en aurai l'occasion.
Moody_Mudskipper le
Veuillez me le faire savoir lorsque vous le mettrez à jour. Merci!
mihagazvoda le