使用assign()与计数器在for循环中出现绘图异常的问题
问题描述
我编写了自定义简化函数lc用于绘制数据的LC曲线,该函数会为数据中选中的每个month生成对应图表。我使用assign函数动态将每个绘图分配给新对象以便后续使用,通过计数器与paste0函数在for循环中更改对象名称。相关代码如下:
library(dplyr) library(ggplot2) library(ecotox) df <- ecotox::lamprey_tox lc<- function(data, dose='dose', months=c(), month='month', total='total', response='response'){ data <- data%>% filter(month %in% c(months)) counter=0 for (i in months){ counter <- counter + 1 a <- data[data$month %in% i,] print(a) lc <- LC_probit((response/ total) ~ log10(as.numeric(dose)), p = c(50, 90), weights = total, data =a) LC50 <- lc$dose[1] #提取LC50 LC90 <- lc$dose[2] #提取LC90 assign(paste0('p', counter), ggplot(data=a,aes(x=log10(as.numeric(dose)),y=(response/total)))+ geom_point(size=4)+ geom_smooth(method="glm", method.args=list(family=binomial(link="probit")), aes(weight=total),colour="blue",se=TRUE, level=0.95) + geom_segment(aes(x = log10(LC50), xend = log10(LC50), y=-Inf, yend=0.5), linetype='dashed', size=1) + geom_segment(aes(x = -Inf, xend = log10(LC50), y=0.5, yend=0.5), linetype='dashed', size=1) + geom_segment(aes(x = log10(LC90), xend = log10(LC90), y=-Inf, yend=0.9), linetype='dashed', size=1) + geom_segment(aes(x = -Inf, xend = log10(LC90), y=0.9, yend=0.9), linetype='dashed', size=1)) print(p1) } }
调用lc(df, months=c('May', 'June', 'August'))后,发现绘图未正确更新:geom_segment未随绘图内容更新,导致其与geom_smooth无法对齐;但将assign中的计数器改为固定值(如1)时,绘图显示正常。请问这一现象的原因是什么?
原因分析与解决方法
核心原因:ggplot延迟求值 + assign使用不当
ggplot的延迟求值特性:ggplot对象不会在创建时立即计算所有参数,而是在执行
print()时才会解析引用的变量。你在geom_segment的aes()中直接引用了循环变量LC50/LC90,当循环结束后,所有ggplot对象都会指向最后一次循环的LC50/LC90值,导致所有图的线段都对应最后一个月份的数值,和当前图的拟合线错位。assign与固定打印p1的错误搭配:每次循环用assign生成p1、p2、p3,但始终只打印p1,后续循环生成的p2/p3完全没被输出;同时p1中的LC50/LC90会因延迟求值,最终被替换为循环最后一次的数值。当把计数器改成固定值1时,每次循环都覆盖p1,且立即打印,此时变量还没被下一次循环修改,所以看起来正常,但本质没解决延迟求值的隐患。
正确解决思路
- 将静态值移出
aes():LC50/LC90是单个常量,不需要用aes()映射数据列,直接作为参数传给geom_segment,创建ggplot时就会固化当前循环的数值。 - 用列表存储绘图对象:替代
assign动态创建对象的方式,更符合R的编程习惯,避免作用域混乱和全局环境污染。 - 打印对应循环的绘图对象:不要固定打印
p1,而是打印当前循环生成的图。
修改后的示例代码
library(dplyr) library(ggplot2) library(ecotox) df <- ecotox::lamprey_tox lc <- function(data, dose='dose', months=c(), month_col='month', total='total', response='response'){ # 过滤目标月份数据 data_filtered <- data %>% filter(.data[[month_col]] %in% months) # 用列表统一存储绘图对象 plot_list <- list() for (i in seq_along(months)){ current_month <- months[i] a <- data_filtered %>% filter(.data[[month_col]] == current_month) # 计算LC值 lc_result <- LC_probit((.data[[response]] / .data[[total]]) ~ log10(as.numeric(.data[[dose]])), p = c(50, 90), weights = .data[[total]], data = a) LC50 <- lc_result$dose[1] LC90 <- lc_result$dose[2] # 创建绘图,geom_segment使用静态参数而非aes映射 current_plot <- ggplot(data=a, aes(x=log10(as.numeric(.data[[dose]])), y=(.data[[response]]/.data[[total]])))+ geom_point(size=4)+ geom_smooth(method="glm", method.args=list(family=binomial(link="probit")), aes(weight=.data[[total]]), colour="blue", se=TRUE, level=0.95) + # 静态常量直接传参,无需放入aes() geom_segment(x = log10(LC50), xend = log10(LC50), y=-Inf, yend=0.5, linetype='dashed', size=1) + geom_segment(x = -Inf, xend = log10(LC50), y=0.5, yend=0.5, linetype='dashed', size=1) + geom_segment(x = log10(LC90), xend = log10(LC90), y=-Inf, yend=0.9, linetype='dashed', size=1) + geom_segment(x = -Inf, xend = log10(LC90), y=0.9, yend=0.9, linetype='dashed', size=1) + ggtitle(paste("LC Curve for", current_month)) # 添加月份标题 # 存储到列表 plot_list[[current_month]] <- current_plot # 打印当前循环的图 print(current_plot) } # 返回绘图列表,方便后续调用 return(plot_list) } # 调用函数 plots <- lc(df, months=c('May', 'June', 'August'))
关键修改说明
- 静态参数移出
aes():geom_segment的坐标是固定LC值,直接作为参数传入,避免延迟求值导致的变量覆盖。 - 列表存储绘图:替代
assign动态创建对象,更易管理且避免全局环境污染。 .data[[colname]]引用列名:避免非标准求值的潜在问题,提升函数健壮性。- 打印当前绘图:确保每次输出对应月份的图表,而非固定的
p1。
内容的提问来源于stack exchange,提问作者scott.pilgrim.vs.r
相关产品推荐
相关产品推荐

