import streamlit as st import plotly.graph_objects as go import numpy as np import pandas as pd from openai import OpenAI # 初始化 tab2 專屬的 session state def init_tab2_session_state(): if 'tab2_chat_history' not in st.session_state: st.session_state.tab2_chat_history = [] # 有加強劑的參數 if 'tab2_booster_ct' not in st.session_state: st.session_state.tab2_booster_ct = 18.0 if 'tab2_booster_days' not in st.session_state: st.session_state.tab2_booster_days = 7 # 沒有加強劑的參數 if 'tab2_no_booster_ct' not in st.session_state: st.session_state.tab2_no_booster_ct = 18.0 if 'tab2_no_booster_days' not in st.session_state: st.session_state.tab2_no_booster_days = 7 # 基礎數據:從 Table 2 論文原始數據 BASE_DATA = { # Ct <= 18, 無加強劑 'ct18_no_booster': { 1: 0.10, 2: 0.25, 3: 0.39, 4: 0.51, 5: 0.61, 6: 0.70, 7: 0.76, 8: 0.81, 9: 0.84, 10: 0.87, 11: 0.89, 12: 0.91, 13: 0.93, 14: 0.94, 15: 0.95, 16: 0.96, 17: 0.96, 18: 0.97, 19: 0.97, 20: 0.98, 21: 0.98 }, # Ct <= 18, 有加強劑 'ct18_booster': { 1: 0.44, 2: 0.65, 3: 0.77, 4: 0.84, 5: 0.89, 6: 0.92, 7: 0.94, 8: 0.95, 9: 0.97, 10: 0.97, 11: 0.97, 12: 0.98, 13: 0.98, 14: 0.99, 15: 0.99, 16: 0.99, 17: 0.99, 18: 0.99, 19: 0.99, 20: 1.00, 21: 1.00 }, # Ct 18-25, 無加強劑 'ct22_no_booster': { 1: 0.49, 2: 0.57, 3: 0.65, 4: 0.72, 5: 0.78, 6: 0.83, 7: 0.86, 8: 0.89, 9: 0.91, 10: 0.92, 11: 0.94, 12: 0.95, 13: 0.96, 14: 0.97, 15: 0.97, 16: 0.98, 17: 0.98, 18: 0.98, 19: 0.98, 20: 0.99, 21: 0.99 }, # Ct 18-25, 有加強劑 'ct22_booster': { 1: 0.68, 2: 0.80, 3: 0.87, 4: 0.91, 5: 0.94, 6: 0.95, 7: 0.96, 8: 0.97, 9: 0.98, 10: 0.98, 11: 0.98, 12: 0.99, 13: 0.99, 14: 0.99, 15: 0.99, 16: 1.00, 17: 1.00, 18: 1.00, 19: 1.00, 20: 1.00, 21: 1.00 } } def get_effectiveness_from_data(data_key, days): """從基礎數據中獲取效益值(帶線性插值)""" data = BASE_DATA[data_key] if days < 1: return 0.0 if days > 21: return data[21] if days in data: return data[days] # 線性插值 day_lower = int(days) day_upper = day_lower + 1 if day_upper > 21: return data[21] eff_lower = data[day_lower] eff_upper = data[day_upper] ratio = days - day_lower effectiveness = eff_lower * (1 - ratio) + eff_upper * ratio return effectiveness def get_quarantine_effectiveness(ct_value, days, has_booster): """獲取隔離效益""" if ct_value > 25: return 0.0 booster_suffix = 'booster' if has_booster else 'no_booster' if ct_value <= 18: data_key = f'ct18_{booster_suffix}' effectiveness = get_effectiveness_from_data(data_key, days) if ct_value < 10: boost_factor = 1.0 + (10 - ct_value) * 0.005 effectiveness = min(effectiveness * boost_factor, 1.0) return effectiveness else: ct18_key = f'ct18_{booster_suffix}' ct22_key = f'ct22_{booster_suffix}' eff_ct18 = get_effectiveness_from_data(ct18_key, days) eff_ct22 = get_effectiveness_from_data(ct22_key, days) ratio = (ct_value - 18) / 7 effectiveness = eff_ct18 * (1 - ratio) + eff_ct22 * ratio return min(effectiveness, 1.0) def get_ct_category(ct_value): """獲取 Ct 值分類""" if ct_value <= 18: return "高病毒量 (Ct ≤ 18)" elif ct_value <= 25: return "中病毒量 (18 < Ct ≤ 25)" else: return "低病毒量 (Ct > 25,假設無傳染性)" def create_3d_plot(ct_mesh, day_mesh, effectiveness_mesh, selected_ct, quarantine_days, user_eff, has_booster, title, colorscale, marker_color): """創建單一 3D 曲面圖""" fig = go.Figure() fig.add_trace(go.Surface( x=ct_mesh, y=day_mesh, z=effectiveness_mesh, colorscale=colorscale, showscale=True, colorbar=dict( title="防疫效益", tickvals=[0, 0.25, 0.5, 0.75, 1.0], ticktext=['0%', '25%', '50%', '75%', '100%'] ), opacity=0.9, name=title, contours=dict( x=dict(show=True, color='white', width=1), y=dict(show=True, color='white', width=1), z=dict(show=True, color='white', width=1) ) )) fig.add_trace(go.Scatter3d( x=[selected_ct], y=[quarantine_days], z=[user_eff], mode='markers+text', marker=dict(size=12, color=marker_color, symbol='circle', line=dict(color='white', width=3)), text=[f'{user_eff*100:.0f}%'], textposition='top center', textfont=dict(size=14, color='white', family='Arial Black'), name='當前情境', showlegend=True )) fig.update_layout( title={ 'text': title, 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16} }, scene=dict( xaxis=dict( title='X-病毒量(Ct值)', range=[10, 25], tickvals=[10, 15, 18, 20, 25], showgrid=True, gridwidth=2, gridcolor='rgb(200, 200, 200)', showbackground=True, backgroundcolor='rgba(240, 240, 240, 0.9)' ), yaxis=dict( title='Y-隔離檢疫天數', range=[1, 21], tickvals=[1, 5, 7, 10, 14, 21], showgrid=True, gridwidth=2, gridcolor='rgb(200, 200, 200)', showbackground=True, backgroundcolor='rgba(240, 240, 240, 0.9)' ), zaxis=dict( title='Z-防疫效益', range=[0, 1], tickvals=[0, 0.25, 0.5, 0.75, 1.0], ticktext=['0%', '25%', '50%', '75%', '100%'], showgrid=True, gridwidth=2, gridcolor='rgb(200, 200, 200)', showbackground=True, backgroundcolor='rgba(240, 240, 240, 0.9)' ), camera=dict( eye=dict(x=1.5, y=-1.5, z=1.3), center=dict(x=0, y=0, z=0) ), aspectmode='manual', aspectratio=dict(x=1, y=1.2, z=0.8) ), height=600, showlegend=True, legend=dict( x=0.02, y=0.98, bgcolor='rgba(255, 255, 255, 0.9)', bordercolor='black', borderwidth=1 ), margin=dict(l=0, r=0, t=40, b=0) ) return fig def render(): """渲染 Tab2 的完整內容""" init_tab2_session_state() # 側邊欄 - 控制面板 with st.sidebar: st.header("🏥 隔離檢疫決策工具") st.markdown("---") # 有加強劑的參數設定 st.subheader("💉 有加強劑情境") booster_ct = st.slider( "🦠 接觸者Ct值", min_value=10.0, max_value=25.0, value=st.session_state.tab2_booster_ct, step=0.5, help="Ct值越低代表病毒量越高 (Ct>25視為無傳染性)", key="tab2_booster_ct_slider" ) st.session_state.tab2_booster_ct = booster_ct booster_days = st.slider( "📅 隔離天數", min_value=1, max_value=21, value=st.session_state.tab2_booster_days, help="需要隔離檢疫的天數", key="tab2_booster_days_slider" ) st.session_state.tab2_booster_days = booster_days booster_eff = get_quarantine_effectiveness(booster_ct, booster_days, True) st.metric( label="📊 防疫效益", value=f"{booster_eff * 100:.0f}%", delta="有加強劑" ) st.markdown("---") # 沒有加強劑的參數設定 st.subheader("⚠️ 無加強劑情境") no_booster_ct = st.slider( "🦠 接觸者Ct值", min_value=10.0, max_value=25.0, value=st.session_state.tab2_no_booster_ct, step=0.5, help="Ct值越低代表病毒量越高 (Ct>25視為無傳染性)", key="tab2_no_booster_ct_slider" ) st.session_state.tab2_no_booster_ct = no_booster_ct no_booster_days = st.slider( "📅 隔離天數", min_value=1, max_value=21, value=st.session_state.tab2_no_booster_days, help="需要隔離檢疫的天數", key="tab2_no_booster_days_slider" ) st.session_state.tab2_no_booster_days = no_booster_days no_booster_eff = get_quarantine_effectiveness(no_booster_ct, no_booster_days, False) st.metric( label="📊 防疫效益", value=f"{no_booster_eff * 100:.0f}%", delta="無加強劑" ) st.markdown("---") # 情境說明 with st.expander("🎯 使用情境", expanded=False): st.markdown(""" 當找到疑似接觸者時,防疫人員需要決定: **「要隔離/檢疫多少天?」** 考量因素: - 接觸者的病毒量 (Ct值) - 是否已接種加強劑 """) with st.expander("✅ Omicron 特性"): st.markdown(""" 相較於 Alpha 變異株: - **傳播更快** 但症狀較輕 - **疫苗保護** 顯著縮短隔離時間 - **Ct > 25** 視為無傳染性 """) with st.expander("💉 疫苗影響"): st.markdown(""" **加強劑的效益:** - 大幅縮短所需隔離天數 - 相同天數下效益更高 - 例: 達到90%效益 - 有加強劑: 5天 - 無加強劑: 11天 """) # 主要內容區 st.markdown("### 📊 隔離檢疫效益 3D 視覺化") # 生成 3D 數據 ct_range = np.arange(10, 25.5, 0.5) day_range = np.arange(1, 22, 1) ct_mesh, day_mesh = np.meshgrid(ct_range, day_range) # 計算兩組效益值 effectiveness_with_booster = np.zeros_like(ct_mesh) effectiveness_without_booster = np.zeros_like(ct_mesh) for i in range(len(day_range)): for j in range(len(ct_range)): effectiveness_with_booster[i, j] = get_quarantine_effectiveness(ct_mesh[i, j], day_mesh[i, j], True) effectiveness_without_booster[i, j] = get_quarantine_effectiveness(ct_mesh[i, j], day_mesh[i, j], False) # 創建兩個並排的圖表 col1, col2 = st.columns(2) with col1: st.markdown("#### 💉 有加強劑情境") fig1 = create_3d_plot( ct_mesh, day_mesh, effectiveness_with_booster, booster_ct, booster_days, booster_eff, True, "隔離檢疫效益 - 有加強劑 (Omicron 變異株)", [ [0.0, 'rgb(239, 68, 68)'], [0.33, 'rgb(245, 158, 11)'], [0.67, 'rgb(16, 185, 129)'], [1.0, 'rgb(59, 130, 246)'] ], 'blue' ) st.plotly_chart(fig1, use_container_width=True) ct_category_booster = get_ct_category(booster_ct) st.info(f"**情境:** Ct值 = **{booster_ct}** ({ct_category_booster}), 隔離 **{booster_days}** 天 | 💉 已接種加強劑") if booster_eff >= 0.9: st.success("💡 **意義:** 隔離時間充足,可有效防止疫情傳播") elif booster_eff >= 0.7: st.warning("💡 **意義:** 隔離效果良好,但建議視情況延長") elif booster_eff >= 0.5: st.warning("💡 **意義:** 隔離效果一般,建議延長隔離時間") else: st.error("💡 **意義:** 隔離效果不足,需要大幅延長隔離時間") with col2: st.markdown("#### ⚠️ 無加強劑情境") fig2 = create_3d_plot( ct_mesh, day_mesh, effectiveness_without_booster, no_booster_ct, no_booster_days, no_booster_eff, False, "隔離檢疫效益 - 無加強劑 (Omicron 變異株)", [ [0.0, 'rgb(239, 68, 68)'], [0.33, 'rgb(245, 158, 11)'], [0.67, 'rgb(16, 185, 129)'], [1.0, 'rgb(59, 130, 246)'] ], 'red' ) st.plotly_chart(fig2, use_container_width=True) ct_category_no_booster = get_ct_category(no_booster_ct) st.info(f"**情境:** Ct值 = **{no_booster_ct}** ({ct_category_no_booster}), 隔離 **{no_booster_days}** 天 | ⚠️ 未接種加強劑") if no_booster_eff >= 0.9: st.success("💡 **意義:** 隔離時間充足,可有效防止疫情傳播") elif no_booster_eff >= 0.7: st.warning("💡 **意義:** 隔離效果良好,但建議視情況延長") elif no_booster_eff >= 0.5: st.warning("💡 **意義:** 隔離效果一般,建議延長隔離時間") else: st.error("💡 **意義:** 隔離效果不足,需要大幅延長隔離時間") # 疫苗建議 improvement = (booster_eff - no_booster_eff) * 100 if improvement > 0: st.info(f"💉 **提示:** 若接種加強劑,在相同參數下可提升 {improvement:.0f}% 效益") # 底部說明區域 st.markdown("---") col_a, col_b = st.columns(2) with col_a: with st.expander("💡 操作說明", expanded=False): st.markdown(""" **3D 圖表說明:** - 🔵 **左側圖表**: 有加強劑的隔離效益 (藍綠色) - 🔴 **右側圖表**: 無加強劑的隔離效益 (橙紅色) - 💎 **菱形標記**: 您當前選擇的情境 - 兩圖對比可清楚看出**疫苗的效益差異** **互動操作:** - 🖱️ 拖曳旋轉視角 - 🔍 滾輪縮放 - 🎚️ 使用左側滑桿調整參數 """) with col_b: with st.expander("📊 查看詳細數據", expanded=False): # 創建對比表格 test_days_list = [3, 5, 7, 10, 14] data = { '隔離天數': test_days_list, '有加強劑 (Ct=' + str(booster_ct) + ')': [f"{get_quarantine_effectiveness(booster_ct, d, True)*100:.0f}%" for d in test_days_list], '無加強劑 (Ct=' + str(no_booster_ct) + ')': [f"{get_quarantine_effectiveness(no_booster_ct, d, False)*100:.0f}%" for d in test_days_list], '效益差異': [f"+{(get_quarantine_effectiveness(booster_ct, d, True) - get_quarantine_effectiveness(no_booster_ct, d, False))*100:.0f}%" for d in test_days_list] } df = pd.DataFrame(data) st.dataframe(df, use_container_width=True)