Phân tích before-after | repeated measures

Author

Duc Nguyen

Published

October 29, 2026

1 Dataset

Dữ liệu được sử dụng theo ví dụ minh họa trang 78 Verma, J. P. Repeated Measures Design for Empirical Researchers, 2016.

Bố trí thí nghiệm theo dõi tác động của việc “ngồi thiền/meditation” qua 6 tuần (biến yếu tố thời gian) đến “khả năng suy luận/reasoning ability” (biến outcome) của 10 người tham gia (biến ngẫu nhiên).

Dữ liệu có:

10 đối tượng: S1–S10

4 lần đo: week_0, week_2, week_4, week_6

Cùng một đối tượng được đo lặp lại ở cả 4 thời điểm.

library(readxl)
df <- read_excel("data_oneway_anova_repeated.xlsx")

df <- as.data.frame(df)

df
   subject week_0 week_2 week_4 week_6
1       S1     31     36     35     37
2       S2     31     34     34     35
3       S3     32     31     37     35
4       S4     30     32     36     35
5       S5     34     33     37     37
6       S6     35     34     36     38
7       S7     36     31     31     38
8       S8     36     35     30     40
9       S9     32     31     35     36
10     S10     33     32     34     36

Lấy factor cột subject

df$subject <- factor(df$subject,
                     levels = c("S1", "S2", "S3", "S4", "S5",
                                "S6", "S7", "S8", "S9", "S10"))

Chuyển về dạng long

library(dplyr)
library(tidyr)

df %>% tidyr:::pivot_longer(
  
  cols = 2:5,
  names_to = "week",
  values_to = "score"
  
) -> df_long

df_long <- as.data.frame(df_long)

df_long |> dplyr:::arrange(subject, week) -> df_long

df_long
   subject   week score
1       S1 week_0    31
2       S1 week_2    36
3       S1 week_4    35
4       S1 week_6    37
5       S2 week_0    31
6       S2 week_2    34
7       S2 week_4    34
8       S2 week_6    35
9       S3 week_0    32
10      S3 week_2    31
11      S3 week_4    37
12      S3 week_6    35
13      S4 week_0    30
14      S4 week_2    32
15      S4 week_4    36
16      S4 week_6    35
17      S5 week_0    34
18      S5 week_2    33
19      S5 week_4    37
20      S5 week_6    37
21      S6 week_0    35
22      S6 week_2    34
23      S6 week_4    36
24      S6 week_6    38
25      S7 week_0    36
26      S7 week_2    31
27      S7 week_4    31
28      S7 week_6    38
29      S8 week_0    36
30      S8 week_2    35
31      S8 week_4    30
32      S8 week_6    40
33      S9 week_0    32
34      S9 week_2    31
35      S9 week_4    35
36      S9 week_6    36
37     S10 week_0    33
38     S10 week_2    32
39     S10 week_4    34
40     S10 week_6    36

2 Mô tả dữ liệu

df_long |> dplyr:::group_by(week) |> 
  dplyr:::summarise(mean_score = mean(score),
                    sd_score = sd(score),
                    count = n()) |> as.data.frame() -> df_sum

df_sum$se_score <- df_sum$sd_score / sqrt(df_sum$count) 

df_sum$note <- "subject_mean"

df_sum
    week mean_score sd_score count  se_score         note
1 week_0       33.0 2.160247    10 0.6831301 subject_mean
2 week_2       32.9 1.791957    10 0.5666667 subject_mean
3 week_4       34.5 2.368778    10 0.7490735 subject_mean
4 week_6       36.7 1.636392    10 0.5174725 subject_mean
library(ggplot2)

ggplot() +
  
  geom_line(data = df_long,
       mapping = aes(x = week,
                     y = score,
                     group = subject,
                     color = subject),
       alpha = 0.2,
       lwd = 2) +
  
 
  
  geom_point(data = df_sum,
            mapping = aes(x = week,
                          y = mean_score),
            size = 3,
            color = "blue") +
  
  geom_line(data = df_sum,
            mapping = aes(x = week,
                          y = mean_score,
                          group = note),
            color = "blue",
            alpha = 0.5,
            lwd = 2) +
  
  geom_errorbar(data = df_sum,
            mapping = aes(x = week,
                          ymin = mean_score - se_score,
                          ymax = mean_score + se_score,
                          group = note),
            color = "blue",
            alpha = 0.9,
            lwd = 1,
            width = 0.2) +
  
  guides(color = guide_legend(override.aes = list(alpha = 1,
                                                  lwd = 1))) +
  
  theme_bw(base_size = 16) +
  
  theme(axis.text = element_text(face = "bold", color = "black"),
        axis.title = element_text(face = "bold", color = "black"),
       
        legend.text = element_text(face = "bold", color = "black"),
        legend.title = element_text(face = "bold", color = "black")) +
  
  theme(legend.key.width = unit(1.5, "cm")) +
  
  labs(title = "Chỉ tiêu theo dõi thay đổi theo thời gian",
       subtitle = "Mean ± SE") +
  
   geom_point(data = df_long,
       mapping = aes(x = week,
                     y = score,
                     group = subject),
       position = position_jitter(height = 0,
                                  width = 0),
       color = "red",
       alpha = 0.6) 

