在R中的循环中将ggplot对象存储在列表中

53

我的问题与这个问题类似:在循环中生成绘图对象(在本例中为直方图)时,似乎所有的绘图对象都被最新的绘图覆盖。

为了调试,在循环内部,我打印了索引和生成的绘图,两者都显示正确。但是当我查看存储在列表中的绘图时,它们除了标签之外都相同。

(我使用multiplot来制作一个组合图像,但是如果逐个打印print (myplots[[1]])print(myplots[[4]]),则会得到相同的结果。)

因为我已经附加了一个数据框(不像类似问题的发帖者),所以我不确定如何解决这个问题。

(顺便说一下,在我这里,列的类型是原始数据集中的factor,但是如果它们是整数,则会出现同样的问题。)

这是一个可重现的例子:

library(ggplot2)
source("http://peterhaschke.com/Code/multiplot.R") #load multiplot function

#make sample data
col1 <- c(2, 4, 1, 2, 5, 1, 2, 0, 1, 4, 4, 3, 5, 2, 4, 3, 3, 6, 5, 3, 6, 4, 3, 4, 4, 3, 4, 
          2, 4, 3, 3, 5, 3, 5, 5, 0, 0, 3, 3, 6, 5, 4, 4, 1, 3, 3, 2, 0, 5, 3, 6, 6, 2, 3, 
          3, 1, 5, 3, 4, 6)
col2 <- c(2, 4, 4, 0, 4, 4, 4, 4, 1, 4, 4, 3, 5, 0, 4, 5, 3, 6, 5, 3, 6, 4, 4, 2, 4, 4, 4, 
          1, 1, 2, 2, 3, 3, 5, 0, 3, 4, 2, 4, 5, 5, 4, 4, 2, 3, 5, 2, 6, 5, 2, 4, 6, 3, 3, 
          3, 1, 4, 3, 5, 4)
col3 <- c(2, 5, 4, 1, 4, 2, 3, 0, 1, 3, 4, 2, 5, 1, 4, 3, 4, 6, 3, 4, 6, 4, 1, 3, 5, 4, 3, 
          2, 1, 3, 2, 2, 2, 4, 0, 1, 4, 4, 3, 5, 3, 2, 5, 2, 3, 3, 4, 2, 4, 2, 4, 5, 1, 3, 
          3, 3, 4, 3, 5, 4)
col4 <- c(2, 5, 2, 1, 4, 1, 3, 4, 1, 3, 5, 2, 4, 3, 5, 3, 4, 6, 3, 4, 6, 4, 3, 2, 5, 5, 4,
          2, 3, 2, 2, 3, 3, 4, 0, 1, 4, 3, 3, 5, 4, 4, 4, 3, 3, 5, 4, 3, 5, 3, 6, 6, 4, 2, 
          3, 3, 4, 4, 4, 6)
data2 <- data.frame(col1,col2,col3,col4)
data2[,1:4] <- lapply(data2[,1:4], as.factor)
colnames(data2)<- c("A","B","C", "D")

#generate plots
myplots <- list()  # new empty list
for (i in 1:4) {
  p1 <- ggplot(data=data.frame(data2),aes(x=data2[ ,i]))+ 
    geom_histogram(fill="lightgreen") +
    xlab(colnames(data2)[ i])
  print(i)
  print(p1)
  myplots[[i]] <- p1  # add each plot into plot list
}
multiplot(plotlist = myplots, cols = 4)

当我查看绘图列表中情节对象的摘要时,我看到的就是这个。

> summary(myplots[[1]])
data: A, B, C, D [60x4]
mapping:  x = data2[, i]
faceting: facet_null() 
-----------------------------------
geom_histogram: fill = lightgreen 
stat_bin:  
position_stack: (width = NULL, height = NULL)

我认为mapping: x = data2[, i]是问题所在,但我束手无策!我不能发图片,所以如果我对问题的解释令人困惑,你需要运行我的示例并查看图形。

谢谢!


多图链接已失效。 - baxx
链接对我有效。我添加了一篇带有图表的文章。 - Emy
5个回答

94
除了其他优秀的答案之外,这里有一个使用“正常”评估而不是eval的解决方案。由于for循环没有单独的变量作用域(即它们在当前环境中执行),我们需要使用local来包装for块;此外,我们还需要将i作为一个局部变量 —— 我们可以通过将其重新赋值给自己的名称1来实现:
myplots <- vector('list', ncol(data2))

for (i in seq_along(data2)) {
    message(i)
    myplots[[i]] <- local({
        i <- i
        ggplot(data2, aes(x = data2[[i]])) +
            geom_histogram(fill = "lightgreen") +
            xlab(colnames(data2)[i])
    })
}

然而,一种更加简洁的方法是完全放弃使用for循环,而是使用列表函数来构建结果。这种方法有几种可能的方式。以下是我认为最简单的方式:
plot_data_column = function (data, column) {
    ggplot(data, aes_string(x = column)) +
        geom_histogram(fill = "lightgreen") +
        xlab(column)
}

myplots <- lapply(colnames(data2), plot_data_column, data = data2)

这有几个优点:它更简单,而且不会在环境中造成混乱(使用循环变量i)。
这可能看起来有点混乱:为什么 i <- i 会有任何效果呢?因为通过执行赋值操作,我们创建了一个新的、局部的变量,其名称与外部作用域中的变量相同。我们也可以使用不同的名称,例如 local_i <- i

