Wen1201 commited on
Commit
6b5847f
·
verified ·
1 Parent(s): 02f7b03

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +177 -10
  2. app.py +540 -0
  3. mcnemar_core.py +241 -0
  4. mcnemar_llm_assistant.py +251 -0
  5. mcnemar_utils.py +338 -0
  6. requirements.txt +6 -0
README.md CHANGED
@@ -1,12 +1,179 @@
1
- ---
2
- title: Mcnemar
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.2.0
8
- app_file: app.py
9
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # McNemar 檢定分析系統 - 寶可夢對戰特徵分析
2
+
3
+ ## 📋 系統簡介
4
+
5
+ 這是一個基於 Streamlit 的 McNemar 檢定分析系統,專為分析寶可夢對戰數據設計,結合 AI 助手提供統計解釋和對戰策略建議。
6
+
7
+ ## 🎯 主要功能
8
+
9
+ ### 1. McNemar 檢定分析
10
+ - ✅ 統計顯著性檢定(p 值)
11
+ - ✅ 勝算比 (Odds Ratio) 計算
12
+ - ✅ 95% 信賴區間估計
13
+ - ✅ 不一致配對分析
14
+ - ✅ 效果大小評估
15
+
16
+ ### 2. 視覺化圖表
17
+ - 📊 列聯表熱力圖
18
+ - 📈 勝算比森林圖
19
+ - 📉 不一致配對分布圖
20
+ - 🎨 顯著性水準視覺化
21
+
22
+ ### 3. AI 智能助手
23
+ - 💬 自然語言對話
24
+ - 📖 統計指標解釋
25
+ - 🎮 對戰策略建議
26
+ - 📚 McNemar 檢定教學
27
+ - 🔍 結果深度分析
28
+
29
+ ## 📦 安裝步驟
30
+
31
+ ### 1. 安裝依賴套件
32
+ ```bash
33
+ pip install -r requirements.txt
34
+ ```
35
+
36
+ ### 2. 準備資料
37
+ 將寶可夢對戰資料 CSV 檔放在同一目錄下,檔名為 `poke_mc_hong_2.csv`
38
+
39
+ **資料格式要求:**
40
+ - 必須包含 `cs_特徵名稱` 和 `cn_特徵名稱` 欄位
41
+ - cs_ = 勝方 (Champion/Winner)
42
+ - cn_ = 敗方 (Challenger/Loser)
43
+ - 數值為 0 或 1 (0=較低, 1=較高)
44
+
45
+ **範例欄位:**
46
+ ```
47
+ cs_HP, cn_HP # 血量
48
+ cs_Attack, cn_Attack # 攻擊
49
+ cs_Speed, cn_Speed # 速度
50
+ cs_Defense, cn_Defense # 防禦
51
+ ```
52
+
53
+ ### 3. 設定 OpenAI API Key
54
+ - 在系統左側邊欄輸入您的 OpenAI API Key
55
+ - API Key 用於 AI 助手功能
56
+
57
+ ### 4. 執行程式
58
+ ```bash
59
+ streamlit run mcnemar_app.py
60
+ ```
61
+
62
+ ## 🔧 檔案結構
63
+
64
+ ```
65
+ mcnemar_app/
66
+ ├── mcnemar_app.py # Streamlit 主程式
67
+ ├── mcnemar_core.py # McNemar 檢定核心邏輯
68
+ ├── mcnemar_llm_assistant.py # AI 對話助手
69
+ ├── mcnemar_utils.py # 視覺化工具
70
+ ├── requirements.txt # 依賴套件
71
+ ├── README.md # 說明文件
72
+ └── poke_mc_hong_2.csv # 寶可夢資料(需自行準備)
73
+ ```
74
+
75
+ ## 📊 使用方式
76
+
77
+ ### Step 1: 載入資料
78
+ 1. 選擇「使用預設資料集」或「上傳您的資料」
79
+ 2. 如果上傳,請確保 CSV 格式正確
80
+
81
+ ### Step 2: 執行分析
82
+ 1. 在「McNemar 分析」頁面選擇要分析的特徵
83
+ 2. 點擊「開始分析」按鈕
84
+ 3. 查看結果的四個子頁面:
85
+ - 📊 概覽:關鍵指標和摘要
86
+ - 📉 列聯表:配對數據分析
87
+ - 🎯 勝算比:效果大小評估
88
+ - 📋 詳細報告:完整文字報告
89
+
90
+ ### Step 3: 使用 AI 助手
91
+ 1. 切換到「AI 助手」頁面
92
+ 2. 在聊天框輸入問題,或點擊快速問題按鈕
93
+ 3. AI 會根據分析結果提供解釋和建議
94
+
95
+ ## 💡 統計指標說明
96
+
97
+ ### McNemar 統計量
98
+ 用於檢定配對資料中比例是否有差異的卡方統計量
99
+
100
+ ### p 值 (p-value)
101
+ - p < 0.05:顯著(拒絕虛無假設)
102
+ - p ≥ 0.05:不顯著(無法拒絕虛無假設)
103
+
104
+ ### 勝算比 (Odds Ratio)
105
+ - OR > 1:勝方更可能較高
106
+ - OR = 1:無差異
107
+ - OR < 1:敗方更可能較高
108
+
109
+ ### 不一致配對
110
+ - **b**: 勝方高且敗方低的配對數
111
+ - **c**: 勝方低且敗方高的配對數
112
+ - McNemar 檢定只使用這些不一致的配對
113
+
114
+ ## 🎮 應用場景
115
+
116
+ ### 1. 特徵重要性分析
117
+ 判斷哪些寶可夢特徵(HP、攻擊、速度等)對勝負影響最大
118
+
119
+ ### 2. 組隊策略制定
120
+ 根據統計結果選擇重要特徵較高的寶可夢
121
+
122
+ ### 3. 對戰機制理解
123
+ 理解不同特徵在實戰中的作用
124
+
125
+ ### 4. 教學用途
126
+ 學習 McNemar 檢定的原理和應用
127
+
128
+ ## ⚙️ 技術架構
129
+
130
+ ### 核心技術
131
+ - **Streamlit**: Web 應用框架
132
+ - **pandas**: 資料處理
133
+ - **statsmodels**: McNemar 檢定
134
+ - **plotly**: 互動式視覺化
135
+ - **OpenAI GPT-4o-mini**: AI 助手
136
+
137
+ ### 特色設計
138
+ - ✅ Session 隔離(多用戶支援)
139
+ - ✅ 執行緒安全
140
+ - ✅ 自動清理過期資料
141
+ - ✅ 響應式 UI 設計
142
+ - ✅ 完整錯誤處理
143
+
144
+ ## 🔒 隱私與安全
145
+
146
+ - 所有分析在本地執行
147
+ - Session 資料獨立儲存
148
+ - 超過 1 小時自動清理
149
+ - API Key 不會被儲存
150
+
151
+ ## 📝 範例問題(給 AI 助手)
152
+
153
+ - "為什麼這個特徵顯著/不顯著?"
154
+ - "勝算比 2.5 代表什麼意思?"
155
+ - "我該如何組建隊伍?"
156
+ - "不一致配對是什麼?"
157
+ - "McNemar 檢定和 t 檢定有什麼不同?"
158
+ - "這個結果對實戰有什麼啟示?"
159
+
160
+ ## 🚀 未來功能規劃
161
+
162
+ - [ ] 批次分析多個特徵
163
+ - [ ] 特徵重要性排序
164
+ - [ ] RAG 文獻檢索系統
165
+ - [ ] 歷史分析紀錄
166
+ - [ ] 結果比較功能
167
+ - [ ] 匯出 PDF 報告
168
+
169
+ ## 📧 聯絡資訊
170
+
171
+ 如有問題或建議,歡迎聯繫開發團隊。
172
+
173
+ ## 📄 授權
174
+
175
+ 本專案僅供學術研究和教學使用。
176
+
177
  ---
