Wen1201 commited on
Commit
9d95a80
·
verified ·
1 Parent(s): e8792d0

Upload 2 files

Browse files
Files changed (2) hide show
  1. bayesian_core.py +100 -40
  2. bayesian_utils.py +30 -14
bayesian_core.py CHANGED
@@ -18,7 +18,7 @@ class BayesianHierarchicalAnalyzer:
18
 
19
  # 儲存各 session 的分析結果
20
  _session_results = {}
21
-
22
  def __init__(self, session_id):
23
  """
24
  初始化分析器
@@ -30,53 +30,78 @@ class BayesianHierarchicalAnalyzer:
30
  self.df = None
31
  self.model = None
32
  self.trace = None
33
-
 
 
 
 
 
 
 
 
34
  def load_data(self, csv_path_or_df):
35
  """
36
- 載入資料
37
 
38
  Args:
39
  csv_path_or_df: CSV 檔案路徑或 DataFrame
40
 
41
- Expected columns:
42
- - Trial_Type: 屬性名稱 (e.g., Water, Fire, Grass)
43
- - rc: 控制組(速度慢)的勝場
44
- - nc: 控制組總場
45
- - rt: 實驗組(速度快)的勝場
46
- - nt: 實驗組總場
47
  """
48
  if isinstance(csv_path_or_df, str):
49
  self.df = pd.read_csv(csv_path_or_df)
50
  else:
51
  self.df = csv_path_or_df.copy()
52
 
53
- # 驗證必要欄位
54
- required_cols = ['Trial_Type', 'rc', 'nc', 'rt', 'nt']
55
- missing_cols = [col for col in required_cols if col not in self.df.columns]
56
 
57
- if missing_cols:
58
- raise ValueError(f"資料缺少必要欄位: {missing_cols}")
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- return True
61
-
62
  def validate_data(self):
63
  """驗證資料有效性"""
64
  if self.df is None:
65
  raise ValueError("請先載入資料")
66
 
67
  # 檢查數值欄位
68
- for col in ['rc', 'nc', 'rt', 'nt']:
 
 
 
 
 
 
 
69
  if not pd.api.types.is_numeric_dtype(self.df[col]):
70
  raise ValueError(f"欄位 {col} 必須是數值類型")
71
 
72
  # 檢查邏輯約束
73
- if (self.df['rc'] > self.df['nc']).any():
74
- raise ValueError("rc (勝場數) 不能大於 nc (總場數)")
75
 
76
- if (self.df['rt'] > self.df['nt']).any():
77
- raise ValueError("rt (勝場數) 不能大於 nt (總場數)")
78
 
79
- return True
80
 
81
  def run_analysis(self, n_samples=2000, n_tune=1000, n_chains=2, target_accept=0.95):
82
  """
@@ -96,9 +121,19 @@ class BayesianHierarchicalAnalyzer:
96
  self.validate_data()
97
 
98
  # 準備資料
99
- trial_labels = self.df['Trial_Type'].values
100
  num_trials = len(self.df)
101
 
 
 
 
 
 
 
 
 
 
 
102
  # 建立模型
103
  with pm.Model() as self.model:
104
  # --- 先驗分佈 (Priors) ---
@@ -106,16 +141,29 @@ class BayesianHierarchicalAnalyzer:
106
  tau = pm.Gamma('tau', alpha=0.001, beta=0.001)
107
  sigma = pm.Deterministic('sigma', 1 / pm.math.sqrt(tau))
108
 
109
- # --- 各屬性特定效應 (Trial-specific effects) ---
110
  mu = pm.Normal('mu', mu=0, sigma=10, shape=num_trials)
111
  delta = pm.Normal('delta', mu=d, sigma=1 / pm.math.sqrt(tau), shape=num_trials)
112
 
113
  # --- 轉換與似然函數 (Logit Link & Likelihood) ---
114
- pc = pm.Deterministic('pc', pm.math.invlogit(mu))
115
- pt = pm.Deterministic('pt', pm.math.invlogit(mu + delta))
 
 
 
 
 
 
 
 
 
116
 
117
- rc_obs = pm.Binomial('rc_obs', n=self.df['nc'].values, p=pc, observed=self.df['rc'].values)
118
- rt_obs = pm.Binomial('rt_obs', n=self.df['nt'].values, p=pt, observed=self.df['rt'].values)
 
 
 
 
119
 