3 Mở đầu

ANOVA thông thường (one-way ANOVA) là dạng thiết kế nghiên cứu để so sánh giữa các nhóm đối tượng với nhau, mỗi đối tượng chỉ đo một lần, không lặp lại (between-subjects design).

Repeated measures one-way ANOVA là dạng thiết kế nghiên cứu theo dõi cùng 1 đối tượng ở các thời điểm khác nhau (repeated measures), có đặc điểm là mỗi đối tượng tự làm nhóm đối chứng cho chính mình, loại bỏ được phần biến thiên giữa các cá nhân và chỉ kiểm định đúng sự thay đổi trong nội bộ từng người (within-subjects design).

Khi xét ở 2 thời điểm, repeated measures ANOVA và paired t-test cho kết quả giống hệt nhau, vì bản chất repeated measures ANOVA là paired t-test cho trường hợp so sánh từ 2 thời điểm trở lên.

Ưu và nhược của thiết kế này link.

4 Kiểm tra giả định

Tham khảo #1

4.1 Giả định về phân bố chuẩn

Mỗi tuần ta đều khảo sát trên 10 người, ta muốn đánh giá xem trung bình chỉ số “suy luận” có thay đổi theo từng tuần này không. Như vậy “week” là biến yếu tố. Ta xem xét dữ liệu ở từng tuần có tuân theo phân bố chuẩn hay không.

Dùng kiểm định Shapiro-Wilk Normality Test

library(rstatix)

df_long %>% dplyr:::group_by(week) %>% rstatix:::shapiro_test(score) |> as.data.frame()
    week variable statistic         p
1 week_0    score 0.9241773 0.3931157
2 week_2    score 0.9046545 0.2462523
3 week_4    score 0.8794111 0.1284765
4 week_6    score 0.9016076 0.2281626
library(MVN)

MVN:::mvn(df[, -1])
$multivariate_normality
           Test Statistic p.value     Method      MVN
1 Henze-Zirkler     0.736   0.148 asymptotic ✓ Normal

$univariate_normality
              Test Variable Statistic p.value Normality
1 Anderson-Darling   week_0     0.318   0.475  ✓ Normal
2 Anderson-Darling   week_2     0.389   0.313  ✓ Normal
3 Anderson-Darling   week_4     0.515   0.144  ✓ Normal
4 Anderson-Darling   week_6     0.392   0.307  ✓ Normal

$descriptives
  Variable  n Mean Std.Dev Median Min Max  25th  75th   Skew Kurtosis
1   week_0 10 33.0   2.160   32.5  30  36 31.25 34.75  0.209    1.667
2   week_2 10 32.9   1.792   32.5  31  36 31.25 34.00  0.401    1.857
3   week_4 10 34.5   2.369   35.0  30  37 34.00 36.00 -0.846    2.543
4   week_6 10 36.7   1.636   36.5  35  40 35.25 37.75  0.667    2.580

$data
   week_0 week_2 week_4 week_6
1      31     36     35     37
2      31     34     34     35
3      32     31     37     35
4      30     32     36     35
5      34     33     37     37
6      35     34     36     38
7      36     31     31     38
8      36     35     30     40
9      32     31     35     36
10     33     32     34     36

$subset
NULL

$outlierMethod
[1] "none"

attr(,"class")
[1] "mvn"

p-value > 0.05 là dữ liệu có phân bố chuẩn.

Dùng đồ thị QQ plot

library(ggplot2)
library(qqplotr)
ggplot(data = df_long,
       mapping = aes(sample = score, 
                     color = week, 
                     fill = week)) +
  
    qqplotr:::stat_qq_band(alpha = 0.2) +
  
    qqplotr:::stat_qq_line() +
  
    qqplotr:::stat_qq_point() +

    facet_wrap(~ week,
               scale = "free",
               nrow = 2,
               ncol = 2) +
  
  theme_bw()

Hầu như các điểm dữ liệu đều nằm trong vùng 95% CI của đường reference line nên dữ liệu phân bố chuẩn.

Kiểm tra outlier

library(rstatix)

df_long %>% dplyr:::group_by(week) %>% rstatix:::identify_outliers(score) |> as.data.frame()
    week subject score is.outlier is.extreme
1 week_4      S8    30       TRUE      FALSE

Có 1 giá trị được xem như outlier. Ta có thể giữ lại giá trị này hoặc loại bỏ, tùy vào quyết định chuyên môn. Ở đây ta vẫn giữ lại giá trị này.