如何在ggplot图形内部使用整洁评估编写函数?

7
我想编写一个函数,用于在 ggplot 中进行美学映射。该函数应该有两个参数:var 应该映射到 aesthetic。下面的第一个代码块实际上可以工作。
然而,我想要的是在 geom_point 函数中进行映射,而不是在初始的 ggplot 函数内进行映射。这时会收到以下错误信息:
错误::= 只能在准引用的参数内使用
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. 块(Block):抛出错误

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)

如果将参数 aesthetic 作为带有 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

原因

如果我们查看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时,aestheticvar会被分配给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是原因,我们可以通过删除x和y来修改aes()的源代码。由于geom_*()默认使用inherit.aes = TRUE继承aes,因此您应该能够运行它。

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)

更新

简而言之,参数的顺序很重要。在使用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

:=用于代替命名参数

这种方式不能正常工作,因为在testNSE()的第一个位置使用了 !!a := !!b,而第一个位置已经有了名称x。因此它试图将size := Sepal.Width分配给x,而不是将Sepal.Width分配给size

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

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

有趣的答案。我不明白的是,在非函数上下文中,geom_point() 中的 aes() 为什么会继承 ggplot 调用中的 xy 参数,而在函数上下文中却没有这种情况发生。 - TimTeaFan
我也一样,我能理解的是它是ggplot2工作方式的一部分。但是如果没有对NSE或ggplot2源代码的深入了解,我无法说明具体原因。 - yusuzech
非常好的答案,非常有帮助! - Till
我越想,就越觉得问题的根源不在于 xy。似乎更多是 := 的 NSE 问题。如果我们只使用左手边的 NSE,即 size = !! var,一切都正常:`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 and LHS NSE:") print(aes(!!(aesthetic) := !!var)) print("without x and y and only RHS NSE:") print(aes(size = !!var))} ` - TimTeaFan
我更新了我的回答。当我们尝试使用:=时,xy是问题的根源; 当占据命名参数的位置时,似乎:=无效。在你的例子中,它之所以起作用,是因为它没有使用:=或评估LHS。 - yusuzech

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接