120
  # --- 其他統計量 ---
121
  delta_new = pm.Normal('delta_new', mu=d, sigma=1 / pm.math.sqrt(tau))
@@ -128,14 +176,14 @@ class BayesianHierarchicalAnalyzer:
128
  chains=n_chains,
129
  target_accept=target_accept,
130
  return_inferencedata=True,
131
- progressbar=False, # 在 Streamlit 中關閉進度條
132
- discard_tuned_samples=False # 👈 加這行!保留 tune 樣本
133
  )
134
 
135
  # 生成摘要統計
136
  summary = az.summary(self.trace, var_names=['d', 'sigma', 'or_speed'], hdi_prob=0.95)
137
 
138
- # 計算各屬性的 delta 統計量
139
  delta_posterior = self.trace.posterior['delta'].values.reshape(-1, num_trials)
140
  delta_mean = delta_posterior.mean(axis=0)
141
  delta_std = delta_posterior.std(axis=0)
@@ -144,12 +192,12 @@ class BayesianHierarchicalAnalyzer:
144
  # 判斷顯著性(HDI 不包含 0)
145
  delta_significant = (delta_hdi[:, 0] > 0) | (delta_hdi[:, 1] < 0)
146
 
147
- # 計算控制組和實驗組的勝率
148
- pc_posterior = self.trace.posterior['pc'].values.reshape(-1, num_trials)
149
- pt_posterior = self.trace.posterior['pt'].values.reshape(-1, num_trials)
150
 
151
- pc_mean = pc_posterior.mean(axis=0)
152
- pt_mean = pt_posterior.mean(axis=0)
153
 
154
  # 整理結果
