【问题标题】:Error on final object when generating ggplot objects in for loop with dplyr select()使用 dplyr select() 在 for 循环中生成 ggplot 对象时最终对象出错
【发布时间】:2022-01-15 15:18:15
【问题描述】:

我想在一个数据框中使用多对变量绘制许多图,所有变量都具有相同的 x。我将图存储在命名列表中。为简单起见,下面是一个示例,每个图中只有 1 个变量。

这个函数的关键是一个select() 调用,这里显然不是必需的,但我的实际数据。

函数的主体在每个变量上都可以正常工作,但是当我遍历一个变量列表时,列表中的最后一个总是产生

get(ll) 中的错误:找不到对象“d”。

(或任何最后一个变量,如果不是 'd')。将data <- df %>% select(x,ll) 替换为data <- df 可以避免该错误。

## make data
df2 <- data.frame(x = 1:10,
                  a = 1:10,
                  b = 2:11,
                  c = 101:110,
                  d = 10*(1:10))

## make function
testfun <- function(df = df2, vars = letters[1:4]){
  ## initialize list to store plots
  plotlist <- list()
  
  for (ll in vars){
    ## subset data
    data <- df %>% select(x, ll) ## comment out select() to get working function
    # print(data) ## uncomment to check that dataframe subset works correctly
    
    ## plot variable vs. x
    p <- ggplot(data,
           aes(x = x, y = get(ll))) +
      geom_point() +
      ylab(ll)
    
    ## add plot to named list
    plotlist[[ll]] <- p
    # print(p) ## uncomment to see that each plot is being made
  }
  return(plotlist) ## unnecessary, being explicit for troubleshooting
}

## use function
pl <- testfun(df2)
## error ?
pl

我有一个解决方法,通过在我的实际数据框中重命名变量来避免select(),但我很好奇为什么这不起作用?有什么想法吗?

【问题讨论】:

  • 使用dplyr::select?没有运行代码,只是认为它调用了错误的select。另一个问题可能是没有使用NSE
  • 我仍然使用dplyr::select 得到错误(一个合理的建议,因为 plotly 也有一个选择功能),但是@NelsonGon 的下面的建议有效

标签: r for-loop ggplot2 dplyr


【解决方案1】:

get() 可以工作,但不能直接与ll 一起工作。试试y = get(!!ll)y = {{ll}}

ggplot(或者可能是aes,很难说)等待运行此代码,直到它的绘图对象被引用,正如所提供代码中的错误所示。当每个 ggplot 计算 get(ll) 时,for 循环已经完成。因此,ll 对所有四个 ggplots 计算为循环变量 "d" 的最后一个值。 ll 在错误中为“d”使得它看起来像是最后一个失败的 ggplot 对象,但它实际上正在评估导致此错误的第一个对象。

在循环体中,我们想要一种方法来评估 ll 变量并将生成的字符串(“a”、“b”、“c”或“d”)粘贴到这段代码中,即其余的直到稍后才会运行。将 y = get(ll) 更改为 y = get(!!ll) 是一种方法:!! 对未计算的表达式(在 Tidyverse 文档中称为“代码蓝图”)执行“surgery”,以便传递给 ggplot 的表达式包含文字像"a" 这样的字符串,而不是变量引用ll

testfun <- function(df = df2, vars = letters[1:4]){
  plotlist <- list()
  
  for (ll in vars){
    data <- df %>% select(x, ll)
    
    p <- ggplot(data,
                aes(x = x, y = get(!!ll))) +
                geom_point() +
                ylab(ll)
    
    plotlist[[ll]] <- p
  }
  return(plotlist)
}

继续阅读以获取解释和替代解决方案。


循环问题:后期绑定

在给定的函数或 R 的全局范围内,任何给定名称都只有一个变量。 for (x in xs) 循环反复将该变量重新绑定到新值。这意味着在 for 循环完成后,该变量仍然存在并保留分配给它的最后一个值。这是一种可能会绊倒你的方法:

vars <- c("a", "b", "c", "d")

results <- list()

for (ll in vars){
  message("in for loop, ll: ", ll)
  func <- function () { ll }
  results[[ll]] <- c(ll, func)
}
message("after for loop, ll: ", ll)
# after for loop, now ll is "d"

for (vec in results) {
  message(vec[[1]], " ", vec[[2]]())
}

这个输出

in for loop, ll: a
in for loop, ll: b
in for loop, ll: c
in for loop, ll: d
after for loop, ll: d
a d
b d
c d
d d

这里构造的四个函数中的每一个都使用相同的外部作用域变量ll,在 for 循环之后实际调用函数时,它是“d”。后期绑定部分是在查找其值时使用函数调用时(后期)的变量值,而不是定义函数时(早期)时变量的值。

NSE 问题

虽然 OP 并没有在循环中创建函数,但它们正在调用 ggplot。 ggplot 做了类似于创建函数的事情:它将一些代码作为参数,直到稍后才计算。 ggplot(或者可能是 aes)“captures”来自某些参数的代码,而不是运行它们。在 OP 的情况下,get(ll) 直到稍后才会被评估。

当此代码被评估时,它位于一个带有“data mask”的新上下文中,允许直接引用数据框的名称。这部分很棒,这就是我们想要的——这就是让get("a") 工作的原因。但是评估发生在后面的事实对 OP 来说是一个问题:get(ll) 中的ll 评估为“d”,例如get("d"),因为代码是在 for 循环迭代之后评估的,其中 ll 有期望值。

