如何向量化R的plotmath表达式构建,批量生成带插值数据的椭圆标签
我最近在绘制对应不同置信水平的椭圆图,需要给每个椭圆添加包含卡方统计量的plotmath数学标签,但目前只能手动逐个创建lab1、lab2、lab3这样的表达式,想实现向量化批量生成,但直接把bquote结果放进向量的方法完全失效,想请教怎么解决这个问题?
原始实现(手动逐个创建标签)
先贴出我当前能运行的完整代码,核心痛点就是标签部分需要手动写三次:
library(car) # 用于绘制椭圆 library(heplots) # 用于给椭圆添加标签 ybar <- c(0, 0) S <- matrix(c(1, .5, .5, 2), 2, 2) rownames(S) <- colnames(S) <- c("y1", "y2") levels <- c(0.50, 0.68, 0.95) # 椭圆对应的置信水平 c <- qchisq(levels, df = 2) |> round(2) |> print() # 这里是最想优化的部分——手动逐个创建标签表达式 lab1 <- bquote(paste("c =", chi[2]^2, "(", .(levels[1]), ") =", .(c[1]))) lab2 <- bquote(paste("c =", chi[2]^2, "(", .(levels[2]), ") =", .(c[2]))) lab3 <- bquote(paste("c =", chi[2]^2, "(", .(levels[3]), ") =", .(c[3]))) # 绘制椭圆并添加标签 e1 <- ellipse(ybar, S, radius=qchisq(levels[1], 2), col = "blue", fill=TRUE, fill.alpha = 0.5, add=FALSE, xlim=c(-8, 8), ylim=c(-9.5, 9.5), asp=1, grid = FALSE, xlab = expression(y[1]), ylab = expression(y[2]), cex.lab = 1.5) label.ellipse(e1, label = lab1, label.pos = "S", cex = 1.2) e2 <- ellipse(ybar, S, radius=qchisq(levels[2], 2), col="blue", fill=TRUE, fill.alpha=0.3) label.ellipse(e2, label = lab2, label.pos = "N", cex = 1.2) e3 <- ellipse(ybar, S, radius=qchisq(levels[3], 2), col="blue", fill=TRUE, fill.alpha=0.1) label.ellipse(e3, label = lab3, label.pos = "N", cex = 1.2)
我尝试过的方法(但失败了)
我本来想直接把bquote的结果塞进向量里,像这样简化代码:
# 尝试向量化,但运行后椭圆上完全不显示标签 lab <- c( bquote(paste("c =", chi^2, "(", .(levels[1]), ") =", .(c[1]))), bquote(paste("c =", chi^2, "(", .(levels[2]), ") =", .(c[2]))), bquote(paste("c =", chi^2, "(", .(levels[3]), ") =", .(c[3]))) ) # 之后用lab[1], lab[2]调用label.ellipse,但没有任何标签显示
结果椭圆上完全看不到标签,显然这种方法不太行。
可行的解决方案
方案1:用Map/lapply批量生成标签表达式
bquote本身不是向量化函数,我们可以用Map(或lapply)遍历levels和c的每个元素,批量生成符合要求的plotmath表达式对象:
# 批量生成标签表达式 lab <- Map(function(lev, ci) { bquote(paste("c =", chi[2]^2, "(", .(lev), ") =", .(ci))) }, levels, c) # 也可以用lapply的写法(结果完全一致) # lab <- lapply(seq_along(levels), function(i) { # bquote(paste("c =", chi[2]^2, "(", .(levels[i]), ") =", .(c[i]))) # })
甚至可以把椭圆绘制的代码也一起批量处理,不用重复写三次:
# 先初始化绘图画布 plot(NA, xlim=c(-8, 8), ylim=c(-9.5, 9.5), asp=1, grid=FALSE, xlab=expression(y[1]), ylab=expression(y[2]), cex.lab=1.5) # 批量绘制椭圆+添加标签 Map(function(lev, ci, lbl, alpha) { e <- ellipse(ybar, S, radius=qchisq(lev, 2), col="blue", fill=TRUE, fill.alpha=alpha, add=TRUE) # 给第一个椭圆单独设置标签位置 label.pos <- ifelse(lev == levels[1], "S", "N") label.ellipse(e, label=lbl, label.pos=label.pos, cex=1.2) }, levels, c, lab, c(0.5, 0.3, 0.1))
这样扩展更多置信水平的椭圆也非常方便,只要修改levels向量就行。
方案2:用Vectorize包装bquote实现“内部”向量化
如果你想要更贴近“向量化函数”的写法,可以用Vectorize把bquote包装成支持向量化输入的函数:
# 定义向量化的bquote函数 bquote_vec <- Vectorize(function(lev, ci) { bquote(paste("c =", chi[2]^2, "(", .(lev), ") =", .(ci))) }, vectorize.args = c("lev", "ci")) # 直接生成标签列表 lab <- bquote_vec(levels, c)
生成的lab是包含三个plotmath表达式的列表,完全可以直接用于label.ellipse。
方案3:用latex2exp简化plotmath写法
如果觉得bquote的语法有点繁琐,可以用latex2exp包把LaTeX语法转换成R的plotmath表达式,而且它天然支持向量化:
install.packages("latex2exp") library(latex2exp) # 用LaTeX语法批量生成标签,自动向量化 lab <- TeX(sprintf("c = \\chi_2^2(%s) = %s", levels, c))
这种方式可读性更高,用LaTeX写数学公式也更顺手,生成的lab直接可以按索引调用。
为什么直接用c()组合bquote结果不行?
因为bquote返回的是call类型的对象,当你用c()把它们组合时,R会自动把这些call对象强制转换成字符串,而label.ellipse需要的是plotmath表达式(call或expression对象),所以就不会显示标签了。而Map、lapply或Vectorize的结果是列表,每个元素都保持了原来的call类型,因此能被正确识别。
内容来源于stack exchange

