Kiểm định thống kê so sánh giữa các nhóm

Author

Duc Nguyen | tuhocr.com

Published

October 29, 2026

Tài liệu biên soạn trong chuyên đề R Statistics

1 Import dataset

Sử dụng dataset students.csv, chọn những bạn đã tốt nghiệp (graduated == 1).

students <- read.csv("students.csv")

df <- students[ , -c(1,3,8,10,12)]

df <- df |> subset(graduated == 1)

row.names(df) <- NULL
# head(df)

1.1 Chia group theo độ tuổi

library(dplyr)

df |> dplyr:::mutate(age_group = case_when(
  
  age >= 30 ~ ">= 30",
  
  age < 30 ~ "< 30",
  
  .default = NA
  
  )) -> df

df$age_group <- factor(df$age_group)

1.2 Subset 50 bạn trong nhóm tuổi

id_upper_30 <- which(df$age_group == ">= 30")

set.seed(1)
sample(x = id_upper_30,
       size = 50,
       replace = F) -> id_upper_30_ok

id_upper_30_ok
 [1] 1486  792   10  700  888  295 1264 1009  470 1193 1573  113  724 1548 1661  904  687  712  866  781  962  434  565 1388 1250 1522 1238  500   91  816 1628
[32]  550 1237 1705 1430 1077  529  658 1637   46  382  492  935 1401 1691  969 1298 1625  180  373
id_under_30 <- which(df$age_group == "< 30")

set.seed(1)
sample(x = id_under_30,
       size = 50,
       replace = F) -> id_under_30_ok

id_under_30_ok
 [1] 1068  714  137  979 1609  493  313  283 1270 1395  628 1362 1593  344 1697   38 1182  766  923  508  919  401 1708 1026  842  895  980  340 1740  583 1514
[32]  119 1496  558 1606  585  934  358  612 1200   41  564  394  260 1281  397   40  455  851 1481
df_sample <- df[ c(id_upper_30_ok, id_under_30_ok) , ]
row.names(df_sample) <- NULL
# aggregate(salary ~ age_group,
#           data = df_sample,
#           FUN = mean)
# điều chỉnh dữ liệu cho nhóm upper luôn cao hơn under
set.seed(1)
df_sample$salary[which(df_sample$age_group == ">= 30")] <- df_sample$salary[which(df_sample$age_group == ">= 30")] + 1500
df_sample <- df_sample[ , -11]

df_sample$ID <- c(1:length(id_upper_30_ok),
                  1:length(id_under_30_ok))

dim(df_sample)
[1] 100  13

2 Independent two-sample | So sánh giữa hai nhóm độc lập

2.1 Parametric independent two-sample t-test

2.1.1 Check normality | Kiểm tra giả thuyết phân bố chuẩn

H0: normality (p-value >= 0.05)

H1: NOT normality (p-value < 0.05)

upper_30 <- df_sample$salary[which(df_sample$age_group == ">= 30")]

under_30 <- df_sample$salary[which(df_sample$age_group == "< 30")]
library(car)

car:::qqPlot(upper_30)

[1] 38 23
car:::qqPlot(under_30)

[1] 33 35
shapiro.test(upper_30)

    Shapiro-Wilk normality test

data:  upper_30
W = 0.96657, p-value = 0.1673
ks.test(x = jitter(upper_30),
        y = "pnorm",
        mean = mean(upper_30),
        sd = sd(upper_30))

    Exact one-sample Kolmogorov-Smirnov test

data:  jitter(upper_30)
D = 0.1036, p-value = 0.6192
alternative hypothesis: two-sided
library(nortest)
nortest:::lillie.test(upper_30)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  upper_30
D = 0.10354, p-value = 0.1987
nortest:::ad.test(upper_30)

    Anderson-Darling normality test

data:  upper_30
A = 0.80719, p-value = 0.03421
shapiro.test(under_30)

    Shapiro-Wilk normality test

data:  under_30
W = 0.97963, p-value = 0.5366
ks.test(x = jitter(under_30),
        y = "pnorm",
        mean = mean(under_30),
        sd = sd(under_30))

    Exact one-sample Kolmogorov-Smirnov test

data:  jitter(under_30)
D = 0.073134, p-value = 0.934
alternative hypothesis: two-sided
library(nortest)
nortest:::lillie.test(under_30)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  under_30
D = 0.073247, p-value = 0.7217
nortest:::ad.test(under_30)

    Anderson-Darling normality test

data:  under_30
A = 0.37747, p-value = 0.3961

2.1.2 Check variance homogeneity | Kiểm tra giả thuyết phương sai đồng nhất

H0: phương sai giữa các nhóm bằng nhau

H1: có ít nhất một nhóm có phương sai khác biệt

# kiểm định giữa 2 nhóm
# phổ biến nhất, không yêu cầu 2 vector bằng nhau
stats:::var.test(upper_30,
                 under_30)

    F test to compare two variances

data:  upper_30 and under_30
F = 1.0588, num df = 49, denom df = 49, p-value = 0.8424
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
 0.6008322 1.8657694
sample estimates:
ratio of variances 
           1.05878 
stats:::var.test(salary ~ age_group,
                 data = df_sample)

    F test to compare two variances

data:  salary by age_group
F = 0.94448, num df = 49, denom df = 49, p-value = 0.8424
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
 0.5359719 1.6643581
sample estimates:
ratio of variances 
         0.9444835 
# kiểm định giữa 2 hay nhiều nhóm
# mạnh khi dữ liệu có phân phối chuẩn
# nhạy với vi phạm giả định chuẩn
stats:::bartlett.test(df_sample$salary, # vector numeric
                      df_sample$age_group) # vector categorical

    Bartlett test of homogeneity of variances

data:  df_sample$salary and df_sample$age_group
Bartlett's K-squared = 0.039555, df = 1, p-value = 0.8424
# kiểm định giữa 2 hay nhiều nhóm
# phù hợp khi dữ liệu chuẩn và không chuẩn
library(car)
# dữ liệu phân bố chuẩn
car::leveneTest(df_sample$salary,
                df_sample$age_group,
                center = "mean")
Levene's Test for Homogeneity of Variance (center = "mean")
      Df F value Pr(>F)
group  1  0.7997 0.3734
      98               
# dữ liệu lệch chuẩn Brown–Forsythe test (biến thể của Levene's test)
car::leveneTest(df_sample$salary,
                df_sample$age_group,
                center = "median")
Levene's Test for Homogeneity of Variance (center = "median")
      Df F value Pr(>F)
group  1  0.5789 0.4486
      98               
# kiểm định giữa 2 hay nhiều nhóm
# khi dữ liệu không có phân bố chuẩn
# Performs a Fligner-Killeen (median) test of the null that the variances in each of the groups (samples) are the same.
stats::fligner.test(df_sample$salary,
                   df_sample$major)

    Fligner-Killeen test of homogeneity of variances

data:  df_sample$salary and df_sample$major
Fligner-Killeen:med chi-squared = 16.767, df = 5, p-value = 0.004964

2.1.3 Vẽ đồ thị boxplot

boxplot(upper_30, under_30,
        las = 1,
        names = c("upper_30", "under_30"),
        main = "Salary | upper_30 vs under_30")

2.1.4 Kiểm định two-sample t-test

2.1.4.1 Dữ liệu phân bố chuẩn và phương sai đồng nhất

NORMALITY | VARIANCE HOMOGENEITY (EQUAL VARIANCE)

