web-dev-qa-db-fra.com

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

Lors de l'utilisation de l'opérateur de tuyau %>% avec des packages tels que dplyr, ggvis, dycharts, etc., comment faire une étape conditionnellement? 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

Le chemin est long:

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

Existe-t-il un meilleur moyen sans toute la redondance?

72
rmf

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

X<-1
Y<-T

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

Dans ifelse, si Y est TRUE if ajoutera 1, sinon il renverra simplement la dernière valeur de X. Le . est un remplaçant 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.

Edit Comme @BenBolker l'a souligné, vous ne voudrez peut-être pas ifelse, alors voici une version if.

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

Merci à @Frank d'avoir indiqué que je devrais utiliser { accolades autour de mes instructions if et ifelse pour continuer la chaîne.

71
John Paul

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


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

when renvoie la valeur résultant de l'action de la première condition valide. Mettez la condition à gauche de ~, et l'action à sa 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.

24
Lorenz Walthert

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

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 de la fonction `if`, Ni autour d'une fonction ifelse - uniquement autour de l'instruction if ... else .... Cependant, si l'espace réservé aux points n'apparaît que dans un appel de fonction imbriqué, magrittr dirigera par défaut le côté gauche vers le premier argument de la main droite. côté. Ce comportement est ignoré 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 à chaque fois qu'il apparaît dans la fonction `if`, Car . + 1 Et . + 2 Sont 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 de `if`(TRUE, 1 + 1, 1 + 2), qui est le comportement souhaité dans ce cas.

Pour plus d'informations sur la façon dont l'opérateur de tuyau magrittr traite l'espace réservé aux points, consultez le fichier d'aide pour %>% , en particulier la section "Utilisation du point à des fins secondaires".

14
Cameron Bieganek

Il me semble plus facile de reculer un peu dans les tuyaux (bien que 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

Ceci est 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é). Ce serait bien de modifier cela pour retourner . automatiquement si la condition est fausse ... (attention: Je pense que cela fonctionne mais je n'ai pas vraiment testé/pensé trop ...)

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
12
Ben Bolker

J'aime purrr::when et 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 des expressions de fonctions (la notation des formules est prise en charge) et l'entrée est retourné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
  }
}
4
Moody_Mudskipper