Files
2025-10-18 11:56:59 +07:00

185 lines
9.1 KiB
R
Raw Permalink 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 KHÔNG THUA KÉM (NON-INFERIORITY)
# - So sánh hai giá trị trung bình.
# 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 Không thua kém (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", "Biên không thua kém (δ):", value = 5, min = 0),
numericInput("sd", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
numericInput("diff", "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ả tương đương."),
hr(),
h5("Tham số Kiểm định"),
sliderInput("alpha_ni", "Mức ý nghĩa (α, một phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
sliderInput("power_ni", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
hr(),
actionButton("go_ni", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
),
mainPanel(
width = 8,
tabsetPanel(
id = "ni_results_tabs",
type = "pills",
tabPanel("Kết quả & Diễn giải",
withSpinner(uiOutput("ni_result_text"), type = 6, color = "#007bff")),
tabPanel("Phân tích & Đồ thị",
withSpinner(plotOutput("ni_margin_plot"), type = 6, color = "#007bff")),
tabPanel("Giải thích Tham số",
uiOutput("ni_params_ui")),
tabPanel("Giả thuyết, Công thức & Ví dụ",
h4("Giả thuyết thống kê"),
p("Mục tiêu là chứng minh can thiệp mới (Test) không thua kém can thiệp chuẩn (Standard) một cách đáng kể."),
p(strong("Giả thuyết không (H0): Can thiệp mới THUA KÉM."), "Sự khác biệt hiệu quả lớn hơn hoặc bằng một ngưỡng \\(\\delta\\) đã định trước."),
withMathJax(HTML("$$H_0: \\mu_S - \\mu_T \\ge \\delta$$")),
p(strong("Giả thuyết thay thế (Ha): Can thiệp mới KHÔNG THUA KÉM.")),
withMathJax(HTML("$$H_a: \\mu_S - \\mu_T < \\delta$$")),
hr(),
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\mu_T - \\mu_S - \\delta)^2}$$")),
p(strong("Trong đó:")),
tags$ul(
withMathJax(tags$li("\\(\\delta\\) là biên không thua kém (non-inferiority margin).")),
withMathJax(tags$li("\\(\\sigma\\) là độ lệch chuẩn chung của kết quả.")),
withMathJax(tags$li("\\(Z_{\\alpha}\\) là Z-score một phía (ví dụ: 1.645 cho \\(\\alpha=0.05\\)).")),
withMathJax(tags$li("\\(Z_{\\beta}\\) là Z-score của công suất (ví dụ: 0.84 cho power=80%).")),
withMathJax(tags$li("\\(\\mu_T - \\mu_S\\) là sự khác biệt trung bình thực sự kỳ vọng giữa hai nhóm."))
),
hr(),
h4("Ví dụ Y tế công cộng"),
p(strong("Bối cảnh nghiên cứu:")),
p("Một thuốc hạ huyết áp mới (T) được kỳ vọng có hiệu quả tương tự thuốc chuẩn (S) nhưng dễ sử dụng hơn. Các nhà lâm sàng đồng ý rằng nếu thuốc T làm giảm huyết áp kém hơn không quá 5 mmHg so với thuốc S, nó vẫn được coi là chấp nhận được (không thua kém). Độ lệch chuẩn của thay đổi huyết áp là 15 mmHg."),
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_ni <- reactiveValues()
observeEvent(input$go_ni, {
# Tính toán
z_alpha <- qnorm(1 - input$alpha_ni)
z_beta <- qnorm(input$power_ni)
numerator <- 2 * (input$sd^2) * (z_alpha + z_beta)^2
denominator <- (input$diff - input$delta)^2
validate(
need(denominator > 0, "Lỗi: Mẫu số của công thức bằng 0. Khác biệt kỳ vọng không thể bằng biên không thua kém.")
)
n_per_group <- ceiling(numerator / denominator)
rv_ni$n <- n_per_group
# Dữ liệu cho biểu đồ
margin_range <- seq(input$delta * 0.5, input$delta * 1.5, length.out = 100)
plot_data <- data.frame(
margin = margin_range,
n = ceiling(2 * (input$sd^2) * (z_alpha + z_beta)^2 / (input$diff - margin_range)^2)
)
rv_ni$plot_data <- plot_data
})
output$ni_result_text <- renderUI({
if (input$go_ni == 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_ni$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 không thua kém, nghiên cứu của bạn cần:"),
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_ni$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_ni$n * 2)
)
)
})
output$ni_margin_plot <- renderPlot({
req(rv_ni$plot_data)
ggplot(rv_ni$plot_data, aes(x = margin, y = n)) +
geom_line(color = "#007bff", size = 1.2) +
geom_vline(xintercept = input$delta, linetype = "dashed", color = "red", size = 1) +
labs(
title = "Ảnh hưởng của Biên không thua kém (δ) lên Cỡ mẫu",
subtitle = "Biên càng lớn (dễ dãi hơn), cỡ mẫu yêu cầu càng nhỏ",
x = "Biên không thua kém (δ)",
y = "Cỡ mẫu mỗi nhóm (n)"
) +
annotate("text", x = input$delta * 1.05, y = max(rv_ni$plot_data$n) * 0.9,
label = paste0("Biên đã chọn\nδ = ", input$delta),
color = "red", hjust = 0) +
theme_minimal(base_size = 14)
})
output$ni_params_ui <- renderUI({
tagList(
h4("Giải thích các tham số chính"),
tags$div(class="alert alert-light",
h5("Biên không thua kém (Non-inferiority Margin - δ)"),
p("Đây là tham số ", tags$strong("quan trọng nhất và mang tính chủ quan nhất"), " trong thiết kế nghiên cứu không thua kém. Nó định nghĩa mức độ 'thua kém' tối đa có thể chấp nhận được về mặt lâm sàng. Việc lựa chọn δ phải được biện minh một cách chặt chẽ dựa trên bằng chứng y học và ý kiến chuyên gia, thường là một phần hiệu quả của thuốc chuẩn đã được chứng minh trước đây.")
),
tags$div(class="alert alert-light",
h5("Khác biệt trung bình kỳ vọng (μT - μS)"),
p("Đây là sự khác biệt thực sự mà bạn dự đoán giữa hai phương pháp. Trong nhiều trường hợp, để đảm bảo an toàn và tính toán cỡ mẫu đủ lớn, các nhà nghiên cứu thường đặt giá trị này bằng 0 (giả định hai phương pháp có hiệu quả chính xác như nhau).")
),
tags$div(class="alert alert-light",
h5("Mức ý nghĩa (α, một phía)"),
p("Bởi vì giả thuyết của nghiên cứu không thua kém là một chiều (chỉ quan tâm đến việc có 'thua kém' hay không), chúng ta sử dụng mức ý nghĩa một phía. Giá trị α = 0.05 một phía tương ứng với Z-score là 1.645, khác với 1.96 trong kiểm định hai phía.")
)
)
})
}
# ==============================================================================
# PHẦN 4: CHẠY ỨNG DỤNG
# ==============================================================================
shinyApp(ui, server)