非常感谢,特别是lapply版本;我想将其功能化,但无法弄清楚,并决定使用(表面上更容易,实际上很糟糕的)for循环。我认为这是一个变量作用域问题,在R中经常与它们斗争! - LizPS
2
@BigTimeStats 这是拥有许多非常大的图形的问题,而不是这两个解决方案中的任何一个。一个常见的解决方案是对您绘制的数据点数量进行子采样(通常,这样的大型图形无法可靠地显示所有单个数据点),或者在绘制之前计算摘要统计信息(并绘制这些而不是原始数据)。但有时候两者都不起作用。在这种情况下,唯一的解决方案是避免同时在内存中拥有多个图形。 - Konrad Rudolph
3
@BigTimeStats 环境面板中的估算值极不可靠。这主要是因为它单独估计每个对象的大小,但在R中有很多对象(尤其是数据框)共享内存:如果您通过修改一个列从另一个数据框创建一个数据框,则它们将共享所有剩余列的内存。 - Konrad Rudolph
为什么你在函数中有 'data' 和 'data2'? - baxx
1
@M-- 其实两者都不应该出现在这里。我保留了OP代码中的print()(OP似乎希望在每次循环迭代中显示当前图形!),但我认为它在这里放错了位置(invisible()也是如此,它在这里没有任何效果)。 - undefined
显示剩余5条评论

21

由于经常引用传递的表达式,因此在循环结束时计算的 i 是该时间下的i ,也就是它的最终值。您可以通过在每次迭代中 eval(substitute(替换正确的值来解决此问题。

myplots <- list()  # new empty list
for (i in 1:4) {
    p1 <- eval(substitute(
        ggplot(data=data.frame(data2),aes(x=data2[ ,i]))+ 
          geom_histogram(fill="lightgreen") +
          xlab(colnames(data2)[ i])
    ,list(i = i)))
    print(i)
    print(p1)
    myplots[[i]] <- p1  # add each plot into plot list
}
multiplot(plotlist = myplots, cols = 4)

2
诊断是正确的,但解决方案有些复杂。在本地上下文中捕获i更容易。问题在于R中的for循环没有作用域,因此您需要使用localfor (i in 1:4) local({i = i; … rest of the loop … })。自我分配i = i并非偶然 - 实际上这是必需的。也可以使用不同的变量名。无论如何,通过使用“适当”的列表函数而不是for,所有这些都将是不必要的,而for在R中实际上是一个糟糕的语言结构。 - Konrad Rudolph
@KonradRudolph local很好。 - Rorschach
1
啊,我忘了一件事:如果使用local,则对myplots[[i]]的赋值需要使用<<-运算符而不是本地赋值。 - Konrad Rudolph
@KonradRudolph 有没有可能你想使用其中一个“apply”函数来添加解决方案。在这种情况下,似乎还需要进行替换或本地化?此外,为什么“local”比“substitute”更好呢? - Rorschach
我更喜欢使用local,因为它看起来像是执行标准评估(尽管当然不是这种情况)。它隐藏了evalsubstitute。实际上,如果在美学中使用列名,则lapplyfor都不需要捕获变量i。我会添加一个答案。 - Konrad Rudolph
如果图形数量超过5-6个,则可能需要重复最后一行multiplot(plotlist = myplots, cols = 4)以显示所有图形。 - BData

4

我已经运行了问题和答案中的代码,将geom_histogram更改为geom_bar以避免错误:Error: StatBin requires a continuous x variable

以下是可视化效果的代码:

问题

#generate plots
myplots <- list()  # new empty list
for (i in 1:4) {
  p1 <- ggplot(data=data.frame(data2),aes(x=data2[ ,i]))+ 
    geom_bar(fill="lightgreen") +
    xlab(colnames(data2)[ i])
  print(i)
  print(p1)
  myplots[[i]] <- p1  # add each plot into plot list
}

multiplot(plotlist = myplots, cols = 4)
#> Loading required package: grid

答案

myplots <- vector('list', ncol(data2))

for (i in seq_along(data2)) {
    message(i)
    myplots[[i]] <- local({
        i <- i
        p1 <- ggplot(data2, aes(x = data2[[i]])) +
            geom_bar(fill = "lightgreen") +
            xlab(colnames(data2)[i])
        print(p1)
    })
}


multiplot(plotlist = myplots, cols = 4)

使用lapply得到相同的结果:


plot_data_column = function (data, column) {
    ggplot(data, aes_string(x = column)) +
        geom_bar(fill = "lightgreen") +
        xlab(column)
}

myplots <- lapply(colnames(data2), plot_data_column, data = data2)

multiplot(plotlist = myplots, cols = 4)
#> Loading required package: grid

本文档由reprex package (v0.3.0)于2021-04-09创建


1

使用 lapply 也可以,因为 x 存在于匿名函数环境中(以 mtcars 作为数据):

plot <- lapply(seq_len(ncol(mtcars)), FUN = function(x) {
  ggplot(data = mtcars) + 
    geom_line(aes(x = mpg, y = mtcars[ , x]), size = 1.4, color = "midnightblue", inherit.aes = FALSE) +
    labs(x="Date", y="Value", title = "Revisions 1M", subtitle = colnames(mtcars)[x]) +
    theme_wsj() +
    scale_colour_wsj("colors6")
})

0
这里是另一种解决方案:
#generate plots
myplots <- list()  # new empty list
for (col in colnames(data2)) {
  p1 <- ggplot(data=data.frame(data2),aes(x=!!ensym(col)))+ 
    geom_bar(fill="lightgreen") +
    xlab(col)
  myplots[[col]] <- p1  # add each plot into plot list
}

multiplot(plotlist = myplots, cols = 4)
#> Loading required package: grid

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