178
 
179
+ **Powered by Streamlit & OpenAI GPT-4o-mini** 🚀
app.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import uuid
4
+ from datetime import datetime, timedelta
5
+ import atexit
6
+ import os
7
+
8
+ # 頁面配置
9
+ st.set_page_config(
10
+ page_title="McNemar Test Analysis - Pokémon Battles",
11
+ page_icon="⚔️",
12
+ layout="wide",
13
+ initial_sidebar_state="expanded"
14
+ )
15
+
16
+ # 自定義 CSS
17
+ st.markdown("""
18
+ <style>
19
+ .streamlit-expanderHeader {
20
+ background-color: #e8f1f8;
21
+ border: 1px solid #b0cfe8;
22
+ border-radius: 5px;
23
+ font-weight: 600;
24
+ color: #1b4f72;
25
+ }
26
+ .streamlit-expanderHeader:hover {
27
+ background-color: #d0e7f8;
28
+ }
29
+ .stMetric {
30
+ background-color: #f8fbff;
31
+ padding: 10px;
32
+ border-radius: 5px;
33
+ border: 1px solid #d0e4f5;
34
+ }
35
+ .stButton > button {
36
+ width: 100%;
37
+ border-radius: 20px;
38
+ font-weight: 600;
39
+ transition: all 0.3s ease;
40
+ }
41
+ .stButton > button:hover {
42
+ transform: translateY(-2px);
43
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2);
44
+ }
45
+ .success-box {
46
+ background-color: #d4edda;
47
+ border: 1px solid #c3e6cb;
48
+ border-radius: 5px;
49
+ padding: 10px;
50
+ margin: 10px 0;
51
+ }
52
+ .warning-box {
53
+ background-color: #fff3cd;
54
+ border: 1px solid #ffeaa7;
55
+ border-radius: 5px;
56
+ padding: 10px;
57
+ margin: 10px 0;
58
+ }
59
+ </style>
60
+ """, unsafe_allow_html=True)
61
+
62
+ # 導入自定義模組
63
+ from mcnemar_core import McNemarAnalyzer
64
+ from mcnemar_llm_assistant import McNemarLLMAssistant
65
+ from mcnemar_utils import (
66
+ plot_contingency_table_heatmap,
67
+ plot_odds_ratio_forest,
68
+ plot_discordant_pairs,
69
+ plot_p_value_significance,
70
+ create_results_summary_table,
71
+ export_results_to_text
72
+ )
73
+
74
+ # 清理函數
75
+ def cleanup_old_sessions():
76
+ """清理超過 1 小時的 session"""
77
+ current_time = datetime.now()
78
+ for session_id in list(McNemarAnalyzer._session_results.keys()):
79
+ result = McNemarAnalyzer._session_results.get(session_id)
80
+ if result:
81
+ result_time = datetime.fromisoformat(result['timestamp'])
82
+ if current_time - result_time > timedelta(hours=1):
83
+ McNemarAnalyzer.clear_session_results(session_id)
84
+
85
+ # 註冊清理函數
86
+ atexit.register(cleanup_old_sessions)
87
+
88
+ # 初始化 session state
89
+ if 'session_id' not in st.session_state:
90
+ st.session_state.session_id = str(uuid.uuid4())
91
+ if 'analysis_results' not in st.session_state:
92
+ st.session_state.analysis_results = None
93
+ if 'chat_history' not in st.session_state:
94
+ st.session_state.chat_history = []
95
+ if 'analyzer' not in st.session_state:
96
+ st.session_state.analyzer = None
97
+
98
+ # 標題
99
+ st.title("⚔️ McNemar Test Analysis System")
100
+ st.markdown("### 寶可夢對戰特徵顯著性分析")
101
+ st.markdown("---")
102
+
103
+ # Sidebar
104
+ with st.sidebar:
105
+ st.header("⚙️ 配置設定")
106
+
107
+ # OpenAI API Key
108
+ api_key = st.text_input(
109
+ "OpenAI API Key",
110
+ type="password",
111
+ help="輸入您的 OpenAI API key 以使用 AI 助手"
112
+ )
113
+
114
+ if api_key:
115
+ st.session_state.api_key = api_key
116
+ st.success("✅ API Key 已載入")
117
+
118
+ st.markdown("---")
119
+
120
+ # 清理按鈕
121
+ if st.button("🧹 清理過期資料"):
122
+ cleanup_old_sessions()
123
+ st.success("✅ 清理完成")
124
+ st.rerun()
125
+
126
+ st.markdown("---")
127
+
128
+ # 資料來源選擇
129
+ st.subheader("📊 資料來源")
130
+ data_source = st.radio(
131
+ "選擇資料來源:",
132
+ ["使用預設資料集", "上傳您的資料"]
133
+ )
134
+
135
+ uploaded_file = None
136
+ if data_source == "上傳您的資料":
137
+ uploaded_file = st.file_uploader(
138
+ "上傳 CSV 檔案",
139
+ type=['csv'],
140
+ help="上傳寶可夢對戰資料(需包含 cs_特徵 和 cn_特徵 欄位)"
141
+ )
142
+
143
+ with st.expander("📖 資料格式說明"):
144
+ st.markdown("""
145
+ **必要欄位格式:**
146
+ - `cs_特徵名稱`: 勝方的特徵值 (0 或 1)
147
+ - `cn_特徵名稱`: 敗方的特徵值 (0 或 1)
148
+
149
+ **範例:**
150
+ - `cs_HP`: 勝方 HP 是否較高
151
+ - `cn_HP`: 敗方 HP 是否較高
152
+ - `cs_Attack`: 勝方攻擊是否較高
153
+ - `cn_Attack`: 敗方攻擊是否較高
154
+
155
+ **數值含義:**
156
+ - 1 = 較高/較快/較重
157
+ - 0 = 較低/較慢/較輕
158
+ """)
159
+
160
+ st.markdown("---")
161
+
162
+ # 關於系統
163
+ with st.expander("ℹ️ 關於此系統"):
164
+ st.markdown("""
165
+ **McNemar 檢定分析系統**
166
+
167
+ 本系統使用 McNemar 檢定來分析寶可夢對戰中,
168
+ 勝方與敗方在各項特徵上是否有顯著差異。
169
+
170
+ **主要功能:**
171
+ - 🔬 統計顯著性檢定
172
+ - 📊 勝算比分析
173
+ - 📈 視覺化圖表
174
+ - 💬 AI 助手解釋
175
+ - 🎮 對戰策略建議
176
+
177
+ **適用場景:**
178
+ - 分析哪些特徵對勝負影響最大
179
+ - 理解特徵重要性排序
180
+ - 制定組隊策略
181
+ """)
182
+
183
+ # 主要內容區 - 雙 Tab
184
+ tab1, tab2 = st.tabs(["📊 McNemar 分析", "💬 AI 助手"])
185
+
186
+ # Tab 1: McNemar 分析
187
+ with tab1:
188
+ st.header("📊 McNemar 檢定分析")
189
+
190
+ # 載入資料
191
+ if data_source == "使用預設資料集":
192
+ # 檢查預設資料是否存在
193
+ default_data_path = "poke_mc_hong_2.csv"
194
+ if os.path.exists(default_data_path):
195
+ df = pd.read_csv(default_data_path)
196
+ st.success(f"✅ 已載入預設資料集({len(df)} 筆對戰記錄)")
197
+ else:
198
+ st.warning("⚠️ 找不到預設資料集,請上傳您的資料")
199
+ df = None
200
+ else:
201
+ if uploaded_file is not None:
202
+ df = pd.read_csv(uploaded_file)
203
+ st.success(f"✅ 已載入資料({len(df)} 筆對戰記錄)")
204
+ else:
205
+ df = None
206
+ st.info("📁 請在左側上傳 CSV 檔案")
207
+
208
+ if df is not None:
209
+ # 初始化分析器
210
+ if st.session_state.analyzer is None:
211
+ st.session_state.analyzer = McNemarAnalyzer(st.session_state.session_id)
212
+ st.session_state.analyzer.load_data(df)
213
+
214
+ # 取得可用特徵
215
+ available_features = st.session_state.analyzer.get_available_features()
216
+
217
+ if not available_features:
218
+ st.error("❌ 資料中找不到有效的特徵欄位(需要 cs_* 和 cn_* 格式)")
219
+ else:
220
+ # 參數設定區
221
+ st.subheader("🎯 選擇分析特徵")
222
+
223
+ col1, col2 = st.columns([3, 1])
224
+
225
+ with col1:
226
+ selected_feature = st.selectbox(
227
+ "選擇要分析的特徵:",
228
+ options=available_features,
229
+ format_func=lambda x: McNemarAnalyzer.FEATURE_LABELS.get(x, x),
230
+ help="選擇一個特徵來比較勝方與敗方的差異"
231
+ )
232
+
233
+ with col2:
234
+ st.markdown("<br>", unsafe_allow_html=True)
235
+ analyze_button = st.button("🔬 開始分析", type="primary", use_container_width=True)
236
+
237
+ # 執行分析
238
+ if analyze_button:
239
+ with st.spinner("分析中..."):
240
+ try:
241
+ results = st.session_state.analyzer.run_analysis(selected_feature)
242
+ st.session_state.analysis_results = results
243
+ st.success("✅ 分析完成!")
244
+ except Exception as e:
245
+ st.error(f"❌ 分析失敗: {str(e)}")
246
+
247
+ # 顯示結果
248
+ if st.session_state.analysis_results:
249
+ results = st.session_state.analysis_results
250
+
251
+ st.markdown("---")
252
+ st.subheader(f"📈 分析結果:{results['feature_label']}")
253
+
254
+ # 建立結果 tabs
255
+ result_tabs = st.tabs([
256
+ "📊 概覽",
257
+ "📉 列聯表",
258
+ "🎯 勝算比",
259
+ "📋 詳細報告"
260
+ ])
261
+
262
+ # Tab: 概覽
263
+ with result_tabs[0]:
264
+ # 關鍵指標
265
+ metric_cols = st.columns(4)
266
+
267
+ # 顯著性指示
268
+ sig_emoji = "✅" if results['interpretation']['is_significant'] else "⚠️"
269
+ metric_cols[0].metric(
270
+ "統計顯著性",
271
+ results['interpretation']['significance'].split('(')[0].strip(),
272
+ sig_emoji
273
+ )
274
+
275
+ metric_cols[1].metric(
276
+ "p 值",
277
+ f"{results['p_value']:.4f}",
278
+ "顯著" if results['p_value'] < 0.05 else "不顯著"
279
+ )
280
+
281
+ metric_cols[2].metric(
282
+ "勝算比 (OR)",
283
+ f"{results['odds_ratio']:.3f}",
284
+ results['interpretation']['effect_size'].split('(')[0].strip()
285
+ )
286
+
287
+ metric_cols[3].metric(
288
+ "不一致配對數",
289
+ results['discordant_n'],
290
+ f"b={results['discordant_b']}, c={results['discordant_c']}"
291
+ )
292
+
293
+ st.markdown("---")
294
+
295
+ # 摘要表格
296
+ st.markdown("### 📝 結果摘要")
297
+ summary_df = create_results_summary_table(results)
298
+ st.dataframe(summary_df, use_container_width=True, hide_index=True)
299
+
300
+ st.markdown("---")
301
+
302
+ # 視覺化:p 值顯著性
303
+ st.markdown("### 🎨 顯著性水準視覺化")
304
+ p_value_fig = plot_p_value_significance(results['p_value'])
305
+ st.plotly_chart(p_value_fig, use_container_width=True)
306
+
307
+ # Tab: 列聯表
308
+ with result_tabs[1]:
309
+ st.markdown("### 📊 配對列聯表")
310
+
311
+ col1, col2 = st.columns([1, 1])
312
+
313
+ with col1:
314
+ st.dataframe(
315
+ results['contingency_table_labeled'],
316
+ use_container_width=True
317
+ )
318
+
319
+ with col2:
320
+ heatmap_fig = plot_contingency_table_heatmap(
321
+ results['contingency_table_labeled'],
322
+ results['feature_label']
323
+ )
324
+ st.plotly_chart(heatmap_fig, use_container_width=True)
325
+
326
+ st.markdown("---")
327
+
328
+ # 不一致配對分析
329
+ st.markdown("### 🔍 不一致配對分析")
330
+
331
+ st.info(f"""
332
+ **不一致配對** 是 McNemar 檢定的關鍵:
333
+ - **b = {results['discordant_b']}**: 勝方{results['label_pos'].split()[1]}且敗方{results['label_neg'].split()[1]}的配對數
334
+ - **c = {results['discordant_c']}**: 勝方{results['label_neg'].split()[1]}且敗方{results['label_pos'].split()[1]}的配對數
335
+
336
+ McNemar 檢定只使用這些不一致的配對來判斷是否有顯著差異。
337
+ """)
338
+
339
+ discordant_fig = plot_discordant_pairs(
340
+ results['discordant_b'],
341
+ results['discordant_c'],
342
+ results['label_pos'],
343
+ results['label_neg']
344
+ )
345
+ st.plotly_chart(discordant_fig, use_container_width=True)
346
+
347
+ # Tab: 勝算比
348
+ with result_tabs[2]:
349
+ st.markdown("### 🎯 勝算比 (Odds Ratio) 分析")
350
+
351
+ or_fig = plot_odds_ratio_forest(
352
+ results['odds_ratio'],
353
+ results['ci_low'],
354
+ results['ci_high'],
355
+ results['feature_label']
356
+ )
357
+ st.plotly_chart(or_fig, use_container_width=True)
358
+
359
+ st.markdown("---")
360
+
361
+ # 解釋勝算比
362
+ st.markdown("### 📖 勝算比解釋")
363
+
364
+ if results['odds_ratio'] > 1:
365
+ interpretation = f"""
366
+ 勝算比為 **{results['odds_ratio']:.3f}**,表示:
367
+ - 勝方在 **{results['feature_label']}** 上較高的機率是敗方的 **{results['odds_ratio']:.2f} 倍**
368
+ - 95% 信賴區間: [{results['ci_low']:.3f}, {results['ci_high']:.3f}]
369
+ - 效果大小: {results['interpretation']['effect_size']}
370
+
371
+ **結論**:{results['feature_label']} 較高的寶可夢更容易獲勝。
372
+ """
373
+ elif results['odds_ratio'] < 1:
374
+ interpretation = f"""
375
+ 勝算比為 **{results['odds_ratio']:.3f}**,表示:
376
+ - 敗方在 **{results['feature_label']}** 上較高的機率是勝方的 **{1/results['odds_ratio']:.2f} 倍**
377
+ - 95% 信賴區間: [{results['ci_low']:.3f}, {results['ci_high']:.3f}]
378
+ - 效果大小: {results['interpretation']['effect_size']}
379
+
380
+ **結論**:{results['feature_label']} 較低的寶可夢反而更容易獲勝(這很少見!)。
381
+ """
382
+ else:
383
+ interpretation = f"""
384
+ 勝算比為 **1.0**,表示:
385
+ - 勝方和敗方在 **{results['feature_label']}** 上沒有差異
386
+ - 此特徵對勝負沒有影響
387
+ """
388
+
389
+ st.markdown(interpretation)
390
+
391
+ # Tab: 詳細報告
392
+ with result_tabs[3]:
393
+ st.markdown("### 📋 完整分析報告")
394
+
395
+ # 生成文字報告
396
+ text_report = export_results_to_text(results)
397
+
398
+ st.text_area(
399
+ "報告內容",
400
+ text_report,
401
+ height=400
402
+ )
403
+
404
+ # 下載按鈕
405
+ st.download_button(
406
+ label="📥 下載完整報告 (.txt)",
407
+ data=text_report,
408
+ file_name=f"mcnemar_report_{results['feature_name']}_{results['timestamp'][:10]}.txt",
409
+ mime="text/plain"
410
+ )
411
+
412
+ # Tab 2: AI 助手
413
+ with tab2:
414
+ st.header("💬 AI 分析助手")
415
+
416
+ if not st.session_state.get('api_key'):
417
+ st.warning("⚠️ 請在左側輸入您的 OpenAI API Key 以使用 AI 助手")
418
+ elif st.session_state.analysis_results is None:
419
+ st.info("ℹ️ 請先在「McNemar 分析」頁面執行分析")
420
+ else:
421
+ # 初始化 LLM 助手
422
+ if 'llm_assistant' not in st.session_state:
423
+ st.session_state.llm_assistant = McNemarLLMAssistant(
424
+ api_key=st.session_state.api_key,
425
+ session_id=st.session_state.session_id
426
+ )
427
+
428
+ # 聊天容器
429
+ chat_container = st.container()
430
+
431
+ with chat_container:
432
+ for message in st.session_state.chat_history:
433
+ with st.chat_message(message["role"]):
434
+ st.markdown(message["content"])
435
+
436
+ # 使用者輸入
437
+ if prompt := st.chat_input("詢問關於分析結果的任何問題..."):
438
+ # 添加使用者訊息
439
+ st.session_state.chat_history.append({
440
+ "role": "user",
441
+ "content": prompt
442
+ })
443
+
444
+ with st.chat_message("user"):
445
+ st.markdown(prompt)
446
+
447
+ # AI 回應
448
+ with st.chat_message("assistant"):
449
+ with st.spinner("思考中..."):
450
+ try:
451
+ response = st.session_state.llm_assistant.get_response(
452
+ user_message=prompt,
453
+ analysis_results=st.session_state.analysis_results
454
+ )
455
+ st.markdown(response)
456
+ except Exception as e:
457
+ error_msg = f"❌ 錯誤: {str(e)}\n\n請檢查 API key 或重新表達問題。"
458
+ st.error(error_msg)
459
+ response = error_msg
460
+
461
+ # 添加助手回應
462
+ st.session_state.chat_history.append({
463
+ "role": "assistant",
464
+ "content": response
465
+ })
466
+
467
+ st.markdown("---")
468
+
469
+ # 快速問題按鈕
470
+ st.subheader("💡 快速問題")
471
+
472
+ quick_questions = [
473
+ "📊 給我這次分析的總結",
474
+ "🎯 解釋 p 值的意義",
475
+ "🔍 解釋勝算比",
476
+ "⚔️ 這對對戰策略有什麼啟示?",
477
+ "❓ 什麼是 McNemar 檢定?"
478
+ ]
479
+
480
+ cols = st.columns(len(quick_questions))
481
+ for idx, (col, question) in enumerate(zip(cols, quick_questions)):
482
+ if col.button(question, key=f"quick_{idx}"):
483
+ # 根據問題選擇對應的方法
484
+ if "總結" in question:
485
+ response = st.session_state.llm_assistant.generate_summary(
486
+ st.session_state.analysis_results
487
+ )
488
+ elif "p 值" in question:
489
+ response = st.session_state.llm_assistant.explain_metric(
490
+ 'p_value',
491
+ st.session_state.analysis_results
492
+ )
493
+ elif "勝算比" in question:
494
+ response = st.session_state.llm_assistant.explain_metric(
495
+ 'odds_ratio',
496
+ st.session_state.analysis_results
497
+ )
498
+ elif "策略" in question:
499
+ response = st.session_state.llm_assistant.battle_strategy_advice(
500
+ st.session_state.analysis_results
501
+ )
502
+ elif "McNemar" in question:
503
+ response = st.session_state.llm_assistant.explain_mcnemar_test()
504
+ else:
505
+ response = st.session_state.llm_assistant.get_response(
506
+ question,
507
+ st.session_state.analysis_results
508
+ )
509
+
510
+ st.session_state.chat_history.append({
511
+ "role": "user",
512
+ "content": question
513
+ })
514
+
515
+ st.session_state.chat_history.append({
516
+ "role": "assistant",
517
+ "content": response
518
+ })
519
+
520
+ st.rerun()
521
+
522
+ # 重置對話按鈕
523
+ st.markdown("---")
524
+ if st.button("🔄 重置對話"):
525
+ st.session_state.llm_assistant.reset_conversation()
526
+ st.session_state.chat_history = []
527
+ st.success("✅ 對話已重置")
528
+ st.rerun()
529
+
530
+ # Footer
531
+ st.markdown("---")
532
+ st.markdown(
533
+ f"""
534
+ <div style='text-align: center'>
535
+ <p>⚔️ McNemar Test Analysis System for Pokémon Battles | Built with Streamlit & OpenAI</p>
536
+ <p>Session ID: {st.session_state.session_id[:8]} | Powered by GPT-4o-mini</p>
537
+ </div>
538
+ """,
539
+ unsafe_allow_html=True
540
+ )
mcnemar_core.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import re
4
+ from statsmodels.stats.contingency_tables import mcnemar
5
+ from datetime import datetime
6
+ import threading
7
+
8
+ class McNemarAnalyzer:
9
+ """
10
+ McNemar 檢定分析器
11
+ 支援多用戶同時使用,每個 session 獨立處理
12
+ """
13
+
14
+ # 類別級的鎖,用於執行緒安全
15
+ _lock = threading.Lock()
16
+
17
+ # 儲存各 session 的分析結果
18
+ _session_results = {}
19
+
20
+ # 特徵標籤對應
21
+ FEATURE_LABELS = {
22
+ 'HP': 'HP(血量)',
23
+ 'Attack': '攻擊',
24
+ 'Defense': '防禦',
25
+ 'SpAtk': '特攻',
26
+ 'SpDef': '特防',
27
+ 'Speed': '速度',
28
+ 'height': '身高',
29
+ 'weight': '體重',
30
+ 'base_experience': '基礎經驗值',
31
+ }
32
+
33
+ # 數值描述文字
34
+ DEFAULT_VALUE_TEXT = {1: '較高', 0: '較低'}
35
+ FEATURE_VALUE_TEXT = {
36
+ 'HP': DEFAULT_VALUE_TEXT,
37
+ 'Attack': DEFAULT_VALUE_TEXT,
38
+ 'Defense': DEFAULT_VALUE_TEXT,
39
+ 'SpAtk': DEFAULT_VALUE_TEXT,
40
+ 'SpDef': DEFAULT_VALUE_TEXT,
41
+ 'Speed': {1: '較快', 0: '較慢'},
42
+ 'height': {1: '較高', 0: '較矮'},
43
+ 'weight': {1: '較重', 0: '較輕'},
44
+ 'base_experience': DEFAULT_VALUE_TEXT,
45
+ }
46
+
47
+ def __init__(self, session_id):
48
+ """
49
+ 初始化分析器
50
+
51
+ Args:
52
+ session_id: 唯一的 session 識別碼
53
+ """
54
+ self.session_id = session_id
55
+ self.df = None
56
+
57
+ def load_data(self, csv_path_or_df):
58
+ """
59
+ 載入資料
60
+
61
+ Args:
62
+ csv_path_or_df: CSV 檔案路徑或 DataFrame
63
+ """
64
+ if isinstance(csv_path_or_df, str):
65
+ self.df = pd.read_csv(csv_path_or_df)
66
+ else:
67
+ self.df = csv_path_or_df.copy()
68
+
69
+ def get_available_features(self):
70
+ """
71
+ 取得可用的特徵列表
72
+
73
+ Returns:
74
+ list: 特徵名稱列表
75
+ """
76
+ if self.df is None:
77
+ return []
78
+
79
+ # 找出所有 cs_ 開頭的欄位
80
+ cs_cols = [col for col in self.df.columns if col.startswith('cs_')]
81
+ # 提取特徵名稱(移除 cs_ 前綴)
82
+ features = [col.replace('cs_', '') for col in cs_cols]
83
+
84
+ return features
85
+
86
+ def run_analysis(self, feature_name):
87
+ """
88
+ 執行 McNemar 檢定分析
89
+
90
+ Args:
91
+ feature_name: 特徵名稱(例如 'HP', 'Attack')
92
+
93
+ Returns:
94
+ dict: 包含所有分析結果的字典
95
+ """
96
+ with self._lock:
97
+ try:
98
+ if self.df is None:
99
+ raise ValueError("請先載入資料")
100
+
101
+ # 1. 準備資料
102
+ feature_a = f"cs_{feature_name}" # Winner
103
+ feature_b = f"cn_{feature_name}" # Loser
104
+
105
+ if feature_a not in self.df.columns or feature_b not in self.df.columns:
106
+ raise ValueError(f"找不到特徵 {feature_name} 的欄位")
107
+
108
+ var_a = self.df[feature_a]
109
+ var_b = self.df[feature_b]
110
+
111
+ # 2. 建立列聯表(1 before 0)
112
+ var_a = pd.Categorical(var_a, categories=[1, 0], ordered=True)
113
+ var_b = pd.Categorical(var_b, categories=[1, 0], ordered=True)
114
+
115
+ ct = pd.crosstab(var_a, var_b)
116
+ ctm = ct.copy()
117
+
118
+ # 3. 執行 McNemar 檢定
119
+ result = mcnemar(ctm, exact=True, correction=True)
120
+
121
+ # 4. 計算勝算比 (Odds Ratio)
122
+ b = int(ctm.at[1, 0]) # 勝方高 & 敗方低
123
+ c = int(ctm.at[0, 1]) # 勝方低 & 敗方高
124
+
125
+ # Haldane-Anscombe correction(防止除以零)
126
+ bh = b + 0.5 if b == 0 or c == 0 else float(b)
127
+ ch = c + 0.5 if b == 0 or c == 0 else float(c)
128
+
129
+ or_ratio = bh / ch
130
+ ln_or = np.log(or_ratio)
131
+ se = np.sqrt(1.0 / bh + 1.0 / ch)
132
+ z = 1.96
133
+ ci_low = float(np.exp(ln_or - z * se))
134
+ ci_high = float(np.exp(ln_or + z * se))
135
+ n_discordant = b + c
136
+
137
+ # 5. 準備標籤
138
+ feature_label = self.FEATURE_LABELS.get(feature_name, feature_name)
139
+ label_pos, label_neg = self._get_value_labels(feature_name)
140
+
141
+ # 6. 準備列聯表(含標籤)
142
+ ct_labeled = self._create_labeled_table(ct, feature_name)
143
+
144
+ # 7. 整理結果
145
+ results = {
146
+ 'feature_name': feature_name,
147
+ 'feature_label': feature_label,
148
+ 'contingency_table': ctm.to_dict(),
149
+ 'contingency_table_labeled': ct_labeled,
150
+ 'mcnemar_statistic': float(result.statistic),
151
+ 'p_value': float(result.pvalue),
152
+ 'odds_ratio': round(or_ratio, 3),
153
+ 'ci_low': round(ci_low, 3),
154
+ 'ci_high': round(ci_high, 3),
155
+ 'discordant_b': b,
156
+ 'discordant_c': c,
157
+ 'discordant_n': n_discordant,
158
+ 'label_pos': label_pos,
159
+ 'label_neg': label_neg,
160
+ 'interpretation': self._interpret_results(result.pvalue, or_ratio),
161
+ 'timestamp': datetime.now().isoformat()
162
+ }
163
+
164
+ # 儲存到 session results
165
+ self._session_results[self.session_id] = results
166
+
167
+ return results
168
+
169
+ except Exception as e:
170
+ raise Exception(f"分析失敗: {str(e)}")
171
+
172
+ def _create_labeled_table(self, ct, feature_name):
173
+ """建立帶標籤的列聯表"""
174
+ feature_label = self.FEATURE_LABELS.get(feature_name, feature_name)
175
+ mapping = self.FEATURE_VALUE_TEXT.get(feature_name, self.DEFAULT_VALUE_TEXT)
176
+
177
+ # 創建標籤
178
+ row_labels = [f"勝方 {mapping.get(1, '較高')}", f"勝方 {mapping.get(0, '較低')}"]
179
+ col_labels = [f"敗方 {mapping.get(1, '較高')}", f"敗方 {mapping.get(0, '較低')}"]
180
+
181
+ # 重建 DataFrame
182
+ labeled_table = pd.DataFrame(
183
+ ct.values,
184
+ index=row_labels,
185
+ columns=col_labels
186
+ )
187
+
188
+ # 添加總計
189
+ labeled_table['總數'] = labeled_table.sum(axis=1)
190
+ labeled_table.loc['總數'] = labeled_table.sum()
191
+
192
+ return labeled_table
193
+
194
+ def _get_value_labels(self, feature_name):
195
+ """取得特徵的數值標籤"""
196
+ name = self.FEATURE_LABELS.get(feature_name, feature_name)
197
+ m = self.FEATURE_VALUE_TEXT.get(feature_name, self.DEFAULT_VALUE_TEXT)
198
+ pos = f"{name} {m[1]}" # 1
199
+ neg = f"{name} {m[0]}" # 0
200
+ return pos, neg
201
+
202
+ def _interpret_results(self, p_value, odds_ratio):
203
+ """解釋分析結果"""
204
+ # 顯著性判斷
205
+ if p_value < 0.001:
206
+ significance = "極顯著 (p < 0.001)"
207
+ elif p_value < 0.01:
208
+ significance = "非常顯著 (p < 0.01)"
209
+ elif p_value < 0.05:
210
+ significance = "顯著 (p < 0.05)"
211
+ else:
212
+ significance = "不顯著 (p ≥ 0.05)"
213
+
214
+ # 效果大小判斷
215
+ if odds_ratio > 2:
216
+ effect_size = "大效果 (OR > 2)"
217
+ elif odds_ratio > 1.5:
218
+ effect_size = "中等效果 (OR > 1.5)"
219
+ elif odds_ratio > 1:
220
+ effect_size = "小效果 (OR > 1)"
221
+ elif odds_ratio == 1:
222
+ effect_size = "無差異 (OR = 1)"
223
+ else:
224
+ effect_size = f"反向效果 (OR < 1)"
225
+
226
+ return {
227
+ 'significance': significance,
228
+ 'effect_size': effect_size,
229
+ 'is_significant': p_value < 0.05
230
+ }
231
+
232
+ @classmethod
233
+ def get_session_results(cls, session_id):
234
+ """獲取特定 session 的結果"""
235
+ return cls._session_results.get(session_id)
236
+
237
+ @classmethod
238
+ def clear_session_results(cls, session_id):
239
+ """清除特定 session 的結果"""
240
+ if session_id in cls._session_results:
241
+ del cls._session_results[session_id]
mcnemar_llm_assistant.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ import json
3
+
4
+ class McNemarLLMAssistant:
5
+ """
6
+ McNemar 檢定 LLM 問答助手
7
+ 協助用戶理解 McNemar 檢定分析結果
8
+ """
9
+
10
+ def __init__(self, api_key, session_id):
11
+ """
12
+ 初始化 LLM 助手
13
+
14
+ Args:
15
+ api_key: OpenAI API key
16
+ session_id: 唯一的 session 識別碼
17
+ """
18
+ self.client = OpenAI(api_key=api_key)
19
+ self.session_id = session_id
20
+ self.conversation_history = []
21
+
22
+ # 系統提示詞
23
+ self.system_prompt = """You are an expert statistician specializing in McNemar's test and paired categorical data analysis, particularly in the context of Pokémon battle statistics.
24
+
25
+ Your role is to help users understand their McNemar's test results for Pokémon battles, where we compare whether winning and losing Pokémon differ on specific features (HP, Attack, Speed, etc.).
26
+
27
+ You should:
28
+ 1. Explain McNemar's test concepts in simple, accessible terms
29
+ 2. Interpret statistical significance (p-values) clearly
30
+ 3. Explain odds ratios and confidence intervals in context
31
+ 4. Help users understand what discordant pairs mean
32
+ 5. Discuss the practical significance of results for Pokémon battles
33
+ 6. Provide insights about which features matter most for winning
34
+ 7. Suggest battle strategies based on the statistical findings
35
+ 8. Clarify limitations and assumptions of McNemar's test
36
+
37
+ Key concepts to explain when relevant:
38
+ - **McNemar's test**: Tests if proportions differ between paired binary data
39
+ - **p-value**: Probability of seeing these results by chance (< 0.05 is significant)
40
+ - **Odds Ratio**: How much more likely winners are to have higher values than losers
41
+ - **Discordant pairs**: Cases where winner and loser differ on the feature
42
+ - **Concordant pairs**: Cases where both have same value (not used in test)
43
+
44
+ When discussing Pokémon battles:
45
+ - Connect statistical findings to battle mechanics
46
+ - Explain why certain stats matter more (e.g., Speed determines who attacks first)
47
+ - Discuss type advantages and battle strategies
48
+ - Use Pokémon-specific terminology naturally
49
+
50
+ Always be clear, educational, and engaging. Use examples when helpful.
51
+ Format responses with proper markdown for better readability."""
52
+
53
+ def get_response(self, user_message, analysis_results=None):
54
+ """
55
+ 獲取 AI 回應
56
+
57
+ Args:
58
+ user_message: 用戶訊息
59
+ analysis_results: 分析結果字典(可選)
60
+
61
+ Returns:
62
+ str: AI 回應
63
+ """
64
+ # 準備上下文資訊
65
+ context = ""
66
+ if analysis_results:
67
+ context = self._prepare_context(analysis_results)
68
+
69
+ # 添加用戶訊息到歷史
70
+ self.conversation_history.append({
71
+ "role": "user",
72
+ "content": user_message
73
+ })
74
+
75
+ # 構建訊息列表
76
+ messages = [
77
+ {"role": "system", "content": self.system_prompt}
78
+ ]
79
+
80
+ if context:
81
+ messages.append({"role": "system", "content": f"Current Analysis Context:\n{context}"})
82
+
83
+ # 加入對話歷史
84
+ messages.extend(self.conversation_history)
85
+
86
+ try:
87
+ # 調用 OpenAI API
88
+ response = self.client.chat.completions.create(
89
+ model="gpt-4o-mini",
90
+ messages=messages,
91
+ temperature=0.7,
92
+ max_tokens=1500
93
+ )
94
+
95
+ assistant_message = response.choices[0].message.content
96
+
97
+ # 添加助手回應到歷史
98
+ self.conversation_history.append({
99
+ "role": "assistant",
100
+ "content": assistant_message
101
+ })
102
+
103
+ return assistant_message
104
+
105
+ except Exception as e:
106
+ return f"❌ Error: {str(e)}\n\nPlease check your API key and try again."
107
+
108
+ def _prepare_context(self, results):
109
+ """準備分析結果的上下文資訊"""
110
+
111
+ if not results:
112
+ return "No analysis results available yet."
113
+
114
+ context = f"""
115
+ ## Current McNemar Test Analysis
116
+
117
+ ### Feature Analyzed
118
+ - Feature: {results['feature_label']} ({results['feature_name']})
119
+ - Positive label (1): {results['label_pos']}
120
+ - Negative label (0): {results['label_neg']}
121
+
122
+ ### Contingency Table
123
+ ```
124
+ Loser Low Loser High Total
125
+ Winner High {results['contingency_table'].get(1, {}).get(1, 0):<15} {results['contingency_table'].get(1, {}).get(0, 0):<15} {results['contingency_table'].get(1, {}).get(1, 0) + results['contingency_table'].get(1, {}).get(0, 0)}
126
+ Winner Low {results['contingency_table'].get(0, {}).get(1, 0):<15} {results['contingency_table'].get(0, {}).get(0, 0):<15} {results['contingency_table'].get(0, {}).get(1, 0) + results['contingency_table'].get(0, {}).get(0, 0)}
127
+ ```
128
+
129
+ ### Statistical Test Results
130
+ - **McNemar Statistic**: {results['mcnemar_statistic']:.4f}
131
+ - **p-value**: {results['p_value']:.4f}
132
+ - **Significance**: {results['interpretation']['significance']}
133
+ - **Is Significant?**: {'Yes' if results['interpretation']['is_significant'] else 'No'}
134
+
135
+ ### Odds Ratio Analysis
136
+ - **Odds Ratio**: {results['odds_ratio']:.3f}
137
+ - **95% Confidence Interval**: [{results['ci_low']:.3f}, {results['ci_high']:.3f}]
138
+ - **Effect Size**: {results['interpretation']['effect_size']}
139
+
140
+ ### Discordant Pairs (key for McNemar test)
141
+ - **Winner High & Loser Low (b)**: {results['discordant_b']} pairs
142
+ - **Winner Low & Loser High (c)**: {results['discordant_c']} pairs
143
+ - **Total Discordant Pairs**: {results['discordant_n']} pairs
144
+
145
+ ### Interpretation
146
+ {
147
+ f"The results show that winners and losers DIFFER SIGNIFICANTLY on {results['feature_label']}."
148
+ if results['interpretation']['is_significant']
149
+ else f"The results show NO SIGNIFICANT DIFFERENCE between winners and losers on {results['feature_label']}."
150
+ }
151
+
152
+ {
153
+ f"Winners are {results['odds_ratio']:.2f} times more likely to have higher {results['feature_label']} than losers."
154
+ if results['odds_ratio'] > 1
155
+ else f"Losers are {1/results['odds_ratio']:.2f} times more likely to have higher {results['feature_label']} than winners."
156
+ }
157
+ """
158
+ return context
159
+
160
+ def generate_summary(self, analysis_results):
161
+ """自動生成分析結果總結"""
162
+
163
+ summary_prompt = """Based on the McNemar test results provided, please generate a comprehensive summary that includes:
164
+
165
+ 1. **What was tested**: Briefly explain what feature was analyzed and what the test measures
166
+ 2. **Statistical Findings**:
167
+ - Is the result statistically significant?
168
+ - What does the p-value tell us?
169
+ - What does the odds ratio mean in practical terms?
170
+ 3. **Battle Implications**: What does this mean for Pokémon battles?
171
+ 4. **Key Insights**: The most important takeaway from these results
172
+ 5. **Recommendations**: How trainers could use this information
173
+
174
+ Format the summary in clear markdown with appropriate sections."""
175
+
176
+ return self.get_response(summary_prompt, analysis_results)
177
+
178
+ def explain_metric(self, metric_name, analysis_results):
179
+ """解釋特定指標"""
180
+
181
+ metric_explanations = {
182
+ 'mcnemar_statistic': 'McNemar Statistic',
183
+ 'p_value': 'p-value',
184
+ 'odds_ratio': 'Odds Ratio',
185
+ 'confidence_interval': '95% Confidence Interval',
186
+ 'discordant_pairs': 'Discordant Pairs'
187
+ }
188
+
189
+ metric_display = metric_explanations.get(metric_name, metric_name)
190
+
191
+ explain_prompt = f"""Please explain the following metric in the context of this McNemar test analysis:
192
+
193
+ Metric: {metric_display}
194
+
195
+ Include:
196
+ 1. What this metric measures in general
197
+ 2. The value obtained in this analysis
198
+ 3. How to interpret this value for Pokémon battles
199
+ 4. What it tells us about the importance of this feature
200
+ 5. Any limitations or caveats to consider"""
201
+
202
+ return self.get_response(explain_prompt, analysis_results)
203
+
204
+ def compare_features(self):
205
+ """建議比較不同特徵"""
206
+
207
+ compare_prompt = """I'd like to understand how different Pokémon features (HP, Attack, Defense, Speed, etc.) compare in terms of their importance for winning battles.
208
+
209
+ Could you:
210
+ 1. Explain which features are typically most important in Pokémon battles
211
+ 2. Discuss how McNemar's test helps us identify important features
212
+ 3. Suggest which features I should analyze next
213
+ 4. Explain how features might interact (e.g., Speed + Attack)"""
214
+
215
+ return self.get_response(compare_prompt, None)
216
+
217
+ def explain_mcnemar_test(self):
218
+ """解釋 McNemar 檢定的基本概念"""
219
+
220
+ explain_prompt = """Please explain McNemar's test in simple terms, specifically in the context of Pokémon battle analysis.
221
+
222
+ Cover:
223
+ 1. What McNemar's test is and when to use it
224
+ 2. Why it's appropriate for comparing winner vs. loser features
225
+ 3. What "paired data" means in this context
226
+ 4. The difference between discordant and concordant pairs
227
+ 5. How to interpret the results
228
+
229
+ Use Pokémon examples to make it concrete and easy to understand."""
230
+
231
+ return self.get_response(explain_prompt, None)
232
+
233
+ def battle_strategy_advice(self, analysis_results):
234
+ """提供對戰策略建議"""
235
+
236
+ strategy_prompt = f"""Based on the McNemar test results for {analysis_results['feature_label']}, please provide practical battle strategy advice for Pokémon trainers.
237
+
238
+ Consider:
239
+ 1. Should trainers prioritize this feature when building teams?
240
+ 2. How important is this feature compared to others?
241
+ 3. Are there specific Pokémon types or strategies that benefit most?
242
+ 4. What are the implications for competitive play?
243
+ 5. Any exceptions or special cases to be aware of?
244
+
245
+ Be specific and actionable."""
246
+
247
+ return self.get_response(strategy_prompt, analysis_results)
248
+
249
+ def reset_conversation(self):
250
+ """重置對話歷史"""
251
+ self.conversation_history = []
mcnemar_utils.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.graph_objects as go
2
+ import plotly.express as px
3
+ import pandas as pd
4
+ import numpy as np
5
+
6
+ def plot_contingency_table_heatmap(ct_labeled, feature_label, title="列聯表熱力圖"):
7
+ """
8
+ 繪製列聯表熱力圖
9
+
10
+ Args:
11
+ ct_labeled: 帶標籤的列聯表 DataFrame
12
+ feature_label: 特徵標籤
13
+ title: 圖表標題
14
+
15
+ Returns:
16
+ plotly figure
17
+ """
18
+ # 移除總數列和行
19
+ ct_display = ct_labeled.iloc[:-1, :-1].copy()
20
+
21
+ # 創建註解文字
22
+ annotations = []
23
+ for i, row in enumerate(ct_display.index):
24
+ for j, col in enumerate(ct_display.columns):
25
+ annotations.append(
26
+ dict(
27
+ x=j,
28
+ y=i,
29
+ text=str(ct_display.iloc[i, j]),
30
+ font=dict(size=16, color='white' if ct_display.iloc[i, j] > ct_display.values.max()/2 else 'black'),
31
+ showarrow=False
32
+ )
33
+ )
34
+
35
+ fig = go.Figure(data=go.Heatmap(
36
+ z=ct_display.values,
37
+ x=ct_display.columns,
38
+ y=ct_display.index,
39
+ colorscale='Blues',
40
+ showscale=True,
41
+ hoverongaps=False,
42
+ hovertemplate='%{y}<br>%{x}<br>配對數: %{z}<extra></extra>'
43
+ ))
44
+
45
+ fig.update_layout(
46
+ title=f'{title}<br><sub>{feature_label}</sub>',
47
+ xaxis_title='敗方 (Loser)',
48
+ yaxis_title='勝方 (Winner)',
49
+ width=600,
50
+ height=500,
51
+ template='plotly_white',
52
+ annotations=annotations
53
+ )
54
+
55
+ return fig
56
+
57
+ def plot_odds_ratio_forest(or_value, ci_low, ci_high, feature_label):
58
+ """
59
+ 繪製勝算比森林圖
60
+
61
+ Args:
62
+ or_value: 勝算比
63
+ ci_low: 95% 信賴區間下界
64
+ ci_high: 95% 信賴區間上界
65
+ feature_label: 特徵標籤
66
+
67
+ Returns:
68
+ plotly figure
69
+ """
70
+ fig = go.Figure()
71
+
72
+ # 參考線 (OR = 1)
73
+ fig.add_shape(
74
+ type="line",
75
+ x0=1, x1=1,
76
+ y0=-0.5, y1=0.5,
77
+ line=dict(color="red", width=2, dash="dash"),
78
+ )
79
+
80
+ # 信賴區間
81
+ fig.add_trace(go.Scatter(
82
+ x=[ci_low, ci_high],
83
+ y=[0, 0],
84
+ mode='lines',
85
+ line=dict(color='#2d6ca2', width=3),
86
+ showlegend=False,
87
+ hovertemplate='95% CI: [%{x:.3f}]<extra></extra>'
88
+ ))
89
+
90
+ # 點估計
91
+ fig.add_trace(go.Scatter(
92
+ x=[or_value],
93
+ y=[0],
94
+ mode='markers',
95
+ marker=dict(
96
+ size=15,
97
+ color='#d62728',
98
+ line=dict(color='white', width=2)
99
+ ),
100
+ showlegend=False,
101
+ hovertemplate=f'OR: {or_value:.3f}<extra></extra>'
102
+ ))
103
+
104
+ # 添加數值標註
105
+ fig.add_annotation(
106
+ x=or_value,
107
+ y=0.15,
108
+ text=f"OR = {or_value:.3f}<br>95% CI [{ci_low:.3f}, {ci_high:.3f}]",
109
+ showarrow=False,
110
+ font=dict(size=12, color='#1b4f72'),
111
+ bgcolor='rgba(255,255,255,0.8)',
112
+ bordercolor='#2d6ca2',
113
+ borderwidth=1,
114
+ borderpad=4
115
+ )
116
+
117
+ fig.update_layout(
118
+ title=f'勝算比 (Odds Ratio)<br><sub>{feature_label}</sub>',
119
+ xaxis_title='Odds Ratio',
120
+ yaxis=dict(
121
+ showticklabels=False,
122
+ showgrid=False,
123
+ zeroline=False
124
+ ),
125
+ width=700,
126
+ height=300,
127
+ template='plotly_white',
128
+ xaxis=dict(type='log', showgrid=True),
129
+ hovermode='closest'
130
+ )
131
+
132
+ return fig
133
+
134
+ def plot_discordant_pairs(b, c, label_pos, label_neg):
135
+ """
136
+ 繪製不一致配對比較圖
137
+
138
+ Args:
139
+ b: cs=1 & cn=0 的配對數
140
+ c: cs=0 & cn=1 的配對數
141
+ label_pos: 正向標籤
142
+ label_neg: 負向標籤
143
+
144
+ Returns:
145
+ plotly figure
146
+ """
147
+ fig = go.Figure()
148
+
149
+ categories = [
150
+ f'勝方 {label_pos.split()[1]}<br>敗方 {label_neg.split()[1]}',
151
+ f'勝方 {label_neg.split()[1]}<br>敗方 {label_pos.split()[1]}'
152
+ ]
153
+ values = [b, c]
154
+ colors = ['#2d6ca2', '#d62728']
155
+
156
+ fig.add_trace(go.Bar(
157
+ x=categories,
158
+ y=values,
159
+ marker=dict(
160
+ color=colors,
161
+ line=dict(color='white', width=2)
162
+ ),
163
+ text=values,
164
+ textposition='outside',
165
+ textfont=dict(size=16, color='black'),
166
+ hovertemplate='%{x}<br>配對數: %{y}<extra></extra>'
167
+ ))
168
+
169
+ fig.update_layout(
170
+ title='不一致配對分布',
171
+ xaxis_title='配對類型',
172
+ yaxis_title='配對數量',
173
+ width=600,
174
+ height=400,
175
+ template='plotly_white',
176
+ showlegend=False
177
+ )
178
+
179
+ return fig
180
+
181
+ def plot_p_value_significance(p_value):
182
+ """
183
+ 繪製 p 值顯著性指示圖
184
+
185
+ Args:
186
+ p_value: p 值
187
+
188
+ Returns:
189
+ plotly figure
190
+ """
191
+ fig = go.Figure()
192
+
193
+ # 設定顯著性閾值
194
+ thresholds = [0.001, 0.01, 0.05, 1.0]
195
+ labels = ['p < 0.001<br>(極顯著)', 'p < 0.01<br>(非常顯著)',
196
+ 'p < 0.05<br>(顯著)', 'p ≥ 0.05<br>(不顯著)']
197
+ colors = ['#1a5f1a', '#2d8b2d', '#5cb85c', '#d9534f']
198
+
199
+ # 找出 p 值所在區間
200
+ current_idx = 0
201
+ for i, thresh in enumerate(thresholds):
202
+ if p_value < thresh:
203
+ current_idx = i
204
+ break
205
+
206
+ # 繪製區間條
207
+ for i in range(len(thresholds)):
208
+ opacity = 1.0 if i == current_idx else 0.3
209
+ fig.add_trace(go.Bar(
210
+ x=[labels[i]],
211
+ y=[1],
212
+ marker=dict(color=colors[i], opacity=opacity),
213
+ showlegend=False,
214
+ hovertemplate=f'{labels[i]}<extra></extra>'
215
+ ))
216
+
217
+ # 添加 p 值標註
218
+ fig.add_annotation(
219
+ x=labels[current_idx],
220
+ y=1.1,
221
+ text=f"p = {p_value:.4f}",
222
+ showarrow=True,
223
+ arrowhead=2,
224
+ arrowsize=1,
225
+ arrowwidth=2,
226
+ arrowcolor='black',
227
+ font=dict(size=14, color='black', weight='bold'),
228
+ bgcolor='yellow',
229
+ bordercolor='black',
230
+ borderwidth=2,
231
+ borderpad=4
232
+ )
233
+
234
+ fig.update_layout(
235
+ title='顯著性水準',
236
+ xaxis_title='',
237
+ yaxis_title='',
238
+ yaxis=dict(showticklabels=False, showgrid=False),
239
+ width=700,
240
+ height=300,
241
+ template='plotly_white',
242
+ showlegend=False
243
+ )
244
+
245
+ return fig
246
+
247
+ def create_results_summary_table(results):
248
+ """
249
+ 創建結果摘要表格
250
+
251
+ Args:
252
+ results: 分析結果字典
253
+
254
+ Returns:
255
+ pandas DataFrame
256
+ """
257
+ summary_data = {
258
+ '項目': [
259
+ '特徵',
260
+ 'McNemar 統計量',
261
+ 'p 值',
262
+ '顯著性',
263
+ '勝算比 (OR)',
264
+ '95% 信賴區間',
265
+ '不一致配對數',
266
+ '效果大小'
267
+ ],
268
+ '數值': [
269
+ results['feature_label'],
270
+ f"{results['mcnemar_statistic']:.4f}",
271
+ f"{results['p_value']:.4f}",
272
+ results['interpretation']['significance'],
273
+ f"{results['odds_ratio']:.3f}",
274
+ f"[{results['ci_low']:.3f}, {results['ci_high']:.3f}]",
275
+ f"{results['discordant_n']} (b={results['discordant_b']}, c={results['discordant_c']})",
276
+ results['interpretation']['effect_size']
277
+ ]
278
+ }
279
+
280
+ return pd.DataFrame(summary_data)
281
+
282
+ def export_results_to_text(results):
283
+ """
284
+ 匯出結果為純文字格式
285
+
286
+ Args:
287
+ results: 分析結果字典
288
+
289
+ Returns:
290
+ str: 格式化的文字報告
291
+ """
292
+ report = f"""
293
+ ==============================================
294
+ McNemar 檢定分析報告
295
+ ==============================================
296
+
297
+ 分析特徵: {results['feature_label']} ({results['feature_name']})
298
+ 分析時間: {results['timestamp']}
299
+
300
+ ----------------------------------------------
301
+ 1. 列聯表
302
+ ----------------------------------------------
303
+ {results['contingency_table_labeled'].to_string()}
304
+
305
+ ----------------------------------------------
306
+ 2. McNemar 檢定結果
307
+ ----------------------------------------------
308
+ McNemar 統計量: {results['mcnemar_statistic']:.4f}
309
+ p 值: {results['p_value']:.4f}
310
+ 顯著性: {results['interpretation']['significance']}
311
+
312
+ ----------------------------------------------
313
+ 3. 勝算比分析
314
+ ----------------------------------------------
315
+ 勝算比 (OR): {results['odds_ratio']:.3f}
316
+ 95% 信賴區間: [{results['ci_low']:.3f}, {results['ci_high']:.3f}]
317
+ 效果大小: {results['interpretation']['effect_size']}
318
+
319
+ ----------------------------------------------
320
+ 4. 不一致配對
321
+ ----------------------------------------------
322
+ 勝方{results['label_pos'].split()[1]}且敗方{results['label_neg'].split()[1]} (b): {results['discordant_b']}
323
+ 勝方{results['label_neg'].split()[1]}且敗方{results['label_pos'].split()[1]} (c): {results['discordant_c']}
324
+ 總不一致配對數: {results['discordant_n']}
325
+
326
+ ----------------------------------------------
327
+ 5. 解釋
328
+ ----------------------------------------------
329
+ {'結果顯示勝方和敗方在此特徵上有顯著差異。' if results['interpretation']['is_significant'] else '結果顯示勝方和敗方在此特徵上無顯著差異。'}
330
+ 勝算比為 {results['odds_ratio']:.3f},表示{
331
+ '勝方在此特徵上較高的機率是敗方的 ' + str(round(results['odds_ratio'], 2)) + ' 倍。'
332
+ if results['odds_ratio'] > 1
333
+ else '敗方在此特徵上較高的機率是勝方的 ' + str(round(1/results['odds_ratio'], 2)) + ' 倍。'
334
+ }
335
+
336
+ ==============================================
337
+ """
338
+ return report
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit==1.31.0
2
+ pandas==2.1.4
3
+ numpy==1.26.3
4
+ plotly==5.18.0
5
+ statsmodels==0.14.1
6
+ openai>=1.30.0