1 Cài đặt và Gọi thư viện

pkgs <- c("tidyverse", "psych", "irr", "irrCAC", "lme4",
          "fmsb", "knitr", "kableExtra", "readxl")
new_pkgs <- pkgs[!pkgs %in% installed.packages()[, "Package"]]
if (length(new_pkgs)) install.packages(new_pkgs)

library(readxl)
library(tidyverse)   # Xử lý và làm sạch dữ liệu
library(psych)       # Thống kê mô tả
library(irr)         # Cohen's Kappa, ICC
library(irrCAC)      # Gwet's AC2
library(lme4)        # G-Theory (Variance Components)
library(fmsb)        # Radar chart
library(knitr)       # Trình bày bảng
library(kableExtra)  # Định dạng bảng HTML

2 Nạp dữ liệu

setwd("C:/Users/LENOVO/OneDrive/Desktop/CECICS/Phân tích số liệu")
df <- read_excel("Data.xlsx", sheet = "Sheet1")

3 Thống kê mô tả

3.1 Mô tả điểm tổng của 3 raters

Table_Descriptive_Scores <- df %>%
  select(R1, R2, R3, mean_checklist) %>%
  describe() %>%
  as.data.frame() %>%
  select(n, mean, sd, median, min, max, skew, kurtosis)

kable(Table_Descriptive_Scores, digits = 3,
      caption = "Thống kê mô tả điểm tổng của 3 giám khảo") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = FALSE)
Thống kê mô tả điểm tổng của 3 giám khảo
n mean sd median min max skew kurtosis
R1 87 20.908 2.991 21.000 15 26.000 -0.155 -0.966
R2 87 22.517 2.472 23.000 15 26.000 -0.623 -0.246
R3 87 21.655 2.921 22.000 15 26.000 -0.310 -0.906
mean_checklist 87 21.693 2.546 22.333 15 25.667 -0.422 -0.758

3.2 Kiểm tra phân phối chuẩn (Shapiro-Wilk)

Normality_Test_Results <- df %>%
  select(R1, R2, R3, mean_checklist) %>%
  sapply(function(x) {
    test <- shapiro.test(x)
    c(W = test$statistic, p_value = test$p.value)
  }) %>%
  t() %>%
  as.data.frame() %>%
  mutate(Conclusion = ifelse(p_value > 0.05,
                             "Normal Distribution", "Non-normal Distribution"))

kable(Normality_Test_Results, digits = 4,
      caption = "Kiểm định phân phối chuẩn (Shapiro-Wilk)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = FALSE)
Kiểm định phân phối chuẩn (Shapiro-Wilk)
W.W p_value Conclusion
R1 0.9606 0.0095 Non-normal Distribution
R2 0.9405 0.0006 Non-normal Distribution
R3 0.9536 0.0034 Non-normal Distribution
mean_checklist 0.9551 0.0043 Non-normal Distribution

3.3 Q-Q Plot

ggplot(df, aes(sample = mean_checklist)) +
  stat_qq() +
  stat_qq_line(color = "red") +
  theme_minimal() +
  labs(title = "Q-Q Plot: Kiểm tra phân phối chuẩn điểm Checklist trung bình")

4 Độ đồng thuận từng Item: Gwet’s AC2

# Hàm phân tích an toàn từng Item (ordinal 0-1-2, quadratic weights)
analyze_item_robust <- function(i) {
  cols <- paste0(c("R1_I", "R2_I", "R3_I"), i)

  if (!all(cols %in% colnames(df))) {
    message("Không tìm thấy cột cho Item ", i, " — bỏ qua.")
    return(NULL)
  }

  item_data <- na.omit(df[, cols])
  if (nrow(item_data) < 2) {
    message("Item ", i, " không đủ dữ liệu — bỏ qua.")
    return(NULL)
  }

  # Thống kê mô tả
  all_scores <- unlist(item_data)
  pct_dist   <- round(prop.table(table(factor(all_scores, levels = 0:2))) * 100, 1)
  pct_agree  <- round(mean(apply(item_data, 1, function(x) length(unique(x)) == 1)) * 100, 1)

  # Gwet's AC2
  gwet_res <- tryCatch(
    gwet.ac1.raw(item_data, weights = "quadratic"),
    error = function(e) { message("Lỗi AC2 Item ", i, ": ", e$message); NULL }
  )
  if (is.null(gwet_res)) return(NULL)

  res_df  <- gwet_res$est
  ac2_val <- res_df$coeff.val
  p_val   <- if ("p.value" %in% colnames(res_df)) res_df$p.value else NA
  ci_str  <- if (all(c("lcl", "ucl") %in% colnames(res_df))) {
    paste0("[", round(res_df$lcl, 3), "; ", round(res_df$ucl, 3), "]")
  } else if ("conf.int" %in% colnames(res_df)) {
    as.character(res_df$conf.int)
  } else "N/A"

  interpretation <- as.character(
    cut(ac2_val,
        breaks = c(-Inf, 0, 0.20, 0.40, 0.60, 0.80, 1),
        labels = c("Poor", "Slight", "Fair", "Moderate", "Substantial", "Almost Perfect"),
        right = TRUE, include.lowest = TRUE)
  )

  list(
    Desc = data.frame(
      Item = paste0("Item ", i), N = nrow(item_data),
      Mean = round(mean(all_scores), 2), SD = round(sd(all_scores), 3),
      Pct_0 = as.numeric(pct_dist["0"]), Pct_1 = as.numeric(pct_dist["1"]),
      Pct_2 = as.numeric(pct_dist["2"]), Percent_Agreement = pct_agree,
      stringsAsFactors = FALSE
    ),
    Rel = data.frame(
      Item = paste0("Item ", i), Gwet_AC2 = round(ac2_val, 3),
      CI_95 = ci_str,
      p_value = ifelse(is.na(p_val), "N/A", format.pval(p_val, eps = 0.001, digits = 3)),
      Agreement = interpretation,
      stringsAsFactors = FALSE
    )
  )
}
results_list <- Filter(Negate(is.null), lapply(1:12, analyze_item_robust))

if (length(results_list) == 0)
  stop("Không có item nào được phân tích. Kiểm tra lại tên cột trong df.")

Table_Descriptive_Final <- do.call(rbind, lapply(results_list, `[[`, "Desc"))
Table_Gwet_Final         <- do.call(rbind, lapply(results_list, `[[`, "Rel"))
rownames(Table_Descriptive_Final) <- rownames(Table_Gwet_Final) <- NULL

kable(Table_Descriptive_Final,
      caption = "Bảng 1: Mô tả và phân phối điểm theo từng Item") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = FALSE)