# TWO-TAILS
# H0: trung binh upper_30 = trung binh under_30
# H1: trung binh upper_30 ≠ trung binh under_30

t.test(upper_30,
       under_30,
       var.equal = TRUE,
       paired = FALSE,
       alternative = "two.sided") 

    Two Sample t-test

data:  upper_30 and under_30
t = 2.2423, df = 98, p-value = 0.0272
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
  538.9874 8836.1662
sample estimates:
mean of x mean of y 
 45855.14  41167.57 
# LEFT-TAIL
# H0: trung binh upper_30 ≥ trung binh under_30
# H1: trung binh upper_30 < trung binh under_30

t.test(upper_30,
       under_30,
       var.equal = TRUE,
       paired = FALSE,
       alternative = "less")

    Two Sample t-test

data:  upper_30 and under_30
t = 2.2423, df = 98, p-value = 0.9864
alternative hypothesis: true difference in means is less than 0
95 percent confidence interval:
     -Inf 8159.009
sample estimates:
mean of x mean of y 
 45855.14  41167.57 
# RIGHT-TAIL
# H0: trung binh upper_30 ≤ trung binh under_30
# H1: trung binh upper_30 > trung binh under_30

t.test(upper_30,
       under_30,
       var.equal = TRUE,
       paired = FALSE,
       alternative = "greater")

    Two Sample t-test

data:  upper_30 and under_30
t = 2.2423, df = 98, p-value = 0.0136
alternative hypothesis: true difference in means is greater than 0
95 percent confidence interval:
 1216.144      Inf
sample estimates:
mean of x mean of y 
 45855.14  41167.57 

2.1.4.2 Dữ liệu phân bố chuẩn và phương sai không đồng nhất

NORMALITY and VARIANCE HETEROSKEDASTICITY (UNEQUAL VARIANCE)

Sử dụng t-test cải tiến | Welch Two Sample t-test

t.test(upper_30,
       under_30,
       var.equal = FALSE,
       paired = FALSE,
       alternative = "two.sided")

    Welch Two Sample t-test

data:  upper_30 and under_30
t = 2.2423, df = 97.92, p-value = 0.0272
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
  538.9451 8836.2085
sample estimates:
mean of x mean of y 
 45855.14  41167.57 

2.1.4.3 Effect size

Compute the effect size for t-test. T-test conventional effect sizes, proposed by Cohen, are: 0.2 (small effect), 0.5 (moderate effect) and 0.8 (large effect).

library(rstatix)
rstatix:::cohens_d(salary ~ age_group,
                      data = df_sample,
                   var.equal = FALSE,
                   paired = FALSE)
# A tibble: 1 × 7
  .y.    group1 group2 effsize    n1    n2 magnitude
* <chr>  <chr>  <chr>    <dbl> <int> <int> <ord>    
1 salary < 30   >= 30   -0.448    50    50 small    
library(effectsize)

effectsize:::cohens_d(
  upper_30,
  under_30,
  paired = F
)
Cohen's d |       95% CI
------------------------
0.45      | [0.05, 0.84]

- Estimated using pooled SD.

2.2 Nonparametric

2.2.1 Independent two-sample Mann–Whitney U test

Mann–Whitney U test (aka Wilcoxon Rank Sum test) compares two independent groups. Dành cho dữ liệu không đạt phân phối chuẩn (gần chuẩn).

Nonparametric không yêu cầu giả định NORMALITY và VARIANCE HOMOGENEITY.

wilcox.test(upper_30,
       under_30,
       paired = FALSE,
       alternative = "two.sided")

    Wilcoxon rank sum test with continuity correction

data:  upper_30 and under_30
W = 1563, p-value = 0.03122
alternative hypothesis: true location shift is not equal to 0
library(rstatix)
# nếu chỉ có so sánh giữa 2 nhóm thì không cần adjust p-value
rstatix:::wilcox_test(salary ~ age_group,
                      data = df_sample,
                      paired = FALSE,
                      p.adjust.method = "none",
                      alternative = "two.sided") 
# A tibble: 1 × 7
  .y.    group1 group2    n1    n2 statistic      p
* <chr>  <chr>  <chr>  <int> <int>     <dbl>  <dbl>
1 salary < 30   >= 30     50    50       937 0.0312

2.2.2 Effect size

The r value varies from 0 to close to 1. The interpretation values for r commonly in published litterature and on the internet are: 0.10 - < 0.3 (small effect), 0.30 - < 0.5 (moderate effect) and >= 0.5 (large effect).

rstatix:::wilcox_effsize(salary ~ age_group,
                      data = df_sample,
                      paired = FALSE)
# A tibble: 1 × 7
  .y.    group1 group2 effsize    n1    n2 magnitude
* <chr>  <chr>  <chr>    <dbl> <int> <int> <ord>    
1 salary < 30   >= 30    0.216    50    50 small    

2.2.3 Independent two-sample Mood’s Median Test

Performs a Mood’s median test to compare medians of independent samples

# https://rcompanion.org/handbook/F_09.html
library(RVAideMemoire)
RVAideMemoire:::mood.medtest(salary ~ age_group,
                      data = df_sample,            
                      exact = FALSE)

    Mood's median test

data:  salary by age_group
X-squared = 1.96, df = 1, p-value = 0.1615

Testing the equality of the distributions of a numeric response variable in two or more independent groups against shift alternatives.

library(coin)

coin:::median_test(salary ~ age_group,
                      data = df_sample)

    Asymptotic Two-Sample Brown-Mood Median Test

data:  salary by age_group (< 30, >= 30)
Z = -1.592, p-value = 0.1114
alternative hypothesis: true mu is not equal to 0

2.3 Áp dụng package

library(ggstatsplot)
library(tidyverse)

set.seed(20)

ggstatsplot:::ggbetweenstats(data = df_sample,
                     x = age_group,
                     y = salary,
                     type = "parametric",
                     var.equal = TRUE,
                     plot.type = "box",
                     pairwise.comparisons = T,
                     pairwise.display = "all",
                     p.adjust.method = "none",
                     centrality.plotting = T,
                     centrality.path = T,
                     digits = 4L,
                       centrality.point.args = list(size = 4, color = "darkred"),
                     centrality.label.args = list(size = 4,
                                                  nudge_x = 0.4,
                                                  segment.linetype = 4),
                     ggtheme = theme_classic(base_size = 12),
                     bf.message = T,
                     results.subtitle = T,
                     caption = "",
                     ggsignif.args = list(size = 0.5,
                                          vjust = 0,
                                          step_increase = 0.1,
                                          textsize = 5),
                     point.args = list(position = ggplot2::position_jitterdodge(dodge.width = 0.6),
                                       alpha = 0.5,
                                       size = 6,
                                       stroke = 0,
                                       na.rm = TRUE)
                     ) 

library(dabestr)

two_groups_unpaired <- dabestr:::load(df_sample,
  x = age_group, y = salary, 
  paired = NULL,
  idx = c("< 30", ">= 30")
)

two_groups_unpaired.mean_diff <-  dabestr::cohens_d(two_groups_unpaired)


print(two_groups_unpaired.mean_diff)
DABESTR v2025.3.15
==================

Good evening!
The current time is 23:02 PM on Thursday October 29, 2026.

The character(0) Cohen's d between >= 30 and < 30 is 0.448 [95%CI 0.054, 0.851].
The p-value of the two-sided permutation t-test is 0.0272, calculated for legacy purposes only.

