R Умовна оцінка при використанні оператора трубопроводу%>%


94

При використанні оператора трубопроводу %>% з пакетами , такими як 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
}

Чи є кращий спосіб без усієї надмірності?


4
Приклад для роботи (як передбачив Бен) буде кращим, fyi.
Френк

Відповіді:


104

Ось короткий приклад, який використовує переваги .та ifelse:

X<-1
Y<-T

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

У ifelse, якщо Yє, TRUEякщо додасть 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

33

Я думаю, що це так 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).
Cameron

чудово, дякую за роз'яснення! Отож, оскільки вищезазначена проблема не векторизована, ви могли б також написати своє рішення як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
  }
}

"З іншого боку, функції або формули застосовуватимуться до даних лише за умови дотримання відповідної умови." Чи можете ви пояснити, чому ви вирішили це зробити?
mihagazvoda

Тому я обчислюю лише те, що мені потрібно для обчислення, але мені цікаво, чому я не зробив цього за допомогою виразів. З якихось причин здається, що я не хотів використовувати нестандартне оцінювання. Я думаю, що у моїх користувацьких функціях є модифікована версія, я оновлюватимусь, коли отримаю можливість.
Moody_Mudskipper

Будь ласка, повідомте мене, коли ви оновите його. Дякую!
mihagazvoda
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.