# 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 hai yếu tố (Two-Way ANOVA)"), sidebarLayout( sidebarPanel( h4("Tham số đầu vào"), numericInput("a", "Số mức của Yếu tố A:", value = 2, min = 2), numericInput("b", "Số mức của Yếu tố B:", value = 3, min = 2), selectInput("effect_of_interest", "Hiệu ứng cần tính cỡ mẫu:", choices = c("Hiệu ứng tương tác (A x B)" = "interaction", "Hiệu ứng chính của A" = "main_A", "Hiệu ứng chính của B" = "main_B")), numericInput("f2", label = "Effect Size (Cohen's f-squared):", value = 0.15, min = 0.01, step = 0.01), helpText("Cohen's f-squared (f²) đo lường độ lớn của hiệu ứng. Quy ước: 0.02 (nhỏ), 0.15 (trung bình), 0.35 (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("Bậc tự do của tử số (u):"), uiOutput("df_numerator_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"), 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 Two-Way ANOVA"), p("Two-Way ANOVA kiểm tra ba bộ giả thuyết khác nhau:"), tags$ol( tags$li(tags$b("Hiệu ứng chính của Yếu tố A:"), "So sánh trung bình giữa các mức của yếu tố A. \\(H_0: \\mu_{A1} = \\mu_{A2} = \\dots\\)"), tags$li(tags$b("Hiệu ứng chính của Yếu tố B:"), "So sánh trung bình giữa các mức của yếu tố B. \\(H_0: \\mu_{B1} = \\mu_{B2} = \\dots\\)"), tags$li(tags$b("Hiệu ứng tương tác (A x B):"), "Kiểm tra xem hiệu ứng của yếu tố A có phụ thuộc vào mức của yếu tố B hay không (và ngược lại). \\(H_0: \\text{Không có sự tương tác}\\)") ), hr(), h4("Công thức tính toán"), tags$b("1. Bậc tự do của tử số (Numerator df - u):"), p("Giá trị `u` phụ thuộc vào hiệu ứng bạn đang kiểm tra:"), p("$$ u_{A} = a - 1 $$"), p("$$ u_{B} = b - 1 $$"), p("$$ u_{A \\times B} = (a - 1)(b - 1) $$"), p("Trong đó `a` và `b` là số mức của yếu tố A và B."), tags$b("2. Effect Size (Cohen's f-squared):"), p("Cohen's f² đo lường tỷ lệ phương sai được giải thích bởi một hiệu ứng, so với phương sai không được giải thích."), p("$$ f^2 = \\frac{R^2_{effect}}{1 - R^2_{effect}} $$"), p("Trong đó \\(R^2_{effect}\\) là tỷ lệ phương sai được giải thích bởi hiệu ứng đang xét."), hr(), h4("Ví dụ ứng dụng trong Y tế công cộng"), p(tags$b("Tình huống:")), p("Các nhà nghiên cứu muốn đánh giá hiệu quả của một can thiệp mới (ví dụ: một ứng dụng di động về sức khỏe) trong việc tăng số bước đi trung bình hàng ngày. Họ cũng muốn biết liệu hiệu quả của can thiệp có khác nhau giữa nam và nữ hay không."), p(tags$b("Thiết kế nghiên cứu:")), tags$ul( tags$li("Yếu tố A (Can thiệp): Nhóm dùng app, Nhóm đối chứng (không dùng app). (a=2)"), tags$li("Yếu tố B (Giới tính): Nam, Nữ. (b=2)"), tags$li("Phân tích: Two-Way ANOVA."), tags$li(tags$b("Câu hỏi chính (Hiệu ứng tương tác):"), "Liệu ứng dụng có hiệu quả hơn đối với nam hay nữ không? Để trả lời câu hỏi này, họ cần tính cỡ mẫu để có đủ power phát hiện hiệu ứng tương tác.") ), p(tags$b("Tính toán cỡ mẫu:")), p("Nhà nghiên cứu chọn 'Hiệu ứng tương tác' trong ứng dụng. Họ kỳ vọng một hiệu ứng ở mức 'trung bình', và chọn \\(f^2 = 0.15\\). Họ nhập các giá trị này vào ứng dụng để tìm ra số người cần thiết cho mỗi ô trong thiết kế (nam/dùng app, nữ/dùng app, nam/không dùng, nữ/không dùng).") ) ) ) ) ) # --- Logic của máy chủ (Server) --- server <- function(input, output, session) { # Tính toán bậc tự do tử số (u) u <- reactive({ req(input$a, input$b, input$effect_of_interest) if (input$effect_of_interest == "main_A") { return(input$a - 1) } else if (input$effect_of_interest == "main_B") { return(input$b - 1) } else { # interaction return((input$a - 1) * (input$b - 1)) } }) output$df_numerator_display <- renderUI({ withMathJax(paste0("$$ u = ", u(), " $$")) }) # --- PHẦN TÍNH TOÁN CHÍNH --- main_results <- reactive({ req(u(), input$f2, input$sig_level, input$power, input$a, input$b) # pwr.f2.test giải ra v (bậc tự do mẫu số) pwr_result <- tryCatch({ pwr.f2.test( u = u(), f2 = input$f2, sig.level = input$sig_level, power = input$power ) }, error = function(e) NULL) if (is.null(pwr_result)) return(NULL) v <- pwr_result$v # Từ v, tính tổng cỡ mẫu N và cỡ mẫu mỗi ô n # v = N - a*b => N = v + a*b total_N <- ceiling(v + (input$a * input$b)) required_n_per_cell <- ceiling(total_N / (input$a * input$b)) # Tạo dữ liệu cho đồ thị Power sample_sizes_per_cell <- seq(5, required_n_per_cell + 50, by = 1) # Tính lại v cho mỗi n để tính power v_for_plot <- (sample_sizes_per_cell * input$a * input$b) - (input$a * input$b) power_data <- tryCatch({ pwr.f2.test( u = u(), v = v_for_plot, f2 = input$f2, sig.level = input$sig_level ) }, error = function(e) NULL) if(is.null(power_data)) return(list(required_n = required_n_per_cell, power_plot_data = NULL)) list( required_n = required_n_per_cell, power_plot_data = data.frame(SampleSize = sample_sizes_per_cell, 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.")) } tagList( tags$p("Để phát hiện một", tags$b(names(which(c("interaction"="Hiệu ứng tương tác (A x B)", "main_A"="Hiệu ứng chính của A", "main_B"="Hiệu ứng chính của B") == input$effect_of_interest))), "có độ lớn (f²) là", tags$b(input$f2), "với power là", tags$b(input$power), ", 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 ô (cell)")), tags$p(style = "text-align: center; font-style: italic;", "Tổng cỡ mẫu là ", res$required_n * input$a * input$b, ".") ) }) 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 ô)", x = "Cỡ mẫu cho mỗi ô (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(u(), input$sig_level, input$power, input$a, input$b) effect_sizes <- seq(0.01, 0.5, by = 0.01) required_n_values <- sapply(effect_sizes, function(f2_val) { res <- tryCatch({ pwr.f2.test( u = u(), f2 = f2_val, sig.level = input$sig_level, power = input$power )$v }, error = function(e) NA) if(is.na(res)) return(NA) total_N <- res + (input$a * input$b) ceiling(total_N / (input$a * input$b)) }) 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 ô (n)" ) + theme_minimal(base_size = 14) if (!is.null(res) && !is.na(res$required_n)) { p <- p + geom_vline(xintercept = input$f2, linetype = "dotted", color = "blue", size = 1) + geom_point(aes(x = input$f2, y = res$required_n), color = "blue", size = 5, shape = 18) + annotate("text", x = input$f2, 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)