5000 bootstrap samples were taken; the confidence interval is bias-corrected and accelerated.
Any p-value reported is the probability of observing the effect size (or greater),
assuming the null hypothesis of zero difference is true.
For each p-value, 5000 reshuffles of the control and test labels were performed.
dabest_plot(two_groups_unpaired.mean_diff)

https://rpubs.com/lengockhanhi/454078

3 Dependent two sample

3.1 Parametric paired t-test

t.test(df_sample$score1,
       df_sample$score2,
       var.equal = TRUE,
       paired = TRUE,
       alternative = "two.sided") 

    Paired t-test

data:  df_sample$score1 and df_sample$score2
t = -1.2863, df = 99, p-value = 0.2014
alternative hypothesis: true mean difference is not equal to 0
95 percent confidence interval:
 -2.2629386  0.4829386
sample estimates:
mean difference 
          -0.89 
t.test(df_sample$score1 - df_sample$score2,
       mu = 0,
       alternative = "two.sided") 

    One Sample t-test

data:  df_sample$score1 - df_sample$score2
t = -1.2863, df = 99, p-value = 0.2014
alternative hypothesis: true mean is not equal to 0
95 percent confidence interval:
 -2.2629386  0.4829386
sample estimates:
mean of x 
    -0.89 
# head(df_sample)

df_score <- df_sample[ , c("stud.id", "score1", "score2")]
df_score
    stud.id score1 score2
1    127329     71     73
2    699675     69     59
3    373120     56     52
4    645027     60     48
5    887121     81     73
6    263680     74     64
7    644535     90     85
8    920111     85     88
9    534172     68     73
10   152383     54     45
11   165588     67     77
12   358961     93     90
13   854188     71     63
14   355955     50     59
15   684695     55     63
16   526849     59     53
17   731560     74     69
18   728977     76     83
19   244176     87     93
20   337141     72     73
21   714747     70     79
22   245697     70     77
23   606531     49     58
24   149335     74     70
25   281483     76     62
26   195794     47     55
27   488797     83     78
28   451408     73     74
29   729441     60     57
30   587073     58     60
31   195357     40     48
32   863615     72     73
33   219503     51     57
34   551154     78     83
35   672951     81     90
36   969474     74     81
37   831973     74     80
38   364152     85     87
39   287337     92     89
40   192380     90     92
41   970464     86     90
42   383269     56     57
43   597038     86     80
44   141917     64     49
45   885855     74     68
46   906012     59     59
47   553535     51     51
48   456353     65     63
49   654954     83     89
50   988603     69     68
51   253803     67     72
52   595839     66     60
53   711793     63     54
54   429789     52     44
55   130608     48     53
56   461293     67     68
57   448938     90     86
58   809408     70     69
59   529799     84     86
60   786487     88     88
61   918215     90     94
62   447520     72     78
63   859275     76     78
64   861582     88     86
65   394142     79     76
66   595769     78     77
67   846250     67     79
68   747054     72     86
69   749611     58     51
70   748309     85     90
71   139052     42     40
72   921795     46     62
73   869910     61     54
74   678552     61     69
75   520250     61     80
76   819078     36     51
77   371542     67     74
78   122995     78     76
79   565624     66     80
80   350085     59     52
81   648615     85     86
82   621166     73     69
83   760133     81     74
84   211707     74     76
85   320119     61     56
86   249763     73     66
87   181738     75     65
88   707668     75     74
89   111998     64     72
90   869321     73     75
91   612668     55     64
92   685552     65     70
93   726881     56     53
94   288990     85     87
95   681749     67     77
96   923689     59     61
97   390559     78     83
98   954089     57     57
99   901358     75     79
100  160166     81     76
###
df_score1 <-  df_sample[ , c("stud.id", "score1")]

names(df_score1) <- c("id", "score")

df_score1$group <- "hk1"

# df_score1

###
df_score2 <-  df_sample[ , c("stud.id", "score2")]

names(df_score2) <- c("id", "score")

df_score2$group <- "hk2"

# df_score2

df_long <- rbind(df_score1,
                 df_score2)

df_long
        id score group
1   127329    71   hk1
2   699675    69   hk1
3   373120    56   hk1
4   645027    60   hk1
5   887121    81   hk1
6   263680    74   hk1
7   644535    90   hk1
8   920111    85   hk1
9   534172    68   hk1
10  152383    54   hk1
11  165588    67   hk1
12  358961    93   hk1
13  854188    71   hk1
14  355955    50   hk1
15  684695    55   hk1
16  526849    59   hk1
17  731560    74   hk1
18  728977    76   hk1
19  244176    87   hk1
20  337141    72   hk1
21  714747    70   hk1
22  245697    70   hk1
23  606531    49   hk1
24  149335    74   hk1
25  281483    76   hk1
26  195794    47   hk1
27  488797    83   hk1
28  451408    73   hk1
29  729441    60   hk1
30  587073    58   hk1
31  195357    40   hk1
32  863615    72   hk1
33  219503    51   hk1
34  551154    78   hk1
35  672951    81   hk1
36  969474    74   hk1
37  831973    74   hk1
38  364152    85   hk1
39  287337    92   hk1
40  192380    90   hk1
41  970464    86   hk1
42  383269    56   hk1
43  597038    86   hk1
44  141917    64   hk1
45  885855    74   hk1
46  906012    59   hk1
47  553535    51   hk1
48  456353    65   hk1
49  654954    83   hk1
50  988603    69   hk1
51  253803    67   hk1
52  595839    66   hk1
53  711793    63   hk1
54  429789    52   hk1
55  130608    48   hk1
56  461293    67   hk1
57  448938    90   hk1
58  809408    70   hk1
59  529799    84   hk1
60  786487    88   hk1
61  918215    90   hk1
62  447520    72   hk1
63  859275    76   hk1
64  861582    88   hk1
65  394142    79   hk1
66  595769    78   hk1
67  846250    67   hk1
68  747054    72   hk1
69  749611    58   hk1
70  748309    85   hk1
71  139052    42   hk1
72  921795    46   hk1
73  869910    61   hk1
74  678552    61   hk1
75  520250    61   hk1
76  819078    36   hk1
77  371542    67   hk1
78  122995    78   hk1
79  565624    66   hk1
80  350085    59   hk1
81  648615    85   hk1
82  621166    73   hk1
83  760133    81   hk1
84  211707    74   hk1
85  320119    61   hk1
86  249763    73   hk1
87  181738    75   hk1
88  707668    75   hk1
89  111998    64   hk1
90  869321    73   hk1
91  612668    55   hk1
92  685552    65   hk1
93  726881    56   hk1
94  288990    85   hk1
95  681749    67   hk1
96  923689    59   hk1
97  390559    78   hk1
98  954089    57   hk1
99  901358    75   hk1
100 160166    81   hk1
101 127329    73   hk2
102 699675    59   hk2
103 373120    52   hk2
104 645027    48   hk2
105 887121    73   hk2
106 263680    64   hk2
107 644535    85   hk2
108 920111    88   hk2
109 534172    73   hk2
110 152383    45   hk2
111 165588    77   hk2
112 358961    90   hk2
113 854188    63   hk2
114 355955    59   hk2
115 684695    63   hk2
116 526849    53   hk2
117 731560    69   hk2
118 728977    83   hk2
119 244176    93   hk2
120 337141    73   hk2
121 714747    79   hk2
122 245697    77   hk2
123 606531    58   hk2
124 149335    70   hk2
125 281483    62   hk2
126 195794    55   hk2
127 488797    78   hk2
128 451408    74   hk2
129 729441    57   hk2
130 587073    60   hk2
131 195357    48   hk2
132 863615    73   hk2
133 219503    57   hk2
134 551154    83   hk2
135 672951    90   hk2
136 969474    81   hk2
137 831973    80   hk2
138 364152    87   hk2
139 287337    89   hk2
140 192380    92   hk2
141 970464    90   hk2
142 383269    57   hk2
143 597038    80   hk2
144 141917    49   hk2
145 885855    68   hk2
146 906012    59   hk2
147 553535    51   hk2
148 456353    63   hk2
149 654954    89   hk2
150 988603    68   hk2
151 253803    72   hk2
152 595839    60   hk2
153 711793    54   hk2
154 429789    44   hk2
155 130608    53   hk2
156 461293    68   hk2
157 448938    86   hk2
158 809408    69   hk2
159 529799    86   hk2
160 786487    88   hk2
161 918215    94   hk2
162 447520    78   hk2
163 859275    78   hk2
164 861582    86   hk2
165 394142    76   hk2
166 595769    77   hk2
167 846250    79   hk2
168 747054    86   hk2
169 749611    51   hk2
170 748309    90   hk2
171 139052    40   hk2
172 921795    62   hk2
173 869910    54   hk2
174 678552    69   hk2
175 520250    80   hk2
176 819078    51   hk2
177 371542    74   hk2
178 122995    76   hk2
179 565624    80   hk2
180 350085    52   hk2
181 648615    86   hk2
182 621166    69   hk2
183 760133    74   hk2
184 211707    76   hk2
185 320119    56   hk2
186 249763    66   hk2
187 181738    65   hk2
188 707668    74   hk2
189 111998    72   hk2
190 869321    75   hk2
191 612668    64   hk2
192 685552    70   hk2
193 726881    53   hk2
194 288990    87   hk2
195 681749    77   hk2
196 923689    61   hk2
197 390559    83   hk2
198 954089    57   hk2
199 901358    79   hk2
200 160166    76   hk2
p1 <- ggstatsplot:::ggwithinstats(data = df_long,
                     x = group,
                     y = score,
                     type = "parametric",
                     var.equal = TRUE,
                     plot.type = "box",
                     pairwise.comparisons = T,
                     pairwise.display = "all",
                     p.adjust.method = "none",
                     centrality.plotting = T,
                     centrality.path = T,
                     digits = 4L,
                     ggtheme = theme_classic(base_size = 12),
                     bf.message = T,
                     results.subtitle = T,
                     caption = "",
                     centrality.point.args = list(size = 4, color = "darkred"),
                     centrality.label.args = list(size = 4,
                                                  nudge_x = 0.4,
                                                  segment.linetype = 4),
                     ggsignif.args = list(size = 0.5,
                                          vjust = 0,
                                          step_increase = 0.1,
                                          textsize = 5),
                     point.path.args = list(alpha = 0.5, 
                                            lwd = 0.8,
                                            linetype = "solid",
                                            color = "blue"),
                     point.args = list(position = ggplot2::position_jitterdodge(dodge.width = 0,                                                                 jitter.width = 0,                                                                jitter.height = 0),
                                       alpha = 0.5,
                                       size = 6,
                                       stroke = 0, 
                                       na.rm = TRUE)
                     ) 

