-
Notifications
You must be signed in to change notification settings - Fork 0
/
hierarchical-normal-models.qmd
1341 lines (1059 loc) · 50.1 KB
/
hierarchical-normal-models.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 分层正态模型 {#sec-hierarchical-normal-models}
```{r}
#| echo: false
Sys.setenv(CMDSTANR_NO_VER_CHECK = TRUE)
# https://github.com/r-lib/processx/issues/236
Sys.setenv(PROCESSX_NOTIFY_OLD_SIGCHLD = TRUE)
source("_common.R")
```
> This is a bit like asking how should I tweak my sailboat so I can explore the ocean floor.
>
> --- Roger Koenker [^hierarchical-normal-models-1]
[^hierarchical-normal-models-1]: <https://stat.ethz.ch/pipermail/r-help/2013-May/354311.html>
乔治·博克斯说,所有的模型都是错的,但有些是有用的。在真实的数据面前,尽我们所能,结果发现没有最好的模型,只有更好的模型。总是需要自己去构造符合自己需求的模型及其实现,只有自己能够实现,才能在模型的海洋中畅快地遨游。
介绍分层正态模型的定义、结构、估计,分层正态模型与曲线生长模型的关系,分层正态模型与潜变量模型的关系,分层正态模型与线性混合效应的关系。以 **rstan** 包和 **nlme** 包拟合分层正态模型,说明 **rstan** 包的一些用法,比较贝叶斯和频率派方法拟合的结果,给出结果的解释。再对比 16 个不同的 R包实现,总结一般地使用经验,也体会不同 R 包的独特性。
```{r}
#| message: false
library(StanHeaders)
library(ggplot2)
library(rstan)
# 将编译的 Stan 模型与代码文件放在一起
rstan_options(auto_write = TRUE)
# 如果CPU和内存足够,设置成与马尔科夫链一样多
options(mc.cores = 2)
# 调色板
custom_colors <- c(
"#4285f4", # GoogleBlue
"#34A853", # GoogleGreen
"#FBBC05", # GoogleYellow
"#EA4335" # GoogleRed
)
rstan_ggtheme_options(
panel.background = element_rect(fill = "white"),
legend.position = "top"
)
rstan_gg_options(
fill = "#4285f4", color = "white",
pt_color = "#EA4335", chain_colors = custom_colors
)
library(bayesplot)
```
## rstan 包 {#sec-8schools-rstan}
本节以 8schools 数据为例介绍分层正态模型及 **rstan** 包实现,8schools 数据最早来自 @Rubin1981 ,分层正态模型如下:
$$
\begin{aligned}
y_j &\sim \mathcal{N}(\theta_j,\sigma_j^2) \quad
\theta_j = \mu + \tau \times \eta_j \\
\theta_j &\sim \mathcal{N}(\mu, \tau^2) \quad
\eta_j \sim \mathcal{N}(0,1) \\
\mu &\sim \mathcal{N}(0, 100^2) \quad \tau \sim \mathrm{half\_normal}(0,100^2)
\end{aligned}
$$
其中,$y_j,\sigma_j$ 是已知的观测数据,$\theta_j$ 是模型参数, $\eta_j$ 是服从标准正态分布的潜变量,$\mu,\tau$ 是超参数,分别服从正态分布(将方差设置为很大的数,则变成弱信息先验或无信息均匀先验)和半正态分布(随机变量限制为正值)。
### 拟合模型
用 **rstan** 包来拟合模型,下面采用非中心的参数化表示,降低参数的相关性,减少发散的迭代次数,提高采样效率。
```{r}
# 编译模型
eight_schools_fit <- stan(
model_name = "eight_schools",
# file = "code/eight_schools.stan",
model_code = "
// saved as eight_schools.stan
data {
int<lower=0> J; // number of schools
array[J] real y; // estimated treatment effects
array[J] real <lower=0> sigma; // standard error of effect estimates
}
parameters {
real mu; // population treatment effect
real<lower=0> tau; // standard deviation in treatment effects
vector[J] eta; // unscaled deviation from mu by school
}
transformed parameters {
vector[J] theta = mu + tau * eta; // school treatment effects
}
model {
target += normal_lpdf(mu | 0, 100);
target += normal_lpdf(tau | 0, 100);
target += normal_lpdf(eta | 0, 1); // prior log-density
target += normal_lpdf(y | theta, sigma); // log-likelihood
}
",
data = list( # 观测数据
J = 8,
y = c(28, 8, -3, 7, -1, 1, 18, 12),
sigma = c(15, 10, 16, 11, 9, 11, 10, 18)
),
warmup = 1000, # 每条链预处理迭代次数
iter = 2000, # 每条链总迭代次数
chains = 2, # 马尔科夫链的数目
cores = 2, # 指定 CPU 核心数,可以给每条链分配一个
verbose = FALSE, # 不显示迭代的中间过程
refresh = 0, # 不显示采样的进度
seed = 20232023 # 设置随机数种子,不要使用 set.seed() 函数
)
```
### 模型输出
用函数 `print()` 打印输出结果,保留 2 位小数。
```{r}
print(eight_schools_fit, digits = 2)
```
值得一提,数据有限而且规律不明确,数据隐含的信息不是很多,则先验分布的情况将会对参数估计结果产生很大影响。Stan 默认采用无信息的先验分布,当使用非常弱的信息先验时,结果就非常不同了。提取任意一个参数的结果,如查看参数 $\tau$ 的 95% 置信区间。
```{r}
print(eight_schools_fit, pars = "tau", probs = c(0.025, 0.975))
```
从迭代抽样数据获得与 `print(fit)` 一样的结果。以便后续对原始采样数据做任意的进一步分析。**rstan** 包扩展泛型函数 `summary()` 以支持对 stanfit 数据对象汇总,输出各个参数分链条和合并链条的后验分布结果。
### 操作数据
抽取数据对象 `eight_schools_fit` 中的采样数据,合并几条马氏链的结果,返回的结果是一个列表。
```{r}
eight_schools_sim <- extract(eight_schools_fit, permuted = TRUE)
```
返回列表中的每个元素是一个数组,标量参数对应一维数组,向量参数对应二维数组。
```{r}
str(eight_schools_sim)
```
对于列表,适合用函数 `lapply()` 配合算术函数计算 $\mu,\tau$ 等参数的均值。
```{r}
fun_mean <- function(x) {
if (length(dim(x)) > 1) {
apply(x, 2, mean)
} else {
mean(x)
}
}
lapply(eight_schools_sim, FUN = fun_mean)
```
类似地,计算 $\mu,\tau$ 等参数的分位点。
```{r}
fun_quantile <- function(x, probs) {
if (length(dim(x)) > 1) {
t(apply(x, 2, quantile, probs = probs))
} else {
quantile(x, probs = probs)
}
}
lapply(eight_schools_sim, fun_quantile, probs = c(2.5, 25, 50, 75, 97.5) / 100)
```
同理,可以计算最大值 `max()`、最小值 `min()` 和中位数 `median()` 等。
### 采样诊断
获取马尔科夫链迭代点列数据
```{r}
eight_schools_sim <- extract(eight_schools_fit, permuted = FALSE)
```
`eight_schools_sim` 是一个三维数组,1000(次迭代)\* 2 (条链)\* 19(个参数)。如果 `permuted = TRUE` 则会合并马氏链的迭代结果,变成一个列表。
```{r}
# 数据类型
class(eight_schools_sim)
# 1000(次迭代)* 2 (条链)* 19(个参数)
str(eight_schools_sim)
```
提取参数 $\mu$ 的迭代点列,绘制迭代轨迹。
```{r}
#| label: fig-8schools-mu-base
#| fig-cap: Base R 绘制参数 $\mu$ 的迭代轨迹
#| fig-showtext: true
#| par: true
eight_schools_mu_sim <- eight_schools_sim[, , "mu"]
matplot(
eight_schools_mu_sim, xlab = "迭代次数", ylab = expression(mu),
type = "l", lty = "solid", col = custom_colors
)
abline(h = apply(eight_schools_mu_sim, 2, mean), col = custom_colors)
legend(
"topleft", legend = paste("chain", 1:2), box.col = "white",
inset = 0.01, lty = "solid", horiz = TRUE, col = custom_colors
)
```
也可以使用 **rstan** 包提供的函数 `traceplot()` 或者 `stan_trace()` 绘制参数的迭代轨迹图。
```{r}
#| label: fig-8schools-mu-ggplot2
#| fig-cap: rstan 绘制参数 $\mu$ 的迭代轨迹
#| fig-showtext: true
stan_trace(eight_schools_fit, pars = "mu") +
labs(x = "迭代次数", y = expression(mu))
```
### 后验分布
可以用函数 `stan_hist()` 或 `stan_dens()` 绘制后验分布图。下图分别展示参数 $\mu$、$\tau$ 的直方图,以及二者的散点图,参数 $\mu$ 的后验概率密度分布图。
```{r}
#| label: fig-8schools-rstan-posterior
#| fig-cap: rstan 包绘制后验分布图
#| fig-showtext: true
#| fig-height: 6
p1 <- stan_hist(eight_schools_fit, pars = c("mu","tau"), bins = 30)
p2 <- stan_scat(eight_schools_fit, pars = c("mu","tau"), size = 1) +
labs(x = expression(mu), y = expression(tau))
p3 <- stan_dens(eight_schools_fit, pars = "mu") + labs(x = expression(mu))
library(patchwork)
p1 / (p2 + p3)
```
相比于 **rstan** 包,**bayesplot** 包可视化能力更强,支持对特定的参数做变换。**bayesplot** 包的函数 `mcmc_pairs()` 以矩阵图展示多个参数的分布,下图展示参数 $\mu$,$\log(\tau)$ 后验分布图。但是,这些函数都固定了一些标题,不能修改。
```{r}
#| label: fig-8schools-bayesplot-posterior
#| fig-cap: bayesplot 包绘制后验分布图
#| fig-showtext: true
#| fig-height: 6
bayesplot::mcmc_pairs(
eight_schools_fit, pars = c("mu", "tau"), transform = list(tau = "log")
)
```
## 其它 R 包 {#sec-8schools-others}
### nlme
接下来,用 **nlme** 包拟合模型。
```{r}
# 成绩
y <- c(28, 8, -3, 7, -1, 1, 18, 12)
# 标准差
sigma <- c(15, 10, 16, 11, 9, 11, 10, 18)
# 学校编号
g <- 1:8
```
首先,调用 **nlme** 包的函数 `lme()` 拟合模型。
```{r}
library(nlme)
fit_lme <- lme(y ~ 1, random = ~ 1 | g, weights = varFixed(~ sigma^2), method = "REML")
summary(fit_lme)
```
随机效应的标准差 2.917988 ,随机效应部分的估计
```{r}
ranef(fit_lme)
```
类比 Stan 输出结果中的 $\theta$ 向量,每个学校的成绩估计
```{r}
7.785729 + 2.917988 * ranef(fit_lme)
```
### lme4
接着,采用 **lme4** 包拟合模型,发现 **lme4** 包获得与 **nlme** 包一样的结果。
```{r}
control <- lme4::lmerControl(
check.conv.singular = "ignore",
check.nobs.vs.nRE = "ignore",
check.nobs.vs.nlev = "ignore"
)
fit_lme4 <- lme4::lmer(y ~ 1 + (1 | g), weights = 1 / sigma^2, control = control, REML = TRUE)
summary(fit_lme4)
```
### blme
下面使用 **blme** 包 [@Chung2013] ,**blme** 包基于 **lme4** 包,参数估计结果完全一致。
```{r}
# the mode should be at the boundary of the space.
fit_blme <- blme::blmer(
y ~ 1 + (1 | g), control = control, REML = TRUE,
cov.prior = NULL, weights = 1 / sigma^2
)
summary(fit_blme)
```
### MCMCglmm
**MCMCglmm** 包 [@Hadfield2010] 采用 MCMC 算法拟合数据。
```{r}
schools <- data.frame(y = y, sigma = sigma, g = g)
schools$g <- as.factor(schools$g)
# inverse-gamma prior with scale and shape equal to 0.001
prior1 <- list(
R = list(V = diag(schools$sigma^2), fix = 1),
G = list(G1 = list(V = 1, nu = 0.002))
)
# 为可重复
set.seed(20232023)
# 拟合模型
fit_mcmc <- MCMCglmm::MCMCglmm(
y ~ 1, random = ~g, rcov = ~ idh(g):units,
data = schools, prior = prior1, verbose = FALSE
)
# 输出结果
summary(fit_mcmc)
```
R-structure 表示残差方差,这是已知的参数。G-structure 表示随机截距的方差,Location effects 表示固定效应的截距。截距和 **nlme** 包的结果很接近。
### cmdstanr
一般地,**rstan** 包使用的 stan 框架版本低于 **cmdstanr** 包,从 **rstan** 包切换到 **cmdstanr** 包,需要注意语法、函数的变化。**rstan** 和 **cmdstanr** 使用的 Stan 版本不同导致参数估计结果不同,结果可重复的条件非常苛刻,详见 [Stan 参考手册](https://mc-stan.org/docs/reference-manual/reproducibility.html)。在都是较新的版本时,Stan 代码不需要做改动,如下:
```{verbatim, file="code/eight_schools.stan", lang="stan"}
```
此处,给参数 $\mu,\tau$ 添加了非常弱(模糊)的先验,结果将出现较大不同。
```{r}
#| message: false
eight_schools_dat <- list(
J = 8,
y = c(28, 8, -3, 7, -1, 1, 18, 12),
sigma = c(15, 10, 16, 11, 9, 11, 10, 18)
)
library(cmdstanr)
mod_eight_schools <- cmdstan_model(
stan_file = "code/eight_schools.stan",
compile = TRUE, cpp_options = list(stan_threads = TRUE)
)
fit_eight_schools <- mod_eight_schools$sample(
data = eight_schools_dat, # 数据
chains = 2, # 总链条数
parallel_chains = 2, # 并行数目
iter_warmup = 1000, # 每条链预处理的迭代次数
iter_sampling = 1000, # 每条链采样的迭代次数
threads_per_chain = 2, # 每条链设置 2 个线程
seed = 20232023, # 随机数种子
show_messages = FALSE, # 不显示消息
refresh = 0 # 不显示采样迭代的进度
)
```
结果保留 3 位有效数字,模型输出如下:
```{r}
fit_eight_schools$summary(.num_args = list(sigfig = 3, notation = "dec"))
```
模型采样过程的诊断结果如下:
```{r}
fit_eight_schools$diagnostic_summary()
```
分层模型的参数 $\mu,\log(\tau)$ 的后验联合分布呈现经典的漏斗状。
```{r}
#| label: fig-8schools-funnels
#| fig-cap: 参数 $\mu,\log(\tau)$ 的联合分布
#| fig-width: 5
#| fig-height: 4
#| dev: 'tikz'
#| fig-process: !expr to_png
bayesplot::mcmc_scatter(
fit_eight_schools$draws(), pars = c("mu", "tau"),
transform = list(tau = "log"), size = 2
) + labs(x = "$\\mu$", y = "$\\log(\\tau)$")
```
```{r}
#| eval: false
#| echo: false
bayesplot::mcmc_pairs(
fit_eight_schools$draws(), pars = c("mu", "tau"),
transform = list(tau = "log")
)
eight_schools_df <- fit_eight_schools$draws(c("mu", "tau"), format = "draws_df")
ggplot(data = eight_schools_df, aes(x = mu, y = log(tau))) +
geom_point(color = "#4285f4") +
geom_density_2d(color = "#FBBC05", linewidth = 1) +
theme_bw() +
labs(x = expression(mu), y = expression(log(tau)))
```
对于调用 **cmdstanr** 包拟合的模型,适合用 **bayesplot** 包来可视化后验分布和诊断采样。
## 案例:rats 数据 {#sec-thirty-rats}
rats 数据最早来自 @gelfand1990 ,记录 30 只小鼠每隔一周的重量,一共进行了 5 周。第一次记录是小鼠第 8 天的时候,第二次测量记录是第 15 天的时候,一直持续到第 36 天。下面在 R 环境中准备数据。
```{r}
# 总共 30 只老鼠
N <- 30
# 总共进行 5 周
T <- 5
# 小鼠重量
y <- structure(c(
151, 145, 147, 155, 135, 159, 141, 159, 177, 134,
160, 143, 154, 171, 163, 160, 142, 156, 157, 152, 154, 139, 146,
157, 132, 160, 169, 157, 137, 153, 199, 199, 214, 200, 188, 210,
189, 201, 236, 182, 208, 188, 200, 221, 216, 207, 187, 203, 212,
203, 205, 190, 191, 211, 185, 207, 216, 205, 180, 200, 246, 249,
263, 237, 230, 252, 231, 248, 285, 220, 261, 220, 244, 270, 242,
248, 234, 243, 259, 246, 253, 225, 229, 250, 237, 257, 261, 248,
219, 244, 283, 293, 312, 272, 280, 298, 275, 297, 350, 260, 313,
273, 289, 326, 281, 288, 280, 283, 307, 286, 298, 267, 272, 285,
286, 303, 295, 289, 258, 286, 320, 354, 328, 297, 323, 331, 305,
338, 376, 296, 352, 314, 325, 358, 312, 324, 316, 317, 336, 321,
334, 302, 302, 323, 331, 345, 333, 316, 291, 324
), .Dim = c(30, 5))
# 第几天
x <- c(8.0, 15.0, 22.0, 29.0, 36.0)
xbar <- 22.0
```
重复测量的小鼠重量数据 rats 如下 @tbl-rats 所示。
```{r}
#| label: tbl-rats
#| tbl-cap: 小鼠重量数据(部分)
#| echo: false
rownames(y) <- 1:30
knitr::kable(head(y), col.names = paste("第", c(8, 15, 22, 29, 36), "天"), row.names = TRUE)
```
小鼠重量数据的分布和变化情况见下图,由图可以假定 30 只小鼠的重量服从正态分布,而30 只小鼠的重量呈现一种线性增长趋势。
```{r}
#| label: fig-rats
#| fig-cap: 30 只小鼠 5 次测量的数据
#| fig-subcap:
#| - 小鼠重量的分布
#| - 小鼠重量的变化
#| fig-showtext: true
#| par: true
#| echo: false
#| fig-width: 5
#| fig-height: 4.5
#| layout-ncol: 2
matplot(y, xlab = "小鼠编号", ylab = "小鼠重量")
matplot(t(y), xlab = "测量次数", ylab = "小鼠重量", pch = 1)
```
## 频率派方法 {#sec-rats-frequentist}
### nlme {#sec-rats-nlme}
**nlme** 包适合长格式的数据,因此,先将小鼠数据整理成长格式。
```{r}
rats_data <- data.frame(
weight = as.vector(y),
rats = rep(1:30, times = 5),
days = rep(c(8, 15, 22, 29, 36), each = 30)
)
```
将 30 只小鼠的重量变化及回归曲线画出来,发现各只小鼠的回归线的斜率几乎一样,截距略有不同。不同小鼠的出生重量是不同,前面 Stan 采用变截距变斜率的混合效应模型拟合数据。
```{r}
#| label: fig-rats-lm
#| fig-cap: 小鼠重量变化曲线
#| fig-showtext: true
#| fig-width: 6
#| fig-height: 7
ggplot(data = rats_data, aes(x = days, y = weight)) +
geom_point() +
geom_smooth(formula = "y ~ x", method = "lm", se = FALSE) +
theme_bw() +
facet_wrap(facets = ~rats, labeller = "label_both", ncol = 6) +
labs(x = "第几天", y = "重量")
```
小鼠的重量随时间增长,不同小鼠的情况又会有所不同。作为一个参照,首先考虑变截距的随机效应模型。
$$
y_{ij} = \beta_0 + \beta_1 * x_j + \alpha_i + \epsilon_{ij}, \quad i = 1,2,\ldots,30. \quad j = 1,2,3,4,5
$$
其中,$y_{ij}$ 表示第 $i$ 只小鼠在第 $j$ 次测量的重量,一共 30 只小鼠,共测量了 5 次。固定效应部分是 $\beta_0$ 和 $\beta_1$ ,分别表示截距和斜率。随机效应部分是 $\alpha_i$ 和 $\epsilon_{ij}$ ,分别服从正态分布$\alpha_i \sim \mathcal{N}(0, \sigma^2_{\alpha})$ 和 $\epsilon_{ij} \sim \mathcal{N}(0, \sigma^2_{\epsilon})$ 。$\sigma^2_{\alpha}$ 和 $\sigma^2_{\epsilon}$ 分别表示组间方差(group level)和组内方差(individual level)。
```{r}
library(nlme)
rats_lme0 <- lme(data = rats_data, fixed = weight ~ days, random = ~ 1 | rats)
summary(rats_lme0)
```
当然,若考虑不同小鼠的生长速度不同(变化不是很大),可用变截距和变斜率的随机效应模型表示生长曲线模型,下面加载 **nlme** 包调用函数 `lme()` 拟合该模型。
```{r}
library(nlme)
rats_lme <- lme(data = rats_data, fixed = weight ~ days, random = ~ days | rats)
summary(rats_lme)
```
模型输出结果中,固定效应中的截距项 `(Intercept)` 对应 106.56762,斜率 `days` 对应 6.18571。Stan 模型中截距参数 `alpha0` 的后验估计是 106.332,斜率参数 `beta_c` 的后验估计是 6.188。对比 Stan 和 **nlme** 包的拟合结果,可以发现贝叶斯和频率方法的结果是非常接近的。截距参数 `alpha0` 可以看作小鼠的初始(出生)重量,斜率参数 `beta_c` 可以看作小鼠的生长率 growth rate。
函数 `lme()` 的输出结果中,随机效应的随机截距标准差 10.7425835,对应 `tau_alpha`,表示每个小鼠的截距偏移量的波动。而随机斜率的标准差为 0.5105447,对应 `tau_beta`,相对随机截距标准差来说很小。残差标准差为 6.0146608,对应 `tau_c`,表示与小鼠无关的剩余量的波动,比如测量误差。总之,和 Stan 的结果有所不同,但相去不远。主要是前面的 Stan 模型没有考虑随机截距和随机斜率之间的相关性,这可以进一步调整 [@sorensen2016] 。
```{r}
# 参数的置信区间
intervals(rats_lme, level = 0.95)
```
Stan 输出中,截距项 alpha、斜率项 beta 参数的标准差分别是 `tau_alpha` 和 `tau_beta` ,残差标准差参数 `tau_c` 的估计为 6.1。简单起见,没有考虑截距项和斜率项的相关性,即不考虑小鼠出生时的重量和生长率的相关性,一般来说,应该是有关系的。函数 `lme()` 的输出结果中给出了截距项和斜率项的相关性为 -0.343,随机截距和随机斜率的相关性为 -0.159。
计算与 Stan 输出中的截距项 `alpha_c` 对应的量,结合函数 `lme()` 的输出,截距、斜率加和之后,如下
```{r}
106.56762 + 6.18571 * 22
```
值得注意,Stan 代码中对时间 days 做了中心化处理,即 $x_t - \bar{x}$,目的是降低采样时参数 $\alpha_i$ 和 $\beta_i$ 之间的相关性,而在拟合函数 `lme()` 中没有做处理,因此,结果无需转化,而且更容易解释。
```{r}
fit_lm <- lm(weight ~ days, data = rats_data)
summary(fit_lm)
```
采用简单线性模型即可获得与 **nlme** 包非常接近的估计结果,主要是小鼠重量的分布比较正态,且随时间的变化非常线性。
### lavaan
**lavaan** 包 [@Rosseel2012] 主要是用来拟合结构方程模型,而生长曲线模型可以放在该框架下。所以,也可以用 **lavaan** 包来拟合,并且,它提供的函数 `growth()` 可以直接拟合生长曲线模型。
```{r}
#| message: false
library(lavaan)
# 设置矩阵 y 的列名
colnames(y) <- c("t1","t2","t3","t4","t5")
rats_growt_model <- "
# intercept and slope with fixed coefficients
intercept =~ 1*t1 + 1*t2 + 1*t3 + 1*t4 + 1*t5
days =~ 0*t1 + 1*t2 + 2*t3 + 3*t4 + 4*t5
# if we fix the variances to be equal, the models are now identical.
t1 ~~ resvar*t1
t2 ~~ resvar*t2
t3 ~~ resvar*t3
t4 ~~ resvar*t4
t5 ~~ resvar*t5
"
```
其中,算子符号 `=~` 定义潜变量,`~~` 定义残差协方差,intercept 表示截距, days 表示斜率。假定 5 次测量的测量误差(组内方差)是相同的。拟合模型的代码如下:
```{r}
rats_growth_fit <- growth(rats_growt_model, data = y)
```
提供函数 `summary()` 获得模型输出,结果如下:
```{r}
summary(rats_growth_fit, fit.measures = TRUE)
```
输出结果显示 **lavaan** 包的函数 `growth()` 采用极大似然估计方法。协方差部分 `Covariances:` 随机效应中斜率和截距的协方差。截距部分 `Intercepts:` 对应于混合效应模型的固定效应部分。方差部分 `Variances:` 对应于混合效应模型的随机效应部分,包括残差方差、斜率和截距的方差。不难看出,这和前面 **nlme** 包的输出结果差别很大。原因是 **lavaan** 包将测量的次序从 0 开始计,0 代表小鼠出生后的第 8 天。也就是说,**lavaan** 采用的是次序标记,而不是实际数据。将测量发生的时间(第几天)换算成次序(第几次),并从 0 开始计,则函数 `lme()` 的输出和函数 `growth()` 就一致了。
```{r}
# 重新组织数据
rats_data2 <- data.frame(
weight = as.vector(y),
rats = rep(1:30, times = 5),
days = rep(c(0, 1, 2, 3, 4), each = 30)
)
# ML 方法估计模型参数
rats_lme2 <- lme(data = rats_data2, fixed = weight ~ days, random = ~ days | rats, method = "ML")
summary(rats_lme2)
```
可以看到函数 `growth()` 给出的截距和斜率的协方差估计为 8.444,函数 `lme()` 给出对应截距和斜率的标准差分别是 10.652390 和 3.496588,它们的相关系数为 0.227,则函数 `lme()` 给出的协方差估计为 `10.652390*3.496588*0.227` ,即 8.455,协方差估计比较一致。同理,比较两个输出结果中的其它成分,函数 `growth()` 给出的残差方差估计为 36.176,则残差标准差估计为 6.0146,结合函数 `lme()` 给出的 `Random effects:` 中 `Residual`,结果完全一样。函数 `growth()` 给出的 `Intercepts:` 对应于函数 `lme()` 给出的固定效应部分,结果也是完全一样。
针对模型拟合对象 `rats_growth_fit` ,除了函数 `summary()` 可以汇总结果,**lavaan** 包还提供 `AIC()` 、 `BIC()` 和 `logLik()` 等函数,分别可以提取 AIC、BIC 和对数似然值, `AIC()` 和 `logLik()` 结果与前面的函数 `lme()` 的输出是一样的,而 `BIC()` 不同。
### lme4
当采用 **lme4** 包拟合数据的时候,发现输出结果与 **nlme** 包几乎相同。
```{r}
rats_lme4 <- lme4::lmer(weight ~ days + (days | rats), data = rats_data)
summary(rats_lme4)
```
### glmmTMB
glmmTMB 包基于 Template Model Builder (TMB) ,拟合广义线性混合效应模型,公式语法与 **lme4** 包一致。
```{r}
#| message: false
rats_glmmtmb <- glmmTMB::glmmTMB(weight ~ days + (days | rats), REML = TRUE, data = rats_data)
summary(rats_glmmtmb)
```
结果与 **nlme** 包完全一样。
### MASS
**MASS** 包的结果与前面完全一致。
```{r}
rats_mass <- MASS::glmmPQL(
fixed = weight ~ days, random = ~ days | rats,
data = rats_data, family = gaussian(), verbose = FALSE
)
summary(rats_mass)
```
### spaMM
**spaMM** 包的结果与前面完全一致。
```{r}
#| message: false
rats_spamm <- spaMM::fitme(weight ~ days + (days | rats), data = rats_data)
summary(rats_spamm)
```
``` markdown
--------------- Random effects ---------------
Family: gaussian( link = identity )
--- Random-coefficients Cov matrices:
Group Term Var. Corr.
rats (Intercept) 110.1
rats days 0.2495 -0.1507
# of obs: 150; # of groups: rats, 30
```
随机效应的截距方差 110.1,斜率方差 0.2495,则标准差分别是 10.49 和 0.499,相关性为 -0.1507。
``` markdown
-------------- Residual variance ------------
phi estimate was 36.1755
```
残差方差为 36.1755,则标准差为 6.0146。
### hglm
**hglm** 包 [@rönnegård2010] 可以拟合分层广义线性模型,线性混合效应模型和广义线性混合效应模型,随机效应和响应变量服从的分布可以很广泛,使用语法与 **lme4** 包一样。
```{r}
rats_hglm <- hglm::hglm2(weight ~ days + (days | rats), data = rats_data)
summary(rats_hglm)
```
固定效应的截距和斜率都是和 **nlme** 包的输出结果一致。值得注意,随机效应和模型残差都是以发散参数(Dispersion parameter)来表示的,模型残差方差为 37.09572,则标准差为 6.0906,随机效应的随机截距和随机斜率的方差分别为 103.4501 和 0.2407,则标准差分别为 10.1710 和 0.4906,这与 **nlme** 包的结果也是一致的。
### mgcv
先考虑一个变截距的混合效应模型
$$
y_{ij} = \beta_0 + \beta_1 * x_j + \alpha_i + \epsilon_{ij}, \quad i = 1,2,\ldots,30. \quad j = 1,2,3,4,5
$$
假设随机效应服从独立同正态分布,等价于在似然函数中添加一个岭惩罚。广义可加模型在一定形式下和上述混合效应模型存在等价关系,在广义可加模型中,可以样条表示随机效应。**mgcv** 包拟合代码如下。
```{r}
#| message: false
library(mgcv)
rats_data$rats <- as.factor(rats_data$rats)
rats_gam <- gam(weight ~ days + s(rats, bs = "re"), data = rats_data)
```
其中,参数取值 `bs = "re"` 指定样条类型,re 是 Random effects 的简写。
```{r}
summary(rats_gam)
```
其中,残差的方差 Scale est. = 67.303 ,则标准差为 $\sigma_{\epsilon} = 8.2038$ 。随机效应的标准差如下
```{r}
gam.vcomp(rats_gam, rescale = TRUE)
```
`rescale = TRUE` 表示恢复至原数据的尺度,标准差 $\sigma_{\alpha} = 14.033$。可以看到,固定效应和随机效应的估计结果与 **nlme** 包等完全一致。若考虑变截距和变斜率的混合效应模型,拟合代码如下:
```{r}
rats_gam1 <- gam(
weight ~ days + s(rats, bs = "re") + s(rats, by = days, bs = "re"),
data = rats_data, method = "REML"
)
summary(rats_gam1)
```
输出结果中,固定效应部分的结果和 **nlme** 包完全一样。
```{r}
gam.vcomp(rats_gam1, rescale = TRUE)
```
输出结果中,依次是随机效应的截距、斜率和残差的标准差(标准偏差),和 **nlme** 包给出的结果非常接近。
**mgcv** 包还提供函数 `gamm()`,它将混合效应和固定效应分开,在拟合 LMM 模型时,它类似 **nlme** 包的函数 `lme()`。返回一个含有 lme 和 gam 两个元素的列表,前者包含随机效应的估计,后者是固定效应的估计,固定效应中可以添加样条(或样条表示的简单随机效益,比如本节前面提及的模型)。实际上,函数 `gamm()` 分别调用 **nlme** 包和 **MASS** 包来拟合 LMM 模型和 GLMM 模型。
```{r}
rats_gamm <- gamm(weight ~ days, random = list(rats = ~days), method = "REML", data = rats_data)
# LME
summary(rats_gamm$lme)
# GAM
summary(rats_gamm$gam)
```
## 贝叶斯方法 {#sec-rats-bayesianism}
### rstan {#sec-rats-rstan}
初始化模型参数,设置采样算法的参数。
```{r}
# 迭代链
chains <- 4
# 迭代次数
iter <- 1000
# 初始值
init <- rep(list(list(
alpha = rep(250, 30), beta = rep(6, 30),
alpha_c = 150, beta_c = 10,
tausq_c = 1, tausq_alpha = 1,
tausq_beta = 1
)), chains)
```
接下来,基于重复测量数据,建立线性生长曲线模型:
$$
\begin{aligned}
\alpha_c &\sim \mathcal{N}(0,100) \quad \beta_c \sim \mathcal{N}(0,100) \\
\tau^2_{\alpha} &\sim \mathrm{inv\_gamma}(0.001, 0.001) \\
\tau^2_{\beta} &\sim \mathrm{inv\_gamma}(0.001, 0.001) \\
\tau^2_c &\sim \mathrm{inv\_gamma}(0.001, 0.001) \\
\alpha_n &\sim \mathcal{N}(\alpha_c, \tau_{\alpha}) \quad
\beta_n \sim \mathcal{N}(\beta_c, \tau_{\beta}) \\
y_{nt} &\sim \mathcal{N}(\alpha_n + \beta_n * (x_t - \bar{x}), \tau_c) \\
& n = 1,2,\ldots,N \quad t = 1,2,\ldots,T
\end{aligned}
$$
其中, $\alpha_c,\beta_c,\tau_c,\tau_{\alpha},\tau_{\beta}$ 为无信息先验,$\bar{x} = 22$ 表示第 22 天,$N = 30$ 和 $T = 5$ 分别表示实验中的小鼠数量和测量次数,下面采用 Stan 编码、编译、采样和拟合模型。
```{r}
rats_fit <- stan(
model_name = "rats",
model_code = "
data {
int<lower=0> N;
int<lower=0> T;
vector[T] x;
matrix[N,T] y;
real xbar;
}
parameters {
vector[N] alpha;
vector[N] beta;
real alpha_c;
real beta_c; // beta.c in original bugs model
real<lower=0> tausq_c;
real<lower=0> tausq_alpha;
real<lower=0> tausq_beta;
}
transformed parameters {
real<lower=0> tau_c; // sigma in original bugs model
real<lower=0> tau_alpha;
real<lower=0> tau_beta;
tau_c = sqrt(tausq_c);
tau_alpha = sqrt(tausq_alpha);
tau_beta = sqrt(tausq_beta);
}
model {
alpha_c ~ normal(0, 100);
beta_c ~ normal(0, 100);
tausq_c ~ inv_gamma(0.001, 0.001);
tausq_alpha ~ inv_gamma(0.001, 0.001);
tausq_beta ~ inv_gamma(0.001, 0.001);
alpha ~ normal(alpha_c, tau_alpha); // vectorized
beta ~ normal(beta_c, tau_beta); // vectorized
for (n in 1:N)
for (t in 1:T)
y[n,t] ~ normal(alpha[n] + beta[n] * (x[t] - xbar), tau_c);
}
generated quantities {
real alpha0;
alpha0 = alpha_c - xbar * beta_c;
}
",
data = list(N = N, T = T, y = y, x = x, xbar = xbar),
chains = chains, init = init, iter = iter,
verbose = FALSE, refresh = 0, seed = 20190425
)
```
模型输出结果如下:
```{r}
print(rats_fit, pars = c("alpha", "beta"), include = FALSE, digits = 1)
```
`alpha_c` 表示小鼠 5 次测量的平均重量,`beta_c` 表示小鼠体重的增长率,$\alpha_i,\beta_i$ 分别表示第 $i$ 只小鼠在第 22 天(第 3 次测量或 $x_t = \bar{x}$ )的重量和增长率(每日增加的重量)。
对于分量众多的参数向量,比较适合用岭线图展示后验分布,下面调用 **bayesplot** 包绘制参数向量 $\boldsymbol{\alpha},\boldsymbol{\beta}$ 的后验分布。
```{r}
#| label: fig-rats-alpha
#| fig-cap: 参数 $\boldsymbol{\alpha}$ 的后验分布
#| fig-showtext: true
#| fig-width: 6
#| fig-height: 8
#| message: false
# plot(rats_fit, pars = "alpha", show_density = TRUE, ci_level = 0.8, outer_level = 0.95)
bayesplot::mcmc_areas_ridges(rats_fit, pars = paste0("alpha", "[", 1:30, "]")) +
scale_y_discrete(labels = scales::parse_format())
```
参数向量 $\boldsymbol{\alpha}$ 的后验估计可以看作 $x_t = \bar{x}$ 时小鼠的重量,上图即为各个小鼠重量的后验分布。
```{r}
#| label: fig-rats-beta
#| fig-cap: 参数 $\boldsymbol{\beta}$ 的后验分布
#| fig-showtext: true
#| fig-width: 6
#| fig-height: 8
#| message: false
# plot(rats_fit, pars = "beta", ci_level = 0.8, outer_level = 0.95)
bayesplot::mcmc_areas_ridges(rats_fit, pars = paste0("beta", "[", 1:30, "]")) +
scale_y_discrete(labels = scales::parse_format())
```
参数向量 $\boldsymbol{\beta}$ 的后验估计可以看作是小鼠的重量的增长率,上图即为各个小鼠重量的增长率的后验分布。
### cmdstanr
从 rstan 包转 cmdstanr 包是非常容易的,只要语法兼容,模型代码可以原封不动。
```{r}
#| message: false
library(cmdstanr)
mod_rats <- cmdstan_model(
stan_file = "code/rats.stan",
compile = TRUE, cpp_options = list(stan_threads = TRUE)
)
fit_rats <- mod_rats$sample(
data = list(N = N, T = T, y = y, x = x, xbar = xbar), # 数据
chains = 2, # 总链条数
parallel_chains = 2, # 并行数目
iter_warmup = 1000, # 每条链预处理的迭代次数
iter_sampling = 1000, # 每条链采样的迭代次数
threads_per_chain = 2, # 每条链设置 2 个线程
seed = 20232023, # 随机数种子
show_messages = FALSE, # 不显示消息
adapt_delta = 0.9, # 接受率
refresh = 0 # 不显示采样迭代的进度
)
```
模型输出
```{r}
# 显示除了参数 alpha 和 beta 以外的结果
vars <- setdiff(fit_rats$metadata()$stan_variables, c("alpha", "beta"))
fit_rats$summary(variables = vars)
```
诊断信息
```{r}
fit_rats$diagnostic_summary()
```
### brms
**brms** 包是基于 **rstan** 包的,基于 Stan 语言做贝叶斯推断,提供与 lme4 包一致的公式语法,且扩展了模型种类。
```{r}
#| eval: false
rats_brms <- brms::brm(weight ~ days + (days | rats), data = rats_data)
summary(rats_brms)
```
``` markdown
Family: gaussian
Links: mu = identity; sigma = identity
Formula: weight ~ days + (days | rats)
Data: rats_data (Number of observations: 150)
Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
total post-warmup draws = 4000
Group-Level Effects:
~rats (Number of levels: 30)
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sd(Intercept) 11.27 2.23 7.36 16.08 1.00 2172 2939
sd(days) 0.54 0.09 0.37 0.74 1.00 1380 2356
cor(Intercept,days) -0.11 0.24 -0.53 0.39 1.00 920 1541
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept 106.47 2.47 101.61 111.23 1.00 2173 2768
days 6.18 0.11 5.96 6.41 1.00 1617 2177
Family Specific Parameters:
Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma 6.15 0.47 5.30 7.14 1.00 1832 3151
Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```
### rstanarm
**rstanarm** 包与 **brms** 包类似,区别是前者预编译了 Stan 模型,后者根据输入数据和模型编译即时编译,此外,后者支持的模型范围更加广泛。
```{r}
#| eval: false
library(rstanarm)
rats_rstanarm <- stan_lmer(formula = weight ~ days + (days | rats), data = rats_data)
summary(rats_rstanarm)
```
``` markdown
Model Info:
function: stan_lmer
family: gaussian [identity]
formula: weight ~ days + (days | rats)
algorithm: sampling
sample: 4000 (posterior sample size)
priors: see help('prior_summary')
observations: 150
groups: rats (30)
Estimates:
mean sd 10% 50% 90%
(Intercept) 106.575 2.236 103.789 106.559 109.415
days 6.187 0.111 6.048 6.185 6.329
sigma 6.219 0.497 5.626 6.183 6.862
Sigma[rats:(Intercept),(Intercept)] 103.927 42.705 57.329 98.128 159.086
Sigma[rats:days,(Intercept)] -0.545 1.492 -2.361 -0.402 1.162
Sigma[rats:days,days] 0.304 0.112 0.181 0.285 0.445
MCMC diagnostics
mcse Rhat n_eff
(Intercept) 0.043 1.000 2753
days 0.003 1.005 1694
sigma 0.015 1.001 1172
Sigma[rats:(Intercept),(Intercept)] 1.140 1.000 1403
Sigma[rats:days,(Intercept)] 0.054 1.006 772
Sigma[rats:days,days] 0.003 1.000 1456
For each parameter, mcse is Monte Carlo standard error,
n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor
on split chains (at convergence Rhat=1).
```