【问题标题】:How to write functions with tidy evaluation inside ggplot geoms?如何在ggplot geoms中编写具有整洁评估的函数?
【发布时间】:2020-02-06 23:42:26
【问题描述】:

我想编写一个在 ggplot 中执行美学映射的函数。该函数应该有两个参数:var 应该映射到aesthetic。下面的第一个代码块实际上有效。

但是,我不想在初始 ggplot 函数中而是在 geom_point 函数中进行映射。在这里我收到以下错误消息:

错误::= 只能在 quasiquoted 参数中使用

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

【问题讨论】:

    标签: r function ggplot2 tidyeval


    【解决方案1】:

    原因

    如果我们看一下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
    

    := 用于代替命名参数

    它不起作用,因为!!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
    

    【讨论】:

    • 有趣的答案。我不明白为什么在非函数上下文中geom_point() 中的aes() 会从ggplot 调用中继承xy 参数,以及为什么在函数上下文中不会发生这种情况。
    • 我也不是,我能理解的是它是 ggplot2 工作原理的一部分。但是如果没有对NSE或者ggplot2源码有更深入的了解,我也说不清具体原因。
    • 很好的答案,很有帮助!
    • 我想得越多,我就越觉得xy 是问题的根源。这似乎更像是:= 的 NSE 问题。如果我们只使用带有size = !! var 的左侧 NSE,一切正常:testaes &lt;- function(aesthetic, var){ aesthetic &lt;- enquo(aesthetic) var &lt;- 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)) }
    • 我更新了我的答案。当我们尝试使用:= 时,xy 是问题的根源; := 在占据命名参数的位置时似乎不起作用。在您的示例中,它之所以有效,是因为它没有使用 := 或评估 LHS。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-16
    • 1970-01-01
    • 2019-09-17
    • 2020-04-28
    • 2018-06-10
    • 2020-11-25
    • 2020-11-29
    相关资源
    最近更新 更多