ggsave(filename = "p1_paired.png",
       width = 8,
       height = 6,
       plot = p1,
       dpi = 300,
       units = "in")

library(dabestr)

two_groups_paired <- dabestr:::load(df_long,
  x = group, 
  y = score,
  paired = "sequential",
  idx = c("hk1", "hk2"),
  id_col = id
)

two_groups_paired.mean_diff <-  dabestr:::mean_diff(two_groups_paired)

print(two_groups_paired.mean_diff)
DABESTR v2025.3.15
==================

Good evening!
The current time is 23:02 PM on Thursday October 29, 2026.

The paired mean difference between hk2 and hk1 is 0.89 [95%CI -2.85, 4.404].
The p-value of the two-sided permutation t-test is 0.5916, calculated for legacy purposes only.

5000 bootstrap samples were taken; the confidence interval is bias-corrected and accelerated.
Any p-value reported is the probability of observing the effect size (or greater),
assuming the null hypothesis of zero difference is true.
For each p-value, 5000 reshuffles of the control and test labels were performed.
dabest_plot(two_groups_paired.mean_diff)

library(rstatix)
rstatix:::cohens_d(salary ~ age_group,
                      data = df_sample,
                   var.equal = T,
                   paired = T)
# A tibble: 1 × 7
  .y.    group1 group2 effsize    n1    n2 magnitude
* <chr>  <chr>  <chr>    <dbl> <int> <int> <ord>    
1 salary < 30   >= 30   -0.310    50    50 small    
library(effectsize)

effectsize:::cohens_d(
  upper_30,
  under_30,
  paired = T
)
Cohen's d |       95% CI
------------------------
0.31      | [0.02, 0.59]

3.2 Nonparametric | Paired

3.2.1 Wilcoxon Signed Rank test

wilcox.test(upper_30,
       under_30,
       paired = T,
       alternative = "two.sided")

    Wilcoxon signed rank test with continuity correction

data:  upper_30 and under_30
V = 858, p-value = 0.03369
alternative hypothesis: true location shift is not equal to 0

3.2.2 Sign test for paired samples

Khi phân phối lệch chuẩn quá nhiều thì ta dùng sign test.

library(BSDA)
BSDA:::SIGN.test(upper_30,
                 under_30,
                 md = 0,
                 alternative = "two.sided",
                 conf.level = 0.95)

    Dependent-samples Sign-Test

data:  upper_30 and under_30
S = 33, p-value = 0.03284
alternative hypothesis: true median difference is not equal to 0
95 percent confidence interval:
  803.5553 9798.7770
sample estimates:
median of x-y 
     4115.385 

Achieved and Interpolated Confidence Intervals: 

                  Conf.Level   L.E.pt   U.E.pt
Lower Achieved CI     0.9351 955.7794 9699.373
Interpolated CI       0.9500 803.5553 9798.777
Upper Achieved CI     0.9672 628.3350 9913.197
library(DescTools)
DescTools:::SignTest(upper_30,
                     under_30,
                     md = 0,
                     alternative = "two.sided",
                     conf.level = 0.95)

    Dependent-samples Sign-Test

data:  upper_30 and under_30
S = 33, number of differences = 50, p-value = 0.03284
alternative hypothesis: true median difference is not equal to 0
96.7 percent confidence interval:
  628.335 9913.197
sample estimates:
median of the differences 
                 4115.385 

4 t-test cho nhiều nhóm

library(ggplot2)
library(ggsignif)

options(scipen = 999)

dp <- df[ , c("gender",
              "major",
              "salary")]
