这次我们来看一个生态学、环境科学和 R 语言社区里高频出现的包:piecewiseSEM。它解决的不是“能不能跑结构方程模型”,而是“当数据不符合传统 SEM 假设时,怎么把结构方程模型拆开估计,同时保留整体检验能力”。在中文教程里,分段结构方程模型的系统性案例一直不算多,所以这篇文章把原理、R 语言实现、批量比较和排错思路一次讲完。
分段结构方程模型适合哪几类人:
- 做生态学、土壤学、微生物组、农学等方向,经常要画路径图解释变量间关系。
- 数据不满足多元正态分布,或者样本量小,传统
lavaan跑不稳。 - 模型里包含随机效应、空间自相关、时间自相关等,传统 SEM 很难处理。
- 想用 R 语言完成从数据清洗、模型拟合、检验到出表出图的全流程。
这篇文章会带读者完成:理解分段结构方程模型的原理和与传统 SEM 的差异;用piecewiseSEM构建一个最简单的三段式模型;读懂summary()输出中的 dSep 检验、Fisher's C 和 AICc;在多模型、多分组场景下批量建模并输出结果表格;排查安装、收敛、缺失值、随机效应等常见坑。
1. 分段结构方程模型核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | R 语言统计建模包 |
| 核心功能 | 分段结构方程模型拟合、有向分离检验、路径系数提取、多模型比较 |
| 适用数据 | 非正态响应、小样本、含随机效应/自相关结构的生态学与社会科学数据 |
| 模型单元 | lm / glm / lmer / glmer / gls / pgls 等局部模型 |
| 整体检验 | dSep 检验、Fisher's C、AICc |
| 是否支持潜变量 | 不支持反射型潜变量,更适合显变量路径模型 |
| 是否需要 GPU | 不需要,普通 CPU 即可 |
| 运行平台 | Windows / macOS / Linux 上的 R 环境 |
| 安装方式 | CRAN 安装;GitHub 开发版可选用 remotes 安装 |
| 是否支持批量任务 | 支持,可通过循环、lapply、分组数据框批量拟合并汇总结果 |
| 典型场景 | 生态学路径分析、环境因子作用机制、微生物群落多级驱动分析 |
piecewiseSEM的设计目标是让非正态、非独立和层次结构数据也能进入结构方程建模框架,而不是替代所有 SEM 场景。它要解决的是“全局协方差矩阵难以估计”时的替代方案。使用之前,先判断自己的数据是否真的需要分段 SEM,再决定是否引入随机效应和分组结构。
2. 分段结构方程模型原理与适用边界
2.1 为什么需要分段结构方程模型
传统结构方程模型(SEM)通常用最大似然估计同时拟合完整协方差矩阵。它要求数据近似多元正态、样本量足够大,而且很难加入随机效应。遇到以下情况时,传统 SEM 很容易出问题:
- 响应变量是计数、比例、二元 0/1 等非正态类型。
- 研究设计包含地块随机效应、样带嵌套、时间重复测量。
- 样本量不大,但变量关系网络很复杂。
- 数据存在空间自相关,需要在模型中显式处理。
分段结构方程模型的思路是:不一次估计整个系统,而是把全局模型拆成一组局部模型,每个局部模型对应一个响应变量,用lm、glm、lmer等常规回归分别拟合。最后用 d-sep 检验把这些局部模型拼回一个整体框架,检验整体模型是否遗漏了重要的直接路径。由于每个局部模型可以单独指定误差分布、连接函数和随机效应结构,所以分段 SEM 对生态学数据的适应性比传统 SEM 更强。
2.2 有向分离检验与 Fisher's C
分段 SEM 的整体检验依赖 d-sep(有向分离)逻辑。当一个路径图中某些变量直接相连、某些变量没有直接相连时,可以通过条件独立性判断缺失路径是否真的“可以缺失”。具体做法是找到一组“基础集合”(basis set),集合里每个元素对应一对缺失路径,检验它们在给定父变量后是否条件独立。
piecewiseSEM会自动完成这些检验:
- 对每条缺失路径做条件独立性检验。
- 把每个检验的 p 值合并为 Fisher's C 统计量。
- Fisher's C 的计算表达式为:C = -2 * sum(ln(p_i)),其中 p_i 是第 i 个独立性检验的 p 值。
- 该统计量近似服从卡方分布,自由度为 2k,k 是基础集合中独立检验的个数。
- 如果整体模型 p 值大于 0.05,说明模型没有显著遗漏关系,当前结构可接受。
需要强调的是,这里的 p 值不是“模型正确”的证明,而是“不能拒绝当前结构”的证据。解释结果时要用“未发现显著缺失路径”这种表述,而不是直接说“模型完全正确”。
2.3 适用边界
分段 SEM 的优势在于灵活,但也有边界:
- 不适合包含反射性潜变量的模型。如果想建模“压力”“环境质量”这类不可直接观测的构念,应该用
lavaan或者更专业的 SEM/CFA 框架。 - 路径图中不能存在循环结构,必须是单向有向无环图。
- 对缺失路径的检验依赖局部模型是否正确。如果局部模型漏掉了关键协变量,dSep 检验也会失真。
- 对于特别复杂的模型,基础集合可能变得很大,需要手动指定或调整。
- 数据中存在强共线性时,分段局部模型仍然会受影响,变量方向的解释要谨慎。
3. 环境准备与 R 语言安装配置
3.1 运行环境
piecewiseSEM是纯 R 包,不依赖特殊编译工具,普通办公电脑就能跑。建议环境:
- R 版本建议 4.0 以上。
- RStudio 可选,但不是必需。
- 安装依赖包时保持联网。
如果只拟合lm和glm,对内存要求很低;如果拟合大型lmer/glmer并做多模型比较,建议保留足够内存。数据量特别大时,注意清理中间对象。
3.2 安装 piecewiseSEM
CRAN 版本安装:
install.packages("piecewiseSEM")装完后检查:
library(piecewiseSEM) packageVersion("piecewiseSEM")如果希望使用开发版功能,可以用 remotes 从 GitHub 安装:
# 需要先安装 remotes install.packages("remotes") remotes::install_github("jslefche/piecewiseSEM")注意:开发版和 CRAN 版在函数参数、输出列名上可能存在细微差异。正式分析建议固定一个版本,并在脚本开头记录sessionInfo()。
3.3 其他依赖包
分段 SEM 经常要配合以下包使用:
install.packages(c("nlme", "lme4", "lmerTest", "MuMIn"))nlme:支持gls、lme等带相关结构的模型。lme4:支持lmer、glmer等随机效应模型。lmerTest:为混合模型提供近似 p 值。MuMIn:辅助 AICc 计算和多模型比较。
依赖包的版本不要求最新,稳定即可。安装失败时优先查看报错信息中提到的依赖包名称,逐个安装。
4. 准备数据与构建第一个分段结构方程模型
4.1 模拟示例数据
为了把流程讲清楚,这里构造一个含三个响应层次的数据集。假设场景是“环境变量 x1 -> 中间变量 x2/y1 -> 最终响应 y2”,实际分析时替换成自己的变量即可。
set.seed(123) n <- 120 x1 <- rnorm(n) x2 <- 0.6 * x1 + rnorm(n) y1 <- 0.4 * x1 + 0.5 * x2 + rnorm(n) y2 <- 0.3 * y1 + 0.2 * x1 + rnorm(n) dat <- data.frame(x1, x2, y1, y2) head(dat)构造数据时尽量让变量之间真的有因果关系结构,这样后面检验能看出分段 SEM 是否识别到了路径。如果使用自己的数据,先用cor()或pairs()看一下变量间相关方向。
4.2 用 lm 拟合局部方程
分段 SEM 的起点是一串局部方程。最简单的写法:
m1 <- lm(y1 ~ x1 + x2, data = dat) m2 <- lm(y2 ~ y1 + x1, data = dat)每个方程就是一个“响应变量 ~ 一组预测变量”的常规回归。局部方程可以混合使用不同模型类型。下面代码只是一个结构示意,假设数据里有count、area、site列时才可执行:
# m_glm <- glm(count ~ x1 + offset(log(area)), family = poisson, data = dat) # m_lmer <- lmer(y2 ~ y1 + (1 | site), data = dat)注意,psem()合并的模型之间要保证响应变量不重复,并且没有循环路径。局部模型类型可以不同,这是分段 SEM 的灵活性所在。
4.3 合并为分段结构方程模型
library(piecewiseSEM) sem_model <- psem(m1, m2) summary(sem_model)psem()返回一个分段 SEM 对象,它把所有局部模型放进同一个框架。summary()会输出:每个响应变量的局部模型;各预测变量的系数、标准误、p 值、标准化系数;dSep 检验结果;Fisher's C 统计量、自由度和整体 p 值;每个方程的 R²。
打开 R 后建议按这个顺序跑一遍,先确认包没装错,再继续后面的批量操作。
5. 功能测试与效果验证
分段 SEM 的验证不能只看路径系数 p 值,还要看整体拟合。下面把验证拆成几个步骤。
5.1 查看整体模型是否通过检验
summary(sem_model)输出中,最关键的是 Fisher's C 对应的整体 p 值:
- p 值大于 0.05:说明模型缺失路径没有显著信号,当前结构可接受。
- p 值小于等于 0.05:说明模型中可能遗漏了某些直接路径,需要检查 dSep 检验列出的独立关系。
实际操作中,我建议先记录 Fisher's C、自由度和 p 值,再进入系数解释。只有整体检验可接受,路径系数才值得逐条解读。
5.2 解读 dSep 检验
dSep 检验会列出模型中没有直接连接、但理论上可能相关的变量对。例如前面模型中,y2 ~ x2这条关系如果被遗漏,dSep 检验会检查“给定它的父变量后,x2 是否仍然与 y2 相关”。
- 如果某个缺失路径的 p 值很小,说明该路径不该省略,应该把它加进模型。
- 如果所有缺失路径 p 值都较大,说明当前结构足够解释数据。
如果 dSep 检验中出现了多条 p 值很小的缺失路径,不要急着一次性加入所有路径,先回到 DAG 图检查因果逻辑。
5.3 提取路径系数表
除了summary(),还可以单独提取系数表:
sem_coef <- sem.coefs(sem_model) print(sem_coef)sem.coefs()输出列通常包括:
Response:响应变量。Predictor:预测变量。Estimate:非标准化系数。Std.Error:标准误。DF:自由度。Crit.Value:临界值。P.Value:p 值。Std.Estimate:标准化系数。
标准化系数在比较变量相对重要性时非常有用,尤其是变量量纲不一致时。写论文时建议同时报告非标准化系数和标准化系数。
5.4 检验单条路径是否应保留
如果想确认某条路径是否该保留,可以比较保留路径与删除路径的两个模型:
model1 <- psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1, dat)) model2 <- psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1 + x2, dat)) AIC(model1, model2)如果模型数量较多,可以用do.call(AIC, list(...))批量比较:
candidate_models <- list( m_full = psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1 + x2, dat)), m_mid = psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1, dat)), m_simple = psem(lm(y1 ~ x1, dat), lm(y2 ~ y1, dat)) ) do.call(AIC, candidate_models)如果删掉路径后 AICc 显著上升,说明保留路径更合理;如果两个模型差异很小,选择更简洁的模型。
5.5 判断成功与失败
判断标准:
- 模型整体 p 值大于 0.05。
- 各关键路径 p 值小于 0.05。
- 标准化系数方向与理论预期一致。
- dSep 中没有明显需要添加的路径。
常见失败:
- 整体 p 值很小,说明漏路径。
- 某个局部模型出现系数 NaN,通常是共线性或样本量不足。
glmer不收敛,说明模型太复杂或数据离散度过高。
6. 批量建模与多模型比较
分段 SEM 在论文中的常见需求不是只跑一个模型,而是比较多个候选结构,或按分组变量批量拟合。这部分是 R 语言实现中最能提高效率的地方。
6.1 多个候选模型比较
把多个模型放在列表中,然后用AIC比较。这里如果模型列表较长,用do.call(AIC, candidate_models)比手动写参数更稳妥。
candidate_models <- list( m_full = psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1 + x2, dat)), m_mid = psem(lm(y1 ~ x1 + x2, dat), lm(y2 ~ y1 + x1, dat)), m_simple = psem(lm(y1 ~ x1, dat), lm(y2 ~ y1, dat)) ) do.call(AIC, candidate_models)AIC()会按模型输出 AIC 和 AICc,并给出模型权重。模型权重总和为 1,权重越大,模型相对支持度越高。注意,不同局部模型使用REML和ML拟合时,AIC 比较可能有差异,建议固定估计方法。
6.2 按分组批量拟合
如果研究设计包含多个地点、多个年份,你可能希望每个分组单独跑一个分段 SEM。先给模拟数据加一个分组列:
dat$group <- rep(c("A", "B", "C"), length.out = nrow(dat))然后使用split和lapply批量拟合:
group_list <- split(dat, dat$group) fits <- lapply(group_list, function(data) { m1 <- lm(y1 ~ x1 + x2, data = data) m2 <- lm(y2 ~ y1 + x1, data = data) psem(m1, m2) }) summaries <- lapply(fits, function(m) { s <- summary(m) data.frame( Fisher_C = s$Fisher.C, df = s$d.o.f, p_value = s$p.value ) }) result_df <- do.call(rbind, summaries) print(result_df)输出是一个分组汇总表,可以直接用write.csv(result_df, "sem_group_results.csv", row.names = FALSE)保存。这样就能快速比较不同组的路径结构是否一致。
6.3 自定义指标提取脚本
更工程化的做法是写成函数,便于复用:
fit_sem_group <- function(data) { m1 <- lm(y1 ~ x1 + x2, data = data) m2 <- lm(y2 ~ y1 + x1, data = data) sem <- psem(m1, m2) coefs <- sem.coefs(sem) s <- summary(sem) list( fit = sem, coefs = coefs, summary_data = data.frame( Fisher_C = s$Fisher.C, df = s$d.o.f, p_value = s$p.value ) ) }这样每个分组返回拟合对象、系数表和整体检验表,后续绘图和写报告都方便。如果后续只需要输出表格,可以只保留coefs和summary_data,避免保存大量模型对象占用内存。
7. 资源占用与性能观察
piecewiseSEM对应的计算成本主要来自局部模型估计,而不是包本身。性能观察可以从几个角度看:
- 拟合速度:局部模型用
lm很快;glmer因为要做迭代优化,数据量或随机项增加后明显变慢。 - 内存占用:每个局部模型对象会保留数据副本。数据量大或模型数量多时,建议及时用
rm()删除中间对象,并用gc()回收内存。 - 批量任务:可以用
parallel包的mclapply(Linux/macOS)或parLapply(Windows)并行拟合分组模型,减少整体耗时。 - 输出对象:
psem对象会包含所有局部模型,可能会很大。保存中间结果时只保存系数表或summary结果,不要反复保存整个 fit 对象。
建议先在小数据上跑通流程,再扩大到全量数据。先检查局部模型的收敛情况,确认没有警告后,再进入批量循环。批量循环中如果某个分组拟合失败,建议用tryCatch捕获错误,避免整个脚本中断:
safe_fit <- function(data) { tryCatch( { m1 <- lm(y1 ~ x1 + x2, data = data) m2 <- lm(y2 ~ y1 + x1, data = data) s <- summary(psem(m1, m2)) data.frame(Fisher_C = s$Fisher.C, df = s$d.o.f, p_value = s$p.value) }, error = function(e) { data.frame(Fisher_C = NA, df = NA, p_value = NA) } ) }这样即使某组数据拟合失败,也不会影响其他分组的运行结果。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
install.packages("piecewiseSEM")报错 | 网络问题或依赖包未装 | 查看错误信息,确认是否为依赖包报错 | 先安装缺失依赖,再重试 |
调用library(piecewiseSEM)提示冲突 | 包中的函数名与其他包冲突 | 运行conflicts()查看 | 使用piecewiseSEM::显式调用 |
psem()报模型类型不支持 | 提供了非 lm/glm/lmer/glmer 等对象 | 检查传给psem()的每个对象类型 | 使用支持的对象或转换模型 |
summary()中整体 p 值很小 | 模型缺失路径较多 | 查看 dSep 检验中的独立关系 | 补充显著缺失路径,重新拟合 |
| 混合模型不收敛 | 随机效应结构太复杂或样本量不足 | 查看收敛警告,测试简化模型 | 减少随机项、去掉极端分组、重新缩放变量 |
| 数据有缺失值 | 局部模型会自动丢弃缺失行 | 用summary(dat)检查 NA | 决定删除、插补或用完整观测分析 |
| 变量名长度或特殊字符导致脚本报错 | 数据列名不规范 | 查看names(dat) | 用janitor::clean_names()清洗列名 |
| 模型系数方向与预期相反 | 共线性、代理变量或反向因果 | 做相关性分析和 VIF 检查 | 调整变量结构或重新定义因果顺序 |
| 想要包含潜变量 | 分段 SEM 不支持反射型潜变量 | 确认研究假设是否必须用潜变量 | 改用lavaan等传统 SEM 框架 |
| AIC 比较时报错 | 模型对象格式不同或列表拼错 | 检查class()和列表结构 | 统一用psem()返回的对象,用do.call(AIC, ...) |
安装失败是最常见的问题,但很多情况下是某个依赖包没装好,而不是piecewiseSEM本身的问题。建议逐行运行安装命令,不要把多个包混杂在一起。
9. 最佳实践与使用建议
- 先画因果图,再写模型。分段 SEM 的灵活性很高,这既是优点也是风险。没有先验结构,数据驱动地添加路径很容易过拟合。
- 局部模型可以不同,但每个响应变量的预测变量应当来自同一张 DAG,不能随意拼凑。
- 对连续变量,建议先标准化或中心化,解释标准化系数会更直观。
- 模型中加入随机效应时,要确保分组变量有意义,不要每个观测都作为随机截距。
- 比较模型时优先使用 AICc 而不是 AIC,尤其在样本量不特别大的情况下。
- 批量拟合时记录每个模型的 Fisher's C、自由度、p 值和路径系数,方便后面写表。
- 展示路径图时,可以用
plot()或DiagrammeR,但路径图上的系数必须与实际模型输出一一对应。 - 如果数据来源涉及单位内部数据、患者数据、商业数据或未公开调研数据,使用前需要确认授权和脱敏要求。不要直接公开原始数据,只发布汇总统计和模型结果。
- 输出结果前做一次敏感性分析:换一种回归方式,比如把
lm换成glm或lmer,判断结论是否稳定。 - 报告论文时写明 R 版本、
piecewiseSEM版本、局部模型类型、Fisher's C、自由度、p 值和各路径系数,方便其他人复现。
如果模型中有多条理论等价路径,建议用多模型比较而不是手动筛选变量。这样可以从整体拟合和模型简洁度两个维度给出判断依据。
10. 总结与下一步
分段结构方程模型最值得尝试的点,是它把复杂的路径分析拆成了自己能控制的局部回归,同时通过 dSep 检验保持整体判断力。对于生态学、农学、土壤学、微生物组这类经常遇到非正态、随机效应和小样本数据的 R 语言用户,piecewiseSEM比传统 SEM 更容易落地。
第一步要验证的,不是复杂模型,而是最简单的两步或三步路径。先跑psem(lm(...), lm(...)),看summary()里的 Fisher's C 和路径系数是否合理,再逐步增加变量和随机效应。最容易踩的坑是:局部模型拟合没问题,但 dSep 检验发现大量缺失路径,说明理论图没画完整。
后续可以继续扩展的方向:把网格搜索式多模型比较整理成自动化脚本;将分组拟合结果与ggplot2路径图结合,做可视化汇报;与混合效应模型嵌套,处理多层级采样设计;结合MuMIn做模型平均,降低单模型不确定性。
如果这篇文章对你有用,建议收藏备用。下次拿到数据,先画图,再拆方程,最后用piecewiseSEM做整体检验,思路就会顺很多。