import streamlit as st import pandas as pd import numpy as np import uuid from datetime import datetime import os import io # 頁面配置 st.set_page_config( page_title="Bayesian Meta-Analysis - Pokémon Type Comparison", page_icon="🎲", layout="wide", initial_sidebar_state="expanded" ) # 自定義 CSS st.markdown(""" """, unsafe_allow_html=True) # 導入自定義模組 from meta_analysis_core import BayesianMetaAnalyzer from meta_analysis_llm_assistant import MetaAnalysisLLMAssistant from meta_analysis_utils import ( plot_trace_combined, plot_posterior, plot_forest, plot_dag, create_dag_legend_table, format_summary_stats ) # 初始化 session state if 'session_id' not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) if 'analysis_results' not in st.session_state: st.session_state.analysis_results = None if 'analyzer' not in st.session_state: st.session_state.analyzer = None if 'available_types' not in st.session_state: st.session_state.available_types = [] if 'df_full' not in st.session_state: st.session_state.df_full = None if 'chat_history' not in st.session_state: st.session_state.chat_history = [] if 'llm_assistant' not in st.session_state: st.session_state.llm_assistant = None # 標題 st.title("🎲 Bayesian Meta-Analysis System") st.markdown("### 貝氏統合分析模型") st.markdown("---") # Sidebar with st.sidebar: st.header("⚙️ 配置設定") # API 選擇 st.subheader("🤖 AI 助手設定") api_choice = st.radio( "選擇 LLM API", options=["Google Gemini", "Anthropic Claude"], index=0, help="選擇要使用的 AI 助手" ) # API Key 輸入 if api_choice == "Google Gemini": api_key = st.text_input( "Google Gemini API Key", type="password", help="輸入您的 Google Gemini API Key" ) else: # Claude api_key = st.text_input( "Anthropic Claude API Key", type="password", help="輸入您的 Anthropic API Key (https://console.anthropic.com)" ) if api_key: st.session_state.api_key = api_key st.session_state.api_choice = api_choice st.success(f"✅ {api_choice} API Key 已載入") st.markdown("---") # 資料來源(自動載入) st.subheader("📊 資料來源") # 自動載入資料 DATA_PATH = "all_types_wide_30_completed_sas.csv" if st.session_state.df_full is None: try: df_full = pd.read_csv(DATA_PATH) st.session_state.df_full = df_full # 提取可用屬性 type_columns = [col.replace('_win_count', '').replace('_total_battles', '') for col in df_full.columns if '_win_count' in col] available_types = sorted(set(type_columns)) st.session_state.available_types = available_types except FileNotFoundError: st.error(f"❌ 找不到資料檔案:{DATA_PATH}") if st.session_state.df_full is not None: st.success(f"✅ 資料已載入!共 {len(st.session_state.df_full)} 筆記錄") st.info(f"📋 可用屬性數量: {len(st.session_state.available_types)}") # MCMC 參數設定 st.subheader("🔬 蒙地卡羅馬可夫鏈 參數") n_warmup = st.number_input( "Burn-in 樣本數", min_value=500, max_value=5000, value=1000, step=100, help="Burn-in 樣本數" ) n_samples = st.number_input( "Sampling 樣本數", min_value=1000, max_value=10000, value=2000, step=500, help="Sampling 樣本數" ) n_chains = st.selectbox( "鏈數 (Chains)", options=[1, 2, 4], index=1, help="蒙地卡羅馬可夫鏈 數量" ) target_accept = st.slider( "目標接受率 (Target Accept)", min_value=0.80, max_value=0.99, value=0.95, step=0.01, help="NUTS 採樣器的目標接受率,越高越謹慎但越慢" ) st.info(f"💡 總迭代數 = {n_warmup} (Warmup) + {n_samples} (Sample) = {n_warmup + n_samples}") st.markdown("---") # 關於系統 with st.expander("ℹ️ 關於此系統"): st.markdown(""" **貝氏後設分析系統** 本系統使用貝氏階層模型進行後設分析, 比較不同寶可夢屬性在多個道館中的對戰表現。 **主要功能:** - 🎲 貝氏推論與後驗分佈 - 📊 階層模型(借用資訊) - 📈 5 種視覺化圖表 - 📋 完整統計報告 - 🔍 收斂診斷 **分析特色:** - 完整的 MCMC trace(warmup + posterior) - 中文 DAG 節點對照表 - 預測新研究效果 - 異質性評估 """) # 主要內容區 tab1, tab2, tab3 = st.tabs(["📊 貝氏分析", "📈 視覺化結果", "💬 AI 助手"]) # Tab 1: 貝氏分析 with tab1: st.header("📊 貝氏統合分析") if st.session_state.df_full is None: st.warning("⚠️ 資料載入失敗,請確認資料檔案存在") else: # 顯示資料預覽 with st.expander("👀 資料預覽"): st.dataframe(st.session_state.df_full, use_container_width=True) st.markdown("---") # 屬性選擇 st.subheader("🎯 選擇比較屬性") col1, col2 = st.columns(2) with col1: treatment_type = st.selectbox( "實驗組屬性(Treatment)", options=["請選擇..."] + st.session_state.available_types, index=0, help="選擇要分析的實驗組屬性" ) with col2: control_type = st.selectbox( "對照組屬性(Control)", options=["請選擇..."] + st.session_state.available_types, index=0, help="選擇要分析的對照組屬性" ) # 驗證選擇 if treatment_type == "請選擇..." or control_type == "請選擇...": st.warning("⚠️ 請選擇實驗組和對照組屬性") elif treatment_type == control_type: st.error("❌ 實驗組和對照組不能相同") else: st.success(f"✅ 已選擇: {treatment_type} (實驗組) vs {control_type} (對照組)") # 顯示資料摘要 st.subheader("📋 資料摘要") df_full = st.session_state.df_full treatment_wins = df_full[f'{treatment_type}_win_count'].sum() treatment_total = df_full[f'{treatment_type}_total_battles'].sum() control_wins = df_full[f'{control_type}_win_count'].sum() control_total = df_full[f'{control_type}_total_battles'].sum() col1, col2, col3 = st.columns(3) with col1: st.metric( "資料筆數", f"{len(df_full)} 筆", help="道館(研究單位)的數量" ) with col2: treatment_rate = treatment_wins / treatment_total if treatment_total > 0 else 0 st.metric( f"{treatment_type} 平均勝率", f"{treatment_rate:.3f}", help="實驗組的整體勝率" ) with col3: control_rate = control_wins / control_total if control_total > 0 else 0 st.metric( f"{control_type} 平均勝率", f"{control_rate:.3f}", help="對照組的整體勝率" ) st.markdown("---") # 執行分析按鈕 # 只有在正確選擇屬性時才顯示按鈕 if treatment_type != "請選擇..." and control_type != "請選擇..." and treatment_type != control_type: if st.button("🔬 開始貝氏分析", type="primary", use_container_width=True): # 執行分析... with st.spinner("🔄 正在執行 蒙地卡羅馬可夫鏈 抽樣,請稍候..."): try: # 初始化分析器 analyzer = BayesianMetaAnalyzer( session_id=st.session_state.session_id, treatment_type=treatment_type, control_type=control_type ) # 載入資料 analyzer.load_data( df_full=df_full, treatment_type=treatment_type, control_type=control_type ) # 執行分析 results = analyzer.run_analysis( n_warmup=n_warmup, n_samples=n_samples, n_chains=n_chains, target_accept=target_accept ) # 儲存結果 st.session_state.analysis_results = results st.session_state.analyzer = analyzer st.success("✅ 分析完成!請切換到「視覺化結果」頁籤查看圖表") st.balloons() except Exception as e: st.error(f"❌ 分析失敗: {str(e)}") st.exception(e) # 顯示結果(如果已執行分析) if st.session_state.analysis_results is not None: results = st.session_state.analysis_results st.markdown("---") st.subheader("📊 分析結果摘要") overall = results['overall'] # 使用 metric 卡片顯示 col1, col2, col3 = st.columns(3) with col1: st.metric( label="d (整體效應)", value=f"{overall['d_mean']:.4f}", ) st.markdown(f"↑ HDI: [{overall['d_hdi_low']:.3f}, {overall['d_hdi_high']:.3f}]", unsafe_allow_html=True) with col2: st.metric( label="勝算比 (OR)", value=f"{overall['or_mean']:.3f}", ) st.markdown(f"↑ HDI: [{overall['or_hdi_low']:.3f}, {overall['or_hdi_high']:.3f}]", unsafe_allow_html=True) with col3: st.metric( label="sigma (異質性)", value=f"{overall['sigma_mean']:.4f}", ) st.markdown(f"↑ HDI: [{overall['sigma_hdi_low']:.3f}, {overall['sigma_hdi_high']:.3f}]", unsafe_allow_html=True) # 異質性解釋 sigma_mean = overall['sigma_mean'] if sigma_mean > 0.5: st.info("🔴 **高異質性**: 不同道館的結果差異很大") elif sigma_mean > 0.3: st.info("🟡 **中等異質性**: 不同道館的結果有一定差異") else: st.info("🟢 **低異質性**: 不同道館的結果相對一致") st.markdown("---") # 預測效應 st.markdown("#### 🔮 預測新研究效果") pred = results['predictive'] col1, col2, col3 = st.columns(3) with col1: st.metric( "預測 delta_new", f"{pred['delta_new_mean']:.4f}", delta=f"± {pred['delta_new_sd']:.4f}", help="預測第 31 間道館的效應值" ) with col2: st.metric( "預測 OR", f"{pred['or_new_mean']:.4f}", help="預測的勝算比" ) with col3: st.metric( "不確定性倍數", f"{pred['uncertainty_ratio']:.2f}x", help="相比整體效應的不確定性增加" ) st.markdown("---") # 收斂診斷 st.markdown("#### 🔍 模型診斷") diag = results['diagnostics'] col1, col2, col3 = st.columns(3) with col1: rhat_status = "✅ 已收斂" if diag['converged'] else "❌ 未收斂" st.metric("收斂狀態", rhat_status) with col2: if diag['rhat_d'] is not None: st.metric("R-hat (d)", f"{diag['rhat_d']:.4f}") with col3: if diag['ess_d'] is not None: st.metric("ESS (d)", f"{int(diag['ess_d'])}") # 下載報告 st.markdown("---") st.markdown("#### 📥 下載報告") report_text = format_summary_stats(results) st.download_button( label="📄 下載完整分析報告 (.txt)", data=report_text, file_name=f"meta_analysis_{treatment_type}_vs_{control_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", mime="text/plain", use_container_width=True ) # Tab 2: 視覺化結果 with tab2: st.header("📈 視覺化結果") if st.session_state.analysis_results is None: st.info("ℹ️ 請先在「貝氏分析」頁面執行分析") else: results = st.session_state.analysis_results analyzer = st.session_state.analyzer # 創建 5 個 tab viz_tabs = st.tabs([ "🔍 模型結構 (DAG)", "📋 DAG 對照表", "📉 Trace Plot", #"📊 Posterior Plot", "🌲 Forest Plot" ]) # Tab 1: DAG with viz_tabs[0]: st.markdown("### 🔍 模型結構圖 (DAG)") st.markdown(""" **DAG(Directed Acyclic Graph)** 顯示貝氏階層模型的結構: - **橢圓節點**:隨機變數(prior 和 likelihood) - **矩形節點**:觀測資料 - **菱形節點**:確定性變數(由其他變數推導) - **箭頭**:依賴關係 """) with st.spinner("正在生成 DAG..."): try: dag_img = plot_dag(analyzer.model, results) if dag_img: st.image(dag_img, use_column_width=True) else: st.warning("⚠️ 無法生成 DAG 圖(可能需要安裝 Graphviz)") except Exception as e: st.error(f"生成 DAG 失敗: {str(e)}") # Tab 2: DAG 對照表 with viz_tabs[1]: st.markdown("### 📋 DAG 中文對照表") st.markdown("節點說明表格:解釋 DAG 中每個節點的統計意義和實際情境應用") from meta_analysis_utils import create_dag_legend_html dag_html = create_dag_legend_html(results) st.markdown(dag_html, unsafe_allow_html=True) # Tab 3: Trace Plot with viz_tabs[2]: st.markdown("### 📉 Trace Plot(抽樣軌跡)") st.markdown(""" **Trace Plot 用途**: - **左圖**:後驗分佈的密度圖 - **右圖**:完整的 MCMC 軌跡(warmup + posterior) - **紅色虛線**:標記 burn-in 結束點 **判斷收斂**: - 軌跡圖應該要穩定震盪 - 不同鏈應該混合良好 - R-hat < 1.1 表示已收斂 """) with st.spinner("正在生成 Trace Plot..."): try: trace_img = plot_trace_combined(results) if trace_img: st.image(trace_img, use_column_width=True) except Exception as e: st.error(f"生成 Trace Plot 失敗: {str(e)}") # Tab 4: Posterior Plot # Tab 5: Forest Plot #with viz_tabs[4]: with viz_tabs[3]: st.markdown("### 🌲 Forest Plot(各道館效應)") st.markdown(""" **Forest Plot 用途**: - 顯示每個道館的特定效應(delta) - **點**:平均效應值 - **橫線**:95% HDI - **垂直虛線**:零效應參考線 **解讀**: - HDI 不包含 0 → 該道館有顯著效應 - 點在右側(> 0)→ 實驗組優勢 - 點在左側(< 0)→ 對照組優勢 """) with st.spinner("正在生成 Forest Plot..."): try: forest_img = plot_forest(results) if forest_img: st.image(forest_img, use_column_width=True) except Exception as e: st.error(f"生成 Forest Plot 失敗: {str(e)}") # Tab 3: AI 助手 with tab3: st.header("💬 AI 分析助手") if not st.session_state.get('api_key'): st.warning("⚠️ 請在左側輸入您的 API Key 以使用 AI 助手") st.info("💡 支援 Google Gemini 和 Anthropic Claude") elif st.session_state.analysis_results is None: st.info("ℹ️ 請先在「貝氏分析」頁面執行分析") else: # 初始化 LLM 助手 if st.session_state.llm_assistant is None: api_choice = st.session_state.get('api_choice', 'Google Gemini') st.session_state.llm_assistant = MetaAnalysisLLMAssistant( api_key=st.session_state.api_key, session_id=st.session_state.session_id, api_provider=api_choice ) # 聊天容器 chat_container = st.container() with chat_container: for message in st.session_state.chat_history: with st.chat_message(message["role"]): st.markdown(message["content"]) # 使用者輸入 if prompt := st.chat_input("詢問關於分析結果的任何問題..."): # 添加使用者訊息 st.session_state.chat_history.append({ "role": "user", "content": prompt }) with st.chat_message("user"): st.markdown(prompt) # AI 回應 with st.chat_message("assistant"): with st.spinner("思考中..."): try: response = st.session_state.llm_assistant.get_response( user_message=prompt, analysis_results=st.session_state.analysis_results ) st.markdown(response) except Exception as e: error_msg = f"❌ 錯誤: {str(e)}\n\n請檢查 API key 或重新表達問題。" st.error(error_msg) response = error_msg # 添加助手回應 st.session_state.chat_history.append({ "role": "assistant", "content": response }) st.markdown("---") # 快速問題按鈕 st.subheader("💡 快速問題") quick_questions = [ "📊 給我這次分析的總結", "🎯 解釋 d 和勝算比", "🔍 解釋 sigma(異質性)", "❓ 什麼是貝氏統合分析?", "🔮 解釋預測新道館", "🆚 貝氏 vs 頻率論", "⚔️ 對戰策略建議", "🏛️ 比較不同道館" ] cols = st.columns(4) for idx, question in enumerate(quick_questions): col_idx = idx % 4 if cols[col_idx].button(question, key=f"quick_{idx}"): # 根據問題選擇對應的方法 if "總結" in question: response = st.session_state.llm_assistant.generate_summary( st.session_state.analysis_results ) elif "d 和勝算比" in question: response = st.session_state.llm_assistant.explain_metric( 'd', st.session_state.analysis_results ) elif "sigma" in question or "異質性" in question: response = st.session_state.llm_assistant.explain_metric( 'sigma', st.session_state.analysis_results ) elif "貝氏後設分析" in question: response = st.session_state.llm_assistant.explain_bayesian_meta_analysis() elif "預測" in question: response = st.session_state.llm_assistant.explain_predictive_inference( st.session_state.analysis_results ) elif "貝氏" in question and "頻率論" in question: response = st.session_state.llm_assistant.explain_bayesian_meta_analysis() elif "策略" in question: response = st.session_state.llm_assistant.battle_strategy_advice( st.session_state.analysis_results ) elif "道館" in question: response = st.session_state.llm_assistant.compare_gyms( st.session_state.analysis_results ) else: response = st.session_state.llm_assistant.get_response( question, st.session_state.analysis_results ) # 添加到聊天歷史 st.session_state.chat_history.append({ "role": "user", "content": question }) st.session_state.chat_history.append({ "role": "assistant", "content": response }) st.rerun() # 重置對話按鈕 st.markdown("---") if st.button("🔄 重置對話"): st.session_state.llm_assistant.reset_conversation() st.session_state.chat_history = [] st.success("✅ 對話已重置") st.rerun() # Footer st.markdown("---") st.markdown( f"""

🎲 Bayesian Meta-Analysis System for Pokémon Type Comparison | Built with Streamlit & PyMC

Session ID: {st.session_state.session_id[:8]}

""", unsafe_allow_html=True )