File size: 20,911 Bytes
c43ac66 c7930dd a196335 c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd 25bf28f c7930dd c43ac66 c7930dd a196335 c7930dd a196335 c43ac66 c7930dd a196335 c7930dd a196335 c7930dd a196335 c7930dd a196335 c7930dd a196335 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 8e53b26 c43ac66 c7930dd c43ac66 c7930dd c43ac66 a196335 c7930dd a196335 c7930dd a196335 c7930dd a196335 c7930dd a196335 c7930dd c43ac66 c7930dd a196335 c7930dd 9a9e178 c7930dd 9a9e178 71b92f3 9a9e178 71b92f3 9a9e178 71b92f3 a196335 9a9e178 71b92f3 9a9e178 71b92f3 9a9e178 c7930dd 9a9e178 c7930dd 9a9e178 c7930dd 9a9e178 71b92f3 9a9e178 71b92f3 9a9e178 71b92f3 c43ac66 c7930dd 9a9e178 c43ac66 c7930dd c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c43ac66 d9f0e73 c7930dd d9f0e73 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd d9f0e73 c7930dd d9f0e73 c7930dd d9f0e73 c7930dd d9f0e73 c7930dd d9f0e73 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd 9a9e178 c7930dd 9a9e178 71b92f3 c7930dd 9a9e178 c7930dd 9a9e178 c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd c43ac66 c7930dd 71b92f3 c43ac66 9a9e178 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 | """
Simon Two-stage Design 互動式教學工具
賽門式二階段試驗設計 - 基於 Tan et al. (2002) BJC 論文
完整版:含 Trial C、Trial P、4種事前分佈
修改版:使用 Google Gemini API
"""
import gradio as gr
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# ============== 計算函數 ==============
def beta_mean(a, b):
"""Beta 分佈平均值"""
return a / (a + b)
def beta_var(a, b):
"""Beta 分佈變異數"""
return (a * b) / ((a + b)**2 * (a + b + 1))
def beta_ci(a, b, level=0.95):
"""Beta 分佈可信區間"""
lower = (1 - level) / 2
return stats.beta.ppf(lower, a, b), stats.beta.ppf(1-lower, a, b)
def calc_clinical_prior(r0, r1):
"""
Clinical Prior - 使用論文的精確數值
Trial C: Beta(0.7, 2.1)
Trial P: Beta(0.6, 3.0)
"""
if r0 == 0.10 and r1 == 0.30:
return 0.7, 2.1 # Trial C (論文 Table 1)
elif r0 == 0.05 and r1 == 0.20:
return 0.6, 3.0 # Trial P (論文 Table 2)
else:
# 其他情況用計算
m = (r0 + r1) / 2
return m * 2, (1 - m) * 2
def calc_sceptical_prior(r0, r1):
"""
Sceptical Prior - 使用論文的精確數值
Trial C: Beta(1, 9)
Trial P: Beta(0.4, 7.6)
"""
if r0 == 0.10 and r1 == 0.30:
return 1, 9 # Trial C (論文 Table 1)
elif r0 == 0.05 and r1 == 0.20:
return 0.4, 7.6 # Trial P (論文 Table 2)
else:
return r0 * 10, (1 - r0) * 10
def calc_enthusiastic_prior(r0, r1):
"""
Enthusiastic Prior - 使用論文的精確數值
Trial C: Beta(3, 7)
Trial P: Beta(2.4, 9.6)
"""
if r0 == 0.10 and r1 == 0.30:
return 3, 7 # Trial C (論文 Table 1)
elif r0 == 0.05 and r1 == 0.20:
return 2.4, 9.6 # Trial P (論文 Table 2)
else:
return r1 * 10, (1 - r1) * 10
# ============== 主分析函數 ==============
def analyze(trial_type,
s1_success, s1_total, s2_success, s2_total,
prior_type, custom_a, custom_b):
# 根據試驗類型設定參數
if trial_type == "Trial C (初治組)":
r0, r1 = 0.10, 0.30
trial_name = "Trial C:Chemotherapy-naïve(初治組)"
trial_name_en = "Trial C: Chemotherapy-naive"
else:
r0, r1 = 0.05, 0.20
trial_name = "Trial P:Previously treated(曾治療組)"
trial_name_en = "Trial P: Previously treated"
# 基本計算
s1_success, s1_total = int(s1_success), int(s1_total)
s2_success, s2_total = int(s2_success), int(s2_total)
total_success = s1_success + s2_success
total_n = s1_total + s2_total
# 根據事前分佈類型計算參數
if prior_type == "Clinical Prior(臨床事前分佈)":
prior_a, prior_b = calc_clinical_prior(r0, r1)
prior_desc = f"根據專家意見,P(θ<R₀) = P(R₀<θ<R₁) = P(θ>R₁) = 1/3"
prior_name_en = "Clinical Prior"
elif prior_type == "Reference Prior(參考事前分佈)":
prior_a, prior_b = 1, 1 # Uniform
prior_desc = "無資訊事前分佈(Uniform),代表完全不確定"
prior_name_en = "Reference Prior"
elif prior_type == "Sceptical Prior(懷疑性事前分佈)":
prior_a, prior_b = calc_sceptical_prior(r0, r1)
prior_desc = f"平均值 = R₀ = {r0*100:.0f}%,P(θ>R₁) = 5%(偏向不相信藥效)"
prior_name_en = "Sceptical Prior"
elif prior_type == "Enthusiastic Prior(樂觀性事前分佈)":
prior_a, prior_b = calc_enthusiastic_prior(r0, r1)
prior_desc = f"平均值 = R₁ = {r1*100:.0f}%,P(θ<R₀) = 5%(偏向相信藥效)"
prior_name_en = "Enthusiastic Prior"
else: # 自訂
prior_a, prior_b = custom_a, custom_b
prior_desc = "使用者自訂參數"
prior_name_en = "Custom Prior"
# === 計算事後分佈 ===
# Stage 1 後
post1_a = prior_a + s1_success
post1_b = prior_b + (s1_total - s1_success)
# Stage 2 後(最終)
post2_a = prior_a + total_success
post2_b = prior_b + (total_n - total_success)
# === 計算機率 ===
# 事前
prior_prob_lt_r0 = stats.beta.cdf(r0, prior_a, prior_b)
prior_prob_r0_r1 = stats.beta.cdf(r1, prior_a, prior_b) - prior_prob_lt_r0
prior_prob_gt_r1 = 1 - stats.beta.cdf(r1, prior_a, prior_b)
# Stage 1 後
post1_prob_lt_r0 = stats.beta.cdf(r0, post1_a, post1_b)
post1_prob_r0_r1 = stats.beta.cdf(r1, post1_a, post1_b) - post1_prob_lt_r0
post1_prob_gt_r1 = 1 - stats.beta.cdf(r1, post1_a, post1_b)
# Stage 2 後
post2_prob_lt_r0 = stats.beta.cdf(r0, post2_a, post2_b)
post2_prob_r0_r1 = stats.beta.cdf(r1, post2_a, post2_b) - post2_prob_lt_r0
post2_prob_gt_r1 = 1 - stats.beta.cdf(r1, post2_a, post2_b)
# === 頻率學派判定 ===
if trial_type == "Trial C (初治組)":
stage1_threshold = 2 # 至少 2 人有反應
final_threshold = 6 # 至少 6 人有反應
else:
stage1_threshold = 1 # 至少 1 人有反應
final_threshold = 4 # 至少 4 人有反應
stage1_pass = s1_success >= stage1_threshold
final_pass = total_success >= final_threshold
# === 輸出結果 ===
result = f"""
## 📋 試驗資訊:{trial_name}
### 設計參數
- **R₀(無興趣反應率)**: {r0*100:.0f}%
- **R₁(理想反應率)**: {r1*100:.0f}%
---
## 📊 頻率學派分析
### 試驗結果
| 階段 | 成功 (R) | 失敗 (N-R) | 總數 (N) | 反應率 |
|------|----------|------------|----------|--------|
| Stage I | {s1_success} | {s1_total - s1_success} | {s1_total} | {s1_success/s1_total*100:.1f}% |
| Stage II | {s2_success} | {s2_total - s2_success} | {s2_total} | {s2_success/s2_total*100:.1f}% |
| **合計** | **{total_success}** | **{total_n - total_success}** | **{total_n}** | **{total_success/total_n*100:.1f}%** |
### Simon Minimax Design 判定
- Stage I:需 ≥ {stage1_threshold} 人有反應 → {'✅ 通過' if stage1_pass else '❌ 未通過'}(實際 {s1_success} 人)
- 最終:需 ≥ {final_threshold} 人有反應 → {'✅ 有效' if final_pass else '❌ 無效'}(實際 {total_success} 人)
---
## 🔮 貝氏分析
### 事前分佈:{prior_type}
{prior_desc}
**Beta({prior_a:.1f}, {prior_b:.1f})**,平均值 = {beta_mean(prior_a, prior_b)*100:.1f}%
### 機率表(對應論文 Table 1 & 2)
| 階段 | Beta 參數 | π(平均值) | θ ≤ R₀ | R₀ < θ ≤ R₁ | θ > R₁ |
|------|-----------|-------------|--------|-------------|--------|
| Prior | Beta({prior_a:.1f}, {prior_b:.1f}) | **{beta_mean(prior_a, prior_b):.3f}** | {prior_prob_lt_r0:.3f} | {prior_prob_r0_r1:.3f} | {prior_prob_gt_r1:.3f} |
| Stage 1 posterior | Beta({post1_a:.1f}, {post1_b:.1f}) | **{beta_mean(post1_a, post1_b):.3f}** | {post1_prob_lt_r0:.3f} | {post1_prob_r0_r1:.3f} | {post1_prob_gt_r1:.3f} |
| Stage 2 posterior | Beta({post2_a:.1f}, {post2_b:.1f}) | **{beta_mean(post2_a, post2_b):.3f}** | {post2_prob_lt_r0:.3f} | {post2_prob_r0_r1:.3f} | {post2_prob_gt_r1:.3f} |
### 貝氏更新過程(練習用)
| 事前分佈 | Stage I 結果 | Stage I 後分佈 | Stage II 結果 | Stage II 後分佈 |
|----------|-------------|---------------|--------------|----------------|
| Beta({prior_a:.1f},{prior_b:.1f}), π={beta_mean(prior_a, prior_b):.2f} | 成功:{s1_success}, 失敗:{s1_total - s1_success} | Beta({post1_a:.1f},{post1_b:.1f}), π={beta_mean(post1_a, post1_b):.2f} | 成功:{s2_success}, 失敗:{s2_total - s2_success} | Beta({post2_a:.1f},{post2_b:.1f}), π={beta_mean(post2_a, post2_b):.3f} |
### 結果解讀
- **P(θ > R₁) = {post2_prob_gt_r1:.1%}**:藥物反應率超過理想值的機率
- **P(θ > R₀) = {(1-post2_prob_lt_r0):.1%}**:藥物值得繼續研究的機率
"""
if post2_prob_gt_r1 > 0.5:
result += "📌 **結論**:強烈支持進入 Phase III 試驗\n"
elif post2_prob_gt_r1 > 0.25:
result += "📌 **結論**:中度支持,可考慮進一步研究\n"
else:
result += "📌 **結論**:證據不足,需謹慎評估\n"
# === 繪圖(英文標籤)===
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
x = np.linspace(0, 1, 500)
# 左圖:三條分佈曲線
ax1 = axes[0]
ax1.plot(x, stats.beta.pdf(x, prior_a, prior_b), 'b--', lw=2,
label=f'Prior Beta({prior_a:.1f},{prior_b:.1f})')
ax1.plot(x, stats.beta.pdf(x, post1_a, post1_b), 'g-.', lw=2,
label=f'Stage 1 Post Beta({post1_a:.1f},{post1_b:.1f})')
ax1.plot(x, stats.beta.pdf(x, post2_a, post2_b), 'r-', lw=2.5,
label=f'Stage 2 Post Beta({post2_a:.1f},{post2_b:.1f})')
ax1.axvline(r0, color='gray', ls=':', alpha=0.7, label=f'R0={r0}')
ax1.axvline(r1, color='orange', ls=':', alpha=0.7, label=f'R1={r1}')
ax1.set_xlabel('Response Rate (theta)', fontsize=12)
ax1.set_ylabel('Density', fontsize=12)
ax1.set_title(f'{prior_name_en} - Bayesian Update\n{trial_name_en}', fontsize=12)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
ax1.set_xlim(0, 0.8)
# 右圖:4 種事前分佈比較
ax2 = axes[1]
# 計算所有 4 種事前分佈
clin_a, clin_b = calc_clinical_prior(r0, r1)
ref_a, ref_b = 1, 1
scep_a, scep_b = calc_sceptical_prior(r0, r1)
enth_a, enth_b = calc_enthusiastic_prior(r0, r1)
ax2.plot(x, stats.beta.pdf(x, clin_a, clin_b), 'b-', lw=1.5, alpha=0.7,
label=f'Clinical ({clin_a:.1f},{clin_b:.1f})')
ax2.plot(x, stats.beta.pdf(x, ref_a, ref_b), 'gray', ls='--', lw=1.5, alpha=0.7,
label=f'Reference ({ref_a},{ref_b})')
ax2.plot(x, stats.beta.pdf(x, scep_a, scep_b), 'r-', lw=1.5, alpha=0.7,
label=f'Sceptical ({scep_a:.1f},{scep_b:.1f})')
ax2.plot(x, stats.beta.pdf(x, enth_a, enth_b), 'g-', lw=1.5, alpha=0.7,
label=f'Enthusiastic ({enth_a:.1f},{enth_b:.1f})')
ax2.axvline(r0, color='gray', ls=':', alpha=0.5)
ax2.axvline(r1, color='orange', ls=':', alpha=0.5)
ax2.set_xlabel('Response Rate (theta)', fontsize=12)
ax2.set_ylabel('Density', fontsize=12)
ax2.set_title(f'Comparison of 4 Prior Types\n{trial_name_en} (Figure 1 & 4 style)', fontsize=12)
ax2.legend(fontsize=9)
ax2.grid(alpha=0.3)
ax2.set_xlim(0, 0.8)
plt.tight_layout()
# 建立摘要給 LLM
summary = f"""
Trial: {trial_name}
R0={r0}, R1={r1}
Stage1: {s1_success}/{s1_total}, Stage2: {s2_success}/{s2_total}, Total: {total_success}/{total_n}
Prior: {prior_type}, Beta({prior_a:.1f},{prior_b:.1f})
Posterior: Beta({post2_a:.1f},{post2_b:.1f})
P(theta>R0)={1-post2_prob_lt_r0:.3f}, P(theta>R1)={post2_prob_gt_r1:.3f}
Frequentist: Stage1={'Pass' if stage1_pass else 'Fail'}, Final={'Effective' if final_pass else 'Ineffective'}
"""
return result, fig, summary
# ============== LLM 解釋功能(OpenAI ChatGPT)==============
def get_llm_explanation(summary_str, question, api_key):
"""呼叫 OpenAI API (ChatGPT) 獲得 LLM 解釋"""
if not api_key or api_key.strip() == "":
return "⚠️ 請先輸入 OpenAI API Key\n\n(到 https://platform.openai.com/api-keys 取得)"
if not summary_str:
return "⚠️ 請先執行分析,再請 AI 解釋"
try:
from openai import OpenAI
client = OpenAI(api_key=api_key)
system_prompt = """你是一位專業的生物統計學家和臨床試驗專家,專門教導學生理解 Simon Two-stage Design 和貝氏分析。
你的教學基於 Tan et al. (2002) 發表在 British Journal of Cancer 的論文:
"A Bayesian re-assessment of two Phase II trials of gemcitabine in metastatic nasopharyngeal cancer"
請用繁體中文回答,語氣友善且易懂,像是老師在跟學生解釋。回答時請:
1. 用簡單的語言解釋統計結果的意義
2. 說明頻率學派和貝氏方法的差異
3. 解釋不同事前分佈(Clinical, Reference, Sceptical, Enthusiastic)的意義
4. 給出是否建議進入下一期試驗的建議"""
user_prompt = f"""以下是一個 Simon Two-stage Design 臨床試驗的分析結果:
{summary_str}
學生的問題:{question if question else "請幫我解釋這個結果代表什麼意思?不同的事前分佈會如何影響結論?"}
請用簡單易懂的方式回答。"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_tokens=1500,
temperature=0.7
)
return response.choices[0].message.content
except Exception as e:
return f"❌ API 呼叫失敗:{str(e)}\n\n請確認:\n1. API Key 是否正確\n2. 帳戶是否有餘額"
# ============== Gradio 介面 ==============
with gr.Blocks(
title="Simon Two-stage Design 教學工具",
theme=gr.themes.Soft(primary_hue="blue")
) as demo:
# 儲存分析結果
analysis_summary = gr.State("")
# 標題
gr.Markdown("""
# 🔬 Simon Two-stage Design 互動式教學工具
## 基於 Tan et al. (2002) British Journal of Cancer 論文
**A Bayesian re-assessment of two Phase II trials of gemcitabine in metastatic nasopharyngeal cancer**
""")
# 故事背景
with gr.Accordion("📖 教學故事線", open=True):
gr.Markdown("""
> 🎯 **核心問題**:新藥開發很燒錢,怎麼「早點知道該不該繼續」?
- 💰 一個新藥開發平均要 **10-20 億美元**
- ⏰ 耗時 **10-15 年**
- 📉 Phase II 失敗率高達 **70%**
↓
「如果藥沒效,能不能早點停?」
「如果有希望,再多收病人確認」
↓
> **1989 年 Richard Simon 提出解決方案:**
> 👉 **Simon Two-stage Design**
---
### 🏥 今天的案例:Gemcitabine 治療鼻咽癌
新加坡國家癌症中心要測試 **gemcitabine(健擇)** 對轉移性鼻咽癌的療效,設計了兩個試驗:
| 試驗 | 對象 | R₀(沒興趣) | R₁(理想) |
|------|------|-------------|-----------|
| **Trial C** | 初治患者 | 10% | 30% |
| **Trial P** | 曾治療患者 | 5% | 20% |
- 若反應率 ≤ R₀ → 藥物不值得繼續研究
- 若反應率 ≥ R₁ → 值得進入 Phase III
👇 **請選擇試驗、輸入數據,看看結果如何!**
""")
with gr.Accordion("📐 方法說明:四種事前分佈", open=False):
gr.Markdown("""
貝氏分析需要設定「事前分佈」,代表看到數據**之前**的信念:
| 類型 | 定義 | 立場 |
|------|------|------|
| **Clinical** | P(θ<R₀) = P(R₀<θ<R₁) = P(θ>R₁) = 1/3 | 均衡看法 |
| **Reference** | Uniform (1,1) | 完全不確定 |
| **Sceptical** | 平均=R₀,P(θ>R₁)=5% | 懷疑,不太信 |
| **Enthusiastic** | 平均=R₁,P(θ<R₀)=5% | 樂觀,相信藥效 |
**Beta-Binomial 共軛更新**:
```
事前:Beta(α, β) + 數據:n人中r人成功 → 事後:Beta(α+r, β+n-r)
```
📚 *Tan SB et al. Br J Cancer. 2002;86(6):843-850.*
""")
gr.Markdown("---")
# 主要設定區
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### ⚙️ 試驗設定")
trial_type = gr.Radio(
choices=["Trial C (初治組)", "Trial P (曾治療組)"],
value="Trial C (初治組)",
label="選擇試驗類型"
)
gr.Markdown("### 📝 輸入試驗數據")
gr.Markdown("*請根據論文填入數據,或點下方按鈕載入*")
with gr.Row():
s1_success = gr.Number(value=None, label="Stage I 成功人數", precision=0)
s1_total = gr.Number(value=None, label="Stage I 總人數", precision=0)
with gr.Row():
s2_success = gr.Number(value=None, label="Stage II 成功人數", precision=0)
s2_total = gr.Number(value=None, label="Stage II 總人數", precision=0)
gr.Markdown("### 🔮 選擇事前分佈")
prior_type = gr.Radio(
choices=[
"Clinical Prior(臨床事前分佈)",
"Reference Prior(參考事前分佈)",
"Sceptical Prior(懷疑性事前分佈)",
"Enthusiastic Prior(樂觀性事前分佈)",
"Custom(自訂)"
],
value="Clinical Prior(臨床事前分佈)",
label="事前分佈類型"
)
with gr.Row(visible=False) as custom_row:
custom_a = gr.Number(value=1, label="自訂 α", precision=1)
custom_b = gr.Number(value=1, label="自訂 β", precision=1)
# 載入預設值按鈕
gr.Markdown("### 📚 載入論文數據(或自己填完後對答案)")
with gr.Row():
load_c_btn = gr.Button("📥 載入 Trial C 數據", size="sm")
load_p_btn = gr.Button("📥 載入 Trial P 數據", size="sm")
btn = gr.Button("🚀 執行分析", variant="primary", size="lg")
with gr.Column(scale=2):
gr.Markdown("### 📊 分析結果")
output_text = gr.Markdown()
output_plot = gr.Plot()
# 載入預設值函數
def load_trial_c():
return "Trial C (初治組)", 3, 15, 4, 10
def load_trial_p():
return "Trial P (曾治療組)", 7, 13, 6, 14
load_c_btn.click(load_trial_c, outputs=[trial_type, s1_success, s1_total, s2_success, s2_total])
load_p_btn.click(load_trial_p, outputs=[trial_type, s1_success, s1_total, s2_success, s2_total])
# 顯示/隱藏自訂參數
def toggle_custom(prior):
return gr.Row(visible=(prior == "Custom(自訂)"))
prior_type.change(toggle_custom, prior_type, custom_row)
# 執行分析
btn.click(
fn=analyze,
inputs=[trial_type, s1_success, s1_total, s2_success, s2_total,
prior_type, custom_a, custom_b],
outputs=[output_text, output_plot, analysis_summary]
)
# ========== LLM 解釋區 ==========
gr.Markdown("---")
gr.Markdown("### 🤖 AI 助教解釋")
with gr.Row():
with gr.Column(scale=2):
user_question = gr.Textbox(
label="你的問題(選填)",
placeholder="例如:為什麼 Sceptical Prior 和 Enthusiastic Prior 的結論會不同?",
lines=2
)
with gr.Column(scale=1):
api_key = gr.Textbox(
label="OpenAI API Key",
placeholder="sk-...",
type="password"
)
ask_ai_btn = gr.Button("🧠 請 AI 助教解釋", variant="secondary")
ai_response = gr.Markdown()
ask_ai_btn.click(
fn=get_llm_explanation,
inputs=[analysis_summary, user_question, api_key],
outputs=[ai_response]
)
# 習題
gr.Markdown("---")
with gr.Accordion("✏️ 論文導讀問題", open=False):
gr.Markdown("""
### 問題 1:頻率學派 vs 貝氏方法
論文中提到「The conclusions drawn using the Bayesian approach were in general agreement with those obtained from the frequentist analysis」,但貝氏方法有什麼額外的優點?
### 問題 2:事前分佈的影響
比較 Trial C 使用 Sceptical Prior 和 Enthusiastic Prior 的結果(論文 Table 1),為什麼 P(θ>R₁) 的值差異很大?
### 問題 3:Trial C vs Trial P
Trial P 的結果(48% 反應率)比 Trial C(28%)好很多,論文作者如何解釋這個現象?
### 問題 4:實務應用
如果你是藥廠的統計師,你會選擇哪種事前分佈來分析結果?為什麼?
""")
# 頁尾
gr.Markdown("""
---
<center>
🔬 Simon Two-stage Design 教學工具<br>
Based on Tan et al. (2002) British Journal of Cancer<br>
<small>Powered by Gradio + OpenAI</small>
</center>
""")
if __name__ == "__main__":
demo.launch() |