smartTranscend commited on
Commit
fbb910f
·
verified ·
1 Parent(s): 3c4b07c

Upload 5 files

Browse files
README (2).md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: COVID-19 Precision Prevention - Digital Twin Model
3
+ emoji: 🦠
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ sdk_version: 1.32.0
8
+ app_file: app_combined.py
9
+ pinned: false
10
+ ---
11
+
12
+ # COVID-19 Precision Prevention: Digital Twin Model
13
+
14
+ Interactive 3D visualization system for precision COVID-19 prevention strategies using viral load (Ct value) data.
15
+
16
+ ## Features
17
+
18
+ - **Tab 1: Contact Tracing (Alpha Variant)** - Determine optimal retrospective tracing duration based on confirmed case Ct values
19
+ - **Tab 2: Quarantine & Isolation (Omicron Variant)** - Optimize quarantine duration based on Ct values and booster vaccination status
20
+ - **AI Assistant** - GPT-4 powered intelligent analysis and recommendations
21
+ - **3D Visualization** - Interactive exploration of effectiveness across different scenarios
22
+
23
+ ## Data Source
24
+
25
+ Based on research from: "A Bayesian Digital Twin Model for Precision Control of Emerging Infectious Diseases Using Viral Load Data" (Lin et al., 2025)
26
+
27
+ ## Usage
28
+
29
+ 1. Enter your OpenAI API Key in the sidebar
30
+ 2. Adjust parameters (Ct value, days, vaccination status)
31
+ 3. Explore 3D visualizations
32
+ 4. Get AI-powered strategy recommendations
app_combined (2).py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+
4
+ # 設置頁面配置
5
+ st.set_page_config(
6
+ page_title="精準傳染病防疫-元宇宙時間數位雙胞胎模型",
7
+ page_icon="🦠",
8
+ layout="wide"
9
+ )
10
+
11
+ # 初始化 session state - 直接從環境變數讀取
12
+ if 'api_key' not in st.session_state:
13
+ st.session_state.api_key = os.getenv("OPENAI_API_KEY", "") # 從環境變數讀取
14
+
15
+
16
+ # 主標題
17
+ st.title("🦠 精準傳染病防疫-元宇宙時間數位雙胞胎模型")
18
+ st.markdown("透過 AI 深度學習模型,根據確診者的病毒量(Ct值)操縱最佳追蹤策略")
19
+
20
+ # 側邊欄 - API Key 設定(兩個分頁共用)
21
+ with st.sidebar:
22
+
23
+
24
+ st.markdown("---")
25
+ st.markdown("""
26
+ ### 📖 系統說明
27
+
28
+ **分頁1: 接觸者追蹤**
29
+ - 基於 Alpha 變異株
30
+ - 問題:確診者要往回追蹤幾天?
31
+
32
+ **分頁2: 隔離檢疫**
33
+ - 基於 Omicron 變異株
34
+ - 問題:接觸者要隔離幾天?
35
+ """)
36
+
37
+ # 創建分頁
38
+ tab1, tab2 = st.tabs(["📊 接觸者追蹤 ", "🏥 隔離檢疫 "])
39
+
40
+ with tab1:
41
+ # 導入原有的 Table 1 系統
42
+ import tab1_contact_tracing
43
+ tab1_contact_tracing.render()
44
+
45
+ with tab2:
46
+ # 導入新的 Table 2 系統
47
+ import tab2_quarantine
48
+ tab2_quarantine.render()
49
+
50
+ # 頁腳
51
+ st.markdown("---")
52
+ st.markdown("""
53
+ <div style='text-align: center; color: gray; font-size: 12px;'>
54
+ <p>📖 數據來源: Lin et al. (2025) - A Bayesian Digital Twin Model for Precision Control of Emerging Infectious Diseases</p>
55
+ <p>🤖 AI 功能: 使用 OpenAI GPT-4 模型提供智能分析</p>
56
+ <p>⚠️ 本工具僅供參考,實際防疫策略需由專業人員評估</p>
57
+ </div>
58
+ """, unsafe_allow_html=True)
requirements (2).txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit==1.29.0
2
+ plotly==5.18.0
3
+ numpy==1.24.3
4
+ pandas==2.0.3
5
+ openai>=1.30.0
6
+ python-dotenv
tab1_contact_tracing (1).py ADDED
@@ -0,0 +1,725 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import plotly.graph_objects as go
3
+ import numpy as np
4
+ import pandas as pd
5
+ from openai import OpenAI
6
+
7
+ # 初始化 tab1 專屬的 session state
8
+ def init_tab1_session_state():
9
+ if 'tab1_chat_history' not in st.session_state:
10
+ st.session_state.tab1_chat_history = []
11
+ if 'tab1_selected_ct' not in st.session_state:
12
+ st.session_state.tab1_selected_ct = 18.0
13
+ if 'tab1_tracing_days' not in st.session_state:
14
+ st.session_state.tab1_tracing_days = 7
15
+
16
+ # 效益計算函數(保持原樣)
17
+ def get_effectiveness_for_ct18(days):
18
+ if days <= 1: return 0.06
19
+ elif days <= 2: return 0.18
20
+ elif days <= 3: return 0.31
21
+ elif days <= 4: return 0.42
22
+ elif days <= 5: return 0.52
23
+ elif days <= 6: return 0.60
24
+ elif days <= 7: return 0.67
25
+ elif days <= 8: return 0.72
26
+ elif days <= 9: return 0.77
27
+ elif days <= 10: return 0.81
28
+ elif days <= 11: return 0.84
29
+ elif days <= 12: return 0.87
30
+ elif days <= 13: return 0.89
31
+ elif days <= 14: return 0.91
32
+ elif days <= 15: return 0.92
33
+ elif days <= 16: return 0.93
34
+ elif days <= 17: return 0.95
35
+ elif days <= 20: return 0.97
36
+ elif days <= 34: return 0.97 + (days - 20) * 0.03 / 14
37
+ else: return 1.0
38
+
39
+ def get_effectiveness_for_ct22(days):
40
+ if days <= 1: return 0.01
41
+ elif days <= 2: return 0.03
42
+ elif days <= 3: return 0.07
43
+ elif days <= 4: return 0.12
44
+ elif days <= 5: return 0.18
45
+ elif days <= 6: return 0.24
46
+ elif days <= 7: return 0.30
47
+ elif days <= 8: return 0.36
48
+ elif days <= 9: return 0.41
49
+ elif days <= 10: return 0.47
50
+ elif days <= 11: return 0.52
51
+ elif days <= 12: return 0.57
52
+ elif days <= 13: return 0.62
53
+ elif days <= 14: return 0.66
54
+ elif days <= 17: return 0.66 + (days - 14) * 0.10 / 3
55
+ elif days <= 21: return 0.76 + (days - 17) * 0.10 / 4
56
+ elif days <= 28: return 0.86 + (days - 21) * 0.08 / 7
57
+ elif days <= 48: return 0.94 + (days - 28) * 0.06 / 20
58
+ else: return 1.0
59
+
60
+ def get_effectiveness_for_ct30(days):
61
+ if days <= 8: return 0
62
+ elif days <= 9: return 0.01
63
+ elif days <= 10: return 0.02
64
+ elif days <= 11: return 0.03
65
+ elif days <= 12: return 0.05
66
+ elif days <= 13: return 0.08
67
+ elif days <= 14: return 0.11
68
+ elif days <= 15: return 0.15
69
+ elif days <= 16: return 0.20
70
+ elif days <= 17: return 0.26
71
+ elif days <= 18: return 0.32
72
+ elif days <= 19: return 0.37
73
+ elif days <= 20: return 0.43
74
+ elif days <= 21: return 0.48
75
+ elif days <= 24: return 0.48 + (days - 21) * 0.14 / 3
76
+ elif days <= 28: return 0.62 + (days - 24) * 0.14 / 4
77
+ elif days <= 54: return 0.76 + (days - 28) * 0.24 / 26
78
+ else: return 1.0
79
+
80
+ def get_effectiveness(ct_value, days):
81
+ if ct_value <= 18:
82
+ ratio = (ct_value - 10) / 8
83
+ higher_eff = get_effectiveness_for_ct18(days) * 1.1
84
+ effectiveness = higher_eff * (1 - ratio) + get_effectiveness_for_ct18(days) * ratio
85
+ elif ct_value <= 22:
86
+ ratio = (ct_value - 18) / 4
87
+ effectiveness = get_effectiveness_for_ct18(days) * (1 - ratio) + get_effectiveness_for_ct22(days) * ratio
88
+ elif ct_value <= 30:
89
+ ratio = (ct_value - 22) / 8
90
+ effectiveness = get_effectiveness_for_ct22(days) * (1 - ratio) + get_effectiveness_for_ct30(days) * ratio
91
+ else:
92
+ ratio = min((ct_value - 30) / 5, 1)
93
+ effectiveness = get_effectiveness_for_ct30(days) * (1 - ratio * 0.3)
94
+
95
+ return min(max(effectiveness, 0), 1.0)
96
+
97
+ def find_days_for_target_effectiveness(ct_value, target_effectiveness):
98
+ for days in range(1, 61):
99
+ eff = get_effectiveness(ct_value, days)
100
+ if eff >= target_effectiveness:
101
+ return days
102
+ return 60
103
+
104
+ def get_virus_level(ct_value):
105
+ if ct_value <= 18:
106
+ return "高病毒量"
107
+ elif ct_value <= 25:
108
+ return "中病毒量"
109
+ else:
110
+ return "低病毒量"
111
+
112
+ def generate_summary_prompt(ct_value, days, effectiveness):
113
+ virus_level = get_virus_level(ct_value)
114
+
115
+ comparison_data = []
116
+ for test_days in [3, 7, 14, 21, 30]:
117
+ eff = get_effectiveness(ct_value, test_days)
118
+ comparison_data.append(f"追蹤{test_days}天: {eff*100:.1f}%")
119
+
120
+ prompt = f"""你是一位 COVID-19 防疫專家。請根據以下數據生成一份專業的防疫策略效益分析報告:
121
+
122
+ **當前參數:**
123
+ - Ct 值:{ct_value}
124
+ - 病毒量等級:{virus_level}
125
+ - 回溯追蹤天數:{days} 天
126
+ - 追蹤效益:{effectiveness*100:.1f}%
127
+
128
+ **不同追蹤天數的效益比較:**
129
+ {chr(10).join(comparison_data)}
130
+
131
+ 請提供:
132
+ 1. 當前策略的效益評估(是否充足?)
133
+ 2. 基於 Ct 值的感染階段判斷
134
+ 3. 具體的追蹤建議(是否需要調整天數?)
135
+ 4. 可能遺漏的接觸者風險評估
136
+ 5. 實務操作建議
137
+
138
+ 請用繁體中文回答,使用清晰的結構和專業但易懂的語言。"""
139
+
140
+ return prompt
141
+
142
+ def extract_query_params(text):
143
+ import re
144
+
145
+ text_lower = text.lower()
146
+
147
+ # 提取 Ct 值
148
+ ct_patterns = [
149
+ r'ct\s*[=值]?\s*(\d+\.?\d*)',
150
+ r'(\d+\.?\d*)\s*ct',
151
+ ]
152
+
153
+ ct_value = None
154
+ for pattern in ct_patterns:
155
+ match = re.search(pattern, text_lower)
156
+ if match:
157
+ try:
158
+ ct_value = float(match.group(1))
159
+ if 10 <= ct_value <= 35:
160
+ break
161
+ except:
162
+ continue
163
+
164
+ # 提取天數
165
+ day_patterns = [
166
+ r'(\d+)\s*天',
167
+ r'追[蹤跡]\s*(\d+)',
168
+ r'回[朔溯]\s*(\d+)',
169
+ ]
170
+
171
+ days = None
172
+ for pattern in day_patterns:
173
+ match = re.search(pattern, text)
174
+ if match:
175
+ try:
176
+ days = int(match.group(1))
177
+ if 1 <= days <= 60:
178
+ break
179
+ except:
180
+ continue
181
+
182
+ # 檢查是否在詢問「要幾天才能達到X%效益」
183
+ target_eff = None
184
+ reverse_query_patterns = [
185
+ r'要.*?(\d+).*?%',
186
+ r'達到.*?(\d+).*?%',
187
+ r'(\d+).*?%.*?效益',
188
+ r'100%',
189
+ r'百分之百',
190
+ ]
191
+
192
+ for pattern in reverse_query_patterns:
193
+ match = re.search(pattern, text)
194
+ if match:
195
+ if pattern in ['100%', '百分之百']:
196
+ target_eff = 1.0
197
+ break
198
+ else:
199
+ try:
200
+ target_eff = float(match.group(1)) / 100
201
+ if 0 <= target_eff <= 1:
202
+ break
203
+ except:
204
+ continue
205
+
206
+ return ct_value, days, target_eff
207
+
208
+ def call_gpt4(prompt, api_key, tools=None, tool_choice=None):
209
+ try:
210
+ client = OpenAI(api_key=api_key)
211
+
212
+ messages = [
213
+ {"role": "system", "content": "你是一位專業的 COVID-19 防疫策略分析專家,擅長解釋接觸者追蹤效益,並提供清晰實用的建議。當用戶詢問特定 Ct 值和追蹤天數的效益時,你必須使用 calculate_effectiveness 函數來獲取精確數據。當用戶詢問需要多少天才能達到某個效益時,你必須使用 find_required_days 函數。"},
214
+ {"role": "user", "content": prompt}
215
+ ]
216
+
217
+ if tools:
218
+ response = client.chat.completions.create(
219
+ model="gpt-4",
220
+ messages=messages,
221
+ tools=tools,
222
+ tool_choice=tool_choice,
223
+ temperature=0.7,
224
+ max_tokens=1500
225
+ )
226
+ else:
227
+ response = client.chat.completions.create(
228
+ model="gpt-4",
229
+ messages=messages,
230
+ temperature=0.7,
231
+ max_tokens=1500
232
+ )
233
+
234
+ return response
235
+ except Exception as e:
236
+ return f"❌ API 呼叫失敗: {str(e)}"
237
+
238
+ def process_gpt4_response(response, api_key):
239
+ if isinstance(response, str):
240
+ return response, None, None
241
+
242
+ message = response.choices[0].message
243
+
244
+ if message.tool_calls:
245
+ import json
246
+
247
+ tool_call = message.tool_calls[0]
248
+ function_name = tool_call.function.name
249
+ arguments = json.loads(tool_call.function.arguments)
250
+
251
+ result_content = ""
252
+ extracted_ct = None
253
+ extracted_days = None
254
+
255
+ if function_name == "calculate_effectiveness":
256
+ ct_value = arguments.get("ct_value")
257
+ days = arguments.get("days")
258
+
259
+ extracted_ct = ct_value
260
+ extracted_days = days
261
+
262
+ effectiveness = get_effectiveness(ct_value, days)
263
+ virus_level = get_virus_level(ct_value)
264
+
265
+ result_content = json.dumps({
266
+ "ct_value": ct_value,
267
+ "days": days,
268
+ "effectiveness_percentage": round(effectiveness * 100, 1),
269
+ "virus_level": virus_level
270
+ }, ensure_ascii=False)
271
+
272
+ elif function_name == "find_required_days":
273
+ ct_value = arguments.get("ct_value")
274
+ target_effectiveness = arguments.get("target_effectiveness")
275
+
276
+ required_days = find_days_for_target_effectiveness(ct_value, target_effectiveness)
277
+ actual_effectiveness = get_effectiveness(ct_value, required_days)
278
+ virus_level = get_virus_level(ct_value)
279
+
280
+ extracted_ct = ct_value
281
+ extracted_days = required_days
282
+
283
+ result_content = json.dumps({
284
+ "ct_value": ct_value,
285
+ "target_effectiveness_percentage": round(target_effectiveness * 100, 1),
286
+ "required_days": required_days,
287
+ "actual_effectiveness_percentage": round(actual_effectiveness * 100, 1),
288
+ "virus_level": virus_level
289
+ }, ensure_ascii=False)
290
+
291
+ client = OpenAI(api_key=api_key)
292
+ second_response = client.chat.completions.create(
293
+ model="gpt-4",
294
+ messages=[
295
+ {"role": "system", "content": "你是一位專業的 COVID-19 防疫策略分析專家,擅長解釋接觸者追蹤效益,並提供清晰實用的建議。"},
296
+ {"role": "user", "content": message.content if hasattr(message, 'content') and message.content else "請分析效益"},
297
+ {
298
+ "role": "assistant",
299
+ "content": None,
300
+ "tool_calls": [tool_call.model_dump()]
301
+ },
302
+ {
303
+ "role": "tool",
304
+ "tool_call_id": tool_call.id,
305
+ "content": result_content
306
+ }
307
+ ],
308
+ temperature=0.7,
309
+ max_tokens=1500
310
+ )
311
+
312
+ return second_response.choices[0].message.content, extracted_ct, extracted_days
313
+
314
+ return message.content, None, None
315
+
316
+ def render():
317
+ """渲染 Tab1 的完整內容"""
318
+ init_tab1_session_state()
319
+
320
+ # 側邊欄 - 控制面板
321
+ with st.sidebar:
322
+ st.header("📊 防疫決策工具 (接觸者追蹤)")
323
+
324
+ # 互動式參數調整
325
+ st.subheader("🎛️ 參數設定")
326
+
327
+ selected_ct = st.slider(
328
+ "🦠 確診者Ct值",
329
+ min_value=10.0,
330
+ max_value=35.0,
331
+ value=st.session_state.tab1_selected_ct,
332
+ step=0.5,
333
+ help="Ct值越低代表病毒量越高",
334
+ key="tab1_ct_slider"
335
+ )
336
+ st.session_state.tab1_selected_ct = selected_ct
337
+ st.caption("10 (高病毒量) ← → 35 (低病毒量)")
338
+
339
+ tracing_days = st.slider(
340
+ "📅 接觸者往回追蹤天數",
341
+ min_value=1,
342
+ max_value=54,
343
+ value=st.session_state.tab1_tracing_days,
344
+ help="需要追溯過去幾天的接觸者",
345
+ key="tab1_days_slider"
346
+ )
347
+ st.session_state.tab1_tracing_days = tracing_days
348
+ st.caption("1天 ← → 54天")
349
+
350
+ # 當前效益顯示
351
+ current_eff = get_effectiveness(selected_ct, tracing_days)
352
+ st.metric(
353
+ label="📊 當前情境效益",
354
+ value=f"{current_eff * 100:.0f}%",
355
+ delta="追蹤效益"
356
+ )
357
+
358
+ # AI 分析按鈕
359
+ st.markdown("---")
360
+ if st.button("🤖 生成 AI 效益分析報告", type="primary", use_container_width=True, key="tab1_ai_report"):
361
+ if not st.session_state.api_key:
362
+ st.error("❌ 請先輸入 OpenAI API Key")
363
+ else:
364
+ with st.spinner("AI 正在分析中..."):
365
+ prompt = generate_summary_prompt(selected_ct, tracing_days, current_eff)
366
+ response = call_gpt4(prompt, st.session_state.api_key)
367
+ final_response, _, _ = process_gpt4_response(response, st.session_state.api_key)
368
+ st.session_state.tab1_chat_history.append({
369
+ "type": "auto_summary",
370
+ "ct": selected_ct,
371
+ "days": tracing_days,
372
+ "effectiveness": current_eff,
373
+ "response": final_response
374
+ })
375
+
376
+ st.markdown("---")
377
+
378
+ # 情境說明
379
+ with st.expander("🎯 使用情境", expanded=False):
380
+ st.markdown("""
381
+ 當發現確診者時,防疫人員需要決定:
382
+ **「要往回追蹤多少天的接觸者?」**
383
+ """)
384
+
385
+ with st.expander("✅ 關鍵指標:Ct值"):
386
+ st.markdown("""
387
+ RT-PCR檢測的「循環閾值」
388
+ - **Ct值 ≤ 18** = 病毒量很高
389
+ - **Ct值 18-25** = 病毒量中等
390
+ - **Ct值 > 25** = 病毒量較低
391
+ """)
392
+
393
+ with st.expander("📈 決策邏輯"):
394
+ st.markdown("""
395
+ **低Ct值**(高病毒量)
396
+ → 最近才感染
397
+ → 追蹤「近期」接觸者即可
398
+
399
+ **高Ct值**(低病毒量)
400
+ → 可能感染較久
401
+ → 需追溯「更早」的接觸者
402
+ """)
403
+
404
+ # 主要內容區 - 使用兩欄布局
405
+ col1, col2 = st.columns([2, 1])
406
+
407
+ with col1:
408
+ # 生成3D數據
409
+ ct_range = np.arange(10, 35.5, 0.5)
410
+ day_range = np.arange(1, 61, 1)
411
+ ct_mesh, day_mesh = np.meshgrid(ct_range, day_range)
412
+
413
+ # 計算效益值
414
+ effectiveness_mesh = np.zeros_like(ct_mesh)
415
+ for i in range(len(day_range)):
416
+ for j in range(len(ct_range)):
417
+ effectiveness_mesh[i, j] = get_effectiveness(ct_mesh[i, j], day_mesh[i, j])
418
+
419
+ # 創建3D曲面圖
420
+ fig = go.Figure()
421
+
422
+ # 添加曲面
423
+ fig.add_trace(go.Surface(
424
+ x=ct_mesh,
425
+ y=day_mesh,
426
+ z=effectiveness_mesh,
427
+ colorscale=[
428
+ [0.0, 'rgb(239, 68, 68)'],
429
+ [0.33, 'rgb(245, 158, 11)'],
430
+ [0.67, 'rgb(16, 185, 129)'],
431
+ [1.0, 'rgb(59, 130, 246)']
432
+ ],
433
+ showscale=True,
434
+ colorbar=dict(
435
+ title="追蹤效益",
436
+ tickvals=[0, 0.25, 0.5, 0.75, 1.0],
437
+ ticktext=['0%', '25%', '50%', '75%', '100%']
438
+ ),
439
+ opacity=0.9,
440
+ name='效益曲面',
441
+ contours=dict(
442
+ x=dict(show=True, color='white', width=1, highlightwidth=2),
443
+ y=dict(show=True, color='white', width=1, highlightwidth=2),
444
+ z=dict(show=True, color='white', width=1, highlightwidth=2)
445
+ )
446
+ ))
447
+
448
+ # 添加當前天數的切片線
449
+ ct_line = np.arange(10, 35.1, 0.3)
450
+ eff_line = [get_effectiveness(ct, tracing_days) for ct in ct_line]
451
+ day_line = [tracing_days] * len(ct_line)
452
+
453
+ fig.add_trace(go.Scatter3d(
454
+ x=ct_line,
455
+ y=day_line,
456
+ z=eff_line,
457
+ mode='lines',
458
+ line=dict(color='yellow', width=8),
459
+ name=f'第{tracing_days}天'
460
+ ))
461
+
462
+ # 添加用戶選擇的點
463
+ user_eff = get_effectiveness(selected_ct, tracing_days)
464
+ fig.add_trace(go.Scatter3d(
465
+ x=[selected_ct],
466
+ y=[tracing_days],
467
+ z=[user_eff],
468
+ mode='markers+text',
469
+ marker=dict(size=10, color='blue', symbol='circle',
470
+ line=dict(color='white', width=2)),
471
+ text=[f'{user_eff*100:.0f}%'],
472
+ textposition='top center',
473
+ textfont=dict(size=14, color='white'),
474
+ name='當前情境'
475
+ ))
476
+
477
+ # 設置佈局
478
+ fig.update_layout(
479
+ title={
480
+ 'text': '精準追蹤接觸者效益 3D 圖 (Alpha 變異株)',
481
+ 'x': 0.5,
482
+ 'xanchor': 'center'
483
+ },
484
+ scene=dict(
485
+ xaxis=dict(
486
+ title='X-病毒量(Ct值)',
487
+ range=[10, 35],
488
+ tickvals=[10, 15, 20, 25, 30, 35],
489
+ showgrid=True,
490
+ gridwidth=2,
491
+ gridcolor='rgb(200, 200, 200)',
492
+ showbackground=True,
493
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
494
+ ),
495
+ yaxis=dict(
496
+ title='Y-回朔追蹤接觸者天數',
497
+ range=[1, 60],
498
+ tickvals=[1, 10, 20, 30, 40, 50, 60],
499
+ showgrid=True,
500
+ gridwidth=2,
501
+ gridcolor='rgb(200, 200, 200)',
502
+ showbackground=True,
503
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
504
+ ),
505
+ zaxis=dict(
506
+ title='Z-效益',
507
+ range=[0, 1],
508
+ tickvals=[0, 0.25, 0.5, 0.75, 1.0],
509
+ ticktext=['0%', '25%', '50%', '75%', '100%'],
510
+ showgrid=True,
511
+ gridwidth=2,
512
+ gridcolor='rgb(200, 200, 200)',
513
+ showbackground=True,
514
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
515
+ ),
516
+ camera=dict(
517
+ eye=dict(x=1.5, y=-1.5, z=1.3),
518
+ center=dict(x=0, y=0, z=0)
519
+ ),
520
+ aspectmode='manual',
521
+ aspectratio=dict(x=1, y=1.2, z=0.8)
522
+ ),
523
+ height=700,
524
+ showlegend=True,
525
+ legend=dict(
526
+ x=0.02,
527
+ y=0.98,
528
+ bgcolor='rgba(255, 255, 255, 0.8)'
529
+ ),
530
+ margin=dict(l=0, r=0, t=40, b=0)
531
+ )
532
+
533
+ st.plotly_chart(fig, use_container_width=True)
534
+
535
+ # 情境狀態顯示
536
+ if selected_ct <= 18:
537
+ status = "⚠️ 高病毒量 - 近期感染"
538
+ color = "red"
539
+ elif selected_ct <= 25:
540
+ status = "⚡ 中病毒量 - 感染數天"
541
+ color = "orange"
542
+ else:
543
+ status = "💊 低病毒量 - 感染較久"
544
+ color = "blue"
545
+
546
+ st.info(f"**情境:** 確診者 Ct值 = **{selected_ct}**,追蹤 **{tracing_days}** 天 | {status}")
547
+
548
+ # 效益解釋
549
+ effectiveness = get_effectiveness(selected_ct, tracing_days)
550
+ if effectiveness >= 0.9:
551
+ st.success("💡 **意義:** 追蹤時間充足,可以找到大部分接觸者")
552
+ elif effectiveness >= 0.7:
553
+ st.warning("💡 **意義:** 追蹤效果良好,但仍可能遺漏部分接觸者")
554
+ elif effectiveness >= 0.5:
555
+ st.warning("💡 **意義:** 追蹤效果一般,建議延長追蹤時間")
556
+ else:
557
+ st.error("💡 **意義:** 追蹤效果不足,需要大幅延長追蹤時間")
558
+
559
+ with col2:
560
+ st.subheader("🤖 AI 助手對話")
561
+
562
+ # 聊天輸入區
563
+ st.markdown("##### 💬 快速查詢")
564
+ user_input = st.text_input(
565
+ "輸入問題(例如:Ct��20追蹤14天的效益如何?)",
566
+ placeholder="例如: Ct 20, 要回溯幾天才能達到100%?",
567
+ label_visibility="collapsed",
568
+ key="tab1_user_input"
569
+ )
570
+
571
+ if st.button("📤 發送", use_container_width=True, key="tab1_send"):
572
+ if not st.session_state.api_key:
573
+ st.error("❌ 請先在側邊欄輸入 OpenAI API Key")
574
+ elif user_input.strip():
575
+ with st.spinner("AI 思考中..."):
576
+ # 提取參數
577
+ extracted_ct, extracted_days, target_eff = extract_query_params(user_input)
578
+
579
+ # 構建對話 prompt
580
+ chat_prompt = f"""用戶問題:{user_input}
581
+
582
+ 當前系統參數作為參考:
583
+ - Ct 值:{selected_ct}
584
+ - 追蹤天數:{tracing_days}
585
+ - 當前效益:{current_eff*100:.1f}%
586
+
587
+ 請根據用戶的具體問題,使用相應的工具函數來獲取精確數據並給出專業建議。
588
+
589
+ 請用繁體中文回答,簡潔專業。只需要:
590
+ 1. 直接回答數據結果(Ct值、天數、效益)
591
+ 2. 提供2-3點簡短的策略調整建議即可
592
+ 不要解釋接觸者追蹤的背景知識或一般性原理。"""
593
+
594
+ # 定義 function calling tools
595
+ tools = [
596
+ {
597
+ "type": "function",
598
+ "function": {
599
+ "name": "calculate_effectiveness",
600
+ "description": "計算特定 Ct 值和追蹤天數組合的接觸者追蹤效益。當用戶詢問具體的 Ct 值和天數時,必須使用此函數獲取精確數據。",
601
+ "parameters": {
602
+ "type": "object",
603
+ "properties": {
604
+ "ct_value": {
605
+ "type": "number",
606
+ "description": "確診者的 Ct 值,範圍 10-35。Ct 值越低代表病毒量越高。"
607
+ },
608
+ "days": {
609
+ "type": "integer",
610
+ "description": "需要回溯追蹤的天數,範圍 1-60 天。"
611
+ }
612
+ },
613
+ "required": ["ct_value", "days"]
614
+ }
615
+ }
616
+ },
617
+ {
618
+ "type": "function",
619
+ "function": {
620
+ "name": "find_required_days",
621
+ "description": "反向查詢:給定 Ct 值和目標效益百分比,計算需要回溯多少天才能達到該效益。當用戶詢問「要幾天才能達到X%效益」時使用。",
622
+ "parameters": {
623
+ "type": "object",
624
+ "properties": {
625
+ "ct_value": {
626
+ "type": "number",
627
+ "description": "確診者的 Ct 值,範圍 10-35。"
628
+ },
629
+ "target_effectiveness": {
630
+ "type": "number",
631
+ "description": "目標效益,範圍 0-1 (例如 0.9 代表 90%, 1.0 代表 100%)。"
632
+ }
633
+ },
634
+ "required": ["ct_value", "target_effectiveness"]
635
+ }
636
+ }
637
+ }
638
+ ]
639
+
640
+ # 根據問題類型決定 tool_choice
641
+ tool_choice = "auto"
642
+ if target_eff is not None and extracted_ct is not None:
643
+ tool_choice = {"type": "function", "function": {"name": "find_required_days"}}
644
+ elif extracted_ct and extracted_days:
645
+ tool_choice = {"type": "function", "function": {"name": "calculate_effectiveness"}}
646
+
647
+ response = call_gpt4(chat_prompt, st.session_state.api_key, tools=tools, tool_choice=tool_choice)
648
+ final_response, result_ct, result_days = process_gpt4_response(response, st.session_state.api_key)
649
+
650
+ # 更新參數
651
+ if result_ct is not None and result_days is not None:
652
+ st.session_state.tab1_selected_ct = float(result_ct)
653
+ st.session_state.tab1_tracing_days = int(result_days)
654
+
655
+ st.session_state.tab1_chat_history.append({
656
+ "type": "user_query",
657
+ "question": user_input,
658
+ "response": final_response
659
+ })
660
+
661
+ # 如果有更新參數,重新渲染頁面
662
+ if result_ct is not None and result_days is not None:
663
+ st.rerun()
664
+
665
+ # 顯示對話歷史
666
+ st.markdown("---")
667
+ st.markdown("##### 📜 對話記錄")
668
+
669
+ if st.session_state.tab1_chat_history:
670
+ # 反向顯示(最新的在上面)
671
+ for i, chat in enumerate(reversed(st.session_state.tab1_chat_history)):
672
+ with st.container():
673
+ if chat["type"] == "auto_summary":
674
+ st.markdown(f"**🤖 AI 報告** (Ct={chat['ct']}, {chat['days']}天, 效益={chat['effectiveness']*100:.0f}%)")
675
+ st.markdown(chat["response"])
676
+ else:
677
+ st.markdown(f"**👤 問題:** {chat['question']}")
678
+ st.markdown(f"**🤖 回答:** {chat['response']}")
679
+ st.markdown("---")
680
+
681
+ if st.button("🗑️ 清除對話記錄", use_container_width=True, key="tab1_clear_chat"):
682
+ st.session_state.tab1_chat_history = []
683
+ st.rerun()
684
+ else:
685
+ st.info("💡 點擊「生成 AI 報告」或在上方輸入問題開始對話")
686
+
687
+ # 底部說明區域
688
+ st.markdown("---")
689
+
690
+ col_a, col_b = st.columns(2)
691
+
692
+ with col_a:
693
+ with st.expander("💡 操作說明", expanded=False):
694
+ st.markdown("""
695
+ **3D 圖表操作:**
696
+ - 🖱️ **拖曳旋轉**:用滑鼠拖曳3D圖表可以旋轉視角
697
+ - 🔍 **縮放**:使用滑鼠滾輪可以縮放
698
+ - 🎚️ **調整參數**:使用左側滑桿調整Ct值和追蹤天數
699
+ - 💡 **黃色線**:顯示當前追蹤天數下,不同Ct值的效益
700
+ - 🔵 **藍色點**:您當前選擇的情境
701
+
702
+ **AI 助手功能:**
703
+ - 📊 **自動報告**:點擊「生成 AI 報告」獲得當前策略的完整分析
704
+ - 💬 **快速查詢**:直接輸入 Ct 值和天數快速獲得效益評估
705
+ - 🔄 **反向查詢**:詢問「Ct X 要幾天達到 Y% 效益」獲得所需天數
706
+ - 🔗 **參數連動**:對話查詢後左側滑桿會自動更新到對應數值
707
+ """)
708
+
709
+ with col_b:
710
+ with st.expander("📊 查看詳細數據", expanded=False):
711
+ # 創建示例數據表
712
+ data = {
713
+ 'Ct值': [15, 18, 22, 28, 33],
714
+ f'追蹤{tracing_days}天的效益': [
715
+ f"{get_effectiveness(15, tracing_days)*100:.1f}%",
716
+ f"{get_effectiveness(18, tracing_days)*100:.1f}%",
717
+ f"{get_effectiveness(22, tracing_days)*100:.1f}%",
718
+ f"{get_effectiveness(28, tracing_days)*100:.1f}%",
719
+ f"{get_effectiveness(33, tracing_days)*100:.1f}%"
720
+ ],
721
+ '病毒量等級': ['高', '高', '中', '低', '低']
722
+ }
723
+
724
+ df = pd.DataFrame(data)
725
+ st.dataframe(df, use_container_width=True)
tab2_quarantine (4).py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import plotly.graph_objects as go
3
+ import numpy as np
4
+ import pandas as pd
5
+ from openai import OpenAI
6
+
7
+ # 初始化 tab2 專屬的 session state
8
+ def init_tab2_session_state():
9
+ if 'tab2_chat_history' not in st.session_state:
10
+ st.session_state.tab2_chat_history = []
11
+ # 有加強劑的參數
12
+ if 'tab2_booster_ct' not in st.session_state:
13
+ st.session_state.tab2_booster_ct = 18.0
14
+ if 'tab2_booster_days' not in st.session_state:
15
+ st.session_state.tab2_booster_days = 7
16
+ # 沒有加強劑的參數
17
+ if 'tab2_no_booster_ct' not in st.session_state:
18
+ st.session_state.tab2_no_booster_ct = 18.0
19
+ if 'tab2_no_booster_days' not in st.session_state:
20
+ st.session_state.tab2_no_booster_days = 7
21
+
22
+ # 基礎數據:從 Table 2 論文原始數據
23
+ BASE_DATA = {
24
+ # Ct <= 18, 無加強劑
25
+ 'ct18_no_booster': {
26
+ 1: 0.10, 2: 0.25, 3: 0.39, 4: 0.51, 5: 0.61,
27
+ 6: 0.70, 7: 0.76, 8: 0.81, 9: 0.84, 10: 0.87,
28
+ 11: 0.89, 12: 0.91, 13: 0.93, 14: 0.94, 15: 0.95,
29
+ 16: 0.96, 17: 0.96, 18: 0.97, 19: 0.97, 20: 0.98, 21: 0.98
30
+ },
31
+ # Ct <= 18, 有加強劑
32
+ 'ct18_booster': {
33
+ 1: 0.44, 2: 0.65, 3: 0.77, 4: 0.84, 5: 0.89,
34
+ 6: 0.92, 7: 0.94, 8: 0.95, 9: 0.97, 10: 0.97,
35
+ 11: 0.97, 12: 0.98, 13: 0.98, 14: 0.99, 15: 0.99,
36
+ 16: 0.99, 17: 0.99, 18: 0.99, 19: 0.99, 20: 1.00, 21: 1.00
37
+ },
38
+ # Ct 18-25, 無加強劑
39
+ 'ct22_no_booster': {
40
+ 1: 0.49, 2: 0.57, 3: 0.65, 4: 0.72, 5: 0.78,
41
+ 6: 0.83, 7: 0.86, 8: 0.89, 9: 0.91, 10: 0.92,
42
+ 11: 0.94, 12: 0.95, 13: 0.96, 14: 0.97, 15: 0.97,
43
+ 16: 0.98, 17: 0.98, 18: 0.98, 19: 0.98, 20: 0.99, 21: 0.99
44
+ },
45
+ # Ct 18-25, 有加強劑
46
+ 'ct22_booster': {
47
+ 1: 0.68, 2: 0.80, 3: 0.87, 4: 0.91, 5: 0.94,
48
+ 6: 0.95, 7: 0.96, 8: 0.97, 9: 0.98, 10: 0.98,
49
+ 11: 0.98, 12: 0.99, 13: 0.99, 14: 0.99, 15: 0.99,
50
+ 16: 1.00, 17: 1.00, 18: 1.00, 19: 1.00, 20: 1.00, 21: 1.00
51
+ }
52
+ }
53
+
54
+ def get_effectiveness_from_data(data_key, days):
55
+ """從基礎數據中獲取效益值(帶線性插值)"""
56
+ data = BASE_DATA[data_key]
57
+
58
+ if days < 1:
59
+ return 0.0
60
+
61
+ if days > 21:
62
+ return data[21]
63
+
64
+ if days in data:
65
+ return data[days]
66
+
67
+ # 線性插值
68
+ day_lower = int(days)
69
+ day_upper = day_lower + 1
70
+
71
+ if day_upper > 21:
72
+ return data[21]
73
+
74
+ eff_lower = data[day_lower]
75
+ eff_upper = data[day_upper]
76
+
77
+ ratio = days - day_lower
78
+ effectiveness = eff_lower * (1 - ratio) + eff_upper * ratio
79
+
80
+ return effectiveness
81
+
82
+ def get_quarantine_effectiveness(ct_value, days, has_booster):
83
+ """獲取隔離效益"""
84
+ if ct_value > 25:
85
+ return 0.0
86
+
87
+ booster_suffix = 'booster' if has_booster else 'no_booster'
88
+
89
+ if ct_value <= 18:
90
+ data_key = f'ct18_{booster_suffix}'
91
+ effectiveness = get_effectiveness_from_data(data_key, days)
92
+
93
+ if ct_value < 10:
94
+ boost_factor = 1.0 + (10 - ct_value) * 0.005
95
+ effectiveness = min(effectiveness * boost_factor, 1.0)
96
+
97
+ return effectiveness
98
+
99
+ else:
100
+ ct18_key = f'ct18_{booster_suffix}'
101
+ ct22_key = f'ct22_{booster_suffix}'
102
+
103
+ eff_ct18 = get_effectiveness_from_data(ct18_key, days)
104
+ eff_ct22 = get_effectiveness_from_data(ct22_key, days)
105
+
106
+ ratio = (ct_value - 18) / 7
107
+ effectiveness = eff_ct18 * (1 - ratio) + eff_ct22 * ratio
108
+
109
+ return min(effectiveness, 1.0)
110
+
111
+ def get_ct_category(ct_value):
112
+ """獲取 Ct 值分類"""
113
+ if ct_value <= 18:
114
+ return "高病毒量 (Ct ≤ 18)"
115
+ elif ct_value <= 25:
116
+ return "中病毒量 (18 < Ct ≤ 25)"
117
+ else:
118
+ return "低病毒量 (Ct > 25,假設無傳染性)"
119
+
120
+ def create_3d_plot(ct_mesh, day_mesh, effectiveness_mesh, selected_ct, quarantine_days,
121
+ user_eff, has_booster, title, colorscale, marker_color):
122
+ """創建單一 3D 曲面圖"""
123
+ fig = go.Figure()
124
+
125
+ fig.add_trace(go.Surface(
126
+ x=ct_mesh,
127
+ y=day_mesh,
128
+ z=effectiveness_mesh,
129
+ colorscale=colorscale,
130
+ showscale=True,
131
+ colorbar=dict(
132
+ title="防疫效益",
133
+ tickvals=[0, 0.25, 0.5, 0.75, 1.0],
134
+ ticktext=['0%', '25%', '50%', '75%', '100%']
135
+ ),
136
+ opacity=0.9,
137
+ name=title,
138
+ contours=dict(
139
+ x=dict(show=True, color='white', width=1),
140
+ y=dict(show=True, color='white', width=1),
141
+ z=dict(show=True, color='white', width=1)
142
+ )
143
+ ))
144
+
145
+ fig.add_trace(go.Scatter3d(
146
+ x=[selected_ct],
147
+ y=[quarantine_days],
148
+ z=[user_eff],
149
+ mode='markers+text',
150
+ marker=dict(size=12, color=marker_color, symbol='circle',
151
+ line=dict(color='white', width=3)),
152
+ text=[f'{user_eff*100:.0f}%'],
153
+ textposition='top center',
154
+ textfont=dict(size=14, color='white', family='Arial Black'),
155
+ name='當前情境',
156
+ showlegend=True
157
+ ))
158
+
159
+ fig.update_layout(
160
+ title={
161
+ 'text': title,
162
+ 'x': 0.5,
163
+ 'xanchor': 'center',
164
+ 'font': {'size': 16}
165
+ },
166
+ scene=dict(
167
+ xaxis=dict(
168
+ title='X-病毒量(Ct值)',
169
+ range=[10, 25],
170
+ tickvals=[10, 15, 18, 20, 25],
171
+ showgrid=True,
172
+ gridwidth=2,
173
+ gridcolor='rgb(200, 200, 200)',
174
+ showbackground=True,
175
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
176
+ ),
177
+ yaxis=dict(
178
+ title='Y-隔離檢疫天數',
179
+ range=[1, 21],
180
+ tickvals=[1, 5, 7, 10, 14, 21],
181
+ showgrid=True,
182
+ gridwidth=2,
183
+ gridcolor='rgb(200, 200, 200)',
184
+ showbackground=True,
185
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
186
+ ),
187
+ zaxis=dict(
188
+ title='Z-防疫效益',
189
+ range=[0, 1],
190
+ tickvals=[0, 0.25, 0.5, 0.75, 1.0],
191
+ ticktext=['0%', '25%', '50%', '75%', '100%'],
192
+ showgrid=True,
193
+ gridwidth=2,
194
+ gridcolor='rgb(200, 200, 200)',
195
+ showbackground=True,
196
+ backgroundcolor='rgba(240, 240, 240, 0.9)'
197
+ ),
198
+ camera=dict(
199
+ eye=dict(x=1.5, y=-1.5, z=1.3),
200
+ center=dict(x=0, y=0, z=0)
201
+ ),
202
+ aspectmode='manual',
203
+ aspectratio=dict(x=1, y=1.2, z=0.8)
204
+ ),
205
+ height=600,
206
+ showlegend=True,
207
+ legend=dict(
208
+ x=0.02,
209
+ y=0.98,
210
+ bgcolor='rgba(255, 255, 255, 0.9)',
211
+ bordercolor='black',
212
+ borderwidth=1
213
+ ),
214
+ margin=dict(l=0, r=0, t=40, b=0)
215
+ )
216
+
217
+ return fig
218
+
219
+ def render():
220
+ """渲染 Tab2 的完整內容"""
221
+ init_tab2_session_state()
222
+
223
+ # 側邊欄 - 控制面板
224
+ with st.sidebar:
225
+ st.header("🏥 隔離檢疫決策工具")
226
+
227
+ st.markdown("---")
228
+
229
+ # 有加強劑的參數設定
230
+ st.subheader("💉 有加強劑情境")
231
+
232
+ booster_ct = st.slider(
233
+ "🦠 接觸者Ct值",
234
+ min_value=10.0,
235
+ max_value=25.0,
236
+ value=st.session_state.tab2_booster_ct,
237
+ step=0.5,
238
+ help="Ct值越低代表病毒量越高 (Ct>25視為無傳染性)",
239
+ key="tab2_booster_ct_slider"
240
+ )
241
+ st.session_state.tab2_booster_ct = booster_ct
242
+
243
+ booster_days = st.slider(
244
+ "📅 隔離天數",
245
+ min_value=1,
246
+ max_value=21,
247
+ value=st.session_state.tab2_booster_days,
248
+ help="需要隔離檢疫的天數",
249
+ key="tab2_booster_days_slider"
250
+ )
251
+ st.session_state.tab2_booster_days = booster_days
252
+
253
+ booster_eff = get_quarantine_effectiveness(booster_ct, booster_days, True)
254
+ st.metric(
255
+ label="📊 防疫效益",
256
+ value=f"{booster_eff * 100:.0f}%",
257
+ delta="有加強劑"
258
+ )
259
+
260
+ st.markdown("---")
261
+
262
+ # 沒有加強劑的參數設定
263
+ st.subheader("⚠️ 無加強劑情境")
264
+
265
+ no_booster_ct = st.slider(
266
+ "🦠 接觸者Ct值",
267
+ min_value=10.0,
268
+ max_value=25.0,
269
+ value=st.session_state.tab2_no_booster_ct,
270
+ step=0.5,
271
+ help="Ct值越低代表病毒量越高 (Ct>25視為無傳染性)",
272
+ key="tab2_no_booster_ct_slider"
273
+ )
274
+ st.session_state.tab2_no_booster_ct = no_booster_ct
275
+
276
+ no_booster_days = st.slider(
277
+ "📅 隔離天數",
278
+ min_value=1,
279
+ max_value=21,
280
+ value=st.session_state.tab2_no_booster_days,
281
+ help="需要隔離檢疫的天數",
282
+ key="tab2_no_booster_days_slider"
283
+ )
284
+ st.session_state.tab2_no_booster_days = no_booster_days
285
+
286
+ no_booster_eff = get_quarantine_effectiveness(no_booster_ct, no_booster_days, False)
287
+ st.metric(
288
+ label="📊 防疫效益",
289
+ value=f"{no_booster_eff * 100:.0f}%",
290
+ delta="無加強劑"
291
+ )
292
+
293
+ st.markdown("---")
294
+
295
+ # 情境說明
296
+ with st.expander("🎯 使用情境", expanded=False):
297
+ st.markdown("""
298
+ 當找到疑似接觸者時,防疫人員需要決定:
299
+ **「要隔離/檢疫多少天?」**
300
+
301
+ 考量因素:
302
+ - 接觸者的病毒量 (Ct值)
303
+ - 是否已接種加強劑
304
+ """)
305
+
306
+ with st.expander("✅ Omicron 特性"):
307
+ st.markdown("""
308
+ 相較於 Alpha 變異株:
309
+ - **傳播更快** 但症狀較輕
310
+ - **疫苗保護** 顯著縮短隔離時間
311
+ - **Ct > 25** 視為無傳染性
312
+ """)
313
+
314
+ with st.expander("💉 疫苗影響"):
315
+ st.markdown("""
316
+ **加強劑的效益:**
317
+ - 大幅縮短所需隔離天數
318
+ - 相同天數下效益更高
319
+ - 例: 達到90%效益
320
+ - 有加強劑: 5天
321
+ - 無加強劑: 11天
322
+ """)
323
+
324
+ # 主要內容區
325
+ st.markdown("### 📊 隔離檢疫效益 3D 視覺化")
326
+
327
+ # 生成 3D 數據
328
+ ct_range = np.arange(10, 25.5, 0.5)
329
+ day_range = np.arange(1, 22, 1)
330
+ ct_mesh, day_mesh = np.meshgrid(ct_range, day_range)
331
+
332
+ # 計算兩組效益值
333
+ effectiveness_with_booster = np.zeros_like(ct_mesh)
334
+ effectiveness_without_booster = np.zeros_like(ct_mesh)
335
+
336
+ for i in range(len(day_range)):
337
+ for j in range(len(ct_range)):
338
+ effectiveness_with_booster[i, j] = get_quarantine_effectiveness(ct_mesh[i, j], day_mesh[i, j], True)
339
+ effectiveness_without_booster[i, j] = get_quarantine_effectiveness(ct_mesh[i, j], day_mesh[i, j], False)
340
+
341
+ # 創建兩個並排的圖表
342
+ col1, col2 = st.columns(2)
343
+
344
+ with col1:
345
+ st.markdown("#### 💉 有加強劑情境")
346
+ fig1 = create_3d_plot(
347
+ ct_mesh, day_mesh, effectiveness_with_booster,
348
+ booster_ct, booster_days,
349
+ booster_eff,
350
+ True,
351
+ "隔離檢疫效益 - 有加強劑 (Omicron 變異株)",
352
+ [
353
+ [0.0, 'rgb(239, 68, 68)'],
354
+ [0.33, 'rgb(245, 158, 11)'],
355
+ [0.67, 'rgb(16, 185, 129)'],
356
+ [1.0, 'rgb(59, 130, 246)']
357
+ ],
358
+ 'blue'
359
+ )
360
+ st.plotly_chart(fig1, use_container_width=True)
361
+
362
+ ct_category_booster = get_ct_category(booster_ct)
363
+ st.info(f"**情境:** Ct值 = **{booster_ct}** ({ct_category_booster}), 隔離 **{booster_days}** 天 | 💉 已接種加強劑")
364
+
365
+ if booster_eff >= 0.9:
366
+ st.success("💡 **意義:** 隔離時間充足,可有效防止疫情傳播")
367
+ elif booster_eff >= 0.7:
368
+ st.warning("💡 **意義:** 隔離效果良好,但建議視情況延長")
369
+ elif booster_eff >= 0.5:
370
+ st.warning("💡 **意義:** 隔離效果一般,建議延長隔離時間")
371
+ else:
372
+ st.error("💡 **意義:** 隔離效果不足,需要大幅延長隔離時間")
373
+
374
+ with col2:
375
+ st.markdown("#### ⚠️ 無加強劑情境")
376
+ fig2 = create_3d_plot(
377
+ ct_mesh, day_mesh, effectiveness_without_booster,
378
+ no_booster_ct, no_booster_days,
379
+ no_booster_eff,
380
+ False,
381
+ "隔離檢疫效益 - 無加強劑 (Omicron 變異株)",
382
+ [
383
+ [0.0, 'rgb(239, 68, 68)'],
384
+ [0.33, 'rgb(245, 158, 11)'],
385
+ [0.67, 'rgb(16, 185, 129)'],
386
+ [1.0, 'rgb(59, 130, 246)']
387
+ ],
388
+ 'red'
389
+ )
390
+ st.plotly_chart(fig2, use_container_width=True)
391
+
392
+ ct_category_no_booster = get_ct_category(no_booster_ct)
393
+ st.info(f"**情境:** Ct值 = **{no_booster_ct}** ({ct_category_no_booster}), 隔離 **{no_booster_days}** 天 | ⚠️ 未接種加強劑")
394
+
395
+ if no_booster_eff >= 0.9:
396
+ st.success("💡 **意義:** 隔離時間充足,可有效防止疫情傳播")
397
+ elif no_booster_eff >= 0.7:
398
+ st.warning("💡 **意義:** 隔離效果良好,但建議視情況延長")
399
+ elif no_booster_eff >= 0.5:
400
+ st.warning("💡 **意義:** 隔離效果一般,建議延長隔離時間")
401
+ else:
402
+ st.error("💡 **意義:** 隔離效果不足,需要大幅延長隔離時間")
403
+
404
+ # 疫苗建議
405
+ improvement = (booster_eff - no_booster_eff) * 100
406
+ if improvement > 0:
407
+ st.info(f"💉 **提示:** 若接種加強劑,在相同參數下可提升 {improvement:.0f}% 效益")
408
+
409
+ # 底部說明區域
410
+ st.markdown("---")
411
+
412
+ col_a, col_b = st.columns(2)
413
+
414
+ with col_a:
415
+ with st.expander("💡 操作說明", expanded=False):
416
+ st.markdown("""
417
+ **3D 圖表說明:**
418
+ - 🔵 **左側圖表**: 有加強劑的隔離效益 (藍綠色)
419
+ - 🔴 **右側圖表**: 無加強劑的隔離效益 (橙紅色)
420
+ - 💎 **菱形標記**: 您當前選擇的情境
421
+ - 兩圖對比可清楚看出**疫苗的效益差異**
422
+
423
+ **互動操作:**
424
+ - 🖱️ 拖曳旋轉視角
425
+ - 🔍 滾輪縮放
426
+ - 🎚️ 使用左側滑桿調整參數
427
+ """)
428
+
429
+ with col_b:
430
+ with st.expander("📊 查看詳細數據", expanded=False):
431
+ # 創建對比表格
432
+ test_days_list = [3, 5, 7, 10, 14]
433
+
434
+ data = {
435
+ '隔離天數': test_days_list,
436
+ '有加強劑 (Ct=' + str(booster_ct) + ')': [f"{get_quarantine_effectiveness(booster_ct, d, True)*100:.0f}%" for d in test_days_list],
437
+ '無加強劑 (Ct=' + str(no_booster_ct) + ')': [f"{get_quarantine_effectiveness(no_booster_ct, d, False)*100:.0f}%" for d in test_days_list],
438
+ '效益差異': [f"+{(get_quarantine_effectiveness(booster_ct, d, True) - get_quarantine_effectiveness(no_booster_ct, d, False))*100:.0f}%" for d in test_days_list]
439
+ }
440
+
441
+ df = pd.DataFrame(data)
442
+ st.dataframe(df, use_container_width=True)