Ленивая оценка для ggplot2 внутри функции
в основном я использую ggplot2
для визуализации. Как правило, я проектирую сюжет
интерактивно (т. е. raw ggplot2
код, который использует NSE), но в конце концов, я
часто заканчиваю этот код в функцию, которая получает
данные и переменные для построения. И это всегда немного
кошмар.
Итак, типичные ситуации выглядят следующим образом. У меня есть некоторые данные, и я
создайте для него сюжет (в данном случае очень простой пример, используя
набор данных mpg, который поставляется с ggplot2
).
library(ggplot2)
data(mpg)
ggplot(data = mpg,
mapping = aes(x = class, y = hwy)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
И когда я заканчиваю разработку сюжета, я обычно хочу использовать его для
различных переменных или данных, и т. д. Поэтому я создаю функцию, которая получает
данные и переменные для графика в качестве аргументов. Но из-за NSE это
не так просто, как написать заголовок функции, а затем копировать/вставить и заменить
переменные для аргументов функции. Это не сработает, как показано ниже.
mpg <- mpg
plotfn <- function(data, xvar, yvar){
ggplot(data = data,
mapping = aes(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
plotfn(mpg, class, hwy) # Can't find object
## Don't know how to automatically pick scale for object of type function. Defaulting to continuous.
## Warning: restarting interrupted promise evaluation
## Error in eval(expr, envir, enclos): object 'hwy' not found
plotfn(mpg, "class", "hwy") #
Поэтому я должен вернуться и исправить код, например, с помощью aes_string
вместо этого aes
который использует NSE (в этом примере это довольно легко, но
для более сложных участков с большим количеством преобразований и слоев,
это становится кошмаром).
plotfn <- function(data, xvar, yvar){
ggplot(data = data,
mapping = aes_string(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
plotfn(mpg, "class", "hwy") # Now this works
И дело в том, что я нахожу очень удобным NSE, а также lazyeval
. Так
Мне нравится делать что-то подобное.
mpg <- mpg
plotfn <- function(data, xvar, yvar){
data_gd <- data.frame(
xvar = lazyeval::lazy_eval(substitute(xvar), data = data),
yvar = lazyeval::lazy_eval(substitute(yvar), data = data))
ggplot(data = data_gd,
mapping = aes(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
plotfn(mpg, class, hwy) # Now this works
plotfn(mpg, "class", "hwy") # This still works
plotfn(NULL, rep(letters[1:4], 250), 1:100) # And even this crazyness works
Этот дает моей функции сюжета большую гибкость. Например, вы можете
передайте имена переменных в кавычках или без кавычек и даже данные напрямую
вместо имени переменной (вид злоупотребления ленивой оценкой).
но у этого есть огромная проблема. Функция не может быть использована программно.
dynamically_changing_xvar <- "class"
plotfn(mpg, dynamically_changing_xvar, hwy)
## Error in eval(expr, envir, enclos): object 'dynamically_changing_xvar' not found
# This does not work, because it never finds the object
# dynamically_changing_xvar in the data, and it does not get evaluated to
# obtain the variable name (class)
поэтому я не могу использовать петли (например, lapply) для создания одного и того же графика для различные комбинации переменных или данных.
поэтому я думал злоупотреблять еще более ленивым, стандартные и нестандартные
оценка, и попытаться объединить их все, так что у меня есть оба, гибкость
показано выше, и возможность использования функции программно.
В принципе, то, что я делаю, это использовать tryCatch
до первого lazy_eval
в
выражение для каждой переменной и, если это не удается, оценить анализируемую
выражение.
plotfn <- function(data, xvar, yvar){
data_gd <- NULL
data_gd$xvar <- tryCatch(
expr = lazyeval::lazy_eval(substitute(xvar), data = data),
error = function(e) eval(envir = data, expr = parse(text=xvar))
)
data_gd$yvar <- tryCatch(
expr = lazyeval::lazy_eval(substitute(yvar), data = data),
error = function(e) eval(envir = data, expr = parse(text=yvar))
)
ggplot(data = as.data.frame(data_gd),
mapping = aes(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
plotfn(mpg, class, hwy) # Now this works, again
plotfn(mpg, "class", "hwy") # This still works, again
plotfn(NULL, rep(letters[1:4], 250), 1:100) # And this crazyness still works
# And now, I can also pass a local variable to the function, that contains
# the name of the variable that I want to plot
dynamically_changing_xvar <- "class"
plotfn(mpg, dynamically_changing_xvar, hwy)
Так, в дополнение к вышеупомянутая гибкость, теперь я могу использовать
ОДН-вкладыш или так, произвести много из такого же участка, с различным
переменные (или данные).
lapply(c("class", "fl", "drv"), FUN = plotfn, yvar = hwy, data = mpg)
## [[1]]
##
## [[2]]
##
## [[3]]
Хотя это очень практично, я подозреваю, что это не очень хорошая практика. Но
как плохо практике? Это мой ключевой вопрос. Какие другие альтернативы
могу ли я использовать лучшее из обоих миров?
конечно, я вижу это шаблон может создавать проблемы. Например.
# If I have a variable in the global environment that contains the variable
# I want to plot, but whose name is in the data passed to the function,
# then it will use the name of the variable and not its content
drv <- "class"
plotfn(mpg, drv, hwy) # Here xvar on the plot is drv and not class
А некоторые (многие?) остальные проблемы. Но мне кажется, что выгоды в плане
синтаксис-гибкость перевешивает эти другие проблемы. Есть мысли по этому поводу?
1 ответов
извлечение предлагаемой функции для ясности:
library(ggplot2)
data(mpg)
plotfn <- function(data, xvar, yvar){
data_gd <- NULL
data_gd$xvar <- tryCatch(
expr = lazyeval::lazy_eval(substitute(xvar), data = data),
error = function(e) eval(envir = data, expr = parse(text=xvar))
)
data_gd$yvar <- tryCatch(
expr = lazyeval::lazy_eval(substitute(yvar), data = data),
error = function(e) eval(envir = data, expr = parse(text=yvar))
)
ggplot(data = as.data.frame(data_gd),
mapping = aes(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
такая функция обычно довольно полезна, так как вы можете свободно смешивать строки и имена переменных. Но, как вы говорите, это не всегда безопасно. Рассмотрим следующий пример:
class <- "drv"
Class <- "drv"
plotfn(mpg, class, hwy)
plotfn(mpg, Class, hwy)
что будет генерировать ваша функция? Будут ли они такими же (они не являются)? Мне не совсем ясно, каков будет результат. Программирование с такой функцией может дать неожиданные результаты, в зависимости от того, какие переменные существуют в data
и которые существуют в окружающей среде. Поскольку многие люди используют имена переменных типа x
, xvar
или count
(хотя они, возможно, не должны), все может запутаться.
кроме того, если бы я хотел заставить ту или иную интерпретацию class
, Я не могу.
Я бы сказал, что это похоже на использование attach
: удобно, но в какой-то момент он может укусить вас сзади.
поэтому я бы использовал NSE и SE pair:
plotfn <- function(data, xvar, yvar) {
plotfn_(data,
lazyeval::lazy_eval(xvar, data = data),
lazyeval::lazy_eval(yvar, data = data))
)
}
plotfn_ <- function(data, xvar, yvar){
ggplot(data = data,
mapping = aes_(x = xvar, y = yvar)) +
geom_boxplot() +
geom_jitter(alpha = 0.1, color = "blue")
}
создание их на самом деле проще, чем ваша функция, я думаю. Вы можете выбрать, чтобы захватить все аргументы лениво с lazy_dots
тоже.
теперь мы получаем более легко предсказать результаты при использовании безопасной версии SE:
class <- "drv"
Class <- "drv"
plotfn_(mpg, class, 'hwy')
plotfn_(mpg, Class, 'hwy')
версия NSE все еще затронута, хотя:
plotfn(mpg, class, hwy)
plotfn(mpg, Class, hwy)
(Я нахожу это слегка раздражающим, что ggplot2::aes_
также не принимает строк.)