использование rlang, чтобы разрешить список аргументов, а затем использовать его в пользовательской функции

Я пишу пользовательскую функцию, в которой я хочу, чтобы один из аргументов принимал список переменных. Мне удалось использовать rlang и некоторое элементарное понимание ..., чтобы правильно прочитать этот список в функции. Но я не знаю, как назначить этот список в качестве аргумента другой функции (например, dplyr::group_by). Я полностью воспроизводимый пример ниже вместе с конечным результатом, который я хочу.

# loading the needed libraries
library(dplyr)
library(rlang)
library(datasets)

# defining the custom function
prac.fn <- function(data, vars = ..., measure) {
  # getting the dataframe ready
  df <-
    dplyr::select(.data = data,
                  !!rlang::enquo(vars),
                  !!rlang::enquo(measure))
  # print to see if all variables are included
  print(head(df))

  # summarize by specified grouping variables
  df %>%
    dplyr::group_by(.data = ., c(!!rlang::enquo(vars))) %>%
    dplyr::summarise(mean = mean(!!rlang::enquo(measure)))

}

# use the function (doesn't work)
prac.fn(data = mtcars,
        vars = c(cyl, am),
        measure = wt)
#>                   cyl am    wt
#> Mazda RX4           6  1 2.620
#> Mazda RX4 Wag       6  1 2.875
#> Datsun 710          4  1 2.320
#> Hornet 4 Drive      6  0 3.215
#> Hornet Sportabout   8  0 3.440
#> Valiant             6  0 3.460
#> Error in mutate_impl(.data, dots): Column `c(c(cyl, am))` must be length 32 (the number of rows) or one, not 64

# output I want
mtcars %>%
  dplyr::group_by(cyl, am) %>%
  dplyr::summarise(mean = mean(wt))
#> # A tibble: 6 x 3
#> # Groups:   cyl [?]
#>     cyl    am  mean
#>   <dbl> <dbl> <dbl>
#> 1  4.00  0     2.94
#> 2  4.00  1.00  2.04
#> 3  6.00  0     3.39
#> 4  6.00  1.00  2.76
#> 5  8.00  0     4.10
#> 6  8.00  1.00  3.37

Создано 17 февраля 2018 г. с помощью пакета reprex (v0.2.0).


person Indrajeet Patil    schedule 18.02.2018    source источник


Ответы (1)


В group_by, после преобразования 'vars' в quosure (enquo), сгладьте выражение с помощью quo_squash, преобразуйте его в list (as.list) и удалите первый элемент, т.е. c, затем с помощью !!! оцените его

prac.fn <- function(data,  vars, measure) {
    data %>%
        select(!!rlang::enquo(vars),
         !!rlang::enquo(measure)) %>%
        dplyr::group_by(!!! as.list(quo_squash(rlang::enquo(vars)))[-1]) %>%
        dplyr::summarise(mean = mean(!!rlang::enquo(measure)))      

 }

-тестирование

prac.fn(data = mtcars,
        vars = c(cyl, am),
         measure = wt)
# A tibble: 6 x 3
# Groups: cyl [?]
#    cyl    am  mean
#  <dbl> <dbl> <dbl>
#1  4.00  0     2.94
#2  4.00  1.00  2.04
#3  6.00  0     3.39
#4  6.00  1.00  2.76
#5  8.00  0     4.10
#6  8.00  1.00  3.37

Проверка с большим количеством групп

prac.fn(data = mtcars,
        vars = c(cyl, am, gear),
         measure = wt)
# A tibble: 10 x 4
# Groups: cyl, am [?]
#     cyl    am  gear  mean
#   <dbl> <dbl> <dbl> <dbl>
# 1  4.00  0     3.00  2.46
# 2  4.00  0     4.00  3.17
# 3  4.00  1.00  4.00  2.11
# 4  4.00  1.00  5.00  1.83
# 5  6.00  0     3.00  3.34
# 6  6.00  0     4.00  3.44
# 7  6.00  1.00  4.00  2.75
# 8  6.00  1.00  5.00  2.77
# 9  8.00  0     3.00  4.10
#10  8.00  1.00  5.00  3.37

Неясно, всегда ли OP хотел использовать c() для аргумента vars, т.е. если есть одна переменная группировки, функция работает, если поведение передачи аргумента одинаково

prac.fn(data = mtcars,
        vars = c(cyl),
         measure = wt)
#<quosure>
#  expr: ^c(cyl)
#  env:  global
# A tibble: 3 x 2
#    cyl  mean
#  <dbl> <dbl>
#1  4.00  2.29
#2  6.00  3.12
#3  8.00  4.00

Но если нам нужно изменить поведение, т. е. vars = cyl без c(), тогда к нему нужно обратиться с помощью оператора if/else, т.е.