dim(dp)
[1] 1753    3
dp <- na.omit(dp)
dim(dp)
[1] 1753    3
p1 <- ggplot(data = dp,
       mapping = aes(x = gender,
                     y = salary,
                     fill = gender)) +

  # geom_boxplot() +

   geom_violin(alpha = 0.1,
              show.legend = F) +

  geom_point(position = position_jitter(width = 0.1, height = 0),
             mapping = aes(color = gender),
             alpha = 0.5) +

    stat_summary(fun.data = mean_cl_boot,
               geom ='crossbar',
               width = 0.6,
               alpha = 0.5,
               aes(fill = gender)) +

   ggsignif:::geom_signif(comparisons =  list(c("Male", "Female")),
                         map_signif_level = function(p_ok) {
                           options(scipen = 1)
                           options(digits = 2)
                          capture.output(cat(p_ok)) -> p_ok_final
                         paste0("p", " = ", p_ok_final)

                         },
                         textsize = 4,
                         vjust = -0.5,
                         step_increase = 0.6,
                         test = "t.test",
                         test.args = list(paired = F)
                         ) +

  facet_grid(~ major) +

  scale_y_continuous(limits = c(10000,100000),
                     breaks = seq(from = 10000,
                                  to = 100000,
                                  by = 10000)) +

  theme_bw(base_size = 13) +

  labs(x = "")

# p1

ggsave(filename = "p1.png",
       plot = p1,
       width = 16,
       height = 4,
       units = "in",
       dpi = 300)

5 ANOVA 1 yếu tố | Parametric | Không bắt cặp

5.1 Check assumption

5.1.1 Normality

library(ggplot2)
library(ggpubr)

car:::qqPlot(PlantGrowth$weight[1:10])

[1] 4 1
ggpubr:::ggqqplot(data = PlantGrowth, 
                  x = "weight", 
                  facet.by = "group")

ctrl <- PlantGrowth$weight[1:10]

shapiro.test(ctrl)

    Shapiro-Wilk normality test

data:  ctrl
W = 1, p-value = 0.7
ks.test(x = jitter(ctrl),
        y = "pnorm",
        mean = mean(ctrl),
        sd = sd(ctrl))

    Exact one-sample Kolmogorov-Smirnov test

data:  jitter(ctrl)
D = 0.2, p-value = 0.9
alternative hypothesis: two-sided
library(nortest)
nortest:::lillie.test(ctrl)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  ctrl
D = 0.2, p-value = 0.5
nortest:::ad.test(ctrl)

    Anderson-Darling normality test

data:  ctrl
A = 0.3, p-value = 0.6
trt1 <- PlantGrowth$weight[11:20]

shapiro.test(trt1)

    Shapiro-Wilk normality test

data:  trt1
W = 0.9, p-value = 0.5
ks.test(x = jitter(trt1),
        y = "pnorm",
        mean = mean(trt1),
        sd = sd(trt1))

    Exact one-sample Kolmogorov-Smirnov test

data:  jitter(trt1)
D = 0.2, p-value = 0.8
alternative hypothesis: two-sided
library(nortest)
nortest:::lillie.test(trt1)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  trt1
D = 0.2, p-value = 0.4
nortest:::ad.test(trt1)

    Anderson-Darling normality test

data:  trt1
A = 0.3, p-value = 0.4
library(tidyr)
library(dplyr)

# reshape tu long qua wide
PlantGrowth_wide <- PlantGrowth %>%
  group_by(group) %>%
  mutate(id = row_number()) %>%
  ungroup() %>% as.data.frame() %>%
  pivot_wider(
    names_from = group,
    values_from = weight
  )

PlantGrowth_wide  <- PlantGrowth_wide[, -1]

library(MVN)

MVN:::mvn(PlantGrowth_wide)
$multivariate_normality
           Test Statistic p.value     Method      MVN
1 Henze-Zirkler       0.5    0.44 asymptotic ✓ Normal

$univariate_normality
              Test Variable Statistic p.value Normality
1 Anderson-Darling     ctrl      0.28    0.56  ✓ Normal
2 Anderson-Darling     trt1      0.33    0.44  ✓ Normal
3 Anderson-Darling     trt2      0.31    0.50  ✓ Normal

$descriptives
  Variable  n Mean Std.Dev Median Min Max 25th 75th Skew Kurtosis
1     ctrl 10  5.0    0.58    5.2 4.2 6.1  4.5  5.3 0.27      2.3
2     trt1 10  4.7    0.79    4.5 3.6 6.0  4.2  4.9 0.56      2.3
3     trt2 10  5.5    0.44    5.4 4.9 6.3  5.3  5.7 0.57      2.3

$data
# A tibble: 10 × 3
    ctrl  trt1  trt2
   <dbl> <dbl> <dbl>
 1  4.17  4.81  6.31
 2  5.58  4.17  5.12
 3  5.18  4.41  5.54
 4  6.11  3.59  5.5 
 5  4.5   5.87  5.37
 6  4.61  3.83  5.29
 7  5.17  6.03  4.92
 8  4.53  4.89  6.15
 9  5.33  4.32  5.8 
10  5.14  4.69  5.26

$subset
NULL

$outlierMethod
[1] "none"

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

5.1.2 Variance homogeneity

library(car)

car:::leveneTest(weight ~ group,
           data = PlantGrowth)
Levene's Test for Homogeneity of Variance (center = median)
      Df F value Pr(>F)
group  2    1.12   0.34
      27               
stats:::bartlett.test(weight ~ group,
                      data = PlantGrowth)

    Bartlett test of homogeneity of variances

data:  weight by group
Bartlett's K-squared = 3, df = 2, p-value = 0.2
library(ggplot2)

ggplot(data = PlantGrowth,
       mapping = aes(x = weight, 
                     colour = group)) +
  geom_density() +
  
  theme_classic(base_size = 16)

ggplot(data = PlantGrowth,
       mapping = aes(x = group, 
                     y = weight,
                     fill = group)) +
  geom_boxplot() +
  
  theme_classic(base_size = 16)

5.2 Run ANOVA one-way

# ANOVA type I | variance equal
fit1 <- aov(weight ~ group,
           data = PlantGrowth)

summary(fit1)
            Df Sum Sq Mean Sq F value Pr(>F)  
group        2   3.77   1.883    4.85  0.016 *
Residuals   27  10.49   0.389                 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
fit2 <- stats:::oneway.test(weight ~ group,
                            data = PlantGrowth,
                            var.equal = T)

fit2

    One-way analysis of means

data:  weight and group
F = 5, num df = 2, denom df = 27, p-value = 0.02
# ANOVA type I | variance unequal Welch's ANOVA
fit3 <- stats:::oneway.test(weight ~ group,
                            data = PlantGrowth,
                            var.equal = F)

fit3

    One-way analysis of means (not assuming equal variances)

data:  weight and group
F = 5, num df = 2, denom df = 17, p-value = 0.02
library(car)

# ANOVA type II 
car:::Anova(fit1, 
            type = "II")
Anova Table (Type II tests)

Response: weight
          Sum Sq Df F value Pr(>F)  
group       3.77  2    4.85  0.016 *
Residuals  10.49 27                 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# ANOVA type III 
car:::Anova(fit1, type = "III")
Anova Table (Type III tests)

Response: weight
            Sum Sq Df F value Pr(>F)    
(Intercept)  253.2  1  651.60 <2e-16 ***
group          3.8  2    4.85  0.016 *  
Residuals     10.5 27                   
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

5.3 Effect size

library(effectsize)

# Eta-squared
effectsize:::eta_squared(fit1)
# Effect Size for ANOVA

Parameter | Eta2 |       95% CI
-------------------------------
group     | 0.26 | [0.04, 1.00]

- One-sided CIs: upper bound fixed at [1.00].
# eta-squared
# Tỷ lệ phương sai của weight được giải thích bởi group.
# 0.01  Nhỏ
# 0.06  Trung bình
# 0.14  Lớn
# Omega-squared
# Ước lượng effect size ít thiên lệch hơn trong quần thể.
effectsize:::omega_squared(fit1)
# Effect Size for ANOVA