Bảng 1: Mô tả và phân phối điểm theo từng Item
Item N Mean SD Pct_0 Pct_1 Pct_2 Percent_Agreement
Item 1 87 1.96 0.192 0.0 3.8 96.2 97.7
Item 2 87 1.80 0.397 0.0 19.5 80.5 81.6
Item 3 87 1.81 0.420 1.1 16.5 82.4 87.4
Item 4 87 1.79 0.560 7.3 6.5 86.2 87.4
Item 5 87 1.71 0.524 3.4 21.8 74.7 70.1
Item 6 87 1.74 0.512 3.4 19.2 77.4 85.1
Item 7 87 1.87 0.374 1.1 11.1 87.7 77.0
Item 8 87 1.88 0.430 3.8 4.6 91.6 88.5
Item 9 87 1.54 0.635 7.7 30.3 62.1 39.1
Item 10 87 1.41 0.586 5.0 49.0 46.0 31.0
Item 11 87 1.42 0.619 6.9 44.4 48.7 66.7
Item 12 87 1.35 0.727 14.9 35.2 49.8 77.0
kable(Table_Gwet_Final,
      caption = "Bảng 2: Độ đồng thuận Gwet's AC2 (Quadratic Weights)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = FALSE)
Bảng 2: Độ đồng thuận Gwet’s AC2 (Quadratic Weights)
Item Gwet_AC2 CI_95 p_value Agreement
Item 1 0.983 (0.96,1) <0.001 Almost Perfect
Item 2 0.821 (0.73,0.912) <0.001 Almost Perfect
Item 3 0.970 (0.952,0.988) <0.001 Almost Perfect
Item 4 0.964 (0.938,0.991) <0.001 Almost Perfect
Item 5 0.899 (0.853,0.945) <0.001 Almost Perfect
Item 6 0.961 (0.939,0.983) <0.001 Almost Perfect
Item 7 0.951 (0.929,0.974) <0.001 Almost Perfect
Item 8 0.970 (0.947,0.994) <0.001 Almost Perfect
Item 9 0.679 (0.57,0.787) <0.001 Substantial
Item 10 0.726 (0.671,0.78) <0.001 Substantial
Item 11 0.873 (0.835,0.912) <0.001 Almost Perfect
Item 12 0.903 (0.865,0.941) <0.001 Almost Perfect

5 Boxplot so sánh điểm 3 Giám khảo

# Bảng màu: fill nhạt + border/whisker/median tối hơn cùng gam
fill_colors   <- c("R1" = "#70AE98", "R2" = "#E58B88", "R3" = "#ECBE7A")
border_colors <- c("R1" = "#3D7A66", "R2" = "#B34744", "R3" = "#B8882E")

box_width <- 0.42        # ~70% so với default 0.6
cap_width <- box_width / 2   # Chiều dài cap = 1/2 độ dày box

df %>%
  select(id, R1, R2, R3) %>%
  pivot_longer(cols = c(R1, R2, R3), names_to = "Rater", values_to = "Score") %>%
  ggplot(aes(x = Rater, y = Score, fill = Rater, color = Rater)) +

  # ① Vẽ cap (ngang đầu whisker) TRƯỚC để nằm dưới box
  stat_boxplot(geom = "errorbar", width = cap_width, linewidth = 0.7) +

  # ② Vẽ box (color áp dụng cho viền, median, whisker tự động)
  geom_boxplot(width = box_width, alpha = 0.80,
               outlier.shape = NA, linewidth = 0.7) +

  # ③ Jitter points
  geom_jitter(width = 0.12, alpha = 0.35,
              color = "gray50", size = 1.5, show.legend = FALSE) +

  # Gán màu
  scale_fill_manual(
    values = fill_colors,
    labels = c("R1" = "R1 — Live", "R2" = "R2 — Video", "R3" = "R3 — Video")
  ) +
  scale_color_manual(values = border_colors) +

  # Nhãn trục X tiếng Anh
  scale_x_discrete(
    labels = c("R1" = "R1\n(Live)", "R2" = "R2\n(Video)", "R3" = "R3\n(Video)")
  ) +

  # Ẩn legend màu viền (trùng với legend fill)
  guides(color = "none") +

  theme_bw(base_size = 12) +
  theme(
    panel.grid.major.x = element_blank(),
    panel.grid.minor   = element_blank(),
    legend.position    = "right",
    plot.title         = element_text(face = "bold", size = 13),
    plot.subtitle      = element_text(color = "gray40", size = 11)
  ) +
  labs(
    title    = "Score distribution across three raters on the OSCE checklist",
    subtitle = "R1: Live assessment  |  R2, R3: Video-based assessment",
    x        = "Rater",
    y        = "Total Checklist Score",
    fill     = "Rater"
  )

6 Biểu đồ Bland-Altman

library(ggplot2)
bias_val  <- mean(df$video_vs_live, na.rm = TRUE)
sd_diff   <- sd(df$video_vs_live,   na.rm = TRUE)
upper_loa <- bias_val + 1.96 * sd_diff
lower_loa <- bias_val - 1.96 * sd_diff
# Tính giới hạn trục Y có padding
y_padding <- (upper_loa - lower_loa) * 0.15
y_min <- lower_loa - y_padding
y_max <- upper_loa + y_padding

# Vị trí X cố định cho nhãn (bên trái plot)
x_label <- min(df$avg_axis, na.rm = TRUE)

df$avg_axis <- (df$R1 + df$mean_video_rater) / 2

ggplot(df, aes(x = avg_axis, y = video_vs_live)) +

  # Vùng LoA tô nhạt giúp dễ đọc hơn
  annotate("rect",
           xmin = -Inf, xmax = Inf,
           ymin = lower_loa, ymax = upper_loa,
           fill = "#FFCCCC", alpha = 0.15) +

  geom_point(alpha = 0.6, color = "steelblue", size = 2.5) +

  # Đường bias
  geom_hline(yintercept = bias_val,  color = "darkred",
             linewidth = 1) +
  # Đường LoA
  geom_hline(yintercept = upper_loa, color = "#CC0000",
             linetype = "dashed", linewidth = 0.8) +
  geom_hline(yintercept = lower_loa, color = "#CC0000",
             linetype = "dashed", linewidth = 0.8) +
  # Đường zero
  geom_hline(yintercept = 0, color = "black",
             linetype = "dotted", linewidth = 0.6) +

  # Nhãn đặt bên TRÁI, vjust dương = xuống dưới đường (tránh bị cắt)
  annotate("label",
           x = x_label, y = upper_loa,
           label = paste("Upper LoA:", round(upper_loa, 2)),
           vjust = -0.3, hjust = 0,
           color = "#CC0000", fill = "white",
           label.size = 0, size = 3.5) +
  annotate("label",
           x = x_label, y = bias_val,
           label = paste("Bias:", round(bias_val, 2)),
           vjust = -0.3, hjust = 0,
           color = "darkred", fill = "white",
           label.size = 0, size = 3.5, fontface = "bold") +
  annotate("label",
           x = x_label, y = lower_loa,
           label = paste("Lower LoA:", round(lower_loa, 2)),
           vjust = 1.3, hjust = 0,
           color = "#CC0000", fill = "white",
           label.size = 0, size = 3.5) +

  # Mở rộng trục Y để nhãn không bị cắt
  scale_y_continuous(limits = c(y_min, y_max)) +

  # clip = "off" là lưới phòng thủ cuối cùng
  coord_cartesian(clip = "off") +

  theme_bw(base_size = 12) +
  theme(
    plot.title   = element_text(face = "bold", size = 13),
    plot.subtitle = element_text(color = "gray40", size = 11),
    panel.grid.minor = element_blank()
  ) +
  labs(
    title    = "Bland-Altman Plot: Agreement between Live and Video-based Rating",
    subtitle = paste("N = 87 students  |  Systematic Bias =", round(bias_val, 2)),
    x = "Average Score (Live + Video) / 2",
    y = "Difference (Video \u2212 Live Score)"
  )

7 Hồi quy Borderline (BRS)

model_brs       <- lm(mean_checklist ~ GRS, data = df)
intercept       <- coef(model_brs)[1]
slope           <- coef(model_brs)[2]
final_cut_score <- intercept + slope * 2

kable(
  data.frame(
    Intercept_a  = round(intercept, 3),
    Slope_b      = round(slope, 3),
    R_Squared    = round(summary(model_brs)$r.squared, 3),
    BRS_CutScore = round(final_cut_score, 2)
  ),
  caption = "Thông số hồi quy Borderline (BRS)"
) %>%
  kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)