忽略数据掩码部分,这里有一个名为 run.later 的函数,与 ggplot 一样,它不运行它的任何一个参数。当我们稍后运行该代码时,我们再次发现 ll 对于所有四个保存的表达式的计算结果为“d”。

vars <- c("a", "b", "c", "d")

unevaluated.exprs <- list();

run.later <- function(name, something) {
  expr <- substitute(something)
  unevaluated.exprs[[name]] <<- c(name, expr)
}

for (ll in vars){
  run.later(ll, ll)
}

for (vec in unevaluated.exprs) {
  message(c(vec[[1]], " ", eval(vec[[2]])))
}

打印

a d
b d
c d
d d

这就是问题的ll 部分。来自 Python 等语言的“不要在循环中定义函数(如果它们引用循环变量)”的经验法则可以概括为 R 为“不要定义函数或以其他方式编写不会立即在一个循环(如果该代码引用循环变量)。”


解决范围问题而不是元编程

顶部提供的!! 解决方案使用元编程来评估循环中的ll 变量,而不是稍后再评估它。

理论上,可以改为在循环的每次迭代中动态创建变量,然后使用元编程仔细引用动态创建的变量名称。但更优雅的方法是使用相同的变量名但在不同的范围内。这就是 Nithin 对函数的回答:每个函数都会创建一个新的范围和 tada,您可以在每个函数中使用相同的变量名。这是另一个版本,更接近 OP 的代码:

testfun <- function(df = df2, vars = letters[1:4]){
  plotlist <- list()

  plot.fn <- function(var) {
      data <- df %>% select(x, var)
      p <- ggplot(data,
          aes(x = x, y = get(var))) +
          geom_point() +
          ylab(var)
      plotlist[[ll]] <<- p
  }
  
  for (ll in vars){
    plot.fn(ll)
  }
  return(plotlist)
}

pl <- testfun(df2)
pl

此代码中有 4 个不同的变量,称为 var,循环的每次迭代都引用一个不同的变量。


更漂亮的元编程

我认为(尚未测试)get(!!ll) 在这里等同于 {{ll}}get() 将字符串作为变量查找,但这也是粘贴 ll 求值的字符串符号的原因进入表达式确实。双花括号似乎更常见,大致可以理解为“将这个表达式的结果评估为另一个上下文中的变量”或“将这个字符串模板化到表达式中”。

【讨论】:

    【解决方案2】:

    像这样写一个自定义函数

    plot_fn<- function(df,y){
      df %>% ggplot(aes(x=x, 
                        y=get(y))+
              geom_point()+
              ylab(y)
        }
    

    使用 purrr:::map 迭代绘图

    map(letters[1:4],~plot_fn(df=df2,y=.x))
    

    【讨论】:

      【解决方案3】:

      问题是我们不能使用get 在“编程”范式中访问dplyr/tidyverse 数据。相反,我们应该使用非标准评估来访问数据。我在下面提供了一个简化的函数(最初我认为这是一个函数屏蔽问题,因为我快速浏览了这个问题)。

      testfun <- function(df = df2, vars = letters[1:4]){
       
        
        lapply(vars, function(y) {
          ggplot(df,
                 aes(x = x, y = .data[[y]] )) +
            geom_point() +
            ylab(y)
          
        })
      
      
      }
      

      打电话

      plots <- testfun(df2)
      plots[[1]]
      

      编辑

      由于 OP 想知道问题是什么,我按照要求使用了传统循环

      testfun2 <- function(df = df2, vars = letters[1:4]){
        ## initialize list to store plots
        plotlist <- list()
        
        for (ll in vars){
          ## subset data
          d_t <- df %>% select(x, ll) ## comment out select() to get working function
          # print(data) ## uncomment to check that dataframe subset works correctly
          ## plot variable vs. x
          p <- ggplot(d_t,
                      aes(x = x, y = .data[[ll]])) +
            geom_point() +
            ylab(ll)
          ## add plot to named list
         plotlist[[ll]] <- p
           ## uncomment to see that each plot is being made
        }
        plotlist
      
      }
      pl <- testfun2(df2)
      pl[[1]]
      
      

      get 不起作用的原因是我们需要使用非标准评估作为文档state。使用get的相关问题可能是useful

      第一个情节

      【讨论】:

      • 我在这个lapply 中添加了dplyr::select 调用,并且在示例和我的实际数据中都运行良好。谢谢。 (但我仍然不完全确定为什么除了我还不了解的 NSE 细节)
      • 错误说明了什么?这只是掩饰。您在其他具有名为select 的函数的包之前加载了dplyr。您可以使用conflicted 或检查回溯以查看函数来自该错误的位置。我个人更喜欢getAnywheremethods
      • .data[[y]] 是 NSE 套件的一部分,其中包括 sym!! 和最新的 {{}}。在大多数情况下,除非您在标准评估 (NSE) 上使用,否则您无法访问函数内部的 dplyr 函数中的数据。 select 用于“顶级”,而.data 和朋友则更“低级”并专注于开发。
      • 在 OP 中:>get(ll) 中的错误:找不到对象“d”。
      • 编辑了答案,我们不能只使用get
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-11-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-05
      相关资源
      最近更新 更多