Parameter | Omega2 |       95% CI
---------------------------------
group     |   0.20 | [0.00, 1.00]

- One-sided CIs: upper bound fixed at [1.00].
# Cohen's f
# Thước đo effect size dựa trên độ lớn khác biệt giữa các trung bình nhóm.
effectsize:::cohens_f(fit1)
# Effect Size for ANOVA

Parameter | Cohen's f |      95% CI
-----------------------------------
group     |      0.60 | [0.19, Inf]

- One-sided CIs: upper bound fixed at [Inf].
library(rstatix)

# Eta-squared
rstatix:::anova_test(
  data = PlantGrowth,
  dv = weight,
  effect.size = "pes",
  between = group)
ANOVA Table (type II tests)

  Effect DFn DFd   F     p p<.05  pes
1  group   2  27 4.8 0.016     * 0.26

5.4 Post-hoc analysis | Multiple comparisons

5.4.1 Tukey

TukeyHSD(fit1)
  Tukey multiple comparisons of means
    95% family-wise confidence level

Fit: aov(formula = weight ~ group, data = PlantGrowth)

$group
           diff   lwr  upr p adj
trt1-ctrl -0.37 -1.06 0.32  0.39
trt2-ctrl  0.49 -0.20 1.19  0.20
trt2-trt1  0.87  0.17 1.56  0.01
plot(TukeyHSD(fit1),
     las = 1)

library(agricolae)

agricolae:::HSD.test(fit1,
                     trt = "group",
                     console = TRUE)

Study: fit1 ~ "group"

HSD Test for weight 

Mean Square Error:  0.39 

group,  means

     weight  std  r  se Min Max Q25 Q50 Q75
ctrl    5.0 0.58 10 0.2 4.2 6.1 4.5 5.2 5.3
trt1    4.7 0.79 10 0.2 3.6 6.0 4.2 4.6 4.9
trt2    5.5 0.44 10 0.2 4.9 6.3 5.3 5.4 5.7

Alpha: 0.05 ; DF Error: 27 
Critical Value of Studentized Range: 3.5 

Minimun Significant Difference: 0.69 

Treatments with the same letter are not significantly different.

     weight groups
trt2    5.5      a
ctrl    5.0     ab
trt1    4.7      b

5.4.2 Bonferroni

• “bonferroni” Kiểm soát family-wise error rate (FWER), khá bảo thủ

• “holm” Kiểm soát FWER, thường ít bảo thủ hơn Bonferroni

• “BH” Kiểm soát false discovery rate (FDR)

pairwise.t.test(
  x = PlantGrowth$weight,
  g = PlantGrowth$group,
  p.adjust.method = "bonferroni",
  pool.sd = TRUE
)

    Pairwise comparisons using t tests with pooled SD 

data:  PlantGrowth$weight and PlantGrowth$group 

     ctrl trt1
trt1 0.58 -   
trt2 0.26 0.01

P value adjustment method: bonferroni 
pairwise.t.test(
  x = PlantGrowth$weight,
  g = PlantGrowth$group,
  p.adjust.method = "BH",
  pool.sd = TRUE
)

    Pairwise comparisons using t tests with pooled SD 

data:  PlantGrowth$weight and PlantGrowth$group 

     ctrl trt1
trt1 0.19 -   
trt2 0.13 0.01

P value adjustment method: BH 
pairwise.t.test(
  x = PlantGrowth$weight,
  g = PlantGrowth$group,
  p.adjust.method = "holm",
  pool.sd = TRUE
)

    Pairwise comparisons using t tests with pooled SD 

data:  PlantGrowth$weight and PlantGrowth$group 

     ctrl trt1
trt1 0.19 -   
trt2 0.18 0.01

P value adjustment method: holm 
pairwise.t.test(
  x = PlantGrowth$weight,
  g = PlantGrowth$group,
  p.adjust.method = "bonferroni",
  pool.sd = FALSE
)

    Pairwise comparisons using t tests with non-pooled SD 

data:  PlantGrowth$weight and PlantGrowth$group 

     ctrl trt1
trt1 0.75 -   
trt2 0.14 0.03

P value adjustment method: bonferroni 

5.5 Vẽ đồ thị

library(ggstatsplot)
library(tidyverse)

set.seed(20)

kq <- ggstatsplot:::ggbetweenstats(data = PlantGrowth,
                     x = group,
                     y = weight,
                     var.equal = TRUE,
                     type = "parametric", 
                     plot.type = "box",
                     pairwise.comparisons = T,
                     pairwise.display = "all",
                     p.adjust.method = "bonferroni",
                     # p.adjust.method = "none",
                     centrality.plotting = T,
                     centrality.path = T,
                     digits = 4L,
                     centrality.point.args = list(size = 4, color = "darkred"),
                     centrality.label.args = list(size = 4,
                                                  nudge_x = 0.4,
                                                  segment.linetype = 4),
                     ggtheme = theme_classic(base_size = 12),
                     bf.message = T,
                     results.subtitle = T,
                     caption = "",
                     ggsignif.args = list(size = 0.5,
                                          vjust = 0,
                                          step_increase = 0.1,
                                          textsize = 5),
                     point.args = list(position = ggplot2::position_jitterdodge(dodge.width = 0,                                                                 jitter.width = 0,                                                                jitter.height = 0),
                                       alpha = 0.5,
                                       size = 6,
                                       stroke = 0, 
                                       na.rm = TRUE)
                     )  +
  coord_cartesian(ylim = c(3,8))
# kq

ggstatsplot:::extract_stats(kq)
$subtitle_data
# A tibble: 1 × 14
  statistic    df df.error p.value method                    effectsize estimate conf.level conf.low conf.high conf.method conf.distribution n.obs expression
      <dbl> <dbl>    <dbl>   <dbl> <chr>                     <chr>         <dbl>      <dbl>    <dbl>     <dbl> <chr>       <chr>             <int> <list>    
1      4.85     2       27  0.0159 One-way analysis of means Omega2        0.204       0.95  0.00238         1 ncp         F                    30 <language>

$caption_data
# A tibble: 6 × 17
  term          pd prior.distribution prior.location prior.scale  bf10 method                          log_e_bf10 effectsize         estimate std.dev conf.level
  <chr>      <dbl> <chr>                       <dbl>       <dbl> <dbl> <chr>                                <dbl> <chr>                 <dbl>   <dbl>      <dbl>
1 mu         1     cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
2 group-ctrl 0.596 cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
3 group-trt1 0.985 cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
4 group-trt2 0.989 cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
5 sig2       1     cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
6 g_group    1     cauchy                          0       0.707  3.66 Bayes factors for linear models       1.30 Bayesian R-squared    0.167   0.161       0.95
  conf.low conf.high conf.method n.obs expression
     <dbl>     <dbl> <chr>       <int> <list>    
1        0     0.381 HDI            30 <language>
2        0     0.381 HDI            30 <language>
3        0     0.381 HDI            30 <language>
4        0     0.381 HDI            30 <language>
5        0     0.381 HDI            30 <language>
6        0     0.381 HDI            30 <language>

$pairwise_comparisons_data
# A tibble: 3 × 6
  group1 group2 p.value p.adjust.method test        expression
  <chr>  <chr>    <dbl> <chr>           <chr>       <list>    
1 ctrl   trt1    0.583  Bonferroni      Student's t <language>
2 ctrl   trt2    0.263  Bonferroni      Student's t <language>
3 trt1   trt2    0.0134 Bonferroni      Student's t <language>

