R Evaluasi bersyarat saat menggunakan operator pipa%>%

94

Bila menggunakan operator pipa %>%dengan paket seperti dplyr, ggvis, dycharts, dll, bagaimana saya melakukan langkah bersyarat? Sebagai contoh;

step_1 %>%
step_2 %>%

if(condition)
step_3

Pendekatan ini sepertinya tidak berhasil:

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

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

Ada jalan panjang:

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

Apakah ada cara yang lebih baik tanpa semua redundansi?

rmf
sumber
4
Contoh untuk bekerja dengan (seperti yang diberikan Ben) akan lebih disukai, fyi.
Frank

Jawaban:

104

Berikut adalah contoh cepat yang memanfaatkan .dan ifelse:

X<-1
Y<-T

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

Di ifelse, jika Yadalah TRUEjika akan menambahkan 1, jika tidak maka hanya akan mengembalikan nilai terakhir X. Ini .adalah stand-in yang memberi tahu fungsi di mana output dari langkah rantai sebelumnya pergi, jadi saya bisa menggunakannya di kedua cabang.

Edit Seperti yang ditunjukkan @BenBolker, Anda mungkin tidak menginginkannya ifelse, jadi ini adalah ifversinya.

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

Terima kasih kepada @Frank karena telah menunjukkan bahwa saya harus menggunakan {kawat gigi di sekitar pernyataan ifdan saya ifelseuntuk melanjutkan rantai.

John Paul
sumber
4
Saya suka versi pasca-edit. ifelsetampaknya tidak wajar untuk aliran kendali.
Frank
7
Satu hal yang perlu diperhatikan: jika ada langkah selanjutnya dalam rantai, gunakan {}. Misalnya, jika Anda tidak memilikinya di sini, hal-hal buruk terjadi (hanya mencetak Ykarena alasan tertentu): X %>% "+"(1) %>% {if(Y) "+"(1) else .} %>% "*"(5)
Frank
Penggunaan alias magrittr addakan membuat contoh lebih jelas.
ctbrown
Dalam istilah kode golf, contoh khusus ini dapat ditulis X %>% add(1*Y)tetapi tentu saja itu tidak menjawab pertanyaan asli
talat
1
Satu hal penting dalam blok bersyarat di antara {}adalah Anda harus merujuk argumen sebelumnya dari pipa dplyr (juga disebut LHS) dengan titik (.) - jika tidak, blok bersyarat tidak menerima. argumen!
Agile Bean
33

Saya pikir itu kasusnya purrr::when. Mari kita simpulkan beberapa angka jika jumlahnya di bawah 25, jika tidak, kembalikan 0.


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

whenmengembalikan nilai yang dihasilkan dari tindakan kondisi valid pertama. Letakkan ketentuan di sebelah kiri ~, dan tindakan di sebelah kanannya. Di atas, kami hanya menggunakan satu ketentuan (dan kemudian kasus lain), tetapi Anda dapat memiliki banyak ketentuan.

Anda dapat dengan mudah mengintegrasikannya ke dalam pipa yang lebih panjang.

Lorenz Walthert
sumber
2
bagus! Ini juga memberikan alternatif yang lebih intuitif untuk 'saklar'.
Steve G. Jones
16

Berikut adalah variasi jawaban yang diberikan oleh @JohnPaul. Variasi ini menggunakan `if`fungsi sebagai pengganti if ... else ...pernyataan gabungan .

library(magrittr)

X <- 1
Y <- TRUE

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

Perhatikan bahwa dalam kasus ini kurung kurawal tidak diperlukan di sekitar `if`fungsi, atau di sekitar ifelsefungsi — hanya di sekitar if ... else ...pernyataan. Namun, jika placeholder titik hanya muncul dalam panggilan fungsi bersarang, maka magrittr secara default akan menyalurkan sisi kiri ke argumen pertama dari sisi kanan. Perilaku ini diganti dengan menyertakan ekspresi dalam tanda kurung kurawal. Perhatikan perbedaan antara kedua rantai ini:

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

Placeholder titik bertumpuk dalam panggilan fungsi kedua kali muncul dalam `if`fungsi, karena . + 1dan . + 2ditafsirkan sebagai `+`(., 1)dan `+`(., 2), masing-masing. Jadi, ekspresi pertama mengembalikan hasil `if`(1, TRUE, 1 + 1, 1 + 2), (anehnya, `if`tidak mengeluh tentang argumen ekstra yang tidak terpakai), dan ekspresi kedua mengembalikan hasil `if`(TRUE, 1 + 1, 1 + 2), yang merupakan perilaku yang diinginkan dalam kasus ini.

Untuk informasi lebih lanjut tentang bagaimana operator pipa magrittr memperlakukan placeholder titik, lihat file bantuan untuk %>%, khususnya bagian tentang "Menggunakan titik untuk tujuan sekunder".

Cameron Bieganek
sumber
Apa perbedaan antara menggunakan `ìf`dan ifelse? apakah mereka identik dalam perilaku?
Agile Bean
@AgileBean Perilaku ifdan ifelsefungsi tidak identik. The ifelseFungsi adalah Vectorized if. Jika Anda memberikan iffungsi dengan vektor logis, itu akan mencetak peringatan dan hanya akan menggunakan elemen pertama dari vektor logis itu. Bandingkan `if`(c(T, F), 1:2, 3:4)dengan ifelse(c(T, F), 1:2, 3:4).
Cameron Bieganek
bagus, terima kasih atas klarifikasinya! Jadi karena masalah di atas adalah non-vektorisasi, Anda juga dapat menulis solusi Anda sebagaiX %>% { ifelse(Y, .+1, .+2) }
Agile Bean
12

Tampaknya paling mudah bagi saya untuk mundur dari pipa sedikit kecil (walaupun saya akan tertarik untuk melihat solusi lain), misalnya:

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

Ini adalah sedikit modifikasi dari jawaban @ JohnPaul (Anda mungkin tidak benar-benar menginginkannya ifelse, yang mengevaluasi kedua argumennya dan dalam bentuk vektorisasi). Alangkah baiknya untuk memodifikasi ini untuk mengembalikan .secara otomatis jika kondisinya salah ... ( hati-hati : Saya pikir ini berfungsi tetapi belum benar-benar menguji / terlalu memikirkannya ...)

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
sumber
8

Saya suka purrr::whendan solusi dasar lain yang disediakan di sini semuanya hebat tetapi saya menginginkan sesuatu yang lebih kompak dan fleksibel jadi saya merancang fungsi pif(pipa jika), lihat kode dan dokumen di akhir jawaban.

Argumen dapat berupa ekspresi fungsi (notasi rumus didukung), dan input dikembalikan secara default jika kondisinya adalah FALSE.

Digunakan pada contoh dari jawaban lain:

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

Contoh lain:

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

Fungsi

#' 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
sumber
"Fungsi atau rumus di sisi lain akan diterapkan pada data hanya jika kondisi yang relevan terpenuhi." Bisakah Anda menjelaskan mengapa Anda memutuskan untuk melakukannya?
mihagazvoda
Jadi saya hanya menghitung apa yang perlu saya hitung, tetapi saya bertanya-tanya mengapa saya tidak melakukannya dengan ekspresi. Untuk beberapa alasan sepertinya saya tidak ingin menggunakan evaluasi non standar. Saya rasa saya memiliki versi modifikasi dalam fungsi kustom saya, saya akan memperbaruinya ketika saya mendapat kesempatan.
Moody_Mudskipper
Beri tahu saya jika Anda memperbaruinya. Terima kasih!
mihagazvoda