#r #loops #dplyr #purrr #nse
#r #циклы #dplyr #мурлыканье #nse
Вопрос:
Я хочу создать пользовательскую функцию dplyr и выполнить итерацию по ней в идеале с помощью purrr::map, чтобы оставаться в tidyverse.
Чтобы все было как можно проще, я воспроизвожу свою проблему, используя очень простую функцию суммирования.
При создании пользовательских функций с помощью dplyr я столкнулся с проблемой нестандартной оценки (NSE). Я нашел три разных способа справиться с этим. Каждый способ работы с NSE отлично работает, когда функция вызывается напрямую, но не при зацикливании на ней. Ниже вы найдете код для воспроизведения моей проблемы. Каков был бы правильный способ заставить мою функцию работать с purrr::map?
# loading libraries
library(dplyr)
library(tidyr)
library(purrr)
# generate test data
test_tbl <- rbind(tibble(group = rep(sample(letters[1:4], 150, TRUE), each = 4),
score = sample(0:10, size = 600, replace = TRUE)),
tibble(group = rep(sample(letters[5:7], 50, TRUE), each = 3),
score = sample(0:10, size = 150, replace = TRUE))
)
# generate two variables to loop over
test_tbl$group2 <- test_tbl$group
vars <- c("group", "group2")
# summarise function 1 using enquo()
sum_tbl1 <- function(df, x) {
x <- dplyr::enquo(x)
df %>%
dplyr::group_by(!! x) %>%
dplyr::summarise(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
# summarise function 2 using .dots = lazyeval
sum_tbl2 <- function(df, x) {
df %>%
dplyr::group_by_(.dots = lazyeval::lazy(x)) %>%
dplyr::summarize(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
# summarise function 3 using ensym()
sum_tbl3 <- function(df, x) {
df %>%
dplyr::group_by(!!rlang::ensym(x)) %>%
dplyr::summarize(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
# Looping over the functions with map
# each variation produces an error no matter which function I choose
# call within anonymous function without pipe
map(vars, function(x) sum_tbl1(test_tbl, x))
map(vars, function(x) sum_tbl2(test_tbl, x))
map(vars, function(x) sum_tbl3(test_tbl, x))
# call within anonymous function witin pipe
map(vars, function(x) test_tbl %>% sum_tbl1(x))
map(vars, function(x) test_tbl %>% sum_tbl2(x))
map(vars, function(x) test_tbl %>% sum_tbl3(x))
# call with formular notation without pipe
map(vars, ~sum_tbl1(test_tbl, .x))
map(vars, ~sum_tbl2(test_tbl, .x))
map(vars, ~sum_tbl3(test_tbl, .x))
# call with formular notation within pipe
map(vars, ~test_tbl %>% sum_tbl1(.x))
map(vars, ~test_tbl %>% sum_tbl2(.x))
map(vars, ~test_tbl %>% sum_tbl3(.x))
Я знаю, что существуют другие решения для создания сводных таблиц в цикле, такие как прямой вызов map и создание анонимной функции внутри map (см. Код ниже). Однако проблема, которая меня интересует, заключается в том, как работать с NSE в циклах в целом.
# One possibility to create summarize tables in loops with map
vars %>%
map(function(x){
test_tbl %>%
dplyr::group_by(!!rlang::ensym(x)) %>%
dplyr::summarize(score = mean(score, na.rm =TRUE),
n = dplyr::n())
})
Обновить:
Ниже akrun предлагает решение, которое делает возможным вызов через purrr::map(). Прямой вызов функции в этом случае, однако, возможен только путем вызова переменной группировки в виде строки либо напрямую
sum_tbl(test_tbl, “group”)
или косвенно как
sum_tbl(test_tbl, vars[1])
В этом решении невозможно вызвать группирующую переменную обычным способом dplyr как
sum_tbl(test_tbl, group)
В конечном счете, мне кажется, что решения для NSE в пользовательских функциях dpylr могут решить проблему либо на уровне самого вызова функции, тогда использование map / lapply невозможно, либо NSE может быть адресован для работы с итерациями, тогда переменные могут вызываться только как «строки».
Building on akruns answer I built a workaround function which allows both strings and normal variable names in the function call. However, there are definitely better ways to make this possible. Ideally, there is a more straight-forward way of dealing with NSE in custom dplyr functions, so that a workaround, like the one below, is not necessary in the first place.
sum_tbl <- function(df, x) {
x_var <- dplyr::enquo(x)
x_env <- rlang::get_env(x_var)
if(identical(x_env,empty_env())) {
# works, when x is a string and in loops via map/lapply
sum_tbl <- df %>%
dplyr::group_by(!! rlang::sym(x)) %>%
dplyr::summarise(score = mean(score, na.rm = TRUE),
n = dplyr::n())
} else {
# works, when x is a normal variable name without quotation marks
x = dplyr::enquo(x)
sum_tbl <- df %>%
dplyr::group_by(!! x) %>%
dplyr::summarise(score = mean(score, na.rm = TRUE),
n = dplyr::n())
}
return(sum_tbl)
}
Окончательное обновление / решение
В обновленной версии своего ответа акрун предоставляет решение, которое учитывает четыре способа вызова переменной x:
- как обычное (нестроковое) имя переменной:
sum_tbl(test_tbl, group)
- в качестве имени строки:
sum_tbl(test_tbl, "group")
- в виде индексированного вектора:
sum_tbl(test_tbl, !!vars[1])
- и как вектор внутри
purr::map()
:map(vars, ~ sum_tbl(test_tbl,
!!.x))
В (3) и (4) необходимо вывести переменную x из кавычек с помощью !!
.
Если бы я использовал функцию только для себя, это не было бы проблемой, но как только другие члены команды будут использовать функцию, мне нужно будет объяснить, задокументировать функцию.
Чтобы избежать этого, я теперь расширил решение akrun для учета всех четырех способов без снятия кавычек. Однако я не уверен, создало ли это решение другие подводные камни.
sum_tbl <- function(df, x) {
# if x is a symbol such as group without strings, than turn it into a string
if(is.symbol(get_expr(enquo(x)))) {
x <- quo_name(enquo(x))
# if x is a language object such as vars[1], evaluate it
# (this turns it into a symbol), then turn it into a string
} else if (is.language(get_expr(enquo(x)))) {
x <- eval(x)
x <- quo_name(enquo(x))
}
# this part of the function works with normal strings as x
sum_tbl <- df %>%
dplyr::group_by(!! rlang::sym(x)) %>%
dplyr::summarise(score = mean(score, na.rm = TRUE),
n = dplyr::n())
return(sum_tbl)
}
Ответ №1:
Мы можем просто использовать group_by_at
, которая может принимать строку в качестве аргумента
sum_tbl1 <- function(df, x) {
df %>%
dplyr::group_by_at(x) %>%
dplyr::summarise(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
и затем вызываем как
out1 <- map(vars, ~ sum_tbl1(test_tbl, .x))
Или другой вариант — преобразовать в sym
bol, а затем вычислить ( !!
) внутри group_by
sum_tbl2 <- function(df, x) {
df %>%
dplyr::group_by(!! rlang::sym(x)) %>%
dplyr::summarise(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
out2 <- map(vars, ~ sum_tbl2(test_tbl, .x))
identical(out1 , out2)
#[1] TRUE
Если мы указываем один из параметров, нам не нужно указывать второй аргумент, таким образом, мы также можем работать без анонимного вызова
map(vars, sum_tbl2, df = test_tbl)
Обновить
Если бы мы хотели использовать ее с условиями, упомянутыми в обновленном сообщении OP
sum_tbl3 <- function(df, x) {
x1 <- enquo(x)
x2 <- quo_name(x1)
df %>%
dplyr::group_by_at(x2) %>%
dplyr::summarise(score = mean(score, na.rm =TRUE),
n = dplyr::n())
}
sum_tbl3(test_tbl, group)
# A tibble: 7 x 3
# group score n
# <chr> <dbl> <int>
#1 a 5.43 148
#2 b 5.01 144
#3 c 5.35 156
#4 d 5.19 152
#5 e 5.65 72
#6 f 5.31 36
#7 g 5.24 42
sum_tbl3(test_tbl, "group")
# A tibble: 7 x 3
# group score n
# <chr> <dbl> <int>
#1 a 5.43 148
#2 b 5.01 144
#3 c 5.35 156
#4 d 5.19 152
#5 e 5.65 72
#6 f 5.31 36
#7 g 5.24 42
или вызов из ‘vars’
sum_tbl3(test_tbl, !!vars[1])
# A tibble: 7 x 3
# group score n
# <chr> <dbl> <int>
#1 a 5.43 148
#2 b 5.01 144
#3 c 5.35 156
#4 d 5.19 152
#5 e 5.65 72
#6 f 5.31 36
#7 g 5.24 42
и с map
map(vars, ~ sum_tbl3(test_tbl, !!.x))
#[[1]]
# A tibble: 7 x 3
# group score n
# <chr> <dbl> <int>
#1 a 5.43 148
#2 b 5.01 144
#3 c 5.35 156
#4 d 5.19 152
#5 e 5.65 72
#6 f 5.31 36
#7 g 5.24 42
#[[2]]
# A tibble: 7 x 3
# group2 score n
# <chr> <dbl> <int>
#1 a 5.43 148
#2 b 5.01 144
#3 c 5.35 156
#4 d 5.19 152
#5 e 5.65 72
#6 f 5.31 36
#7 g 5.24 42
Комментарии:
1. Большое вам спасибо за ваш ответ! Это решение находится на месте. Мне просто интересно, почему существует такая большая путаница в отношении NSE и dplyr и всех обходных путей, которые я нашел в Интернете, включая виньетку tidyverse о программировании с dplyr ( dplyr.tidyverse.org/articles/programming.html ) когда есть такое простое решение, как group_by_at.
2. @Hellwalker С помощью
tidyverse
ежедневно создается множество функций. Итак, когда создавалась статья, вероятно, существовало всего несколько способов справиться с этим3. Поиграв с вашим решением, я понял, что это помогает сделать возможным вызов через map (), но теперь прямой вызов функции приводит к ошибке. Идеальным решением было бы работать в обоих направлениях, напрямую и через map / lapply. На данный момент похоже, что проблема NSE может быть решена только на одном уровне: либо при непосредственном вызове функции, либо при создании функции, которая затем может использоваться только в итерациях.
4. @Hellwalker Извините, я не понял, как вы вызываете.
sum_tbl1(test_tbl, vars[1])
у меня все работает нормально илиsum_tbl1(test_tbl1, "group")
если вы заинтересованы в отмене кавычек, тогда в игру вступаетenquo
в вашей функции5. Основываясь на вашем обновленном ответе, я придумал подход, при котором удаление кавычек не требуется при вызове функции. Еще раз спасибо!