155
  results = {
@@ -157,6 +205,17 @@ class BayesianHierarchicalAnalyzer:
157
  'n_trials': num_trials,
158
  'trial_labels': trial_labels.tolist(),
159
 
 
 
 
 
 
 
 
 
 
 
 
160
  # 整體效應
161
  'overall': {
162
  'd_mean': float(summary.loc['d', 'mean']),
@@ -175,15 +234,15 @@ class BayesianHierarchicalAnalyzer:
175
  'or_hdi_high': float(summary.loc['or_speed', 'hdi_97.5%']),
176
  },
177
 
178
- # 各屬性的效應
179
  'by_trial': {
180
  'delta_mean': delta_mean.tolist(),
181
  'delta_std': delta_std.tolist(),
182
  'delta_hdi_low': delta_hdi[:, 0].tolist(),
183
  'delta_hdi_high': delta_hdi[:, 1].tolist(),
184
  'delta_significant': delta_significant.tolist(),
185
- 'pc_mean': pc_mean.tolist(),
186
- 'pt_mean': pt_mean.tolist(),
187
  },
188
 
189
  # 原始資料
@@ -215,8 +274,9 @@ class BayesianHierarchicalAnalyzer:
215
  return results
216
 
217
  except Exception as e:
218
- raise Exception(f"分析失敗: {str(e)}")
219
 
 
220
  def _compute_diagnostics(self, summary):
221
  """計算收斂診斷指標"""
222
  try:
 
18
 
19
  # 儲存各 session 的分析結果
20
  _session_results = {}
21
+
22
  def __init__(self, session_id):
23
  """
24
  初始化分析器
 
30
  self.df = None
31
  self.model = None
32
  self.trace = None
33
+
34
+ # 👇 加入這些屬性
35
+ self.col_trial_type = None # 配對名稱欄位
36
+ self.col_control_win = None # 控制組勝場欄位
37
+ self.col_control_total = None # 控制組總場欄位
38
+ self.col_treatment_win = None # 實驗組勝場欄位
39
+ self.col_treatment_total = None # 實驗組總場欄位
40
+
41
+
42
  def load_data(self, csv_path_or_df):
43
  """
44
+ 載入資料 (自動識別欄位名稱)
45
 
46
  Args:
47
  csv_path_or_df: CSV 檔案路徑或 DataFrame
48
 
49
+ Expected format:
50
+ 1 欄: 配對名稱 (Trial_Type)
51
+ 2 欄: 控制組勝場 (例如 water_win)
52
+ 3 欄: 控制組總場 (例如 water_battles)
53
+ 4 欄: 實驗組勝場 (例如 fire_win)
54
+ 5 欄: 實驗組總場 (例如 fire_battles)
55
  """
56
  if isinstance(csv_path_or_df, str):
57
  self.df = pd.read_csv(csv_path_or_df)
58
  else:
59
  self.df = csv_path_or_df.copy()
60
 
61
+ # 檢查欄位數量
62
+ if len(self.df.columns) < 5:
63
+ raise ValueError(f"資料至少需要 5 欄,目前只有 {len(self.df.columns)} 欄")
64
 
65
+ # 自動識別欄位名稱 (假設前 5 欄按照固定順序)
66
+ cols = self.df.columns.tolist()
67
+ self.col_trial_type = cols[0]
68
+ self.col_control_win = cols[1]
69
+ self.col_control_total = cols[2]
70
+ self.col_treatment_win = cols[3]
71
+ self.col_treatment_total = cols[4]
72
+
73
+ print(f"✓ 自動識別欄位:")
74
+ print(f" - 配對名稱: {self.col_trial_type}")
75
+ print(f" - 控制組: {self.col_control_win}/{self.col_control_total}")
76
+ print(f" - 實驗組: {self.col_treatment_win}/{self.col_treatment_total}")
77
+
78
+ return True
79
 
 
 
80
  def validate_data(self):
81
  """驗證資料有效性"""
82
  if self.df is None:
83
  raise ValueError("請先載入資料")
84
 
85
  # 檢查數值欄位
86
+ numeric_cols = [
87
+ self.col_control_win,
88
+ self.col_control_total,
89
+ self.col_treatment_win,
90
+ self.col_treatment_total
91
+ ]
92
+
93
+ for col in numeric_cols:
94
  if not pd.api.types.is_numeric_dtype(self.df[col]):
95
  raise ValueError(f"欄位 {col} 必須是數值類型")
96
 
97
  # 檢查邏輯約束
98
+ if (self.df[self.col_control_win] > self.df[self.col_control_total]).any():
99
+ raise ValueError(f"{self.col_control_win} (勝場數) 不能大於 {self.col_control_total} (總場數)")
100
 
101
+ if (self.df[self.col_treatment_win] > self.df[self.col_treatment_total]).any():
102
+ raise ValueError(f"{self.col_treatment_win} (勝場數) 不能大於 {self.col_treatment_total} (總場數)")
103
 
104
+ return True
105
 
106
  def run_analysis(self, n_samples=2000, n_tune=1000, n_chains=2, target_accept=0.95):
107
  """
 
121
  self.validate_data()
122
 
123
  # 準備資料
124
+ trial_labels = self.df[self.col_trial_type].values
125
  num_trials = len(self.df)
126
 
127
+ # 提取欄位名稱用於模型
128
+ control_win_name = self.col_control_win
129
+ control_total_name = self.col_control_total
130
+ treatment_win_name = self.col_treatment_win
131
+ treatment_total_name = self.col_treatment_total
132
+
133
+ # 提取前綴用於變數命名 (例如 "water_win" → "water")
134
+ control_prefix = control_win_name.replace('_win', '').replace('_battles', '').replace('_total', '')
135
+ treatment_prefix = treatment_win_name.replace('_win', '').replace('_battles', '').replace('_total', '')
136
+
137
  # 建立模型
138
  with pm.Model() as self.model:
139
  # --- 先驗分佈 (Priors) ---
 
141
  tau = pm.Gamma('tau', alpha=0.001, beta=0.001)
142
  sigma = pm.Deterministic('sigma', 1 / pm.math.sqrt(tau))
143
 
144
+ # --- 各配對特定效應 (Pair-specific effects) ---
145
  mu = pm.Normal('mu', mu=0, sigma=10, shape=num_trials)
146
  delta = pm.Normal('delta', mu=d, sigma=1 / pm.math.sqrt(tau), shape=num_trials)
147
 
148
  # --- 轉換與似然函數 (Logit Link & Likelihood) ---
149
+ # 使用動態命名
150
+ p_control = pm.Deterministic(f'p_{control_prefix}', pm.math.invlogit(mu))
151
+ p_treatment = pm.Deterministic(f'p_{treatment_prefix}', pm.math.invlogit(mu + delta))
152
+
153
+ # 使用動態欄位名稱創建觀測值
154
+ control_obs = pm.Binomial(
155
+ f'{control_win_name}_obs',
156
+ n=self.df[control_total_name].values,
157
+ p=p_control,
158
+ observed=self.df[control_win_name].values
159
+ )
160
 
161
+ treatment_obs = pm.Binomial(
162
+ f'{treatment_win_name}_obs',
163
+ n=self.df[treatment_total_name].values,
164
+ p=p_treatment,
165
+ observed=self.df[treatment_win_name].values
166
+ )
167
 
168
  # --- 其他統計量 ---
169
  delta_new = pm.Normal('delta_new', mu=d, sigma=1 / pm.math.sqrt(tau))
 
176
  chains=n_chains,
177
  target_accept=target_accept,
178
  return_inferencedata=True,
179
+ progressbar=False, # 在 Streamlit 中關閉進度條
180
+ discard_tuned_samples=False # 保留 tune 樣本
181
  )
