关于本教程的数据 患者、诊所、医院、学生、学校、心理治疗师和随访记录均由固定随机种子模拟,不含真实个人信息。模拟机制用于验证代码和解释,不构成任何真实治疗、机构或社会政策证据。
多层模型不应从一条复杂的 lmer()
公式开始。推荐按以下顺序学习:
确认分析单位与依赖结构 → 写出目标关系 → 分解方差 → 区分层内与层间效应 → 选择随机结构 → 拟合与诊断 → 预测、敏感性分析和报告
本页用四套可复现模拟数据贯穿讲解:患者嵌套于诊所的连续结局、学生嵌套于学校的随机斜率模型、治疗时点嵌套于个体再嵌套于治疗师的三层模型,以及患者嵌套于医院的二分类结局。所有主要代码只依赖
R 自带功能、lme4 和页面渲染所需的 knitr。
完成本教程后,你应能够:
lme4::lmer()、lme4::glmer()、lme4::VarCorr()
与预测接口;同一诊所的患者可能共享转诊标准、医生、设备和地区环境;同一学校的学生共享课程和资源;同一个人的日记时点共享稳定人格和经历。即使每一行看起来都是一条完整记录,这些共享因素也会使误差相关。
若忽略这种依赖:
design_map <- data.frame(
领域 = c("医学:单次结局", "医学:重复随访", "社会学", "心理学", "医学:二分类"),
`Level 1` = c("患者", "随访时点", "学生", "治疗时点", "患者"),
`Level 2` = c("诊所", "患者", "学校", "个体", "医院"),
`Level 3/交叉结构` = c("—", "医院", "社区可与学校交叉", "治疗师", "—"),
主要问题 = c(
"诊所内患者是否相关?",
"恢复轨迹是否因患者与医院不同?",
"个体 SES 与学校构成效应是否不同?",
"当天压力与长期压力是否不同?",
"医院差异与干预的条件 OR 是多少?"
),
check.names = FALSE
)
knitr::kable(design_map, caption = "三个应用领域中的层级、单位与问题")| 领域 | Level 1 | Level 2 | Level 3/交叉结构 | 主要问题 |
|---|---|---|---|---|
| 医学:单次结局 | 患者 | 诊所 | — | 诊所内患者是否相关? |
| 医学:重复随访 | 随访时点 | 患者 | 医院 | 恢复轨迹是否因患者与医院不同? |
| 社会学 | 学生 | 学校 | 社区可与学校交叉 | 个体 SES 与学校构成效应是否不同? |
| 心理学 | 治疗时点 | 个体 | 治疗师 | 当天压力与长期压力是否不同? |
| 医学:二分类 | 患者 | 医院 | — | 医院差异与干预的条件 OR 是多少? |
有 ID 不等于自动需要随机截距
“层级”是相对于研究问题和依赖机制定义的,不是变量固有属性。必须说明哪些记录共享什么来源,以及结果要推广到个体、群组还是两者;不能只因为数据含有
hospital_id 就机械添加 (1 | hospital_id)。
令个体 (i) 嵌套于群组 (j):
是个体层变量, 是群组层变量。固定效应 描述所有群组共享的平均条件关系;随机截距 描述第 个群组相对总体截距的偏离,其分布方差为 。
“固定”和“随机”不是日常语言 固定效应不表示变量不会变化;随机效应也不表示变量不重要或群组一定由简单随机抽样获得。这里的区别是:模型直接估计共同系数 ,同时用一个分布描述许多群组偏离 。
设 48 家诊所各纳入 22–36 名患者。干预在诊所层分配,结局为 6 个月收缩压;患者治疗前记录年龄和基线收缩压。由于治疗在诊所层变化,治疗效应的高层独立信息来自诊所数。
set.seed(20260821)
J <- 48L
n_j <- sample(22:36, J, replace = TRUE)
clinic <- sprintf("C%02d", seq_len(J))
treat_j <- sample(rep(0:1, each = J / 2))
u0 <- rnorm(J, 0, 4.5)
med <- do.call(rbind, lapply(seq_len(J), function(j) {
n <- n_j[j]
age <- pmin(pmax(rnorm(n, 60, 11), 30), 85)
baseline <- rnorm(n, 138 + 0.08 * (age - 60), 12)
sbp_6m <- 132 - 4.2 * treat_j[j] +
0.55 * (baseline - 138) + 0.12 * (age - 60) +
u0[j] + rnorm(n, 0, 9)
data.frame(
clinic = clinic[j],
treatment = treat_j[j],
age_c10 = (age - 60) / 10,
baseline_c10 = (baseline - 138) / 10,
sbp_6m = sbp_6m
)
}))
med$treatment <- factor(
med$treatment,
0:1,
c("Usual care", "Intervention")
)
med_audit <- data.frame(
患者数 = nrow(med),
诊所数 = length(unique(med$clinic)),
最小诊所人数 = min(table(med$clinic)),
中位诊所人数 = median(table(med$clinic)),
最大诊所人数 = max(table(med$clinic)),
check.names = FALSE
)
knitr::kable(med_audit, caption = "患者—诊所模拟队列审计")| 患者数 | 诊所数 | 最小诊所人数 | 中位诊所人数 | 最大诊所人数 |
|---|---|---|---|---|
| 1425 | 48 | 22 | 29.5 | 36 |
m_med_empty <- lme4::lmer(
sbp_6m ~ 1 + (1 | clinic),
data = med,
REML = TRUE
)
med_empty_icc <- icc_random_intercept(m_med_empty, "clinic")
med_empty_vc <- as.data.frame(lme4::VarCorr(m_med_empty))
med_empty_variance <- data.frame(
成分 = c("诊所随机截距", "患者层残差"),
方差 = c(
med_empty_vc$vcov[
med_empty_vc$grp == "clinic" & is.na(med_empty_vc$var2)
],
stats::sigma(m_med_empty)^2
),
标准差 = sqrt(c(
med_empty_vc$vcov[
med_empty_vc$grp == "clinic" & is.na(med_empty_vc$var2)
],
stats::sigma(m_med_empty)^2
)),
check.names = FALSE
)
knitr::kable(
med_empty_variance,
digits = 3,
caption = "医学空随机截距模型的方差成分"
)| 成分 | 方差 | 标准差 |
|---|---|---|
| 诊所随机截距 | 27.1 | 5.21 |
| 患者层残差 | 128.6 | 11.34 |
空模型 ICC 为 0.174:同一诊所两名随机患者的 6 个月收缩压在模型尺度上具有约 17.4% 的相关性。它不表示诊所因果造成了 17.4% 的血压。
clinic_raw <- aggregate(sbp_6m ~ clinic, data = med, FUN = mean)
clinic_raw$n <- as.numeric(table(med$clinic)[clinic_raw$clinic])
clinic_re <- lme4::ranef(m_med_empty)$clinic
clinic_partial <- data.frame(
clinic = rownames(clinic_re),
partial = unname(lme4::fixef(m_med_empty)[1] + clinic_re[, 1])
)
clinic_compare <- merge(clinic_raw, clinic_partial, by = "clinic")
clinic_compare <- clinic_compare[order(clinic_compare$sbp_6m), ]
plot_y <- seq_len(nrow(clinic_compare))
plot(
clinic_compare$sbp_6m,
plot_y,
pch = 16,
col = palette_ml["orange"],
xlim = range(c(clinic_compare$sbp_6m, clinic_compare$partial)),
yaxt = "n",
xlab = "6 个月平均收缩压",
ylab = "按原始均值排序的诊所",
main = "原始均值与部分汇聚",
las = 1
)
segments(
clinic_compare$sbp_6m,
plot_y,
clinic_compare$partial,
plot_y,
col = palette_ml["gray"]
)
points(
clinic_compare$partial,
plot_y,
pch = 17,
col = palette_ml["teal"]
)
abline(v = lme4::fixef(m_med_empty)[1], lty = 2, col = palette_ml["navy"])
legend(
"bottomright",
legend = c("原始诊所均值", "部分汇聚均值", "总体截距"),
col = c(palette_ml["orange"], palette_ml["teal"], palette_ml["navy"]),
pch = c(16, 17, NA),
lty = c(NA, NA, 2),
bty = "n"
)诊所原始均值与空随机截距模型的部分汇聚均值。线段连接同一家诊所的两个估计;小样本诊所通常向总体均值收缩更多。
条件模态常被称为 BLUP,但不是没有误差的“诊所真值”。将其用于机构排名、处罚或资源配置时,必须同时呈现不确定性、病例结构和收缩程度。
m_med <- lme4::lmer(
sbp_6m ~ treatment + baseline_c10 + age_c10 + (1 | clinic),
data = med,
REML = TRUE
)
med_labels <- c(
`(Intercept)` = "常规管理、基线协变量为中心值",
treatmentIntervention = "干预 vs 常规管理",
baseline_c10 = "基线收缩压每增加 10 mmHg",
age_c10 = "年龄每增加 10 岁"
)
med_fixed <- lmm_fixed_table(m_med, med_labels)
med_adjusted_icc <- icc_random_intercept(m_med, "clinic")
knitr::kable(
med_fixed,
digits = 3,
caption = "患者—诊所调整后线性混合模型固定效应"
)| 变量 | 估计值 | 标准误 | 95% CI 下限 | 95% CI 上限 | t 值 | |
|---|---|---|---|---|---|---|
| (Intercept) | 常规管理、基线协变量为中心值 | 132.219 | 1.031 | 130.199 | 134.24 | 128.27 |
| treatmentIntervention | 干预 vs 常规管理 | -4.113 | 1.457 | -6.969 | -1.26 | -2.82 |
| baseline_c10 | 基线收缩压每增加 10 mmHg | 5.568 | 0.217 | 5.143 | 5.99 | 25.71 |
| age_c10 | 年龄每增加 10 岁 | 0.573 | 0.228 | 0.127 | 1.02 | 2.52 |
调整后干预组平均 6 个月收缩压比常规管理低 4.11 mmHg(Wald 95% CI:-6.97 至 -1.26)。这是给定基线收缩压、年龄和诊所随机截距后的条件均值差。由于干预在诊所层分配,因果解释还需要诊所层可交换性、足够重叠和清晰的分配机制。
调整后 ICC 为 0.207,高于空模型的 0.174 并不矛盾:移除部分患者层可解释方差后,剩余总方差的构成可以改变。ICC 不是必须随协变量增加而下降的“模型质量分数”。
med_residuals <- residuals(m_med, type = "pearson")
med_fitted <- fitted(m_med)
old_par <- par(mfrow = c(1, 2), mar = c(4.2, 4.2, 3, 1))
plot(
med_fitted,
med_residuals,
pch = 16,
cex = 0.55,
col = grDevices::adjustcolor(palette_ml["teal"], alpha.f = 0.45),
xlab = "条件拟合值",
ylab = "Pearson 残差",
main = "残差与拟合值",
las = 1
)
abline(h = 0, lty = 2, col = palette_ml["vermillion"])
stats::qqnorm(
as.numeric(scale(med_residuals)),
pch = 16,
cex = 0.55,
col = palette_ml["blue"],
main = "残差 Q–Q 图"
)
stats::qqline(as.numeric(scale(med_residuals)), col = palette_ml["vermillion"])患者—诊所线性混合模型的条件残差诊断。左图比较拟合值与残差,右图为标准化残差正态 Q–Q 图;诊断用于寻找非线性、异方差和尾部偏离。
残差图不是唯一诊断。还应检查诊所层影响点、诊所大小、random-effect 分布、基线收缩压的函数形式,以及是否存在诊所特异残差方差。
set.seed(20260822)
J <- 70L
n_j <- sample(24:36, J, replace = TRUE)
school <- sprintf("S%02d", seq_len(J))
school_ses <- rnorm(J, 0, 0.8)
school_ses_gc <- school_ses - mean(school_ses)
support_gc <- rnorm(J, 0, 1)
support_gc <- support_gc - mean(support_gc)
rho <- 0.25
z0 <- rnorm(J)
z1 <- rnorm(J)
b0 <- 5.0 * z0
b1 <- 1.5 * (rho * z0 + sqrt(1 - rho^2) * z1)
soc <- do.call(rbind, lapply(seq_len(J), function(j) {
n <- n_j[j]
ses_wc <- rnorm(n, 0, 0.9)
ses_wc <- ses_wc - mean(ses_wc)
ses <- school_ses[j] + ses_wc
achievement <- 70 +
2.8 * ses_wc + 5.0 * school_ses_gc[j] +
2.0 * support_gc[j] - 1.2 * ses_wc * support_gc[j] +
b0[j] + b1[j] * ses_wc + rnorm(n, 0, 7)
data.frame(
school = school[j],
ses = ses,
school_support_gc = support_gc[j],
achievement = achievement
)
}))
soc$school_ses <- ave(soc$ses, soc$school, FUN = mean)
soc$ses_wc <- soc$ses - soc$school_ses
soc$school_ses_gc <- soc$school_ses - mean(soc$school_ses)
soc_audit <- data.frame(
学生数 = nrow(soc),
学校数 = length(unique(soc$school)),
最小学校人数 = min(table(soc$school)),
最大学校人数 = max(table(soc$school)),
`ses_wc 校内均值最大绝对值` = max(abs(tapply(soc$ses_wc, soc$school, mean))),
check.names = FALSE
)
knitr::kable(soc_audit, digits = 5, caption = "学生—学校模拟数据与中心化审计")| 学生数 | 学校数 | 最小学校人数 | 最大学校人数 | ses_wc 校内均值最大绝对值 |
|---|---|---|---|---|
| 2098 | 70 | 24 | 36 | 0 |
ses_wc
在每所学校内均值为零,回答个体相对本校同伴的位置;这里的
school_ses_gc 是学校平均 SES
相对按学生人数加权的总均值的位置。若研究问题要求每所学校等权,应先提取每校唯一均值再中心化,并在分析计划中说明。学校支持则在学校层按学校等权进行总体均值中心化。
school_means <- aggregate(
cbind(achievement, school_ses) ~ school,
data = soc,
FUN = mean
)
old_par <- par(mfrow = c(1, 2), mar = c(4.2, 4.2, 3, 1))
plot(
soc$ses_wc,
soc$achievement,
pch = 16,
cex = 0.45,
col = grDevices::adjustcolor(palette_ml["teal"], alpha.f = 0.30),
xlab = "学生 SES − 本校平均 SES",
ylab = "成绩",
main = "校内关系",
las = 1
)
abline(stats::lm(achievement ~ ses_wc, data = soc), col = palette_ml["navy"], lwd = 2)
plot(
school_means$school_ses,
school_means$achievement,
pch = 17,
col = palette_ml["orange"],
xlab = "学校平均 SES",
ylab = "学校平均成绩",
main = "校间关系",
las = 1
)
abline(
stats::lm(achievement ~ school_ses, data = school_means),
col = palette_ml["vermillion"],
lwd = 2
)学生 SES 的校内与校间关系。左图展示学生相对本校平均 SES 与成绩,右图展示学校平均 SES 与学校平均成绩;两者是不同层级的问题。
随机截距模型假定 SES 的校内斜率在每所学校相同。允许斜率变化时:
是平均校内斜率, 描述学校间斜率异质性, 描述截距与斜率如何共同变化。加入学校支持 的跨层交互后,平均 SES 斜率为 。
m_soc_empty <- lme4::lmer(
achievement ~ 1 + (1 | school),
data = soc,
REML = TRUE
)
m_soc <- lme4::lmer(
achievement ~ ses_wc + school_ses_gc + school_support_gc +
ses_wc:school_support_gc + (1 + ses_wc | school),
data = soc,
REML = TRUE,
control = lme4::lmerControl(optimizer = "bobyqa")
)
soc_labels <- c(
`(Intercept)` = "平均 SES、平均支持学校的截距",
ses_wc = "学生 SES 的校内效应",
school_ses_gc = "学校平均 SES 的校间效应",
school_support_gc = "学校支持的主效应",
`ses_wc:school_support_gc` = "校内 SES × 学校支持"
)
soc_fixed <- lmm_fixed_table(m_soc, soc_labels)
soc_empty_icc <- icc_random_intercept(m_soc_empty, "school")
soc_vc_raw <- as.data.frame(lme4::VarCorr(m_soc))
soc_vc_table <- data.frame(
群组 = c("学校", "学校", "学校", "残差"),
成分 = c("随机截距", "SES 随机斜率", "截距–斜率相关", "学生层残差"),
数值 = c(
soc_vc_raw$sdcor[
soc_vc_raw$grp == "school" & soc_vc_raw$var1 == "(Intercept)" &
is.na(soc_vc_raw$var2)
],
soc_vc_raw$sdcor[
soc_vc_raw$grp == "school" & soc_vc_raw$var1 == "ses_wc" &
is.na(soc_vc_raw$var2)
],
soc_vc_raw$sdcor[
soc_vc_raw$grp == "school" & !is.na(soc_vc_raw$var2)
],
stats::sigma(m_soc)
),
check.names = FALSE
)
knitr::kable(
soc_fixed,
digits = 3,
caption = "学生—学校随机斜率模型的固定效应"
)| 变量 | 估计值 | 标准误 | 95% CI 下限 | 95% CI 上限 | t 值 | |
|---|---|---|---|---|---|---|
| (Intercept) | 平均 SES、平均支持学校的截距 | 70.88 | 0.644 | 69.62 | 72.143 | 110.06 |
| ses_wc | 学生 SES 的校内效应 | 2.77 | 0.260 | 2.26 | 3.274 | 10.64 |
| school_ses_gc | 学校平均 SES 的校间效应 | 4.32 | 0.719 | 2.91 | 5.732 | 6.01 |
| school_support_gc | 学校支持的主效应 | 2.78 | 0.712 | 1.38 | 4.173 | 3.90 |
| ses_wc:school_support_gc | 校内 SES × 学校支持 | -1.01 | 0.285 | -1.57 | -0.454 | -3.55 |
| 群组 | 成分 | 数值 |
|---|---|---|
| 学校 | 随机截距 | 5.233 |
| 学校 | SES 随机斜率 | 1.598 |
| 学校 | 截距–斜率相关 | 0.287 |
| 残差 | 学生层残差 | 6.994 |
校内 SES 关系估计为每增加 1 单位,成绩平均增加 2.77 分;学校平均 SES 的校间关系为 4.32 分。两者不同,不能用一个原始 SES 系数代替。跨层交互为 -1.01:学校支持越高,SES 的正向校内斜率在本模拟中越弱。它是效应修饰描述,不证明提高支持必然改变 SES 机制。
空模型学校 ICC 为 0.437。随机斜率标准差为 1.598,说明校内 SES
关系存在学校间异质性;截距–斜率相关为 0.287,其解释依赖
ses_wc=0 的零点。
soc_re <- lme4::ranef(m_soc)$school
selected_schools <- rownames(soc_re)[round(seq(1, nrow(soc_re), length.out = 12))]
school_profile <- unique(
soc[c("school", "school_ses_gc", "school_support_gc")]
)
ses_grid <- seq(-2, 2, length.out = 80)
soc_beta <- lme4::fixef(m_soc)
school_predictions <- stats::setNames(
lapply(selected_schools, function(s) {
profile_s <- school_profile[school_profile$school == s, ]
support_s <- profile_s$school_support_gc
school_ses_s <- profile_s$school_ses_gc
soc_beta["(Intercept)"] +
soc_beta["school_ses_gc"] * school_ses_s +
soc_beta["school_support_gc"] * support_s +
soc_re[s, "(Intercept)"] +
(soc_beta["ses_wc"] +
soc_beta["ses_wc:school_support_gc"] * support_s +
soc_re[s, "ses_wc"]) * ses_grid
}),
selected_schools
)
average_school_prediction <-
soc_beta["(Intercept)"] + soc_beta["ses_wc"] * ses_grid
left_ylim <- grDevices::extendrange(
c(unlist(school_predictions), average_school_prediction),
f = 0.05
)
old_par <- par(mfrow = c(1, 2), mar = c(4.2, 4.2, 3, 1))
plot(
NA,
xlim = range(ses_grid),
ylim = left_ylim,
xlab = "学生 SES − 本校平均 SES",
ylab = "条件预测成绩",
main = "学校特异随机斜率",
las = 1
)
for (s in selected_schools) {
lines(
ses_grid,
school_predictions[[s]],
col = grDevices::adjustcolor(palette_ml["blue"], alpha.f = 0.45),
lwd = 1.2
)
}
lines(
ses_grid,
average_school_prediction,
col = palette_ml["vermillion"],
lwd = 2.6
)
support_sd <- stats::sd(
soc$school_support_gc[!duplicated(soc$school)]
)
support_values <- c(-support_sd, 0, support_sd)
support_colors <- c(
palette_ml["orange"], palette_ml["navy"], palette_ml["teal"]
)
support_predictions <- vapply(
support_values,
function(z) {
soc_beta["(Intercept)"] + soc_beta["school_support_gc"] * z +
(soc_beta["ses_wc"] +
soc_beta["ses_wc:school_support_gc"] * z) * ses_grid
},
numeric(length(ses_grid))
)
right_ylim <- grDevices::extendrange(support_predictions, f = 0.05)
plot(
NA,
xlim = range(ses_grid),
ylim = right_ylim,
xlab = "学生 SES − 本校平均 SES",
ylab = "固定部分预测成绩",
main = "学校支持的跨层修饰",
las = 1
)
for (k in seq_along(support_values)) {
lines(
ses_grid,
support_predictions[, k],
col = support_colors[k],
lwd = 2.4
)
}
legend(
"topleft",
legend = c("支持 = −1 SD", "支持 = 平均", "支持 = +1 SD"),
col = support_colors,
lwd = 2.4,
bty = "n"
)学生—学校随机斜率与跨层交互。左图展示部分学校的条件 SES 斜率,右图展示学校支持为低、平均和高水平时的固定部分预测关系。
当两名同校学生的中心化 SES 分别为 时,随机效应诱导的协方差为:
因此不再有一个对所有 SES 都相同的 ICC。
G_soc <- as.matrix(lme4::VarCorr(m_soc)$school)
sigma2_soc <- stats::sigma(m_soc)^2
conditional_corr <- function(x1, x2, G, sigma2) {
z1 <- c(1, x1)
z2 <- c(1, x2)
covariance <- drop(t(z1) %*% G %*% z2)
variance1 <- drop(t(z1) %*% G %*% z1) + sigma2
variance2 <- drop(t(z2) %*% G %*% z2) + sigma2
covariance / sqrt(variance1 * variance2)
}
soc_corr_table <- data.frame(
学生一SES = c(0, -1),
学生二SES = c(0, 1),
同校模型相关 = c(
conditional_corr(0, 0, G_soc, sigma2_soc),
conditional_corr(-1, 1, G_soc, sigma2_soc)
),
check.names = FALSE
)
knitr::kable(
soc_corr_table,
digits = 3,
caption = "随机斜率模型在不同 SES 组合下的同校相关"
)| 学生一SES | 学生二SES | 同校模型相关 |
|---|---|---|
| 0 | 0 | 0.359 |
| -1 | 1 | 0.315 |
160 名参与者分别由 20 名治疗师服务,每人记录 7 个治疗 session。时间从
0 开始,使截距表示基线症状。压力被分成个人内偏离 stress_wp
与个人平均压力 stress_pm_gc。
set.seed(20260823)
K <- 20L
therapists <- sprintf("T%02d", seq_len(K))
therapist_u <- rnorm(K, 0, 1.2)
persons_per_therapist <- 8L
session0 <- 0:6
psy_list <- list()
person_index <- 0L
for (k in seq_len(K)) {
intervention <- sample(rep(0:1, each = persons_per_therapist / 2))
for (p in seq_len(persons_per_therapist)) {
person_index <- person_index + 1L
z0p <- rnorm(1)
z1p <- rnorm(1)
p_u0 <- 3.0 * z0p
p_u1 <- 0.45 * (-0.20 * z0p + sqrt(1 - 0.20^2) * z1p)
stress_pm <- rnorm(1, 0, 0.9)
stress_wp <- rnorm(length(session0), 0, 0.8)
stress_wp <- stress_wp - mean(stress_wp)
symptoms <- 22 - 0.90 * session0 -
0.20 * intervention[p] - 0.35 * session0 * intervention[p] +
1.20 * stress_wp + 2.00 * stress_pm +
therapist_u[k] + p_u0 + p_u1 * session0 +
rnorm(length(session0), 0, 2.5)
psy_list[[person_index]] <- data.frame(
therapist = therapists[k],
person = sprintf("P%03d", person_index),
session0 = session0,
intervention = intervention[p],
stress = stress_pm + stress_wp,
symptoms = symptoms
)
}
}
psy <- do.call(rbind, psy_list)
psy$stress_pm <- ave(psy$stress, psy$person, FUN = mean)
psy$stress_wp <- psy$stress - psy$stress_pm
psy$stress_pm_gc <- psy$stress_pm - mean(psy$stress_pm)
psy$intervention <- factor(
psy$intervention,
0:1,
c("Control", "Intervention")
)
psy_audit <- data.frame(
观测数 = nrow(psy),
个体数 = length(unique(psy$person)),
治疗师数 = length(unique(psy$therapist)),
每人时点数 = paste(range(table(psy$person)), collapse = "–"),
每名治疗师人数 = paste(range(table(unique(psy[c("person", "therapist")])$therapist)), collapse = "–"),
check.names = FALSE
)
knitr::kable(psy_audit, caption = "心理学三层纵向数据审计")| 观测数 | 个体数 | 治疗师数 | 每人时点数 | 每名治疗师人数 |
|---|---|---|---|---|
| 1120 | 160 | 20 | 7–7 | 8–8 |
m_psy <- lme4::lmer(
symptoms ~ session0 * intervention + stress_wp + stress_pm_gc +
(1 + session0 | person) + (1 | therapist),
data = psy,
REML = TRUE,
control = lme4::lmerControl(optimizer = "bobyqa")
)
psy_labels <- c(
`(Intercept)` = "对照组基线症状",
session0 = "对照组每 session 平均变化",
interventionIntervention = "干预组基线差",
stress_wp = "压力的个人内效应",
stress_pm_gc = "个人平均压力的个人间效应",
`session0:interventionIntervention` = "干预组额外 session 变化"
)
psy_fixed <- lmm_fixed_table(m_psy, psy_labels)
psy_vc_raw <- as.data.frame(lme4::VarCorr(m_psy))
psy_vc_table <- data.frame(
成分 = c("个体随机截距 SD", "个体时间随机斜率 SD", "截距–斜率相关", "治疗师截距 SD", "残差 SD"),
数值 = c(
psy_vc_raw$sdcor[
psy_vc_raw$grp == "person" & psy_vc_raw$var1 == "(Intercept)" &
is.na(psy_vc_raw$var2)
],
psy_vc_raw$sdcor[
psy_vc_raw$grp == "person" & psy_vc_raw$var1 == "session0" &
is.na(psy_vc_raw$var2)
],
psy_vc_raw$sdcor[
psy_vc_raw$grp == "person" & !is.na(psy_vc_raw$var2)
],
psy_vc_raw$sdcor[
psy_vc_raw$grp == "therapist" & is.na(psy_vc_raw$var2)
],
stats::sigma(m_psy)
),
check.names = FALSE
)
knitr::kable(
psy_fixed,
digits = 3,
caption = "时点—个体—治疗师三层模型固定效应"
)| 变量 | 估计值 | 标准误 | 95% CI 下限 | 95% CI 上限 | t 值 | |
|---|---|---|---|---|---|---|
| (Intercept) | 对照组基线症状 | 22.335 | 0.524 | 21.307 | 23.363 | 42.59 |
| session0 | 对照组每 session 平均变化 | -0.857 | 0.062 | -0.979 | -0.735 | -13.74 |
| interventionIntervention | 干预组基线差 | -0.621 | 0.531 | -1.661 | 0.419 | -1.17 |
| stress_wp | 压力的个人内效应 | 1.326 | 0.104 | 1.123 | 1.529 | 12.78 |
| stress_pm_gc | 个人平均压力的个人间效应 | 1.654 | 0.289 | 1.088 | 2.220 | 5.73 |
| session0:interventionIntervention | 干预组额外 session 变化 | -0.403 | 0.088 | -0.576 | -0.230 | -4.57 |
| 成分 | 数值 |
|---|---|
| 个体随机截距 SD | 2.869 |
| 个体时间随机斜率 SD | 0.295 |
| 截距–斜率相关 | 0.043 |
| 治疗师截距 SD | 1.642 |
| 残差 SD | 2.508 |
对照组每个 session 症状平均改变 -0.86 分;干预组在此基础上每个 session 额外改变 -0.4 分。当天压力比个人通常水平高 1 单位时,症状平均高 1.33 分;长期平均压力高 1 单位的人之间相差 1.65 分。这两个系数回答不同问题。
只有 20 名治疗师,治疗师方差和任何治疗师排名都具有较大不确定性。随机治疗师截距处理依赖结构,但不会证明治疗师差异由治疗质量造成。
selected_people <- unique(psy$person)[round(seq(1, length(unique(psy$person)), length.out = 12))]
psy_selected <- psy[psy$person %in% selected_people, ]
psy_beta <- lme4::fixef(m_psy)
plot(
NA,
xlim = range(psy$session0),
ylim = range(psy_selected$symptoms),
xlab = "Session(0 = 基线)",
ylab = "症状评分",
main = "个体轨迹与固定部分平均轨迹",
las = 1
)
for (id in selected_people) {
d <- psy_selected[psy_selected$person == id, ]
color <- if (d$intervention[1] == "Intervention") {
grDevices::adjustcolor(palette_ml["teal"], alpha.f = 0.45)
} else {
grDevices::adjustcolor(palette_ml["orange"], alpha.f = 0.45)
}
lines(d$session0, d$symptoms, col = color, lwd = 1.2)
points(d$session0, d$symptoms, col = color, pch = 16, cex = 0.45)
}
session_grid <- 0:6
fixed_control <- psy_beta["(Intercept)"] + psy_beta["session0"] * session_grid
fixed_intervention <- psy_beta["(Intercept)"] +
psy_beta["interventionIntervention"] +
(psy_beta["session0"] +
psy_beta["session0:interventionIntervention"]) * session_grid
lines(session_grid, fixed_control, col = palette_ml["orange"], lwd = 3)
lines(session_grid, fixed_intervention, col = palette_ml["teal"], lwd = 3)
legend(
"topright",
legend = c("对照固定轨迹", "干预固定轨迹"),
col = c(palette_ml["orange"], palette_ml["teal"]),
lwd = 3,
bty = "n"
)部分参与者的症状轨迹及模型固定部分的平均轨迹。细线为观察到的个体轨迹,粗线在压力取平均值时比较对照与干预的固定部分变化。
随机时间斜率允许个体变化速度不同,但不自动消除相邻 session 残差的 AR(1) 相关。若残差仍有明显时间序列结构,应使用支持相应相关结构的方法并做敏感性分析。
二分类感染结局满足:
这里 是给定医院随机效应与模型协变量后的 cluster-specific 条件 OR,通常不等于对医院分布平均后的 marginal OR。
set.seed(20260829)
J <- 56L
n_j <- sample(36:54, J, replace = TRUE)
hospital <- sprintf("H%02d", seq_len(J))
checklist_j <- sample(rep(0:1, each = J / 2))
h_u <- rnorm(J, 0, 0.65)
bin <- do.call(rbind, lapply(seq_len(J), function(j) {
n <- n_j[j]
age <- pmin(pmax(rnorm(n, 62, 12), 18), 90)
emergency <- rbinom(n, 1, 0.28)
eta <- -2.25 - 0.55 * checklist_j[j] +
0.70 * emergency + 0.22 * ((age - 60) / 10) + h_u[j]
data.frame(
hospital = hospital[j],
checklist = checklist_j[j],
emergency = emergency,
age_c10 = (age - 60) / 10,
infection = rbinom(n, 1, plogis(eta))
)
}))
bin$checklist <- factor(bin$checklist, 0:1, c("Usual care", "Checklist"))
bin$emergency <- factor(bin$emergency, 0:1, c("Elective", "Emergency"))
bin_audit <- data.frame(
患者数 = nrow(bin),
医院数 = length(unique(bin$hospital)),
感染事件 = sum(bin$infection),
感染比例 = pct(mean(bin$infection)),
医院人数范围 = paste(range(table(bin$hospital)), collapse = "–"),
check.names = FALSE
)
knitr::kable(bin_audit, caption = "医院感染二分类模拟数据审计")| 患者数 | 医院数 | 感染事件 | 感染比例 | 医院人数范围 |
|---|---|---|---|---|
| 2501 | 56 | 298 | 11.9% | 36–53 |
hospital_rates <- aggregate(infection ~ hospital, data = bin, FUN = mean)
hospital_rates$n <- as.numeric(table(bin$hospital)[hospital_rates$hospital])
hospital_rates <- hospital_rates[order(hospital_rates$infection), ]
plot(
hospital_rates$infection,
seq_len(nrow(hospital_rates)),
pch = 16,
cex = 0.7 + 1.2 * sqrt(hospital_rates$n / max(hospital_rates$n)),
col = palette_ml["blue"],
yaxt = "n",
xlab = "观察感染比例",
ylab = "按感染比例排序的医院",
main = "医院原始感染比例",
las = 1
)
abline(v = mean(bin$infection), lty = 2, col = palette_ml["vermillion"])各医院观察到的感染比例及总体比例。医院样本量和病例结构不同,原始比例不应直接解释为医院质量排名。
m_bin <- lme4::glmer(
infection ~ checklist + emergency + age_c10 + (1 | hospital),
data = bin,
family = stats::binomial,
nAGQ = 1,
control = lme4::glmerControl(
optimizer = "bobyqa",
optCtrl = list(maxfun = 2e5)
)
)
bin_vc <- as.data.frame(lme4::VarCorr(m_bin))
tau2_bin <- bin_vc$vcov[
bin_vc$grp == "hospital" &
bin_vc$var1 == "(Intercept)" &
is.na(bin_vc$var2)
]
stopifnot(length(tau2_bin) == 1L)
icc_latent <- tau2_bin / (tau2_bin + pi^2 / 3)
mor <- exp(stats::qnorm(0.75) * sqrt(2 * tau2_bin))
pearson <- residuals(m_bin, type = "pearson")
pearson_ratio <- sum(pearson^2) /
(stats::nobs(m_bin) - length(lme4::fixef(m_bin)))
bin_coef <- coef(summary(m_bin))
bin_or <- wald_or(m_bin)
bin_labels <- c(
`(Intercept)` = "常规护理、择期、60 岁",
checklistChecklist = "感染检查清单 vs 常规护理",
emergencyEmergency = "急诊 vs 择期",
age_c10 = "年龄每增加 10 岁"
)
bin_results <- data.frame(
变量 = unname(bin_labels[rownames(bin_coef)]),
条件OR = bin_or[, "OR"],
`95% CI 下限` = bin_or[, "lower"],
`95% CI 上限` = bin_or[, "upper"],
`p 值` = vapply(bin_coef[, "Pr(>|z|)"], format_p, character(1)),
check.names = FALSE
)
bin_heterogeneity <- data.frame(
指标 = c("医院随机截距方差", "latent-scale ICC", "median odds ratio", "Pearson 比率"),
数值 = c(tau2_bin, icc_latent, mor, pearson_ratio),
check.names = FALSE
)
knitr::kable(
bin_results,
digits = 3,
caption = "医院感染 logistic GLMM 的条件 OR 与 Wald 区间"
)| 变量 | 条件OR | 95% CI 下限 | 95% CI 上限 | p 值 | |
|---|---|---|---|---|---|
| (Intercept) | 常规护理、择期、60 岁 | 0.126 | 0.094 | 0.169 | <0.001 |
| checklistChecklist | 感染检查清单 vs 常规护理 | 0.571 | 0.380 | 0.856 | 0.00676 |
| emergencyEmergency | 急诊 vs 择期 | 1.562 | 1.197 | 2.037 | 0.001 |
| age_c10 | 年龄每增加 10 岁 | 1.254 | 1.130 | 1.393 | <0.001 |
| 指标 | 数值 |
|---|---|
| 医院随机截距方差 | 0.347 |
| latent-scale ICC | 0.095 |
| median odds ratio | 1.754 |
| Pearson 比率 | 0.905 |
模型显式使用 nAGQ = 1,即 lme4::glmer() 的
Laplace 近似。正式分析应报告积分近似与
optimizer;在事件稀少、群组很小或随机效应较大时,可增加自适应
Gauss–Hermite quadrature 节点并检查结论敏感性。
检查清单的条件 OR 为 0.571(95% CI:0.38 至 0.856)。在相同年龄、急诊状态和相同潜在医院倾向下,检查清单组的感染 odds 较低。
简单 logistic 随机截距模型的 latent ICC 为:
本例为 0.095,它属于潜在 logistic 尺度,不是观察感染 0/1 方差中可直接解释的百分比。Median odds ratio 为 1.754:随机选择两个风险不同的医院,把相同患者从低风险医院移到高风险医院时,医院效应的中位 odds 倍数约为此值。
Pearson 比率为 0.905,没有显示明显过度离散,但单一比率不能排除函数形式错误、零膨胀或遗漏层级。
beta <- lme4::fixef(m_bin)
eta_usual <- unname(beta["(Intercept)"])
eta_check <- eta_usual + unname(beta["checklistChecklist"])
p_typical <- stats::plogis(c(usual = eta_usual, checklist = eta_check))
integrand <- function(u, eta, sd) {
stats::plogis(eta + u) * stats::dnorm(u, 0, sd)
}
p_marginal <- c(
usual = stats::integrate(
integrand,
-Inf,
Inf,
eta = eta_usual,
sd = sqrt(tau2_bin)
)$value,
checklist = stats::integrate(
integrand,
-Inf,
Inf,
eta = eta_check,
sd = sqrt(tau2_bin)
)$value
)
probability_table <- data.frame(
估计尺度 = c(
"60 岁择期参考患者、典型医院:u=0",
"60 岁择期参考患者、对医院随机效应积分"
),
常规护理风险 = c(p_typical["usual"], p_marginal["usual"]),
检查清单风险 = c(p_typical["checklist"], p_marginal["checklist"]),
风险差 = c(diff(p_typical), diff(p_marginal)),
check.names = FALSE
)
knitr::kable(
probability_table,
digits = 3,
caption = "参考患者在典型医院与对医院随机效应积分后的感染概率"
)| 估计尺度 | 常规护理风险 | 检查清单风险 | 风险差 |
|---|---|---|---|
| 60 岁择期参考患者、典型医院:u=0 | 0.112 | 0.067 | -0.045 |
| 60 岁择期参考患者、对医院随机效应积分 | 0.125 | 0.077 | -0.048 |
predict(m_bin, re.form = NA, type = "response")
把医院随机效应设为零,得到典型医院的 fixed-part
概率;它不会自动对医院分布积分。上表第二行仅对医院随机截距积分,仍以 60
岁、择期入院者为参考。若目标是研究人群的总体边际风险,还需按目标人群的年龄、急诊状态等协变量分布进一步标准化。
prob_matrix <- rbind(
`参考患者:典型医院 u=0` = p_typical,
`参考患者:医院效应积分` = p_marginal
)
barplot(
t(prob_matrix),
beside = TRUE,
col = c(palette_ml["orange"], palette_ml["teal"]),
ylim = c(0, max(prob_matrix) * 1.25),
ylab = "预测感染概率",
main = "条件固定部分与边际化概率",
las = 1
)
legend(
"topright",
legend = c("常规护理", "检查清单"),
fill = c(palette_ml["orange"], palette_ml["teal"]),
bty = "n"
)60 岁择期参考患者在典型医院与对医院随机效应积分后的感染概率。非线性 logit 链接使积分结果不同于把随机效应设为零的概率。
若学校严格嵌套于社区,可写作
(1 | community/school);若学生的居住社区与学校互相交叉,应分别估计两套随机截距。把交叉分类强行写成嵌套模型,会把方差归给错误的社会结构。
# 学校严格嵌套于社区;school_id 只需在社区内唯一。
m_nested <- lme4::lmer(
score ~ student_ses + school_resources +
(1 | community_id/school_id),
data = education_data
)
# 学校和居住社区交叉分类。
m_crossed <- lme4::lmer(
score ~ student_ses + school_resources + neighborhood_deprivation +
(1 | school_id) + (1 | neighborhood_id),
data = education_data
)
# 多重成员关系:若一名患者由多名治疗师共同服务,需要明确每名
# 治疗师的成员权重;普通单一 (1 | therapist) 不能表达这种结构。多重成员模型通常需要每个上层成员的权重及支持该结构的软件。若患者更换治疗师,只保留“最后一名治疗师”会丢失真实依赖过程。
fitting_reference <- data.frame(
情形 = c(
"LMM 最终方差成分与固定效应",
"比较不同固定效应的 LMM",
"相同固定效应、比较协方差结构",
"logistic/Poisson GLMM",
"少量高层群组"
),
建议 = c(
"常用 REML,并报告固定与随机部分",
"用 ML 重拟合后再比较",
"可用 REML,但边界检验需谨慎",
"使用 maximum likelihood;说明积分近似与 optimizer",
"优先效应与区间;考虑 bootstrap 或小样本校正"
),
原因 = c(
"REML 减少方差成分有限样本偏差",
"不同固定效应模型的 REML likelihood 不可直接比较",
"随机方差为零处在参数空间边界",
"非高斯模型没有普通 LMM 的 REML",
"渐近 z/t 近似可能过于乐观"
),
check.names = FALSE
)
knitr::kable(fitting_reference, caption = "ML、REML 与不确定性方法的选择")| 情形 | 建议 | 原因 |
|---|---|---|
| LMM 最终方差成分与固定效应 | 常用 REML,并报告固定与随机部分 | REML 减少方差成分有限样本偏差 |
| 比较不同固定效应的 LMM | 用 ML 重拟合后再比较 | 不同固定效应模型的 REML likelihood 不可直接比较 |
| 相同固定效应、比较协方差结构 | 可用 REML,但边界检验需谨慎 | 随机方差为零处在参数空间边界 |
| logistic/Poisson GLMM | 使用 maximum likelihood;说明积分近似与 optimizer | 非高斯模型没有普通 LMM 的 REML |
| 少量高层群组 | 优先效应与区间;考虑 bootstrap 或小样本校正 | 渐近 z/t 近似可能过于乐观 |
lme4::lmer() 默认不提供固定效应 p
值,因为有限样本自由度并不唯一。教程使用估计值、Wald 95% CI 和 t
值描述结果;正式研究可预先指定 Satterthwaite、Kenward–Roger、profile
likelihood 或 parametric
bootstrap。不要在看到结果后选择最有利的区间方法。
随机效应方差的零假设位于边界,普通似然比检验的卡方近似可能不准确。随机结构应首先来自数据层级、重复测量安排和科学问题,而不是逐步寻找最小 p 值。
更多低层观测不能替代更多高层群组 每家医院增加患者能改善该医院均值,但医院层政策、治疗或资源效应的精度主要受医院数限制。报告总行数时必须同时报告每一层的单位数和群组大小分布。
# 已观察个体/群组的条件预测:包含其估计随机效应。
predict(m_psy, newdata = existing_people, re.form = NULL)
# 把所有随机效应设为零:固定部分/典型群组预测。
predict(m_psy, newdata = prediction_grid, re.form = NA)
# 新个体由既有治疗师服务:保留治疗师效应,不包含未知的个体效应。
predict(
m_psy,
newdata = new_people_existing_therapist,
re.form = ~(1 | therapist),
allow.new.levels = TRUE
)
# 新个体和新治疗师:两层随机效应都未知,使用 fixed part。
predict(
m_psy,
newdata = new_people_new_therapist,
re.form = NA,
allow.new.levels = TRUE
)re.form = NA
会同时清除模型中的所有随机效应;它不能表示“新个体、既有治疗师”这种部分已知结构。后者需要像示例一样只保留治疗师项。LMM
identity link 下,对零均值随机效应平均后的均值与 fixed part 对齐;GLMM
的非线性链接下,把随机效应设为零与对其分布积分通常不同。预测还应区分均值置信区间、已有个体的条件预测区间和新群组预测区间。
diagnostic_flags <- data.frame(
模型 = c(
"医学连续 LMM", "社会学随机斜率 LMM",
"心理学三层 LMM", "医学感染 GLMM"
),
奇异拟合 = c(
lme4::isSingular(m_med),
lme4::isSingular(m_soc),
lme4::isSingular(m_psy),
lme4::isSingular(m_bin)
),
优化器报告收敛 = vapply(
list(m_med, m_soc, m_psy, m_bin),
function(model) {
optimizer_code <- model@optinfo$conv$opt
all(optimizer_code == 0) &&
is.null(model@optinfo$conv$lme4$messages)
},
logical(1)
),
check.names = FALSE
)
knitr::kable(diagnostic_flags, caption = "四个教学模型的奇异性与收敛标志")| 模型 | 奇异拟合 | 优化器报告收敛 |
|---|---|---|
| 医学连续 LMM | FALSE | TRUE |
| 社会学随机斜率 LMM | FALSE | TRUE |
| 心理学三层 LMM | FALSE | TRUE |
| 医学感染 GLMM | FALSE | TRUE |
四个教学模型均成功收敛且不是 singular fit,但这只是最低要求。完整诊断还包括:
最大似然 mixed model 可以使用不同时点数的个体,但无偏解释仍依赖缺失机制和模型。常见 likelihood 推断要求:给定已观测历史和模型协变量后,缺失可合理视为 MAR。
多层模型可以表示相关性,却不会自动提供:
医院层干预尤其需要医院层混杂控制和足够医院重叠。患者层显著系数也不应自动翻译成患者改变暴露后的因果效应。完整框架见 因果推断详解。
随机截距不是“未测混杂控制器” 随机截距描述未观测群组偏离的分布,并不保证它与模型协变量独立,也不自动控制所有群组层共同原因。若 random effect 与协变量相关,可考虑加入群组均值的 correlated-random-effects/Mundlak 分解,并说明额外假设。
case_summary <- data.frame(
应用 = c("患者—诊所", "学生—学校", "时点—个体—治疗师", "患者—医院感染"),
数据结构 = c(
sprintf("%d 人/%d 诊所", nrow(med), length(unique(med$clinic))),
sprintf("%d 人/%d 学校", nrow(soc), length(unique(soc$school))),
sprintf(
"%d 次/%d 人/%d 治疗师",
nrow(psy), length(unique(psy$person)), length(unique(psy$therapist))
),
sprintf("%d 人/%d 医院", nrow(bin), length(unique(bin$hospital)))
),
主要结果 = c(
sprintf("干预均值差 %.2f mmHg", med_fixed$估计值[2]),
sprintf(
"within SES %.2f;跨层交互 %.2f",
soc_fixed$估计值[2], soc_fixed$估计值[5]
),
sprintf("干预额外 session 斜率 %.2f", psy_fixed$估计值[6]),
sprintf(
"检查清单条件 OR %.2f;参考患者医院效应边际化风险差 %.3f",
bin_results$条件OR[2], diff(p_marginal)
)
),
关键异质性 = c(
sprintf("空模型 ICC %.3f", med_empty_icc),
sprintf("空模型 ICC %.3f;SES slope SD %.2f", soc_empty_icc, soc_vc_table$数值[2]),
sprintf("治疗师 SD %.2f;个体 slope SD %.2f", psy_vc_table$数值[4], psy_vc_table$数值[2]),
sprintf("latent ICC %.3f;MOR %.2f", icc_latent, mor)
),
check.names = FALSE
)
knitr::kable(case_summary, caption = "四个多层模型应用的动态结果摘要")| 应用 | 数据结构 | 主要结果 | 关键异质性 |
|---|---|---|---|
| 患者—诊所 | 1425 人/48 诊所 | 干预均值差 -4.11 mmHg | 空模型 ICC 0.174 |
| 学生—学校 | 2098 人/70 学校 | within SES 2.77;跨层交互 -1.01 | 空模型 ICC 0.437;SES slope SD 1.60 |
| 时点—个体—治疗师 | 1120 次/160 人/20 治疗师 | 干预额外 session 斜率 -0.40 | 治疗师 SD 1.64;个体 slope SD 0.29 |
| 患者—医院感染 | 2501 人/56 医院 | 检查清单条件 OR 0.57;参考患者医院效应边际化风险差 -0.048 | latent ICC 0.095;MOR 1.75 |
这些结果使用不同 estimand 和尺度,不能按数值大小横向比较。连续模型给均值差与斜率,logistic GLMM 给条件 OR;ICC 在随机斜率和非高斯模型中也具有不同定义。
共分析 [N] 次观测,来自 [P] 名个体和 [J] 个群组。结局为 [定义与尺度],时间以 [零点] 中心化。模型固定部分包含 [变量、非线性与交互],随机部分包含 [群组截距、个体截距、随机斜率及相关]。连续结局使用 [REML/ML] 拟合,模型比较使用 [方法];非高斯结局采用 [link 与 family]。群组、个体和残差标准差分别为 [数值],[ICC/VPC/MOR] 为 [数值与明确定义]。主要 [均值差/斜率/条件 OR/边际风险差] 为 [估计与 95% CI]。诊断显示 [收敛、奇异性、残差、影响群组和过度离散]。结论主要推广至 [目标个体与群组范围],并依赖 [随机效应、缺失、选择和因果假设]。
至少同时报告:
common_errors <- data.frame(
错误 = c(
"忽略群组后把所有行当独立",
"看到 ID 就机械添加随机截距",
"把 fixed/random 理解为变量是否变化",
"ICC 小就忽略聚类",
"把 ICC 写成群组因果贡献",
"用个体数代替群组数评价高层效应",
"ID 只在群组内唯一却当全局唯一",
"把 crossed 结构错写成 nested",
"不分解时间变化变量的 within/between 效应",
"以为总体均值中心化已经分离两个效应",
"需要随机斜率却只放随机截距",
"把随机斜率当成 AR(1) 残差结构",
"比较不同 fixed effects 的 REML likelihood",
"只凭逐步 p 值选择随机结构",
"忽略 convergence 或 singular warning",
"把 GLMM 条件 OR 当成风险比或边际 OR",
"用 random effects 给机构确定排名",
"认为 mixed model 自动处理所有缺失",
"把随机截距当作未测混杂控制器"
),
更好的做法 = c(
"按设计表示相关性并检查每层样本量",
"先说明共享机制和推断单位",
"用共同系数与群组偏离分布解释",
"结合群组大小、群组数和暴露层级",
"称为模型方差/相关结构",
"高层精度主要由高层单位数决定",
"创建全局唯一 ID 或显式交互 ID",
"分别建模学校和社区随机效应",
"加入群组内偏离与群组均值",
"按问题选择 group-mean decomposition",
"有设计支持时拟合并诊断 random slope",
"另查剩余时间序列相关",
"用 ML 重拟合后比较固定部分",
"由设计与科学问题预先确定候选结构",
"排查尺度、信息和模型复杂度并透明报告",
"报告正确尺度并计算目标边际量",
"显示收缩、不确定性和病例结构",
"说明 MAR 等假设并做 MNAR 敏感性分析",
"明确 random-effect 与协变量独立等额外假设"
),
check.names = FALSE
)
knitr::kable(common_errors, caption = "多层模型常见错误及更好的做法")| 错误 | 更好的做法 |
|---|---|
| 忽略群组后把所有行当独立 | 按设计表示相关性并检查每层样本量 |
| 看到 ID 就机械添加随机截距 | 先说明共享机制和推断单位 |
| 把 fixed/random 理解为变量是否变化 | 用共同系数与群组偏离分布解释 |
| ICC 小就忽略聚类 | 结合群组大小、群组数和暴露层级 |
| 把 ICC 写成群组因果贡献 | 称为模型方差/相关结构 |
| 用个体数代替群组数评价高层效应 | 高层精度主要由高层单位数决定 |
| ID 只在群组内唯一却当全局唯一 | 创建全局唯一 ID 或显式交互 ID |
| 把 crossed 结构错写成 nested | 分别建模学校和社区随机效应 |
| 不分解时间变化变量的 within/between 效应 | 加入群组内偏离与群组均值 |
| 以为总体均值中心化已经分离两个效应 | 按问题选择 group-mean decomposition |
| 需要随机斜率却只放随机截距 | 有设计支持时拟合并诊断 random slope |
| 把随机斜率当成 AR(1) 残差结构 | 另查剩余时间序列相关 |
| 比较不同 fixed effects 的 REML likelihood | 用 ML 重拟合后比较固定部分 |
| 只凭逐步 p 值选择随机结构 | 由设计与科学问题预先确定候选结构 |
| 忽略 convergence 或 singular warning | 排查尺度、信息和模型复杂度并透明报告 |
| 把 GLMM 条件 OR 当成风险比或边际 OR | 报告正确尺度并计算目标边际量 |
| 用 random effects 给机构确定排名 | 显示收缩、不确定性和病例结构 |
| 认为 mixed model 自动处理所有缺失 | 说明 MAR 等假设并做 MNAR 敏感性分析 |
| 把随机截距当作未测混杂控制器 | 明确 random-effect 与协变量独立等额外假设 |
predict(..., re.form = NA) 在 logistic GLMM
中是否就是总体边际风险?quick_reference <- data.frame(
目标 = c(
"两层随机截距 LMM", "随机斜率", "三层纵向模型",
"二分类 GLMM", "提取固定效应", "方差成分",
"随机效应条件模态", "奇异性", "典型群组预测", "已有群组条件预测"
),
`R 入口` = c(
"lme4::lmer(y ~ x + (1 | cluster), data=d)",
"lme4::lmer(y ~ x + (1 + x | cluster), data=d)",
"lme4::lmer(y ~ time*A + (1+time|person) + (1|site), data=d)",
"lme4::glmer(y ~ x + (1|cluster), family=binomial, data=d)",
"lme4::fixef(model)",
"lme4::VarCorr(model)",
"lme4::ranef(model, condVar=TRUE)",
"lme4::isSingular(model)",
"predict(model, re.form=NA)",
"predict(model, re.form=NULL)"
),
提醒 = c(
"报告每层样本量和 ICC",
"需要群组内 x 变化与足够群组",
"ID 必须正确,检查剩余序列相关",
"效应通常是条件 OR",
"固定效应不是无条件边际效应的同义词",
"报告 SD、相关和 residual scale",
"不是无误差的群组真值",
"边界诊断,不是自动删项命令",
"GLMM 中不等于积分后的边际均值",
"只适用于已有且已估计随机效应的群组"
),
check.names = FALSE
)
knitr::kable(quick_reference, caption = "多层模型核心 R 入口与解释提醒")| 目标 | R 入口 | 提醒 |
|---|---|---|
| 两层随机截距 LMM | lme4::lmer(y ~ x + (1 | cluster), data=d) | 报告每层样本量和 ICC |
| 随机斜率 | lme4::lmer(y ~ x + (1 + x | cluster), data=d) | 需要群组内 x 变化与足够群组 |
| 三层纵向模型 | lme4::lmer(y ~ time*A + (1+time|person) + (1|site), data=d) | ID 必须正确,检查剩余序列相关 |
| 二分类 GLMM | lme4::glmer(y ~ x + (1|cluster), family=binomial, data=d) | 效应通常是条件 OR |
| 提取固定效应 | lme4::fixef(model) | 固定效应不是无条件边际效应的同义词 |
| 方差成分 | lme4::VarCorr(model) | 报告 SD、相关和 residual scale |
| 随机效应条件模态 | lme4::ranef(model, condVar=TRUE) | 不是无误差的群组真值 |
| 奇异性 | lme4::isSingular(model) | 边界诊断,不是自动删项命令 |
| 典型群组预测 | predict(model, re.form=NA) | GLMM 中不等于积分后的边际均值 |
| 已有群组条件预测 | predict(model, re.form=NULL) | 只适用于已有且已估计随机效应的群组 |
# 1. 审计每层单位、ID、群组大小和 predictor 的层内变化。
table(d$cluster_id)
tapply(d$x, d$cluster_id, stats::var)
# 2. 先拟合空模型并分解方差。
m0 <- lme4::lmer(y ~ 1 + (1 | cluster_id), data = d, REML = TRUE)
lme4::VarCorr(m0)
# 3. 根据问题分解 within/between,并写出有依据的随机结构。
m1 <- lme4::lmer(
y ~ x_within + x_cluster_mean + z_cluster +
x_within:z_cluster + (1 + x_within | cluster_id),
data = d,
REML = TRUE,
control = lme4::lmerControl(optimizer = "bobyqa")
)
# 4. 检查 fixed effects、variance components、singularity、残差和影响群组。
lme4::fixef(m1)
lme4::VarCorr(m1)
lme4::isSingular(m1)
# 5. 区分已有群组条件预测、典型群组预测和新群组预测。
predict(m1, re.form = NULL)
predict(m1, re.form = NA)后续可学习非线性时间轨迹、样条 random slopes、异方差与 AR(1) 残差、cross-classified 与 multiple-membership 模型、Bayesian multilevel models、joint longitudinal–survival models、multilevel mediation、cluster randomized trials、小样本自由度校正、parametric bootstrap、survey-weighted multilevel models,以及模型的外部验证与动态预测。
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] nlme_3.1-169 cli_3.6.6 knitr_1.51 rlang_1.3.0
## [5] xfun_0.60 reformulas_0.4.4 jsonlite_2.0.0 minqa_1.2.8
## [9] htmltools_0.5.9 lme4_2.0-6 sass_0.4.10 rmarkdown_2.31
## [13] grid_4.6.1 evaluate_1.0.5 jquerylib_0.1.4 MASS_7.3-65
## [17] fastmap_1.2.0 yaml_2.3.12 lifecycle_1.0.5 compiler_4.6.1
## [21] Rcpp_1.1.2 lattice_0.22-9 digest_0.6.39 nloptr_2.2.1
## [25] R6_2.6.1 Rdpack_2.6.6 splines_4.6.1 rbibutils_2.4.1
## [29] bslib_0.12.0 Matrix_1.7-5 tools_4.6.1 boot_1.3-32
## [33] cachem_1.1.0