Как написать функции с аккуратной оценкой внутри геомов ggplot? - PullRequest
7 голосов
/ 09 октября 2019

Я хотел бы написать функцию, которая выполняет эстетическое отображение в ggplot. Предполагается, что функция имеет два аргумента: var должен отображаться на aesthetic. Первый блок кода ниже на самом деле работает.

Однако я хотел бы сделать отображение не в начальной функции ggplot, а в функции geom_point. Здесь я получаю следующее сообщение об ошибке:

Ошибка: := может использоваться только в кавычках аргумента

1. Блок: отлично работает

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, !! (aesthetic) := !!var)) + 
    geom_point()
}
myfct(size, Petal.Width)

2. Блок: выдает ошибку

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
    geom_point(aes(!! (aesthetic) := !!var))
}
myfct(size, Petal.Width)

То же происходит и в том случае, если эстетический аргумент передается в виде строки с sym.

# 1. block
myfct <- function(aesthetic, var){
  aesthetic <- sym(aesthetic)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, !! aesthetic := {{var}})) + 
    geom_point()
}
myfct("size", Petal.Width)
# works

# 2. block 
myfct <- function(aesthetic, var){
  aesthetic <- sym(aesthetic)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +
    geom_point(aes(!! aesthetic := {{var}}))
}
myfct("size", Petal.Width)
# doesn't work

1 Ответ

4 голосов
/ 09 октября 2019

Причины

Если мы посмотрим на исходный код aes, мы можем обнаружить, что первое и второе места зарезервированы x и y

> ggplot2::aes
function (x, y, ...) 
{
    exprs <- enquos(x = x, y = y, ..., .ignore_empty = "all")
    aes <- new_aes(exprs, env = parent.frame())
    rename_aes(aes)
}

Давайте определим функцию для просмотракак это повлияет на aes():

testaes <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)

    print("with x and y:")
    print(aes(Sepal.Length,Sepal.Width,!!(aesthetic) := !!var))
    print("without x and y:")
    print(aes(!!(aesthetic) := !!var))

}
> testaes(size, Petal.Width)
[1] "with x and y:"
Aesthetic mapping: 
* `x`    -> `Sepal.Length`
* `y`    -> `Sepal.Width`
* `size` -> `Petal.Width`
[1] "without x and y:"
Aesthetic mapping: 
* `x` -> ``:=`(size, Petal.Width)`

Как вы можете видеть, при использовании := без x и y вместо aesthetic и var назначаются x.

Чтобы систематически решить эту проблему, необходимы дополнительные знания по NSE и исходному коду ggplot2.

обходные пути

всегда присваивают значения первому и второму месту

library(ggplot2)
myfct <- function(aesthetic, var){
  aesthetic <- enquo(aesthetic)
  var <- enquo(var)
  ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
    geom_point(aes(x = Sepal.Length, y = Sepal.Width,!! (aesthetic) := !!var))
}
myfct(size, Petal.Width)

Или написать оболочку для aes

library(ggplot2)

myfct <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)

    # wrapper on aes
    myaes <- function(aesthetic, var){
        aes(x = Sepal.Length, y = Sepal.Width,!! (aesthetic) := !!var)
    }

    ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
        geom_point(mapping = myaes(aesthetic,var))
}
myfct(size, Petal.Width)

или Изменить исходный код

, поскольку x и y являются причиной, мы можем изменить исходное кодирование aes(), удалив x иу. поскольку geom_*() наследует aes, имея значение по умолчанию inherit.aes = TRUE, поэтому вы должны иметь возможность его запустить.

aes_custom <- function(...){
    exprs <- enquos(..., .ignore_empty = "all")
    aes <- ggplot2:::new_aes(exprs, env = parent.frame())
    ggplot2:::rename_aes(aes)
}

myfct <- function(aesthetic, var){
    aesthetic <- enquo(aesthetic)
    var <- enquo(var)
    ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) + 
        geom_point(aes_custom(!!(aesthetic) := !!var))
}
myfct(size, Petal.Width)

UPDATE

Короче говоря, порядок аргументов имеет значение. При использовании NSE мы всегда должны помещать !!x := !!y в положение безымянных аргументов (например, ...) и никогда в положение именованного аргумента.

Мне удалось воспроизвести проблему за пределами ggplot, поэтому первопричинаэто из NSE. Кажется, что := работает только в положении безымянного аргумента (...). Он не оценивается правильно, когда находится в позиции именованного аргумента (x в следующем примере).

library(rlang)
# test funciton
testNSE <- function(x,...){
    exprs <- enquos(x = x, ...)
    print(exprs)
}

# test data
a = quo(size)
b = quo(Sepal.Width)

:= используется вместо безымянного аргумента (...)

Работает правильно

> testNSE(x,!!a := !!b)
<list_of<quosure>>

$x
<quosure>
expr: ^x
env:  global

$size
<quosure>
expr: ^Sepal.Width
env:  global

:= используется вместо именованного аргумента

Не работает, так как !!a := !!b используется в первой позиции testNSE() и первая позиция уже имеет название x. Поэтому он пытается назначить size := Sepal.Width на x вместо Sepal.Width на size.

> testNSE(!!a := !!b)
<list_of<quosure>>

$x
<quosure>
expr: ^^size := ^Sepal.Width
env:  global
...