182
 
183
  # 生成摘要統計
184
  summary = az.summary(self.trace, var_names=['d', 'sigma', 'or_speed'], hdi_prob=0.95)
185
 
186
+ # 計算各配對的 delta 統計量
187
  delta_posterior = self.trace.posterior['delta'].values.reshape(-1, num_trials)
188
  delta_mean = delta_posterior.mean(axis=0)
189
  delta_std = delta_posterior.std(axis=0)
 
192
  # 判斷顯著性(HDI 不包含 0)
193
  delta_significant = (delta_hdi[:, 0] > 0) | (delta_hdi[:, 1] < 0)
194
 
195
+ # 計算控制組和實驗組的勝率 (使用動態變數名稱)
196
+ p_control_posterior = self.trace.posterior[f'p_{control_prefix}'].values.reshape(-1, num_trials)
197
+ p_treatment_posterior = self.trace.posterior[f'p_{treatment_prefix}'].values.reshape(-1, num_trials)
198
 
199
+ p_control_mean = p_control_posterior.mean(axis=0)
200
+ p_treatment_mean = p_treatment_posterior.mean(axis=0)
201
 
202
  # 整理結果
203
  results = {
 
205
  'n_trials': num_trials,
206
  'trial_labels': trial_labels.tolist(),
207
 
208
+ # 欄位名稱資訊
209
+ 'column_names': {
210
+ 'trial_type': self.col_trial_type,
211
+ 'control_win': control_win_name,
212
+ 'control_total': control_total_name,
213
+ 'treatment_win': treatment_win_name,
214
+ 'treatment_total': treatment_total_name,
215
+ 'control_prefix': control_prefix,
216
+ 'treatment_prefix': treatment_prefix
217
+ },
218
+
219
  # 整體效應
220
  'overall': {
221
  'd_mean': float(summary.loc['d', 'mean']),
 
234
  'or_hdi_high': float(summary.loc['or_speed', 'hdi_97.5%']),
235
  },
236
 
237
+ # 各配對的效應 (使用動態鍵名)
238
  'by_trial': {
239
  'delta_mean': delta_mean.tolist(),
240
  'delta_std': delta_std.tolist(),
241
  'delta_hdi_low': delta_hdi[:, 0].tolist(),
242
  'delta_hdi_high': delta_hdi[:, 1].tolist(),
243
  'delta_significant': delta_significant.tolist(),
244
+ f'p_{control_prefix}_mean': p_control_mean.tolist(),
245
+ f'p_{treatment_prefix}_mean': p_treatment_mean.tolist(),
246
  },
247
 
248
  # 原始資料
 
274
  return results
275
 
276
  except Exception as e:
277
+ raise Exception(f"分析失敗: {str(e)}")
278
 
279
+
280
  def _compute_diagnostics(self, summary):
281
  """計算收斂診斷指標"""
282
  try:
bayesian_utils.py CHANGED
@@ -232,7 +232,7 @@ def create_summary_table(results):
232
  overall = results['overall']
233
 
234
  summary_data = {
235
- '參數': ['d (整體效應)', 'sigma (屬性間變異)', 'or_speed (勝算比)'],
236
  '平均值': [
237
  f"{overall['d_mean']:.4f}",
238
  f"{overall['sigma_mean']:.4f}",
@@ -257,9 +257,10 @@ def create_summary_table(results):
257
 
258
  return pd.DataFrame(summary_data)
259
 
 
260
  def create_trial_results_table(results):
261
  """
262
- 創建各屬性結果表格
263
 
264
  Args:
265
  results: 分析結果字典
@@ -270,22 +271,28 @@ def create_trial_results_table(results):
270
  trial_labels = results['trial_labels']
271
  by_trial = results['by_trial']
272
  data = results['data']
 
 
 
 
 
273
 
274
  trial_data = {
275
- '屬性': trial_labels,
276
  'Delta (平均)': [f"{x:.4f}" for x in by_trial['delta_mean']],
277
  'Delta (標準差)': [f"{x:.4f}" for x in by_trial['delta_std']],
278
  '95% HDI 下界': [f"{x:.4f}" for x in by_trial['delta_hdi_low']],
279
  '95% HDI 上界': [f"{x:.4f}" for x in by_trial['delta_hdi_high']],
280
  '顯著性': ['★ 顯著' if sig else '不顯著' for sig in by_trial['delta_significant']],
281
- '控制組勝率': [f"{x:.2%}" for x in by_trial['pc_mean']],
282
- '實驗組勝率': [f"{x:.2%}" for x in by_trial['pt_mean']],
283
- '控制組 (勝/總)': [f"{d['rc']}/{d['nc']}" for d in data],
284
- '實驗組 (勝/總)': [f"{d['rt']}/{d['nt']}" for d in data]
285
  }
286
 
287
  return pd.DataFrame(trial_data)
288
 
 
289
  def export_results_to_text(results):
290
  """
291
  匯出結果為純文字格式
@@ -299,6 +306,7 @@ def export_results_to_text(results):
299
  overall = results['overall']
300
  interp = results['interpretation']
301
  diag = results['diagnostics']
 
302
 
303
  report = f"""
304
  ==============================================
@@ -306,7 +314,7 @@ def export_results_to_text(results):
306
  ==============================================
307
 
308
  分析時間: {results['timestamp']}
309
- 屬性數量: {results['n_trials']}
310
 
311
  ----------------------------------------------
312
  1. 整體效應摘要
@@ -316,7 +324,7 @@ d (整體效應 - Log OR):
316
  - 標準差: {overall['d_sd']:.4f}
317
  - 95% HDI: [{overall['d_hdi_low']:.4f}, {overall['d_hdi_high']:.4f}]
318
 
319
- sigma (屬性間變異):
320
  - 平均值: {overall['sigma_mean']:.4f}
321
  - 標準差: {overall['sigma_sd']:.4f}
322
  - 95% HDI: [{overall['sigma_hdi_low']:.4f}, {overall['sigma_hdi_high']:.4f}]
@@ -344,23 +352,29 @@ ESS (sigma): {int(diag['ess_sigma']) if diag['ess_sigma'] is not None else 'N/A'
344
  異質性: {interp['heterogeneity']}
345
 
346
  ----------------------------------------------
347
- 4. 各屬性詳細結果
348
  ----------------------------------------------
349
  """
350
 
351
- # 添加各屬性的詳細資訊
352
  trial_labels = results['trial_labels']
353
  by_trial = results['by_trial']
354
 
 
 
 
 
 
 
355
  for i, label in enumerate(trial_labels):
356
  sig_marker = "★" if by_trial['delta_significant'][i] else " "
357
  report += f"""
358
  {sig_marker} {label}:
359
  Delta (平均): {by_trial['delta_mean'][i]:.4f}
360
  95% HDI: [{by_trial['delta_hdi_low'][i]:.4f}, {by_trial['delta_hdi_high'][i]:.4f}]
361
- 控制組勝率: {by_trial['pc_mean'][i]:.2%}
362
- 實驗組勝率: {by_trial['pt_mean'][i]:.2%}
363
- 勝率差異: {(by_trial['pt_mean'][i] - by_trial['pc_mean'][i]):.2%}
364
  """
365
 
366
  report += """
@@ -368,6 +382,8 @@ ESS (sigma): {int(diag['ess_sigma']) if diag['ess_sigma'] is not None else 'N/A'
368
  """
369
 
370
  return report
 
 
371
 
372
  def plot_odds_ratio_comparison(results):
373
  """
 
232
  overall = results['overall']
233
 
234
  summary_data = {
235
+ '參數': ['d (整體效應)', 'sigma (配對間變異)', 'or_speed (勝算比)'],
236
  '平均值': [
237
  f"{overall['d_mean']:.4f}",
238
  f"{overall['sigma_mean']:.4f}",
 
257
 
258
  return pd.DataFrame(summary_data)
259
 
260
+
261
  def create_trial_results_table(results):
262
  """
263
+ 創建各配對結果表格 (使用動態欄位名稱)
264
 
265
  Args:
266
  results: 分析結果字典
 
271
  trial_labels = results['trial_labels']
272
  by_trial = results['by_trial']
273
  data = results['data']
274
+ col_names = results['column_names']
275
+
276
+ # 動態獲取勝率欄位的鍵名
277
+ control_key = f"p_{col_names['control_prefix']}_mean"
278
+ treatment_key = f"p_{col_names['treatment_prefix']}_mean"
279
 
280
  trial_data = {
281
+ '配對': trial_labels,
282
  'Delta (平均)': [f"{x:.4f}" for x in by_trial['delta_mean']],
283
  'Delta (標準差)': [f"{x:.4f}" for x in by_trial['delta_std']],
284
  '95% HDI 下界': [f"{x:.4f}" for x in by_trial['delta_hdi_low']],
285
  '95% HDI 上界': [f"{x:.4f}" for x in by_trial['delta_hdi_high']],
286
  '顯著性': ['★ 顯著' if sig else '不顯著' for sig in by_trial['delta_significant']],
287
+ f"{col_names['control_prefix']}勝率": [f"{x:.2%}" for x in by_trial[control_key]],
288
+ f"{col_names['treatment_prefix']}勝率": [f"{x:.2%}" for x in by_trial[treatment_key]],
289
+ f"{col_names['control_prefix']} (勝/總)": [f"{d[col_names['control_win']]}/{d[col_names['control_total']]}" for d in data],
290
+ f"{col_names['treatment_prefix']} (勝/總)": [f"{d[col_names['treatment_win']]}/{d[col_names['treatment_total']]}" for d in data]
291
  }
292
 
293
  return pd.DataFrame(trial_data)
294
 
295
+
296
  def export_results_to_text(results):
297
  """
298
  匯出結果為純文字格式
 
306
  overall = results['overall']
307
  interp = results['interpretation']
308
  diag = results['diagnostics']
309
+ col_names = results['column_names']
310
 
311
  report = f"""
312
  ==============================================
 
314
  ==============================================
315
 
316
  分析時間: {results['timestamp']}
317
+ 配對數量: {results['n_trials']}
318
 
319
  ----------------------------------------------
320
  1. 整體效應摘要
 
324
  - 標準差: {overall['d_sd']:.4f}
325
  - 95% HDI: [{overall['d_hdi_low']:.4f}, {overall['d_hdi_high']:.4f}]
326
 
327
+ sigma (配對間變異):
328
  - 平均值: {overall['sigma_mean']:.4f}
329
  - 標準差: {overall['sigma_sd']:.4f}
330
  - 95% HDI: [{overall['sigma_hdi_low']:.4f}, {overall['sigma_hdi_high']:.4f}]
 
352
  異質性: {interp['heterogeneity']}
353
 
354
  ----------------------------------------------
355
+ 4. 各配對詳細結果
356
  ----------------------------------------------
357
  """
358
 
359
+ # 添加各配對的詳細資訊
360
  trial_labels = results['trial_labels']
361
  by_trial = results['by_trial']
362
 
363
+ # 動態獲取鍵名
364
+ control_key = f"p_{col_names['control_prefix']}_mean"
365
+ treatment_key = f"p_{col_names['treatment_prefix']}_mean"
366
+ control_label = col_names['control_prefix'].capitalize()
367
+ treatment_label = col_names['treatment_prefix'].capitalize()
368
+
369
  for i, label in enumerate(trial_labels):
370
  sig_marker = "★" if by_trial['delta_significant'][i] else " "
371
  report += f"""
372
  {sig_marker} {label}:
373
  Delta (平均): {by_trial['delta_mean'][i]:.4f}
374
  95% HDI: [{by_trial['delta_hdi_low'][i]:.4f}, {by_trial['delta_hdi_high'][i]:.4f}]
375
+ {control_label}勝率: {by_trial[control_key][i]:.2%}
376
+ {treatment_label}勝率: {by_trial[treatment_key][i]:.2%}
377
+ 勝率差異: {(by_trial[treatment_key][i] - by_trial[control_key][i]):.2%}
378
  """
379
 
380
  report += """
 
382
  """
383
 
384
  return report
385
+
386
+
387
 
388
  def plot_odds_ratio_comparison(results):
389
  """