# Tải các thư viện cần thiết library(shiny) library(bslib) library(ggplot2) library(shinycssloaders) library(pwr) # --- Giao diện người dùng (UI) --- ui <- fluidPage( theme = bs_theme(version = 5, bootswatch = "cerulean"), withMathJax(), titlePanel("Tính toán Cỡ mẫu cho Kiểm định t ghép cặp"), sidebarLayout( sidebarPanel( h4("Tham số đầu vào"), numericInput("d", label = "Effect Size (Cohen's d):", value = 0.5, min = 0.1, step = 0.1), helpText("Cohen's d đo lường độ lớn của sự khác biệt trung bình của các cặp. Quy ước: 0.2 (nhỏ), 0.5 (trung bình), 0.8 (lớn)."), selectInput("alternative", "Loại kiểm định:", choices = c("Hai phía (Two-sided)" = "two.sided", "Lớn hơn (Greater)" = "greater", "Nhỏ hơn (Less)" = "less")), sliderInput("sig_level", label = "Mức ý nghĩa (\\(\\alpha\\)):", min = 0.01, max = 0.10, value = 0.05, step = 0.01), sliderInput("power", label = "Power mong muốn (\\(1 - \\beta\\)):", min = 0.50, max = 0.99, value = 0.80, step = 0.01), hr(), helpText("Ứng dụng sử dụng hàm pwr.t.test() từ gói 'pwr'. Kết quả và đồ thị sẽ tự động cập nhật ngay lập tức.") ), mainPanel( tabsetPanel( id = "results_tabs", type = "pills", tabPanel( "Kết quả & Diễn giải", h4("Kết quả tính toán"), withSpinner(uiOutput("sample_size_output"), type = 6, color = "#007bff") ), tabPanel( "Đồ thị Power vs. Cỡ mẫu", h4("Mối quan hệ giữa Power và Cỡ mẫu"), withSpinner(plotOutput("power_plot"), type = 6, color = "#007bff") ), tabPanel( "Phân tích Effect Size", h4("Mối quan hệ giữa Cỡ mẫu và Effect Size"), p("Đồ thị này cho thấy cỡ mẫu cần thiết thay đổi như thế nào khi Effect Size thay đổi."), withSpinner(plotOutput("effect_analysis_plot"), type = 6, color = "#007bff") ), tabPanel( "Giả thuyết và Công thức (Helper)", h4("Giả thuyết của Kiểm định t ghép cặp"), p("Kiểm định này được sử dụng khi các quan sát được thu thập theo cặp, ví dụ như đo lường trên cùng một đối tượng trước và sau một can thiệp. Kiểm định thực chất được thực hiện trên sự khác biệt (difference) của mỗi cặp."), p("Đặt \\(D = X_{sau} - X_{trước}\\), và \\(\\mu_D\\) là trung bình của sự khác biệt trong tổng thể."), p("$$H_0: \\mu_D = 0$$"), p("$$H_a: \\mu_D \\neq 0 \\quad (\\text{hoặc } > 0 \\text{ hoặc } < 0\\text{)}$$"), hr(), h4("Công thức tính toán"), tags$b("1. Thống kê kiểm định (Test Statistic):"), p("Giá trị thống kê t được tính trên sự khác biệt của các cặp:"), p("$$ t = \\frac{\\bar{d} - 0}{s_d / \\sqrt{n}} $$"), p("Trong đó \\(\\bar{d}\\) là trung bình của sự khác biệt trong mẫu, \\(s_d\\) là độ lệch chuẩn của sự khác biệt, và \\(n\\) là số lượng cặp."), tags$b("2. Effect Size (Cohen's d):"), p("Cohen's d cho trường hợp ghép cặp được định nghĩa là:"), p("$$ d = \\frac{|\\mu_D|}{\\sigma_D} $$"), p("Trong đó \\(\\mu_D\\) và \\(\\sigma_D\\) là trung bình và độ lệch chuẩn của sự khác biệt trong tổng thể."), hr(), h4("Ví dụ ứng dụng trong Y tế công cộng"), p(tags$b("Tình huống:")), p("Một nhà nghiên cứu muốn đánh giá hiệu quả của một chương trình giáo dục dinh dưỡng mới nhằm giảm huyết áp tâm thu ở bệnh nhân cao huyết áp. Họ đo huyết áp của một nhóm bệnh nhân trước khi tham gia chương trình và đo lại trên chính những bệnh nhân đó sau khi chương trình kết thúc 3 tháng."), p(tags$b("Thiết kế nghiên cứu:")), tags$ul( tags$li("Đối tượng: Cùng một nhóm bệnh nhân."), tags$li("Đo lường: Huyết áp tâm thu."), tags$li("Thời điểm: Trước và Sau can thiệp."), tags$li("Phân tích: Kiểm định t ghép cặp được sử dụng để xem liệu có sự thay đổi huyết áp trung bình có ý nghĩa thống kê hay không.") ), p(tags$b("Tính toán cỡ mẫu:")), p("Trước khi bắt đầu, nhà nghiên cứu muốn biết cần tuyển bao nhiêu bệnh nhân. Dựa trên các nghiên cứu trước, họ kỳ vọng chương trình sẽ giúp giảm huyết áp trung bình khoảng 5 mmHg (đây là \\(\\mu_D\\)) với độ lệch chuẩn của sự thay đổi là 10 mmHg (đây là \\(\\sigma_D\\))."), p("Do đó, Effect Size (Cohen's d) sẽ là: \\(d = | -5 | / 10 = 0.5\\)."), p("Nhà nghiên cứu có thể nhập d = 0.5, power = 0.8, và alpha = 0.05 vào ứng dụng này để tìm ra số lượng bệnh nhân (số cặp) cần thiết cho nghiên cứu của mình.") ) ) ) ) ) # --- Logic của máy chủ (Server) --- server <- function(input, output, session) { # --- PHẦN TÍNH TOÁN CHÍNH --- main_results <- reactive({ req(input$d, input$sig_level, input$power, input$alternative) pwr_result <- tryCatch({ pwr.t.test( d = input$d, sig.level = input$sig_level, power = input$power, type = "paired", # THAY ĐỔI QUAN TRỌNG alternative = input$alternative ) }, error = function(e) NULL) if (is.null(pwr_result)) return(NULL) required_n <- ceiling(pwr_result$n) sample_sizes <- seq(5, required_n + 50, by = 1) power_data <- tryCatch({ pwr.t.test( n = sample_sizes, d = input$d, sig.level = input$sig_level, type = "paired", # THAY ĐỔI QUAN TRỌNG alternative = input$alternative ) }, error = function(e) NULL) if(is.null(power_data)) return(list(required_n = required_n, power_plot_data = NULL)) list( required_n = required_n, power_plot_data = data.frame(SampleSize = power_data$n, Power = power_data$power) ) }) output$sample_size_output <- renderUI({ res <- main_results() if (is.null(res)) { return(tags$div(class = "alert alert-danger", "Có lỗi xảy ra trong quá trình tính toán. Vui lòng kiểm tra lại các tham số.")) } tagList( tags$p("Để phát hiện một effect size (Cohen's d) là", tags$b(input$d), "với power là", tags$b(input$power), "và mức ý nghĩa", tags$b(input$sig_level), ", bạn cần một cỡ mẫu ước tính là:"), tags$h3(style = "color: #007bff; text-align: center;", paste(res$required_n, "cặp quan sát")) ) }) output$power_plot <- renderPlot({ res <- main_results() req(res$power_plot_data) ggplot(res$power_plot_data, aes(x = SampleSize, y = Power)) + geom_line(color = "#007bff", size = 1.2) + geom_hline(yintercept = input$power, linetype = "dashed", color = "red") + geom_vline(xintercept = res$required_n, linetype = "dashed", color = "darkgreen") + labs(title = "Power vs. Số cặp quan sát", x = "Số cặp (n)", y = "Power (1 - β)") + scale_y_continuous(limits = c(0, 1)) + theme_minimal(base_size = 14) }) # --- PHẦN PHÂN TÍCH EFFECT SIZE --- effect_analysis_plot_data <- reactive({ req(input$sig_level, input$power, input$alternative) effect_sizes <- seq(0.1, 1.5, by = 0.05) required_n_values <- sapply(effect_sizes, function(d_val) { res <- tryCatch({ pwr.t.test( d = d_val, sig.level = input$sig_level, power = input$power, type = "paired", # THAY ĐỔI QUAN TRỌNG alternative = input$alternative )$n }, error = function(e) NA) ceiling(res) }) data.frame(EffectSize = effect_sizes, RequiredN = required_n_values) }) output$effect_analysis_plot <- renderPlot({ plot_data <- effect_analysis_plot_data() res <- main_results() # Lấy kết quả chính để đánh dấu p <- ggplot(plot_data, aes(x = EffectSize, y = RequiredN)) + geom_line(color = "#28a745", size = 1.2) + geom_point(color = "#28a745", size = 3, na.rm = TRUE) + labs( title = paste("Số cặp cần thiết vs. Effect Size (Power cố định =", input$power, ")"), x = "Effect Size (Cohen's d)", y = "Số cặp cần thiết (n)" ) + theme_minimal(base_size = 14) # THÊM ĐÁNH DẤU TRỰC QUAN (IMPROVED) if (!is.null(res) && !is.na(res$required_n)) { p <- p + geom_vline(xintercept = input$d, linetype = "dotted", color = "blue", size = 1) + geom_point(aes(x = input$d, y = res$required_n), color = "blue", size = 5, shape = 18) + annotate("text", x = input$d, y = res$required_n, label = paste("n =", res$required_n), vjust = -1.5, color = "blue", fontface = "bold") } p # In đồ thị }) } # Chạy ứng dụng Shiny shinyApp(ui = ui, server = server)