$descriptive_data
NULL

$one_sample_data
NULL

$tidy_data
NULL

$glance_data
NULL

attr(,"class")
[1] "ggstatsplot_stats" "list"             
ggsave(filename = "kq.png",
       plot = kq,
       width = 8,
       height = 6,
       units = "in",
       dpi = 300)

5.6 Estimated marginal means

Estimated marginal means (EMMs), a.k.a. least-squares means

# https://cran.r-project.org/web/packages/emmeans/vignettes/FAQs.html#what
library(emmeans)

emm <- emmeans:::emmeans(fit1, ~ group)

summary(emm)
 group emmean    SE df lower.CL upper.CL
 ctrl     5.0 0.197 27      4.6      5.4
 trt1     4.7 0.197 27      4.3      5.1
 trt2     5.5 0.197 27      5.1      5.9

Confidence level used: 0.95 
graphics:::pairs(emm, adjust = "bonferroni")
 contrast    estimate    SE df t.ratio p.value
 ctrl - trt1     0.37 0.279 27   1.330  0.5800
 ctrl - trt2    -0.49 0.279 27  -1.770  0.2600
 trt1 - trt2    -0.86 0.279 27  -3.100  0.0100

P value adjustment: bonferroni method for 3 tests 
# plot(emm)

# dấu mũi tên 2 chiều là không khác biệt
# hai dấu mũi tên ngược nhau là khác biệt
emmeans:::plot.emmGrid(emm, 
                       comparisons = F)

6 ANOVA 1 yếu tố | Nonparametric | Không bắt cặp

6.1 Check assumption

6.1.1 Normality

library(ggplot2)
library(ggpubr)
df$major <- factor(df$major)
ggpubr:::ggqqplot(data = df, 
                  x = "salary", 
                  facet.by = "major")

major_ok <- unique(df$major)

for(i in 1:length(major_ok)){
check_normality <- df |> subset(major == major_ok[i])

print(major_ok[i])
kq <- nortest:::ad.test(check_normality$salary)
print(kq)
}
[1] Economics and Finance
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 0.6, p-value = 0.1

[1] Environmental Sciences
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 0.4, p-value = 0.3

[1] Political Science
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 0.9, p-value = 0.03

[1] Mathematics and Statistics
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 0.8, p-value = 0.05

[1] Biology
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 1, p-value = 0.001

[1] Social Sciences
Levels: Biology Economics and Finance Environmental Sciences Mathematics and Statistics Political Science Social Sciences

    Anderson-Darling normality test

data:  check_normality$salary
A = 0.3, p-value = 0.6
### Lưu ý nếu dữ liệu không đều nhau thì nên thực hiện thủ công trích từng vector kiểm riêng normality
df_wide <- df[ , c("major", "salary")] %>%
  group_by(major) %>%
  mutate(id = row_number()) %>%
  ungroup() %>% as.data.frame() %>%
  pivot_wider(
    names_from = major,
    values_from = salary
  )

df_wide <- df_wide[, -1]
library(MVN)
### Vì dữ liệu df_wide bị NA nên khi thực hiện, package sẽ tự động remove NA, làm sai kết quả.
df_wide <- na.omit(df_wide)
MVN:::mvn(df_wide)
$multivariate_normality
           Test Statistic p.value     Method      MVN
1 Henze-Zirkler      0.95    0.34 asymptotic ✓ Normal

$univariate_normality
              Test                   Variable Statistic p.value    Normality
1 Anderson-Darling      Economics and Finance      0.49   0.218     ✓ Normal
2 Anderson-Darling     Environmental Sciences      0.14   0.971     ✓ Normal
3 Anderson-Darling          Political Science      0.72   0.060     ✓ Normal
4 Anderson-Darling Mathematics and Statistics      0.62   0.102     ✓ Normal
5 Anderson-Darling                    Biology      0.90   0.021 ✗ Not normal
6 Anderson-Darling            Social Sciences      0.30   0.585     ✓ Normal

$descriptives
                    Variable   n  Mean Std.Dev Median   Min   Max  25th  75th  Skew Kurtosis
1      Economics and Finance 206 49760    8423  49349 25351 72787 43373 56022 0.209      2.8
2     Environmental Sciences 206 37229    5681  37132 23870 51332 33221 41368 0.031      2.6
3          Political Science 206 36876    9296  36514 11444 70932 30272 42177 0.480      3.7
4 Mathematics and Statistics 206 48594    8203  47554 23867 75597 43098 53688 0.328      3.3
5                    Biology 206 47462    8330  46567 25998 70095 41099 53279 0.279      2.7
6            Social Sciences 206 32044    6205  32032 13300 50705 27613 36021 0.078      3.3

$data
# A tibble: 206 × 6
   `Economics and Finance` `Environmental Sciences` `Political Science` `Mathematics and Statistics` Biology `Social Sciences`
                     <dbl>                    <dbl>               <dbl>                        <dbl>   <dbl>             <dbl>
 1                  45254.                   40553.              33969.                       35599.  49111.            28128.
 2                  50618.                   27007.              37392.                       39343.  51223.            23717.
 3                  42593.                   39704.              30087.                       36204.  49476.            35882.
 4                  60407.                   32519.              31494.                       58081.  46243.            37036.
 5                  35881.                   33069.              39451.                       56996.  39588.            39758.
 6                  51564.                   37804.              20511.                       41619.  64486.            21722.
 7                  43380.                   35192.              44028.                       41373.  45504.            33109.
 8                  71468.                   34191.              37810.                       65013.  39369.            31750.
 9                  51518.                   39038.              42065.                       64525.  35816.            31485.
10                  55664.                   43333.              30612.                       58671.  37626.            25659.
# ℹ 196 more rows

$subset
NULL

$outlierMethod
[1] "none"

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

6.1.2 Variance homogeneity

library(car)

car:::leveneTest(salary ~ major,
           data = df)
Levene's Test for Homogeneity of Variance (center = median)
        Df F value  Pr(>F)    
group    5    16.3 9.6e-16 ***
      1747                    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
stats:::bartlett.test(salary ~ major,
           data = df)

    Bartlett test of homogeneity of variances

data:  salary by major
Bartlett's K-squared = 108, df = 5, p-value <2e-16

6.2 Run ANOVA one-way Nonparametric

Assumptions cho parametric bị vi phạm nên ta áp dụng kiểm định nonparametric.

fit1 <- stats:::kruskal.test(salary ~ major,
           data = df)

fit1 

    Kruskal-Wallis rank sum test

data:  salary by major
Kruskal-Wallis chi-squared = 781, df = 5, p-value <2e-16

6.3 Effect size

rstatix:::kruskal_effsize(salary ~ major,
                          data = df)
# A tibble: 1 × 5
  .y.        n effsize method  magnitude
* <chr>  <int>   <dbl> <chr>   <ord>    
1 salary  1753   0.444 eta2[H] large    

6.4 Post-hoc analysis | Multiple comparisons

6.4.1 Bonferroni

• “bonferroni” Kiểm soát family-wise error rate (FWER), khá bảo thủ • “holm” Kiểm soát FWER, thường ít bảo thủ hơn Bonferroni • “bh” Kiểm soát false discovery rate (FDR)

library(FSA)
FSA:::dunnTest(salary ~ major,
               data = df,
               method = "bonferroni")
                                            Comparison      Z P.unadj   P.adj