Thông số hồi quy Borderline (BRS)
Intercept_a Slope_b R_Squared BRS_CutScore
(Intercept) 18.594 1.369 0.332 21.33
df$Status_BRS <- ifelse(df$mean_checklist >= final_cut_score, "Pass", "Fail")

kable(
  data.frame(
    Total_Students = nrow(df),
    Cut_Score_BRS  = round(final_cut_score, 2),
    Pass_Count     = sum(df$Status_BRS == "Pass"),
    Fail_Count     = sum(df$Status_BRS == "Fail"),
    Pass_Percent   = round(mean(df$Status_BRS == "Pass") * 100, 1),
    Fail_Percent   = round(mean(df$Status_BRS == "Fail") * 100, 1)
  ),
  caption = "Kết quả phân loại Đậu/Rớt theo BRS"
) %>%
  kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)
Kết quả phân loại Đậu/Rớt theo BRS
Total_Students Cut_Score_BRS Pass_Count Fail_Count Pass_Percent Fail_Percent
(Intercept) 87 21.33 52 35 59.8 40.2

#Vẽ biểu đồ hồi quy Boderline

ggplot(df, aes(x = GRS, y = mean_checklist)) +

  # Vùng CI trước để nằm dưới các layer khác
  geom_smooth(method = "lm", se = TRUE,
              color    = "#2E86AB",   # Xanh dương hiện đại
              fill     = "#A8D5E2",   # CI nhạt cùng gam
              linewidth = 1.4, alpha = 0.25) +

  # Điểm dữ liệu
  geom_jitter(width = 0.08, height = 0,
              alpha = 0.65, size = 2.2,
              color = "#2E86AB", shape = 16) +

  # Đường cut-score
  geom_hline(yintercept = final_cut_score,
             linetype = "dashed", linewidth = 0.85,
             color = "#E84855") +

  # Nhãn cut-score — dùng label để có nền trắng
  annotate("label",
           x = 1.55, y = final_cut_score,
           label = paste("Cut-score =", round(final_cut_score, 2)),
           color = "#E84855", fill = "white",
           label.size = 0.3, fontface = "bold", size = 3.8) +

  # Mở rộng trục Y để CI trông rõ hơn
  scale_y_continuous(
    limits = c(13, 27),
    breaks = seq(14, 26, by = 2)
  ) +
  scale_x_continuous(
    limits = c(0.7, 4.3),
    breaks = 1:4,
    labels = c("1\n(Fail)", "2\n(Borderline)", "3\n(Pass)", "4\n(Excellent)")
  ) +

  theme_bw(base_size = 12) +
  theme(
    plot.title        = element_text(face = "bold", size = 13, color = "#1a1a2e"),
    plot.subtitle     = element_text(size = 10.5, color = "gray45", margin = margin(b = 8)),
    axis.title        = element_text(size = 11, color = "gray25"),
    axis.text         = element_text(size = 10, color = "gray35"),
    panel.grid.minor  = element_blank(),
    panel.grid.major  = element_line(color = "gray90", linewidth = 0.4),
    panel.border      = element_rect(color = "gray75", linewidth = 0.6),
    plot.margin       = margin(12, 16, 12, 12)
  ) +
  labs(
    title    = "Borderline Regression: Cut-score Determination",
    subtitle = paste0("Linear regression of mean checklist score on GRS  |  ",
                      "R\u00b2 = ", round(summary(model_brs)$r.squared, 2),
                      "  |  Cut-score = ", round(final_cut_score, 2)),
    x = "Global Rating Scale (GRS)",
    y = "Mean Checklist Score (0 \u2013 26)"
  )

