Files
Shiny--Code/samplesize/equivalence/app.R
2025-10-18 11:56:59 +07:00

187 lines
9.3 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ==============================================================================
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU TƯƠNG ĐƯƠNG (EQUIVALENCE)
# - So sánh hai giá trị trung bình bằng phương pháp Two One-Sided Tests (TOST).
# Author: Gemini & User Collaboration
# Date: 2025-10-17
# ==============================================================================
# ------------------------------------------------------------------------------
# THIẾT LẬP: Tải các thư viện cần thiết
# ------------------------------------------------------------------------------
library(shiny)
library(bslib)
library(ggplot2)
library(shinycssloaders)
# ==============================================================================
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
# ==============================================================================
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cerulean"),
withMathJax(),
titlePanel("Công Cụ Tính Cỡ Mẫu: Nghiên cứu Tương đương (So sánh 2 Trung bình)"),
sidebarLayout(
sidebarPanel(
width = 4,
h4("Nhập Tham Số"),
br(),
h5("Tham số Hiệu ứng & Biến thiên"),
numericInput("delta_eq", "Biên tương đương (±δ):", value = 5, min = 0),
numericInput("sd_eq", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
numericInput("diff_eq", "Khác biệt trung bình kỳ vọng (μT - μS):", value = 0),
helpText("Giá trị 0 có nghĩa là giả định hai nhóm có hiệu quả chính xác như nhau."),
hr(),
h5("Tham số Kiểm định"),
sliderInput("alpha_eq", "Mức ý nghĩa (α, cho mỗi phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
sliderInput("power_eq", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
hr(),
actionButton("go_eq", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
),
mainPanel(
width = 8,
tabsetPanel(
id = "eq_results_tabs",
type = "pills",
tabPanel("Kết quả & Diễn giải",
withSpinner(uiOutput("eq_result_text"), type = 6, color = "#007bff")),
tabPanel("Phân tích & Đồ thị",
withSpinner(plotOutput("eq_margin_plot"), type = 6, color = "#007bff")),
tabPanel("Giải thích Tham số",
uiOutput("eq_params_ui")),
tabPanel("Giả thuyết, Công thức & Ví dụ",
h4("Giả thuyết thống kê: Two One-Sided Tests (TOST)"),
p("Để chứng minh tương đương, chúng ta phải bác bỏ đồng thời CẢ HAI giả thuyết không sau đây:"),
p(strong("1. Giả thuyết H01: Can thiệp mới thua kém đáng kể.")),
withMathJax(HTML("$$H_{01}: \\mu_T - \\mu_S \\le -\\delta$$")),
p(strong("2. Giả thuyết H02: Can thiệp mới vượt trội đáng kể.")),
withMathJax(HTML("$$H_{02}: \\mu_T - \\mu_S \\ge +\\delta$$")),
p("Chỉ khi bác bỏ được cả hai, chúng ta mới có thể chấp nhận giả thuyết thay thế \\(H_a\\):"),
withMathJax(HTML("$$H_a: -\\delta < \\mu_T - \\mu_S < +\\delta$$")),
hr(),
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
p("Công thức cho kiểm định tương đương, giả định khác biệt kỳ vọng là 0:"),
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{\\delta^2}$$")),
p("Công thức tổng quát hơn khi khác biệt kỳ vọng \\(|\\mu_T - \\mu_S| > 0\\):"),
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\delta - |\\mu_T - \\mu_S|)^2}$$")),
hr(),
h4("Ví dụ Y tế công cộng (Nghiên cứu Tương đương sinh học)"),
p(strong("Bối cảnh nghiên cứu:")),
p("Một công ty dược phẩm sản xuất một phiên bản thuốc generic (T) của một loại thuốc hạ huyết áp nổi tiếng (S). Họ cần chứng minh rằng thuốc generic có hiệu quả tương đương sinh học với thuốc gốc. Tiêu chí tương đương được định nghĩa là sự khác biệt trong mức giảm huyết áp trung bình phải nằm trong khoảng \\(\\pm 5\\) mmHg."),
p(strong("Tham số:")),
tags$ul(
tags$li("Biên tương đương \\(\\delta = 5\\) mmHg."),
tags$li("Độ lệch chuẩn \\(\\sigma = 15\\) mmHg."),
tags$li("Kỳ vọng sự khác biệt là 0."),
tags$li("Mức ý nghĩa \\(\\alpha = 0.05\\) và công suất 80%.")
),
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng.")
)
)
)
)
)
# ==============================================================================
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
# ==============================================================================
server <- function(input, output, session) {
rv_eq <- reactiveValues()
observeEvent(input$go_eq, {
# Validate input: Khác biệt kỳ vọng phải nhỏ hơn biên tương đương
validate(
need(abs(input$diff_eq) < input$delta_eq,
"Lỗi: Khác biệt trung bình kỳ vọng phải nằm trong biên tương đương ( |μT - μS| < δ ). Nếu không, không thể chứng minh tương đương.")
)
# Tính toán
z_alpha <- qnorm(1 - input$alpha_eq)
z_beta <- qnorm(input$power_eq) # Với TOST, thường dùng Z_beta thay vì Z_{beta/2}
numerator <- 2 * (input$sd_eq^2) * (z_alpha + z_beta)^2
denominator <- (input$delta_eq - abs(input$diff_eq))^2
n_per_group <- ceiling(numerator / denominator)
rv_eq$n <- n_per_group
# Dữ liệu cho biểu đồ
margin_range <- seq(abs(input$diff_eq) + 0.1, input$delta_eq * 1.5, length.out = 100)
plot_data <- data.frame(
margin = margin_range,
n = ceiling(2 * (input$sd_eq^2) * (z_alpha + z_beta)^2 / (margin_range - abs(input$diff_eq))^2)
)
rv_eq$plot_data <- plot_data
})
output$eq_result_text <- renderUI({
if (input$go_eq == 0) {
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán Cỡ mẫu' để xem kết quả."))
}
req(rv_eq$n)
tagList(
h4("Kết quả tính toán cỡ mẫu"),
p("Với các tham số đã cho, để chứng minh tính tương đương, nghiên cứu của bạn cần:"),
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_eq$n, " người tham gia MỖI NHÓM"),
p(style = "text-align: center; font-size: 1.2em;",
"Tổng cỡ mẫu cần thiết: ", tags$b(rv_eq$n * 2)
)
)
})
output$eq_margin_plot <- renderPlot({
req(rv_eq$plot_data)
ggplot(rv_eq$plot_data, aes(x = margin, y = n)) +
geom_line(color = "#007bff", size = 1.2) +
geom_vline(xintercept = input$delta_eq, linetype = "dashed", color = "red", size = 1) +
labs(
title = "Ảnh hưởng của Biên tương đương (δ) lên Cỡ mẫu",
subtitle = "Biên càng lớn (khoảng tương đương rộng hơn), cỡ mẫu yêu cầu càng nhỏ",
x = "Biên tương đương (δ)",
y = "Cỡ mẫu mỗi nhóm (n)"
) +
annotate("text", x = input$delta_eq * 1.05, y = max(rv_eq$plot_data$n) * 0.9,
label = paste0("Biên đã chọn\nδ = ", input$delta_eq),
color = "red", hjust = 0) +
theme_minimal(base_size = 14)
})
output$eq_params_ui <- renderUI({
tagList(
h4("Giải thích các tham số chính"),
tags$div(class="alert alert-light",
h5("Biên tương đương (Equivalence Margin - ±δ)"),
p("Đây là tham số định nghĩa 'vùng tương đương'. Nó xác định một khoảng chênh lệch (từ ", tags$b("-δ"), " đến ", tags$b("+δ"), ") mà trong đó, sự khác biệt giữa hai can thiệp được coi là không có ý nghĩa về mặt lâm sàng. Việc lựa chọn δ phải dựa trên cơ sở khoa học vững chắc.")
),
tags$div(class="alert alert-light",
h5("Phương pháp Two One-Sided Tests (TOST)"),
p("Đây là phương pháp thống kê tiêu chuẩn để chứng minh tương đương. Thay vì một kiểm định hai phía thông thường (nhằm chứng minh sự khác biệt), TOST thực hiện hai kiểm định một phía để chứng minh ", tags$strong("sự thiếu vắng một sự khác biệt có ý nghĩa."), " Bạn phải chứng minh rằng khoảng tin cậy 90% (tương ứng với hai kiểm định một phía tại α=0.05) của sự khác biệt nằm hoàn toàn bên trong biên tương đương [-\\(\\delta\\), +\\(\\delta\\)].")
)
)
})
}
# ==============================================================================
# PHẦN 4: CHẠY ỨNG DỤNG
# ==============================================================================
shinyApp(ui, server)