import plotly.graph_objects as go import plotly.express as px import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') # 使用非互動式後端 import arviz as az import io import base64 from PIL import Image def plot_trace(trace, var_names=['d', 'sigma']): """ 繪製 Trace Plot(MCMC 收斂診斷) 包含完整的 warmup + posterior Args: trace: ArviZ InferenceData 物件 var_names: 要繪製的變數名稱 Returns: PIL Image """ fig, axes = plt.subplots(len(var_names), 2, figsize=(14, 4 * len(var_names))) if len(var_names) == 1: axes = axes.reshape(1, -1) # 檢查是否有 warmup_posterior has_warmup = hasattr(trace, 'warmup_posterior') and trace.warmup_posterior is not None for idx, var_name in enumerate(var_names): # 左圖: KDE 密度圖(只用 posterior, 不用 warmup) post_data = trace.posterior[var_name].values for chain_idx in range(post_data.shape[0]): from scipy import stats data = post_data[chain_idx].flatten() density = stats.gaussian_kde(data) xs = np.linspace(data.min(), data.max(), 200) axes[idx, 0].plot(xs, density(xs), alpha=0.8, label=f'Chain {chain_idx+1}') axes[idx, 0].set_xlabel(var_name, fontsize=12) axes[idx, 0].set_ylabel('Density', fontsize=12) axes[idx, 0].set_title(f'{var_name}', fontsize=13, fontweight='bold') if idx == 0: axes[idx, 0].legend() # 右圖: Trace 圖(完整 warmup + posterior) if has_warmup: # 有 warmup: 合併繪製 warmup_data = trace.warmup_posterior[var_name].values post_data = trace.posterior[var_name].values n_warmup = warmup_data.shape[1] n_post = post_data.shape[1] # 定義顏色,讓每條鏈用固定顏色 colors = plt.cm.tab10.colors # 使用 matplotlib 的顏色循環 for chain_idx in range(warmup_data.shape[0]): chain_color = colors[chain_idx % len(colors)] # 每條鏈一個固定顏色 # 繪 warmup 部分 x_warmup = np.arange(n_warmup) axes[idx, 1].plot(x_warmup, warmup_data[chain_idx].flatten(), color=chain_color, # 👈 指定顏色 alpha=0.7, linewidth=0.5, label=f'Chain {chain_idx+1}' if idx == 0 else '') # 繪 posterior 部分 (用同樣的顏色!) x_post = np.arange(n_warmup, n_warmup + n_post) axes[idx, 1].plot(x_post, post_data[chain_idx].flatten(), color=chain_color, # 👈 同一個顏色 alpha=0.7, linewidth=0.5) # 加 Tune 結束的紅線 axes[idx, 1].axvline(x=n_warmup, color='red', linestyle='--', linewidth=2, alpha=0.7, label='Tune結束' if idx == 0 else '') else: # 沒有 warmup: 只用 posterior post_data = trace.posterior[var_name].values for chain_idx in range(post_data.shape[0]): axes[idx, 1].plot(post_data[chain_idx].flatten(), alpha=0.7, linewidth=0.5, label=f'Chain {chain_idx+1}' if idx == 0 else '') axes[idx, 1].set_xlabel('Iteration', fontsize=12) axes[idx, 1].set_ylabel(var_name, fontsize=12) axes[idx, 1].set_title(f'{var_name} trace', fontsize=13, fontweight='bold') if idx == 0: axes[idx, 1].legend(loc='upper right', fontsize=9) axes[idx, 1].grid(alpha=0.3) plt.tight_layout() # 轉換為圖片 buf = io.BytesIO() plt.savefig(buf, format='png', dpi=300, bbox_inches='tight') buf.seek(0) img = Image.open(buf) plt.close() return img # ============================================ # 替換說明: # 在 bayesian_utils.py 中,把第 13-51 行的整個 plot_trace 函數 # 替換成上面這個版本 # ============================================ def plot_posterior(trace, var_names=['d', 'sigma', 'or_speed'], hdi_prob=0.95): """ 繪製後驗分佈圖 Args: trace: ArviZ InferenceData 物件 var_names: 要繪製的變數名稱 hdi_prob: HDI 機率 Returns: PIL Image """ fig = az.plot_posterior(trace, var_names=var_names, hdi_prob=hdi_prob, figsize=(14, 5)) plt.tight_layout() # 轉換為圖片 buf = io.BytesIO() plt.savefig(buf, format='png', dpi=300, bbox_inches='tight') buf.seek(0) img = Image.open(buf) plt.close() return img def plot_forest(trace, trial_labels, title='Effect of Speed on Win Rate by Type'): """ 繪製 Forest Plot(各屬性效應) Args: trace: ArviZ InferenceData 物件 trial_labels: 屬性標籤列表 title: 圖表標題 Returns: PIL Image """ num_trials = len(trial_labels) # 計算統計量 delta_posterior = trace.posterior['delta'].values.reshape(-1, num_trials) delta_mean = delta_posterior.mean(axis=0) delta_hdi = az.hdi(trace, var_names=['delta'], hdi_prob=0.95)['delta'].values # 建立圖表 fig, ax = plt.subplots(figsize=(12, max(10, num_trials * 0.4))) y_pos = np.arange(num_trials) # 繪製信賴區間(橫線) ax.hlines(y_pos, delta_hdi[:, 0], delta_hdi[:, 1], color='steelblue', linewidth=3, label='95% HDI') # 繪製平均值(點) ax.scatter(delta_mean, y_pos, color='darkblue', s=120, zorder=3, edgecolors='white', linewidth=1.5, label='Mean') # 標註顯著的點 for i, (mean, hdi) in enumerate(zip(delta_mean, delta_hdi)): if hdi[0] > 0: # 顯著正效應 ax.text(mean, i, ' ★', fontsize=15, ha='left', va='center', color='gold') elif hdi[1] < 0: # 顯著負效應 ax.text(mean, i, ' ☆', fontsize=15, ha='left', va='center', color='red') # 設定軸 ax.set_yticks(y_pos) ax.set_yticklabels(trial_labels, fontsize=11) ax.invert_yaxis() ax.axvline(0, color='red', linestyle='--', linewidth=2, label='No Effect (δ=0)') ax.set_xlabel('Delta (Log Odds Ratio)', fontsize=13) ax.set_title(title, fontsize=15, fontweight='bold', pad=20) ax.legend(loc='lower right') ax.grid(axis='x', alpha=0.3) plt.tight_layout() # 轉換為圖片 buf = io.BytesIO() plt.savefig(buf, format='png', dpi=300, bbox_inches='tight') buf.seek(0) img = Image.open(buf) plt.close() return img def plot_model_dag(analyzer): """ 繪製模型 DAG 圖 Args: analyzer: BayesianHierarchicalAnalyzer 物件 Returns: PIL Image 或 None """ try: gv = analyzer.get_model_graph() # 轉換為 PNG png_bytes = gv.pipe(format='png') # 轉換為 PIL Image img = Image.open(io.BytesIO(png_bytes)) return img except Exception as e: print(f"無法生成 DAG 圖: {e}") return None def create_summary_table(results): """ 創建結果摘要表格 Args: results: 分析結果字典 Returns: pandas DataFrame """ overall = results['overall'] summary_data = { '參數': ['d (整體效應)', 'sigma (屬性間變異)', 'or_speed (勝算比)'], '平均值': [ f"{overall['d_mean']:.4f}", f"{overall['sigma_mean']:.4f}", f"{overall['or_mean']:.4f}" ], '標準差': [ f"{overall['d_sd']:.4f}", f"{overall['sigma_sd']:.4f}", f"{overall['or_sd']:.4f}" ], '95% HDI 下界': [ f"{overall['d_hdi_low']:.4f}", f"{overall['sigma_hdi_low']:.4f}", f"{overall['or_hdi_low']:.4f}" ], '95% HDI 上界': [ f"{overall['d_hdi_high']:.4f}", f"{overall['sigma_hdi_high']:.4f}", f"{overall['or_hdi_high']:.4f}" ] } return pd.DataFrame(summary_data) def create_trial_results_table(results): """ 創建各屬性結果表格 Args: results: 分析結果字典 Returns: pandas DataFrame """ trial_labels = results['trial_labels'] by_trial = results['by_trial'] data = results['data'] trial_data = { '屬性': trial_labels, 'Delta (平均)': [f"{x:.4f}" for x in by_trial['delta_mean']], 'Delta (標準差)': [f"{x:.4f}" for x in by_trial['delta_std']], '95% HDI 下界': [f"{x:.4f}" for x in by_trial['delta_hdi_low']], '95% HDI 上界': [f"{x:.4f}" for x in by_trial['delta_hdi_high']], '顯著性': ['★ 顯著' if sig else '不顯著' for sig in by_trial['delta_significant']], '控制組勝率': [f"{x:.2%}" for x in by_trial['pc_mean']], '實驗組勝率': [f"{x:.2%}" for x in by_trial['pt_mean']], '控制組 (勝/總)': [f"{d['rc']}/{d['nc']}" for d in data], '實驗組 (勝/總)': [f"{d['rt']}/{d['nt']}" for d in data] } return pd.DataFrame(trial_data) def export_results_to_text(results): """ 匯出結果為純文字格式 Args: results: 分析結果字典 Returns: str: 格式化的文字報告 """ overall = results['overall'] interp = results['interpretation'] diag = results['diagnostics'] report = f""" ============================================== 貝氏階層模型分析報告 ============================================== 分析時間: {results['timestamp']} 屬性數量: {results['n_trials']} ---------------------------------------------- 1. 整體效應摘要 ---------------------------------------------- d (整體效應 - Log OR): - 平均值: {overall['d_mean']:.4f} - 標準差: {overall['d_sd']:.4f} - 95% HDI: [{overall['d_hdi_low']:.4f}, {overall['d_hdi_high']:.4f}] sigma (屬性間變異): - 平均值: {overall['sigma_mean']:.4f} - 標準差: {overall['sigma_sd']:.4f} - 95% HDI: [{overall['sigma_hdi_low']:.4f}, {overall['sigma_hdi_high']:.4f}] or_speed (勝算比): - 平均值: {overall['or_mean']:.4f} - 標準差: {overall['or_sd']:.4f} - 95% HDI: [{overall['or_hdi_low']:.4f}, {overall['or_hdi_high']:.4f}] ---------------------------------------------- 2. 模型收斂診斷 ---------------------------------------------- R-hat (d): {f"{diag['rhat_d']:.4f}" if diag['rhat_d'] is not None else 'N/A'} R-hat (sigma): {f"{diag['rhat_sigma']:.4f}" if diag['rhat_sigma'] is not None else 'N/A'} ESS (d): {int(diag['ess_d']) if diag['ess_d'] is not None else 'N/A'} ESS (sigma): {int(diag['ess_sigma']) if diag['ess_sigma'] is not None else 'N/A'} 收斂狀態: {'✓ 已收斂' if diag['converged'] else '✗ 未收斂'} ---------------------------------------------- 3. 結果解釋 ---------------------------------------------- 整體效應: {interp['overall_effect']} 顯著性: {interp['overall_significance']} 效果大小: {interp['effect_size']} 異質性: {interp['heterogeneity']} ---------------------------------------------- 4. 各屬性詳細結果 ---------------------------------------------- """ # 添加各屬性的詳細資訊 trial_labels = results['trial_labels'] by_trial = results['by_trial'] for i, label in enumerate(trial_labels): sig_marker = "★" if by_trial['delta_significant'][i] else " " report += f""" {sig_marker} {label}: Delta (平均): {by_trial['delta_mean'][i]:.4f} 95% HDI: [{by_trial['delta_hdi_low'][i]:.4f}, {by_trial['delta_hdi_high'][i]:.4f}] 控制組勝率: {by_trial['pc_mean'][i]:.2%} 實驗組勝率: {by_trial['pt_mean'][i]:.2%} 勝率差異: {(by_trial['pt_mean'][i] - by_trial['pc_mean'][i]):.2%} """ report += """ ============================================== """ return report def plot_odds_ratio_comparison(results): """ 繪製各屬性的勝算比比較圖(Plotly 版本) Args: results: 分析結果字典 Returns: plotly figure """ trial_labels = results['trial_labels'] delta_mean = results['by_trial']['delta_mean'] # 轉換為勝算比 or_values = [np.exp(d) for d in delta_mean] # 排序 sorted_indices = np.argsort(or_values)[::-1] sorted_labels = [trial_labels[i] for i in sorted_indices] sorted_or = [or_values[i] for i in sorted_indices] sorted_sig = [results['by_trial']['delta_significant'][i] for i in sorted_indices] # 顏色標記 colors = ['#2ecc71' if sig else '#95a5a6' for sig in sorted_sig] fig = go.Figure() fig.add_trace(go.Bar( x=sorted_or, y=sorted_labels, orientation='h', marker=dict( color=colors, line=dict(color='white', width=1) ), text=[f'{or_val:.2f}' for or_val in sorted_or], textposition='outside', hovertemplate='%{y}
OR: %{x:.3f}' )) # 參考線 (OR = 1) fig.add_vline(x=1, line_dash="dash", line_color="red", line_width=2) fig.update_layout( title='各屬性速度效應(勝算比)', xaxis_title='Odds Ratio', yaxis_title='', width=800, height=max(400, len(trial_labels) * 25), template='plotly_white', showlegend=False ) return fig