""" 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₁) = 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(θ= 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₁) = 1/3 | 均衡看法 | | **Reference** | Uniform (1,1) | 完全不確定 | | **Sceptical** | 平均=R₀,P(θ>R₁)=5% | 懷疑,不太信 | | **Enthusiastic** | 平均=R₁,P(θR₁) 的值差異很大? ### 問題 3:Trial C vs Trial P Trial P 的結果(48% 反應率)比 Trial C(28%)好很多,論文作者如何解釋這個現象? ### 問題 4:實務應用 如果你是藥廠的統計師,你會選擇哪種事前分佈來分析結果?為什麼? """) # 頁尾 gr.Markdown(""" ---
🔬 Simon Two-stage Design 教學工具
Based on Tan et al. (2002) British Journal of Cancer
Powered by Gradio + OpenAI
""") if __name__ == "__main__": demo.launch()