Ленивая оценка для 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_ также не принимает строк.)