prac.fnN <- function(data,  vars, measure) {
    vars <- as.list(quo_squash(enquo(vars)))
    vars <- if(length(vars) ==1) vars else vars[-1]
    data %>%
        select(!!! vars,
         !!rlang::enquo(measure)) %>%
        dplyr::group_by(!!! vars) %>%
        dplyr::summarise(mean = mean(!!rlang::enquo(measure))) 


 }

-тестирование

prac.fnN(data = mtcars,
        vars = cyl,
         measure = wt)
# A tibble: 3 x 2
#    cyl  mean
#  <dbl> <dbl>
#1  4.00  2.29
#2  6.00  3.12
#3  8.00  4.00


prac.fnN(data = mtcars,
       vars = c(cyl),
        measure = wt)
# A tibble: 3 x 2
#    cyl  mean
#  <dbl> <dbl>
#1  4.00  2.29
#2  6.00  3.12
#3  8.00  4.00


prac.fnN(data = mtcars,
        vars = c(cyl, am),
         measure = wt)
# A tibble: 6 x 3
# Groups: cyl [?]
#    cyl    am  mean
#  <dbl> <dbl> <dbl>
#1  4.00  0     2.94
#2  4.00  1.00  2.04
#3  6.00  0     3.39
#4  6.00  1.00  2.76
#5  8.00  0     4.10
#6  8.00  1.00  3.37

В дополнение к вышеперечисленным методам, естественным вариантом будет передать аргументы как quos/quo и тогда нам не придется думать о enquo и прочих if/else

prac.fnQ <- function(data,  vars, measure) {
   stopifnot(is_quosures(vars)) 
   stopifnot(is_quosure(measure))

    data %>%
        select(!!! vars, !! measure) %>%
        dplyr::group_by(!!! vars) %>%
        dplyr::summarise(mean = mean(!! measure))   

 }

-тестирование

prac.fnQ(data = mtcars,
       vars = quos(cyl, am),
        measure = quo(wt))
# A tibble: 6 x 3
# Groups: cyl [?]
#    cyl    am  mean
#  <dbl> <dbl> <dbl>
#1  4.00  0     2.94
#2  4.00  1.00  2.04
#3  6.00  0     3.39
#4  6.00  1.00  2.76
#5  8.00  0     4.10
#6  8.00  1.00  3.37

Если нам также нужно проверить, являются ли переменные «мера» (при условии, что у нас есть несколько переменных «мера») numeric

prac.fnQn <- function(data,  vars, measure) {
   stopifnot(is_quosures(vars)) 
   stopifnot(is_quosures(measure))

      data %>%
        select(!!! vars, !!! measure) %>%
        dplyr::group_by(!!! vars) %>%
        summarise_if(is.numeric, mean)       


 }

prac.fnQn(data = mtcars,
       vars = quos(cyl, am),
        measure = quos(wt))
# A tibble: 6 x 3
# Groups: cyl [?]
#    cyl    am    wt
#  <dbl> <dbl> <dbl>
#1  4.00  0     2.94
#2  4.00  1.00  2.04
#3  6.00  0     3.39
#4  6.00  1.00  2.76
#5  8.00  0     4.10
#6  8.00  1.00  3.37
person akrun    schedule 18.02.2018
comment
Спасибо за подробный ответ. Еще один быстрый вопрос: я использую эту функцию внутри пакета, и когда я Check пакет, кажется, что он не доволен вводом ... аргумента vars и выдает предупреждение: Warning: <anonymous>: ... may be used in an incorrect context Должен ли я просто игнорировать его? Или есть способ сказать devtools, что точки имеют смысл в этом контексте? - person Indrajeet Patil; 18.02.2018
comment
@IndrajeetPatil Зачем тебе vars = ...? Вместо этого это может быть просто vars - person akrun; 18.02.2018
comment
Ах, понял! Извините, я думал, что то, как люди иногда обозначают, что функция может принимать множество других аргументов с помощью ..., также относится и к списку аргументов. Он отлично работает после изменения vars = ... только на vars. - person Indrajeet Patil; 18.02.2018
comment
Извините, еще один и, надеюсь, последний вопрос об этой функции. Я хочу, чтобы пользователь вводил только числовые переменные. Как я могу проверить с помощью rlang, если какая-либо из списка введенных measure переменных не является numeric, и, если это так, остановить выполнение функции? - person Indrajeet Patil; 18.02.2018
comment
@IndrajeetPatil Обновил пост с hsummarise_if - person akrun; 19.02.2018
comment
Извините, что снова возвращаюсь к этому. Но я не понимаю, что grouping.vars[-1] делает в выражении if/else. Почему там -1? - person Indrajeet Patil; 24.03.2018
comment
@IndrajeetPatil Если вы используете print(vars) после vars <- as.list(quo_squash(enquo(vars))), вы обнаружите, что для c(cyl) первым элементом списка является c, который нам не нужен, но если вы пройдете в cyl, c не будет - person akrun; 24.03.2018