在循环中将ggplot对象分配给列表的R语言指令

3
我正在使用一个for循环将ggplot分配给列表,然后将其传递给plot_grid()(包cowplot)。plot_grid可以将多个ggplots并排在同一张图中。手动操作没问题,但当我使用for循环时,生成的最后一个图形会在每个子框架中重复显示(如下所示)。换句话说,所有的子框架都显示相同的ggplot。
这是一个玩具级别的例子:
require(cowplot)

dfrm <- data.frame(A=1:10, B=10:1)

v <- c("A","B")
dfmsize <- nrow(dfrm)
myplots <- vector("list",2)

count = 1
for(i in v){
    myplots[[count]] <- ggplot(dfrm, aes(x=1:dfmsize, y=dfrm[,i])) + geom_point() + labs(y=i)
    count = count +1
}
plot_grid(plotlist=myplots)

预期结果:

enter image description here

for loop的图表:

enter image description here

我尝试将列表元素转换为grobs,就像这个问题中描述的那样:

mygrobs <- lapply(myplots, ggplotGrob)
plot_grid(plotlist=mygrobs)

但是我得到了相同的结果。

我认为问题在于循环赋值,而不是plot_grid(),但我看不出我哪里做错了。


这个答案详细介绍了ggplot2的惰性求值的一些细节。 - aosmith
4个回答

6
迄今为止的答案非常接近,但在我看来仍然不够令人满意。问题是,在您的for循环之后:
myplots[[1]]$mapping
#* x -> 1:dfmsize
#* y -> dfrm[, i]
myplots[[1]]$plot_env
#<environment: R_GlobalEnv>

myplots[[2]]$mapping
#* x -> 1:dfmsize
#* y -> dfrm[, i]
myplots[[2]]$plot_env
#<environment: R_GlobalEnv>

i
#[1] "B"

如其他答案所提到的,ggplot直到绘图时才会评估这些表达式,由于这些表达式都在全局环境中,而且i的值是"B",所以得到了不理想的结果。
有几种避免这个问题的方法,其中最简单的方法实际上简化了你的表达式:
myplots = lapply(v, function(col)
            ggplot(dfrm, aes(x=1:dfmsize, y=dfrm[,col])) + geom_point() + labs(y=col))

这种方法能够奏效,是因为在lapply循环中每个值的环境都是不同的

myplots[[1]]$mapping
#* x -> 1:dfmsize
#* y -> dfrm[, col]
myplots[[1]]$plot_env
#<environment: 0x000000000bc27b58>

myplots[[2]]$mapping
#* x -> 1:dfmsize
#* y -> dfrm[, col]
myplots[[2]]$plot_env
#<environment: 0x000000000af2ef40>

eval(quote(dfrm[, col]), env = myplots[[1]]$plot_env)
#[1]  1  2  3  4  5  6  7  8  9 10
eval(quote(dfrm[, col]), env = myplots[[2]]$plot_env)
#[1] 10  9  8  7  6  5  4  3  2  1

因此,即使表达式相同,结果也会有所不同。

如果你想知道lapply的环境中存储/复制了什么 - 毫不奇怪,只是列名:

ls(myplots[[1]]$plot_env)
#[1] "col"

我将eddi的答案标记为最佳答案,因为通过映射,然后在循环的每次迭代中显示plot_env,可以非常清楚地了解正在发生的事情。@jrandall:我没有意识到aes执行非标准评估,正如您所提到的那样,这就是为什么最好使用aes_q。感谢其他人提到lapply作为循环的替代品。 - someguyinafloppyhat
@eddi 我正在尝试说服 ggplotfor 循环期间生成图形时尊重变量值,当我最终检查存储在图形列表中的所有图形时。您对此有什么建议吗?链接 - mavericks

4
我认为这里的问题在于 aes 方法的非标准评估延迟了对 i 的评估,直到图形实际绘制时才进行。绘制时,i 是最后一个值(在玩具示例中是 "B"),因此所有图的 y 美学映射都指向该最后一个值。同时,labs 调用使用标准评估,因此标签正确地引用循环中每个 i 的迭代。
可以通过简单地使用映射函数的标准评估版本 aes_q 来解决这个问题:
require(cowplot)

dfrm <- data.frame(A=1:10, B=10:1)

v <- c("A","B")
dfmsize <- nrow(dfrm)
myplots <- vector("list",2)

count = 1
for(i in v){
    myplots[[count]] <- ggplot(dfrm, aes_q(x=1:dfmsize, y=dfrm[,i])) + geom_point() + labs(y=i)
    count = count +1
}
plot_grid(plotlist=myplots)

我喜欢你明确提到NSE。在将图表分配给列表之前,在循环内部实际打印图表也可以进行验证,这实际上会给出正确的输出(与在运行循环后打印它不同)。 - jakub

3

有一篇很好的解释,说明了ggplot2中懒惰求值和for循环发生的情况,请参见此答案

在这种情况下,我通常会切换到使用aes_stringaes_,以便在ggplot2中将变量用作字符串。

在您的情况下,我发现使用lapply循环比for循环更容易,因为可以避免初始化列表和使用计数器。

首先,我将x变量添加到数据集中。

dfrm$index = 1:nrow(dfrm)

现在,使用lapply循环遍历v中的列。
myplots = lapply(v, function(x) {
    ggplot(dfrm, aes_string(x = "index", y = x)) + 
        geom_point() +
        labs(y = x)
})

plot_grid(plotlist = myplots)

2

我认为ggplot在寻找你的xy变量时会感到困惑,因为实际上你是在动态定义它们。如果你稍微改变for循环来构建一个新的子data.frame作为第一行,它就可以正常工作了。

myplots <- list()
count = 1

for(i in v){
    df <- data.frame(x = 1:dfmsize, y = dfrm[,i])
    myplots[[count]] <- ggplot(df, aes(x=x, y=y)) + geom_point() + labs(y=i)
    count = count + 1
}
plot_grid(plotlist=myplots)

enter image description here


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