Попытка понять, как работает eval(expr, envir = df)
Я построил функцию, которая, кажется, работает, но я не понимаю, почему.
Моя первоначальная проблема заключалась в том, чтобы взять data.frame, который содержит подсчет населения, и расширить его, чтобы воссоздать исходное население. Это достаточно просто, если вы заранее знаете названия столбцов.
library(tidyverse)
set.seed(121)
test_counts <- tibble(Population = letters[1:4], Length = c(1,1,2,1),
Number = sample(1:100, 4))
expand_counts_v0 <- function(Length, Population, Number) {
tibble(Population = Population,
Length = rep(Length, times = Number))
}
test_counts %>% pmap_dfr(expand_counts_v0) %>% # apply it
group_by(Population, Length) %>% # test it
summarise(Number = n()) %>%
ungroup %>%
{ all.equal(., test_counts)}
# [1] TRUE
Однако я хотел обобщить это для функции, которой не нужно знать имена столбцов data.frame, и я заинтересован в NSE, поэтому я написал:
test_counts1 <- tibble(Population = letters[1:4],
Length = c(1,1,2,1),
Number = sample(1:100, 4),
Height = c(100, 50, 45, 90),
Width = c(700, 50, 60, 90)
)
expand_counts_v1 <- function(df, count = NULL) {
countq <- enexpr(count)
names <- df %>% select(-!!countq) %>% names
namesq <- names %>% map(as.name)
cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
) %>% set_names(namesq)
make_tbl <- function(...) {
expr(tibble(!!!cols)) %>% eval(envir = df)
}
df %>% pmap_dfr(make_tbl)
}
Но, когда я тестирую эту функцию, кажется, что строки дублируются 4 раза:
test_counts %>% expand_counts_v1(count = Number) %>%
group_by(Population, Length) %>%
summarise(Number = n()) %>%
ungroup %>%
{ sum(.$Number)/sum(test_counts$Number)}
# [1] 4
Это заставило меня угадать решение, которое было
expand_counts_v2 <- function(df, count = NULL) {
countq <- enexpr(count)
names <- df %>% select(-!!countq) %>% names
namesq <- names %>% map(as.name)
cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
) %>% set_names(namesq)
make_tbl <- function(...) {
expr(tibble(!!!cols)) %>% eval(envir = df)
}
df %>% make_tbl
}
Это похоже на работу:
test_counts %>% expand_counts_v2(count = Number) %>%
group_by(Population, Length) %>%
summarise(Number = n()) %>%
ungroup %>%
{ all.equal(., test_counts)}
# [1] TRUE
test_counts1 %>% expand_counts_v2(count = Number) %>%
group_by(Population, Length, Height, Width) %>%
summarise(Number = n()) %>%
ungroup %>%
{ all.equal(., test_counts1)}
# [1] TRUE
Но я не понимаю почему. Как это оценивается для каждой строки, хотя я больше не использую pmap? Функция должна быть применена к каждой строке, чтобы она работала, поэтому она должна быть как-то, но я не понимаю, как она это делает.
РЕДАКТИРОВАТЬ
После того, как Артем правильно объяснил, что происходит, я понял, что могу сделать это
expand_counts_v2 <- function(df, count = NULL) {
countq <- enexpr(count)
names <- df %>% select(-!!countq) %>% names
namesq <- names %>% map(as.name)
cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
) %>% set_names(namesq)
expr(tibble(!!!cols)) %>% eval_tidy(data = df)
}
Который избавляется от ненужной функции mk_tbl. Однако, как сказал Артем, это действительно работает, потому что репутация векторизована. Итак, это работает, но не переписывая функцию _v0 и отображая ее, что я и пытался повторить. В конце концов я обнаружил, rlang::new_function и написал:
expand_counts_v3 <- function(df, count = NULL) {
countq <- enexpr(count)
names <- df %>% select(-!!countq) %>% names
namesq <- names %>% map(as.name)
cols <- map(namesq, ~ expr(rep(!!., times = !!countq))
) %>% set_names(namesq)
all_names <- df %>% names %>% map(as.name)
args <- rep(0, times = length(all_names)) %>% as.list %>% set_names(all_names)
correct_function <- new_function(args, # this makes the function as in _v0
expr(tibble(!!!cols)) )
pmap_dfr(df, correct_function) # applies it as in _v0
}
который длиннее и, возможно, более уродлив, но работает так, как я хотел.
1 ответ
Вопрос в eval( envir = df )
, который подвергает весь фрейм данных make_tbl()
, Обратите внимание, что вы никогда не используете ...
аргумент внутри make_tbl()
, Вместо этого функция эффективно вычисляет эквивалент
with( df, tibble(Population = rep(Population, times = Number),
Length = rep(Length, times=Number)) )
независимо от того, какие аргументы вы предоставляете ему. Когда вы вызываете функцию через pmap_dfr()
, он по существу вычисляет вышеупомянутое четыре раза (один раз для каждой строки) и объединяет результаты по строкам, что приводит к дублированию записей, которые вы наблюдали. Когда вы удаляете pmap_dfr()
, функция вызывается один раз, но так как rep
само векторизация (попробуйте сделать rep( test_counts$Population, test_counts$Number )
чтобы понять, что я имею в виду), make_tbl()
вычисляет весь результат за один раз.