# 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 Phân tích phương sai một yếu tố (One-Way ANOVA)"), sidebarLayout( sidebarPanel( h4("Tham số đầu vào"), numericInput("k", "Số lượng nhóm so sánh (k):", value = 3, min = 2), numericInput("f", label = "Effect Size (Cohen's f):", value = 0.25, min = 0.05, step = 0.05), helpText("Cohen's f đo lường độ lớn của sự khác biệt giữa các trung bình nhóm. Quy ước: 0.1 (nhỏ), 0.25 (trung bình), 0.4 (lớn)."), 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(), h5("Công thức Effect Size (f):"), # Hiển thị công thức tính effect size uiOutput("effect_size_formula_display") ), 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 One-Way ANOVA"), p("Phân tích phương sai một yếu tố (One-Way ANOVA) được sử dụng để so sánh trung bình của ba hay nhiều nhóm độc lập."), p("$$H_0: \\mu_1 = \\mu_2 = \\dots = \\mu_k$$"), p("$$H_a: \\text{Có ít nhất một cặp trung bình không bằng nhau}$$"), hr(), h4("Công thức tính toán"), tags$b("1. Thống kê kiểm định (F-statistic):"), p("Giá trị thống kê F so sánh sự biến thiên giữa các nhóm (Mean Square Between) với sự biến thiên bên trong mỗi nhóm (Mean Square Within)."), p("$$ F = \\frac{MS_{between}}{MS_{within}} $$"), tags$b("2. Effect Size (Cohen's f):"), p("Cohen's f đo lường độ lớn của sự khác biệt giữa các trung bình nhóm, được chuẩn hóa bằng độ lệch chuẩn bên trong nhóm."), p("$$ f = \\frac{\\sigma_m}{\\sigma} $$"), p("Trong đó \\(\\sigma_m\\) là độ lệch chuẩn của các trung bình nhóm và \\(\\sigma\\) là độ lệch chuẩn bên trong mỗi nhóm."), 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 so sánh hiệu quả của ba liệu pháp khác nhau (A: thuốc mới, B: thuốc tiêu chuẩn, C: giả dược) trong việc giảm mức cholesterol ở bệnh nhân."), p(tags$b("Thiết kế nghiên cứu:")), tags$ul( tags$li("Số lượng nhóm (k): 3."), tags$li("Phân tích: One-Way ANOVA sẽ được sử dụng để xem liệu có sự khác biệt đáng kể về mức giảm cholesterol trung bình giữa ba nhóm hay không.") ), p(tags$b("Tính toán cỡ mẫu:")), p("Trước khi tuyển bệnh nhân, nhà nghiên cứu cần xác định số lượng cho mỗi nhóm. Dựa trên các nghiên cứu sơ bộ, họ kỳ vọng một sự khác biệt ở mức 'trung bình' giữa các liệu pháp. Họ chọn một effect size \\(f = 0.25\\)."), p("Họ có thể nhập các giá trị này (k=3, f=0.25, power=0.8, alpha=0.05) vào ứng dụng để tìm ra số bệnh nhân cần thiết cho mỗi nhóm trị liệu.") ) ) ) ) ) # --- Logic của máy chủ (Server) --- server <- function(input, output, session) { # Hiển thị công thức tính effect size output$effect_size_formula_display <- renderUI({ withMathJax("$$ f = \\sqrt{\\frac{\\sum_{i=1}^{k} p_i(\\mu_i - \\bar{\\mu})^2}{\\sigma^2}} $$") }) # --- PHẦN TÍNH TOÁN CHÍNH --- main_results <- reactive({ req(input$k, input$f, input$sig_level, input$power) pwr_result <- tryCatch({ pwr.anova.test( k = input$k, f = input$f, sig.level = input$sig_level, power = input$power ) }, 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.anova.test( k = input$k, n = sample_sizes, f = input$f, sig.level = input$sig_level ) }, 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 sự khác biệt giữa", tags$b(input$k), "nhóm có độ lớn (effect size f) là", tags$b(input$f), "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, "cho mỗi nhóm")), tags$p(style = "text-align: center; font-style: italic;", "Tổng cỡ mẫu là ", res$required_n * input$k, ".") ) }) 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. Cỡ mẫu (cho mỗi nhóm)", x = "Cỡ mẫu cho mỗi nhóm (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$k, input$sig_level, input$power) effect_sizes <- seq(0.05, 0.6, by = 0.05) required_n_values <- sapply(effect_sizes, function(f_val) { res <- tryCatch({ pwr.anova.test( k = input$k, f = f_val, sig.level = input$sig_level, power = input$power )$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() 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("Cỡ mẫu cần thiết vs. Effect Size (Power cố định =", input$power, ")"), x = "Effect Size (f)", y = "Cỡ mẫu cần thiết cho mỗi nhóm (n)" ) + theme_minimal(base_size = 14) if (!is.null(res) && !is.na(res$required_n)) { p <- p + geom_vline(xintercept = input$f, linetype = "dotted", color = "blue", size = 1) + geom_point(aes(x = input$f, y = res$required_n), color = "blue", size = 5, shape = 18) + annotate("text", x = input$f, y = res$required_n, label = paste("n =", res$required_n), vjust = -1.5, color = "blue", fontface = "bold") } p }) } # Chạy ứng dụng Shiny shinyApp(ui = ui, server = server)