219 lines
8.8 KiB
R
219 lines
8.8 KiB
R
# 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)
|