1                      Biology - Economics and Finance  -2.14 3.2e-02 4.8e-01
2                     Biology - Environmental Sciences  13.68 1.4e-42 2.1e-41
3       Economics and Finance - Environmental Sciences  15.45 7.7e-54 1.2e-52
4                 Biology - Mathematics and Statistics  -0.86 3.9e-01 1.0e+00
5   Economics and Finance - Mathematics and Statistics   1.24 2.1e-01 1.0e+00
6  Environmental Sciences - Mathematics and Statistics -14.06 6.6e-45 9.9e-44
7                          Biology - Political Science  14.18 1.3e-45 1.9e-44
8            Economics and Finance - Political Science  15.88 9.3e-57 1.4e-55
9           Environmental Sciences - Political Science   1.12 2.6e-01 1.0e+00
10      Mathematics and Statistics - Political Science  14.55 6.1e-48 9.1e-47
11                           Biology - Social Sciences  18.07 5.9e-73 8.8e-72
12             Economics and Finance - Social Sciences  19.56 3.4e-85 5.0e-84
13            Environmental Sciences - Social Sciences   6.26 3.9e-10 5.9e-09
14        Mathematics and Statistics - Social Sciences  18.34 4.2e-75 6.2e-74
15                 Political Science - Social Sciences   5.07 4.0e-07 6.0e-06

6.5 Vẽ đồ thị

library(ggstatsplot)
library(tidyverse)

set.seed(20)

kq <- ggstatsplot:::ggbetweenstats(data = df,
                     x = major,
                     y = salary,
                     var.equal = TRUE,
                     type = "nonparametric", 
                     plot.type = "box",
                     pairwise.comparisons = T,
                     pairwise.display = "all",
                     p.adjust.method = "bonferroni",
                     # p.adjust.method = "none",
                     centrality.plotting = T,
                     centrality.path = T,
                     digits = 4L,
                     centrality.point.args = list(size = 4, color = "darkred"),
                     centrality.label.args = list(size = 4,
                                                  nudge_x = 0.4,
                                                  segment.linetype = 4),
                     ggtheme = theme_classic(base_size = 12),
                     bf.message = T,
                     results.subtitle = T,
                     caption = "",
                     ggsignif.args = list(size = 0.5,
                                          vjust = 0,
                                          step_increase = 0.1,
                                          textsize = 5),
                     point.args = list(position = ggplot2::position_jitterdodge(dodge.width = 0,                                                                 jitter.width = 0,                                                                jitter.height = 0),
                                       alpha = 0.5,
                                       size = 6,
                                       stroke = 0, 
                                       na.rm = TRUE)
                     ) 

# kq

ggstatsplot:::extract_stats(kq)
$subtitle_data
# A tibble: 1 × 15
  parameter1 parameter2 statistic df.error   p.value method                       effectsize      estimate conf.level conf.low conf.high conf.method         
  <chr>      <chr>          <dbl>    <int>     <dbl> <chr>                        <chr>              <dbl>      <dbl>    <dbl>     <dbl> <chr>               
1 salary     major           781.        5 1.18e-166 Kruskal-Wallis rank sum test Epsilon2 (rank)    0.446       0.95    0.418         1 percentile bootstrap
  conf.iterations n.obs expression
            <int> <int> <list>    
1             100  1753 <language>

$caption_data
NULL

$pairwise_comparisons_data
# A tibble: 15 × 9
   group1                     group2                     statistic  p.value alternative distribution p.adjust.method test  expression
   <chr>                      <chr>                          <dbl>    <dbl> <chr>       <chr>        <chr>           <chr> <list>    
 1 Biology                    Economics and Finance          2.14  4.79e- 1 two.sided   z            Bonferroni      Dunn  <language>
 2 Biology                    Environmental Sciences        13.7   2.06e-41 two.sided   z            Bonferroni      Dunn  <language>
 3 Biology                    Mathematics and Statistics     0.857 1   e+ 0 two.sided   z            Bonferroni      Dunn  <language>
 4 Biology                    Political Science             14.2   1.92e-44 two.sided   z            Bonferroni      Dunn  <language>
 5 Biology                    Social Sciences               18.1   8.84e-72 two.sided   z            Bonferroni      Dunn  <language>
 6 Economics and Finance      Environmental Sciences        15.4   1.16e-52 two.sided   z            Bonferroni      Dunn  <language>
 7 Economics and Finance      Mathematics and Statistics     1.24  1   e+ 0 two.sided   z            Bonferroni      Dunn  <language>
 8 Economics and Finance      Political Science             15.9   1.39e-55 two.sided   z            Bonferroni      Dunn  <language>
 9 Economics and Finance      Social Sciences               19.6   5.03e-84 two.sided   z            Bonferroni      Dunn  <language>
10 Environmental Sciences     Mathematics and Statistics    14.1   9.92e-44 two.sided   z            Bonferroni      Dunn  <language>
11 Environmental Sciences     Political Science              1.12  1   e+ 0 two.sided   z            Bonferroni      Dunn  <language>
12 Environmental Sciences     Social Sciences                6.26  5.85e- 9 two.sided   z            Bonferroni      Dunn  <language>
13 Mathematics and Statistics Political Science             14.5   9.12e-47 two.sided   z            Bonferroni      Dunn  <language>
14 Mathematics and Statistics Social Sciences               18.3   6.23e-74 two.sided   z            Bonferroni      Dunn  <language>
15 Political Science          Social Sciences                5.07  6.05e- 6 two.sided   z            Bonferroni      Dunn  <language>

$descriptive_data
NULL

$one_sample_data
NULL

$tidy_data
NULL

$glance_data
NULL

attr(,"class")
[1] "ggstatsplot_stats" "list"             
ggsave(filename = "kq_dunn.png",
       plot = kq,
       width = 14,
       height = 10,
       units = "in",
       dpi = 300)

6.6 Summary giữa các nhóm

library(DescTools)

DescTools:::MedianCI(upper_30)
median lwr.ci upr.ci 
 43712  39356  51706 
attr(,"conf.level")
[1] 0.97
quantile(upper_30)
   0%   25%   50%   75%  100% 
22191 36805 43712 54950 71595 
options(pillar.sigfig = 8)
df |> dplyr:::group_by(major) |> 
  dplyr:::summarise(median = DescTools:::MedianCI(salary)[1],
                    lower = DescTools:::MedianCI(salary)[2],
                    upper = DescTools:::MedianCI(salary)[3],
                    Q2 = quantile(salary)[2],
                    Q4 = quantile(salary)[4],
                    IQR = IQR(salary))
# A tibble: 6 × 7
  major                         median     lower     upper        Q2        Q4        IQR
  <fct>                          <dbl>     <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
1 Biology                    47085.666 45676.770 48969.948 41201.484 53332.147 12130.664 
2 Economics and Finance      49223.300 47842.436 50416.159 43166.767 55939.829 12773.062 
3 Environmental Sciences     37691.928 36750.087 38476.916 33365.419 41924.629  8559.2101
4 Mathematics and Statistics 47670.778 46306.616 49317.398 42758.735 53590.981 10832.246 
5 Political Science          36279.911 34444.640 37529.564 29934.009 42064.680 12130.670 
6 Social Sciences            32031.546 31325.999 33200.275 27612.823 36021.198  8408.3754

Các trường hợp phân tích paired samples ghép cặp before-after hay theo dõi theo thời gian được mình trình bày chi tiết ở link này [Paired samples]