8 Biểu đồ Đậu/Rớt

ggplot(df, aes(x = Status_BRS, fill = Status_BRS)) + geom_bar() + geom_text(stat = “count”, aes(label = after_stat(count)), vjust = -0.5) + scale_fill_manual(values = c(“Pass” = “#2ca25f”, “Fail” = “#de2d26”)) + theme_minimal() + labs(title = “Kết quả phân loại Đậu/Rớt theo BRS”, subtitle = paste(“Điểm đậu xác định:”, round(final_cut_score, 2), “/ 26”), x = “Kết quả”, y = “Số lượng sinh viên”)


``` r
#TÍNH TỈ LỆ ĐẬU THEO ANGOFF
library(tidyverse)
library(irr)
library(ggplot2)
cut_angoff <- 18.67
cut_brs <- 21.33
# PHÂN LOẠI VÀ SO SÁNH ---
df <- df %>%
  mutate(
    Status_Angoff = ifelse(mean_checklist >= cut_angoff, "Pass", "Fail"),
    Status_BRS = ifelse(mean_checklist >= cut_brs, "Pass", "Fail")
  )
# Tạo bảng so sánh tỉ lệ
Comparison_Table <- data.frame(
  Method = c("Angoff", "Borderline Regression"),
  Cut_Score = round(c(cut_angoff, cut_brs), 2),
  Pass_Count = c(sum(df$Status_Angoff == "Pass"), sum(df$Status_BRS == "Pass")),
  Pass_Percent = c(mean(df$Status_Angoff == "Pass")*100, mean(df$Status_BRS == "Pass")*100)
)
view(Comparison_Table)
# PHÂN TÍCH ĐỘ ĐỒNG THUẬN (AGREEMENT) ---
# Tính Cohen's Kappa giữa 2 phương pháp
kappa_methods <- kappa2(df[, c("Status_Angoff", "Status_BRS")])
view(kappa_methods)
#VẼ BIỂU ĐỒ SO SÁNH (MOSAIC PLOT)
df_mosaic <- df %>%
  count(Status_Angoff, Status_BRS) %>%
  group_by(Status_Angoff) %>%
  mutate(
    pct   = n / sum(n),
    label = paste0(round(pct * 100, 1), "%\n(n = ", n, ")")
  )

ggplot(df_mosaic,
       aes(x = Status_Angoff, y = pct,
           fill = Status_BRS, label = label)) +

  geom_col(width = 0.52,
           color     = "white",
           linewidth = 1.5) +

  # Nhãn % bên trong cột
  geom_text(position  = position_stack(vjust = 0.5),
            size      = 3.8,
            fontface  = "bold",
            color     = "white",
            lineheight = 1.4) +

  scale_y_continuous(
    labels = scales::percent_format(accuracy = 1),
    expand = expansion(mult = c(0, 0.04))
  ) +

  scale_x_discrete(
    labels = c("Fail" = "Fail (Angoff)",
               "Pass" = "Pass (Angoff)")
  ) +

  scale_fill_manual(
    values = c("Pass" = "#5B9BD5",
               "Fail" = "#F28B82"),
    labels = c("Pass" = "Pass (BRS)",
               "Fail" = "Fail (BRS)")
  ) +

  theme_minimal(base_size = 12) +
  theme(
    plot.title         = element_text(face = "bold", size = 13,
                                      color = "#1a1a2e"),
    plot.subtitle      = element_text(size = 10.5, color = "gray45",
                                      margin = margin(b = 10)),
    axis.title         = element_text(size = 11, color = "gray30"),
    axis.text          = element_text(size = 10.5, color = "gray30"),
    panel.grid.major.x = element_blank(),
    panel.grid.minor   = element_blank(),
    panel.grid.major.y = element_line(color = "gray90", linewidth = 0.4),
    legend.title       = element_text(size = 10.5, face = "bold"),
    legend.text        = element_text(size = 10),
    plot.margin        = margin(12, 16, 12, 12)
  ) +

  labs(
    title    = "Classification Agreement: Angoff vs. Borderline Regression",
    subtitle = paste0("Cohen\u2019s Kappa = ", round(kappa_methods$value, 3),
                      "  |  N = ", nrow(df), " students"),
    x    = "Angoff Classification",
    y    = "Proportion of BRS Classification (%)",
    fill = "BRS Result"
  )

9 So sánh Angoff vs. BRS

#Bảng 2x2 thống kê số lượng SV đậu-rớt giữa 2PP
cut_angoff <- 18.67
cut_brs    <- final_cut_score   # Kết quả từ bước BRS ở trên

df <- df %>%
  mutate(
    Status_Angoff = ifelse(mean_checklist >= cut_angoff, "Pass", "Fail"),
    Status_BRS    = ifelse(mean_checklist >= cut_brs,    "Pass", "Fail")
  )

kappa_methods    <- kappa2(df[, c("Status_Angoff", "Status_BRS")])
agreement_matrix <- table(BRS = df$Status_BRS, Angoff = df$Status_Angoff)

kable(
  data.frame(
    Method       = c("Angoff", "Borderline Regression"),
    Cut_Score    = round(c(cut_angoff, cut_brs), 2),
    Pass_Count   = c(sum(df$Status_Angoff == "Pass"), sum(df$Status_BRS == "Pass")),
    Pass_Percent = round(c(mean(df$Status_Angoff == "Pass"),
                           mean(df$Status_BRS    == "Pass")) * 100, 1)
  ),
  caption = "So sánh tỉ lệ Đậu/Rớt: Angoff vs. BRS"
) %>%
  kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)
So sánh tỉ lệ Đậu/Rớt: Angoff vs. BRS
Method Cut_Score Pass_Count Pass_Percent
Angoff 18.67 75 86.2
(Intercept) Borderline Regression 21.33 52 59.8
kable(as.data.frame.matrix(agreement_matrix),
      caption = "Ma trận đồng thuận phân loại: BRS × Angoff") %>%
  kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)
Ma trận đồng thuận phân loại: BRS × Angoff
Fail Pass
Fail 12 23
Pass 0 52
#Biểu đồ so sánh
ggplot(df, aes(x = Status_Angoff, fill = Status_BRS)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(values = c("Pass" = "#377eb8", "Fail" = "#e41a1c")) +
  theme_minimal() +
  labs(title    = "S\u1ef1 \u0111\u1ed3ng thu\u1eadn trong ph\u00e2n lo\u1ea1i: Angoff vs. BRS",
       subtitle = paste("H\u1ec7 s\u1ed1 Kappa =", round(kappa_methods$value, 3)),
       x = "Ph\u00e2n lo\u1ea1i theo Angoff", y = "T\u1ef7 l\u1ec7 theo BRS",
       fill = "K\u1ebft qu\u1ea3 BRS")

10 Phân tích nhóm Tranh chấp (Pass Angoff – Fail BRS)

df_disputed  <- df %>% filter(mean_checklist >= cut_angoff & mean_checklist < cut_brs)
df_both_pass <- df %>% filter(mean_checklist >= cut_brs)

compare_items <- data.frame(
  Item                 = paste0("Item ", 1:12),
  Disputed_Group_Pct2  = sapply(1:12, function(i)
    mean(df_disputed[[paste0("R1_I", i)]] == 2) * 100),
  Both_Pass_Group_Pct2 = sapply(1:12, function(i)
    mean(df_both_pass[[paste0("R1_I", i)]] == 2) * 100)
) %>%
  mutate(Gap = Both_Pass_Group_Pct2 - Disputed_Group_Pct2)

kable(compare_items %>% arrange(desc(Gap)),
      caption = "So sánh tỷ lệ đạt điểm tối đa (Điểm 2): Nhóm Đậu vs. Tranh chấp",
      digits = 1) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = FALSE)
So sánh tỷ lệ đạt điểm tối đa (Điểm 2): Nhóm Đậu vs. Tranh chấp
Item Disputed_Group_Pct2 Both_Pass_Group_Pct2 Gap
Item 10 17.4 59.6 42.2
Item 11 13.0 53.8 40.8
Item 5 39.1 78.8 39.7
Item 9 30.4 63.5 33.0
Item 6 65.2 94.2 29.0
Item 7 69.6 94.2 24.7
Item 2 60.9 84.6 23.7
Item 4 69.6 88.5 18.9
Item 8 82.6 98.1 15.5
Item 12 34.8 50.0 15.2
Item 3 78.3 88.5 10.2
Item 1 100.0 96.2 -3.8

11 Radar Chart: So sánh năng lực 2 nhóm

calc_pct2 <- function(data)
  sapply(1:12, function(i) mean(data[[paste0("R1_I", i)]] == 2) * 100)

radar_data <- as.data.frame(rbind(
  rep(100, 12), rep(0, 12),
  calc_pct2(df_both_pass),
  calc_pct2(df_disputed)
))
colnames(radar_data) <- paste0("Item ", 1:12)

# Màu sắc — giữ gam cũ, chỉ tinh chỉnh độ đậm
colors_border <- c(rgb(0.2, 0.5, 0.5, 0.85),   # Teal — nhóm Pass
                   rgb(0.8, 0.2, 0.2, 0.85))    # Red  — nhóm Disputed
colors_fill   <- c(rgb(0.2, 0.5, 0.5, 0.18),
                   rgb(0.8, 0.2, 0.2, 0.18))

radarchart(
  radar_data,
  axistype    = 1,

  # Đường line dữ liệu — mỏng hơn
  pcol  = colors_border,
  pfcol = colors_fill,
  plwd  = 1.8,           # Giảm từ 4 → 1.8
  plty  = 1,

  # Mạng nhện — mỏng và nhạt hơn
  cglcol = "grey80",
  cglty  = 1,
  cglwd  = 0.4,          # Giảm từ 0.8 → 0.4

  # Nhãn trục
  axislabcol  = "gray50",
  caxislabels = c("0%", "25%", "50%", "75%", "100%"),

  # Nhãn các biến
  vlcex = 0.78,
  vlabels = paste0("Item ", 1:12),

  title = ""             # Dùng mtext bên dưới để kiểm soát font tốt hơn
)

# Tiêu đề bằng mtext (kiểm soát vị trí và font tốt hơn title trong radarchart)
mtext("Competency Profile: Pass Group vs. Disputed Group",
      side = 3, line = 1.5,
      font = 2, cex = 1.1, col = "#1a1a2e")
mtext("Percentage of students scoring full marks (Score = 2) per checklist item",
      side = 3, line = 0.3,
      font = 1, cex = 0.78, col = "gray45")

# Legend tiếng Anh
legend(
  x = 1.25, y = 1.15,
  legend  = c("Pass group (Pass Angoff + Pass BRS)",
              "Disputed group (Pass Angoff \u2013 Fail BRS)"),
  bty     = "n",
  pch     = 20,
  col     = colors_border,
  text.col = "gray20",
  cex     = 0.82,
  pt.cex  = 2.2
)

12 G-Theory: G-Study và D-Study

12.1 Chuyển sang định dạng dài (Long format)

df_long <- df %>%
  select(id, starts_with("R1_I"), starts_with("R2_I"), starts_with("R3_I")) %>%
  pivot_longer(
    cols          = -id,
    names_to      = c("Rater", "Item"),
    names_pattern = "R([1-3])_I([0-9]+)",
    values_to     = "Score"
  ) %>%
  mutate(across(c(id, Rater, Item), as.factor))

cat(sprintf("Long format: %d hàng | %d sinh viên | %d giám khảo | %d items\n",
            nrow(df_long), nlevels(df_long$id),
            nlevels(df_long$Rater), nlevels(df_long$Item)))
## Long format: 3132 hàng | 87 sinh viên | 3 giám khảo | 12 items

12.2 G-Study: Ước tính Variance Components

# Thiết kế s × r × i (Fully Crossed), ước tính bằng REML
model_g <- lmer(
  Score ~ (1|id) + (1|Rater) + (1|Item) +
          (1|id:Rater) + (1|id:Item) + (1|Rater:Item),
  data = df_long, REML = TRUE
)

vc_named <- as.data.frame(VarCorr(model_g))[, c("grp", "vcov")] %>%
  rename(Source = grp, Variance = vcov) %>%
  mutate(
    Source = dplyr::recode(Source,
      "id"         = "Student (s)",
      "Item"       = "Item (i)",
      "Rater"      = "Rater (r)",
      "id:Item"    = "Student \u00d7 Item (si)",
      "id:Rater"   = "Student \u00d7 Rater (sr)",
      "Rater:Item" = "Rater \u00d7 Item (ri)",
      "Residual"   = "Student \u00d7 Rater \u00d7 Item + error (sri,e)"
    ),
    Variance = round(Variance, 5),
    Pct      = round(Variance / sum(Variance) * 100, 1),
    Meaning  = case_when(
      grepl("^Student \\(s\\)",   Source) ~ "Năng lực thực sự của SV — muốn tối đa",
      grepl("^Item",              Source) ~ "Độ khó trung bình khác nhau giữa items",
      grepl("^Rater \\(r\\)",     Source) ~ "Xu hướng chấm khắt/dễ của từng GK",
      grepl("Student.*Item",      Source) ~ "SV giỏi item này nhưng yếu item kia",
      grepl("Student.*Rater",     Source) ~ "GK đánh giá khác nhau về cùng 1 SV ⚠",
      grepl("Rater.*Item",        Source) ~ "GK không đồng thuận về độ khó item",
      grepl("error",              Source) ~ "Sai số ngẫu nhiên không giải thích được",
      TRUE ~ ""
    )
  )

total_var <- sum(vc_named$Variance)

kable(vc_named %>% rename(`Nguồn biến thiên` = Source, `σ²` = Variance,
                           `%` = Pct, `Ý nghĩa` = Meaning),
      caption = "G-Study: Phân tích Thành phần Phương sai (Thiết kế s × r × i)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"),
                full_width = TRUE, font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(1, background = "#cce5ff", bold = TRUE) %>%
  add_footnote(c("σ²(s): Universe Score Variance — càng lớn càng tốt",
                 "Nguồn sai số lớn nhất cần ưu tiên kiểm soát trong thiết kế"),
               notation = "symbol")
G-Study: Phân tích Thành phần Phương sai (Thiết kế s × r × i)
Nguồn biến thiên σ² % Ý nghĩa
Student × Item (si) 0.15080 48.7 SV giỏi item này nhưng yếu item kia
Student × Rater (sr) 0.00117 0.4 GK đánh giá khác nhau về cùng 1 SV ⚠
Student (s) 0.02035 6.6 Năng lực thực sự của SV — muốn tối đa
Rater × Item (ri) 0.00894 2.9 GK không đồng thuận về độ khó item
Item (i) 0.03779 12.2 Độ khó trung bình khác nhau giữa items
Rater (r) 0.00275 0.9 Xu hướng chấm khắt/dễ của từng GK
Student × Rater × Item + error (sri,e) 0.08803 28.4 SV giỏi item này nhưng yếu item kia
* σ²(s): Universe Score Variance — càng lớn càng tốt
Nguồn sai số lớn nhất cần ưu tiên kiểm soát trong thiết kế

12.3 Biểu đồ G-Study

ggplot(vc_named, aes(x = reorder(Source, Pct), y = Pct)) +
  geom_bar(stat = "identity", width = 0.65,
           fill = "#5B9BD5") +          # Xanh pastel đậm, đồng nhất
  geom_text(aes(label = paste0(Pct, "%")),
            hjust = -0.1, size = 3.8,
            color = "gray25", fontface = "bold") +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title        = element_text(face = "bold", size = 13, color = "#1a1a2e"),
    plot.subtitle     = element_text(size = 10.5, color = "gray45",
                                     margin = margin(b = 8)),
    axis.title        = element_text(size = 11, color = "gray30"),
    axis.text.y       = element_text(size = 10, color = "gray25"),
    axis.text.x       = element_text(size = 10, color = "gray45"),
    panel.grid.major.y = element_blank(),
    panel.grid.minor   = element_blank(),
    panel.grid.major.x = element_line(color = "gray90", linewidth = 0.4),
    legend.position    = "none",
    plot.margin        = margin(12, 16, 12, 12)
  ) +
  labs(
    title    = "Variance Component Analysis (G-Study)",
    subtitle = "OSCE Station: Diabetic Foot Examination  |  Design: Student \u00d7 Rater \u00d7 Item",
    x = "Variance component",
    y = "Contribution (%)"
  )

12.4 D-Study: Mô phỏng các kịch bản

# Trích xuất variance components
vc_raw <- as.data.frame(VarCorr(model_g))[, c("grp", "vcov")]

var_s    <- vc_raw$vcov[vc_raw$grp == "id"]
var_i    <- vc_raw$vcov[vc_raw$grp == "Item"]
var_r    <- vc_raw$vcov[vc_raw$grp == "Rater"]
var_si   <- vc_raw$vcov[vc_raw$grp == "id:Item"]
var_sr   <- vc_raw$vcov[vc_raw$grp == "id:Rater"]
var_ri   <- vc_raw$vcov[vc_raw$grp == "Rater:Item"]
var_srie <- vc_raw$vcov[vc_raw$grp == "Residual"]

# Hàm tính G-coefficient (thiết kế s × r × i)
# Eρ² = G-coefficient tương đối | Φ = G-coefficient tuyệt đối
dstudy_calc <- function(n_r, n_i) {
  var_rel <- var_sr / n_r + var_si / n_i + var_srie / (n_r * n_i)
  var_abs <- var_r  / n_r + var_i  / n_i + var_sr / n_r +
             var_si / n_i + var_ri / (n_r * n_i) + var_srie / (n_r * n_i)
  data.frame(
    n_Raters         = n_r,
    n_Items          = n_i,
    G_relative_Erho2 = round(var_s / (var_s + var_rel), 3),
    G_absolute_Phi   = round(var_s / (var_s + var_abs), 3),
    SEM_relative     = round(sqrt(var_rel), 3),
    SEM_absolute     = round(sqrt(var_abs), 3)
  )
}

# Hàm thêm nhãn đánh giá ngưỡng
add_flag <- function(df) {
  df %>% mutate(
    `Eρ² Đánh giá` = case_when(
      G_relative_Erho2 >= 0.80 ~ "\u2714 Đạt",
      G_relative_Erho2 >= 0.70 ~ "\u26a0 Cận ngưỡng",
      TRUE                     ~ "\u2718 Chưa đạt"
    ),
    `Φ Đánh giá` = case_when(
      G_absolute_Phi >= 0.80 ~ "\u2714 Đạt",
      G_absolute_Phi >= 0.70 ~ "\u26a0 Cận ngưỡng",
      TRUE                   ~ "\u2718 Chưa đạt"
    )
  )
}

# Mô phỏng kịch bản
current    <- dstudy_calc(n_r = 3, n_i = 12)
sim_raters <- do.call(rbind, lapply(1:5, dstudy_calc, n_i = 12))
sim_items  <- do.call(rbind, lapply(c(6, 8, 10, 12, 14, 16, 18, 20),
                                    function(ni) dstudy_calc(n_r = 3, n_i = ni)))
col_names <- c("Raters", "Items", "Eρ² (Relative)", "Φ (Absolute)",
               "SEM Relative", "SEM Absolute", "Đánh giá Eρ²", "Đánh giá Φ")

# Bảng A: Kịch bản hiện tại
kable(add_flag(current), caption = "Bảng A — Kịch bản hiện tại: 3 Giám khảo × 12 Items",
      col.names = col_names, align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"),
                full_width = FALSE, font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(1, bold = TRUE,
           background = ifelse(current$G_relative_Erho2 >= 0.80, "#d4edda", "#fff3cd")) %>%
  add_footnote(c("Eρ²: G-coefficient tương đối — dùng cho quyết định xếp hạng",
                 "Φ: G-coefficient tuyệt đối — dùng cho quyết định pass/fail",
                 "Ngưỡng khuyến nghị: ≥ 0.80 cho high-stakes assessment"),
               notation = "symbol")
Bảng A — Kịch bản hiện tại: 3 Giám khảo × 12 Items
Raters Items Eρ² (Relative) Φ (Absolute) SEM Relative SEM Absolute Đánh giá Eρ² Đánh giá Φ
3 12 0.569 0.508 0.124 0.14 ✘ Chưa đạt ✘ Chưa đạt
* Eρ²: G-coefficient tương đối — dùng cho quyết định xếp hạng
Φ: G-coefficient tuyệt đối — dùng cho quyết định pass/fail
Ngưỡng khuyến nghị: ≥ 0.80 cho high-stakes assessment
# Bảng B: Thay đổi số Giám khảo
kable(add_flag(sim_raters),
      caption = "Bảng B — D-Study: Thay đổi số Giám khảo (12 Items cố định)",
      col.names = col_names, align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"),
                full_width = FALSE, font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(which(sim_raters$G_relative_Erho2 >= 0.80), background = "#d4edda") %>%
  row_spec(which(sim_raters$G_relative_Erho2 >= 0.70 &
                 sim_raters$G_relative_Erho2 <  0.80), background = "#fff3cd") %>%
  row_spec(which(sim_raters$G_relative_Erho2 < 0.70),  background = "#f8d7da") %>%
  row_spec(3, bold = TRUE, extra_css = "border: 2px solid #2C3E50;") %>%
  add_footnote(c("Hàng in đậm có viền = kịch bản hiện tại (3 raters)",
                 "Xanh = Eρ² ≥ 0.80 | Vàng = 0.70–0.79 | Đỏ = < 0.70"),
               notation = "symbol")
Bảng B — D-Study: Thay đổi số Giám khảo (12 Items cố định)
Raters Items Eρ² (Relative) Φ (Absolute) SEM Relative SEM Absolute Đánh giá Eρ² Đánh giá Φ
1 12 0.491 0.423 0.145 0.166 ✘ Chưa đạt ✘ Chưa đạt
2 12 0.548 0.484 0.130 0.147 ✘ Chưa đạt ✘ Chưa đạt
3 12 0.569 0.508 0.124 0.140 ✘ Chưa đạt ✘ Chưa đạt
4 12 0.581 0.521 0.121 0.137 ✘ Chưa đạt ✘ Chưa đạt
5 12 0.588 0.529 0.119 0.135 ✘ Chưa đạt ✘ Chưa đạt
* Hàng in đậm có viền = kịch bản hiện tại (3 raters)
Xanh = Eρ² ≥ 0.80 | Vàng = 0.70–0.79 | Đỏ = < 0.70
# Bảng C: Thay đổi số Items
kable(add_flag(sim_items),
      caption = "Bảng C — D-Study: Thay đổi số Items (3 Giám khảo cố định)",
      col.names = col_names, align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"),
                full_width = FALSE, font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(which(sim_items$G_relative_Erho2 >= 0.80), background = "#d4edda") %>%
  row_spec(which(sim_items$G_relative_Erho2 >= 0.70 &
                 sim_items$G_relative_Erho2 <  0.80), background = "#fff3cd") %>%
  row_spec(which(sim_items$G_relative_Erho2 < 0.70),  background = "#f8d7da") %>%
  row_spec(which(sim_items$n_Items == 12), bold = TRUE,
           extra_css = "border: 2px solid #2C3E50;") %>%
  add_footnote(c("Hàng in đậm có viền = kịch bản hiện tại (12 items)",
                 "Xanh = Eρ² ≥ 0.80 | Vàng = 0.70–0.79 | Đỏ = < 0.70"),
               notation = "symbol")
Bảng C — D-Study: Thay đổi số Items (3 Giám khảo cố định)
Raters Items Eρ² (Relative) Φ (Absolute) SEM Relative SEM Absolute Đánh giá Eρ² Đánh giá Φ
3 6 0.401 0.348 0.174 0.195 ✘ Chưa đạt ✘ Chưa đạt
3 8 0.470 0.413 0.151 0.170 ✘ Chưa đạt ✘ Chưa đạt
3 10 0.525 0.465 0.136 0.153 ✘ Chưa đạt ✘ Chưa đạt
3 12 0.569 0.508 0.124 0.140 ✘ Chưa đạt ✘ Chưa đạt
3 14 0.606 0.544 0.115 0.131 ✘ Chưa đạt ✘ Chưa đạt
3 16 0.636 0.574 0.108 0.123 ✘ Chưa đạt ✘ Chưa đạt
3 18 0.662 0.600 0.102 0.117 ✘ Chưa đạt ✘ Chưa đạt
3 20 0.684 0.622 0.097 0.111 ✘ Chưa đạt ✘ Chưa đạt
* Hàng in đậm có viền = kịch bản hiện tại (12 items)
Xanh = Eρ² ≥ 0.80 | Vàng = 0.70–0.79 | Đỏ = < 0.70

12.5 Biểu đồ D-Study

p_dstudy <- sim_raters %>%
  select(n_Raters, G_relative_Erho2, G_absolute_Phi) %>%
  pivot_longer(-n_Raters, names_to = "Coefficient", values_to = "Value") %>%
  mutate(Coefficient = dplyr::recode(Coefficient,
    "G_relative_Erho2" = "E\u03c1\u00b2 (Relative)",
    "G_absolute_Phi"   = "\u03a6 (Absolute)"
  )) %>%
  ggplot(aes(x = n_Raters, y = Value, color = Coefficient, group = Coefficient)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  geom_hline(yintercept = 0.80, linetype = "dashed", color = "red", linewidth = 0.8) +
  annotate("text", x = 1.2, y = 0.815, label = "Ngưỡng 0.80",
           color = "red", size = 3.5) +
  scale_x_continuous(breaks = 1:5, labels = paste0(1:5, "\nRater")) +
  scale_y_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.1)) +
  scale_color_manual(values = c("E\u03c1\u00b2 (Relative)" = "#2196F3",
                                "\u03a6 (Absolute)"        = "#FF5722")) +
  theme_bw(base_size = 13) +
  labs(title    = "D-Study: G-Coefficient theo số Giám khảo",
       subtitle = "12 Items cố định | Thiết kế s \u00d7 r \u00d7 i",
       x = "Số Giám khảo (n_r)", y = "G-Coefficient", color = "Loại hệ số") +
  theme(legend.position = "bottom",
        plot.title = element_text(face = "bold"))

print(p_dstudy)

ggsave("dstudy_plot.png", plot = p_dstudy, width = 7, height = 5, dpi = 300)

12.6 Tóm tắt kết quả G-Theory

max_error_source <- vc_named$Source[
  which.max(ifelse(vc_named$Source == "Student (s)", -Inf, vc_named$Variance))
]

cat(sprintf(paste(
  "Universe Score Variance (σ²s) = %.5f (%.1f%%)",
  "Nguồn sai số lớn nhất: %s",
  "",
  "G-coefficient kịch bản hiện tại (3 Raters × 12 Items):",
  "  Eρ² = %.3f | Φ = %.3f",
  "",
  "G-coefficient nếu chỉ dùng 1 Giám khảo:",
  "  Eρ² = %.3f | Φ = %.3f",
  sep = "\n"),
  var_s, var_s / total_var * 100,
  max_error_source,
  current$G_relative_Erho2, current$G_absolute_Phi,
  sim_raters$G_relative_Erho2[1], sim_raters$G_absolute_Phi[1]
))
## Universe Score Variance (σ²s) = 0.02035 (6.6%)
## Nguồn sai số lớn nhất: Student × Item (si)
## 
## G-coefficient kịch bản hiện tại (3 Raters × 12 Items):
##   Eρ² = 0.569 | Φ = 0.508
## 
## G-coefficient nếu chỉ dùng 1 Giám khảo:
##   Eρ² = 0.491 | Φ = 0.423

#RADAR CHART SO SÁNH NĂNG LỰC 12 ITEM CỦA TẤT CẢ SINH VIÊN

pct2_all <- sapply(1:12, function(i) {
  all_scores <- c(df[[paste0("R1_I", i)]],
                  df[[paste0("R2_I", i)]],
                  df[[paste0("R3_I", i)]])
  mean(all_scores == 2, na.rm = TRUE) * 100
})

radar_single <- as.data.frame(rbind(rep(100,12), rep(0,12), pct2_all))
colnames(radar_single) <- paste0("Item ", 1:12)

col_border <- rgb(0.13, 0.55, 0.55, 0.90)
col_fill   <- rgb(0.13, 0.55, 0.55, 0.20)

draw_radar <- function() {
  par(mar = c(7, 1, 5, 1),
      cex = 1.2)              # Tăng toàn bộ text base lên 1.2x

  radarchart(radar_single, axistype = 1,
             pcol = col_border, pfcol = col_fill, plwd = 2.0, plty = 1,
             cglcol = "grey80", cglty = 1, cglwd = 0.4,
             axislabcol  = "gray50",
             caxislabels = c("0%", "25%", "50%", "75%", "100%"),
             vlcex   = 1.15,             # 0.95 + 2 size → 1.15
             vlabels = paste0("Item ", 1:12),
             title   = "")

  # Tiêu đề
  mtext("Item Difficulty Profile: Full Cohort (N = 87)",
        side = 3, line = 2.5, font = 2, cex = 1.35, col = "#1a1a2e")
  mtext("Percentage of students achieving full marks (Score = 2) per checklist item",
        side = 3, line = 1.2, font = 1, cex = 0.98, col = "gray45")

  # Legend
  mtext(paste0("\u25CF  Easiest: Item ", which.max(pct2_all),
               " (", round(max(pct2_all), 1), "%)"),
        side = 1, line = 4, cex = 1.0, col = "#2ca25f", adj = 0.2)

  mtext(paste0("\u25CF  Hardest: Item ", which.min(pct2_all),
               " (", round(min(pct2_all), 1), "%)"),
        side = 1, line = 4, cex = 1.0, col = "#de2d26", adj = 0.5)

  mtext(paste0("Mean across items: ", round(mean(pct2_all), 1), "%"),
        side = 1, line = 4, cex = 1.0, col = "gray30", adj = 0.8)

  par(mar = c(5, 4, 4, 2), cex = 1)
}

tiff("Fig_Radar_Difficulty.tiff", width = 9, height = 7.5,
     units = "in", res = 600, compression = "lzw")
draw_radar()
dev.off()
## png 
##   2
draw_radar()