| |
| """ |
| CKD Markov 預測應用 - Streamlit 完整最終版 |
| ======================================================================= |
| 功能完整版本:改進首頁 + 患者預測 + 智能AI對話 |
| """ |
|
|
| import streamlit as st |
| import pandas as pd |
| import numpy as np |
| import plotly.graph_objects as go |
| import plotly.express as px |
| import os |
| from datetime import datetime |
| import json |
| from openai import OpenAI |
|
|
| |
| import sys |
| sys.path.insert(0, os.path.dirname(__file__)) |
|
|
| from ckd_markov_final_predictor import CKDMarkovPredictor |
|
|
| |
| |
| |
| st.set_page_config( |
| page_title="CKD Markov 風險預測系統", |
| page_icon="🏥", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| st.markdown(""" |
| <style> |
| .metric-card { |
| background-color: #f0f2f6; |
| padding: 20px; |
| border-radius: 10px; |
| margin: 10px 0; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
| if "messages" not in st.session_state: |
| st.session_state.messages = [] |
|
|
| if "current_patient_data" not in st.session_state: |
| st.session_state.current_patient_data = None |
|
|
| if "intervention_results" not in st.session_state: |
| st.session_state.intervention_results = None |
|
|
| if "prediction_results" not in st.session_state: |
| st.session_state.prediction_results = None |
|
|
| if "visit_data" not in st.session_state: |
| st.session_state.visit_data = [] |
|
|
| |
| |
| |
| @st.cache_resource |
| def load_model(): |
| """載入Markov模型""" |
| theta_path = './10cov_theta.npy' |
| q_path = './10cov_Q_params.csv' |
| |
| if os.path.exists(theta_path) and os.path.exists(q_path): |
| return CKDMarkovPredictor(theta_path, q_path) |
| else: |
| st.error("⚠️ 模型文件未找到!") |
| return None |
|
|
| |
| |
| |
| def judge_ckd_state_kdigo(egfr, pro_coded): |
| """ |
| 根據 KDIGO 標準判斷 CKD 狀態 |
| |
| 蛋白尿分類: |
| - A1: 正常 (-) → pro_coded < 1 |
| - A2: 微量 (+/-) → pro_coded == 1 |
| - A3: ≥1+ → pro_coded >= 2 |
| """ |
| |
| |
| if pro_coded < 1: |
| albuminuria = "A1" |
| elif pro_coded == 1: |
| albuminuria = "A2" |
| else: |
| albuminuria = "A3" |
| |
| |
| if egfr >= 90: |
| gfr_grade = "G1" |
| if albuminuria == "A1": |
| state_idx, state_name = 0, "Low" |
| elif albuminuria == "A2": |
| state_idx, state_name = 1, "Moderate" |
| else: |
| state_idx, state_name = 2, "High" |
| elif egfr >= 60: |
| gfr_grade = "G2" |
| if albuminuria == "A1": |
| state_idx, state_name = 0, "Low" |
| elif albuminuria == "A2": |
| state_idx, state_name = 1, "Moderate" |
| else: |
| state_idx, state_name = 2, "High" |
| elif egfr >= 45: |
| gfr_grade = "G3a" |
| if albuminuria == "A1": |
| state_idx, state_name = 1, "Moderate" |
| elif albuminuria == "A2": |
| state_idx, state_name = 2, "High" |
| else: |
| state_idx, state_name = 3, "VeryHigh" |
| elif egfr >= 30: |
| gfr_grade = "G3b" |
| if albuminuria == "A1": |
| state_idx, state_name = 2, "High" |
| else: |
| state_idx, state_name = 3, "VeryHigh" |
| elif egfr >= 15: |
| gfr_grade = "G4" |
| state_idx, state_name = 3, "VeryHigh" |
| else: |
| gfr_grade = "G5" |
| state_idx, state_name = 3, "VeryHigh" |
| |
| return state_idx, state_name, gfr_grade, albuminuria |
|
|
|
|
| st.sidebar.title("🏥 CKD Markov 風險預測") |
| st.sidebar.markdown("---") |
|
|
| page = st.sidebar.radio( |
| "選擇功能:", |
| ["🏠 首頁", "👤 單患者預測", "👥 多患者對比", "📊 患者追蹤", "💬 AI 諮詢", "📋 患者記錄"] |
| ) |
|
|
| st.sidebar.markdown("---") |
|
|
| |
| with st.sidebar.expander("🔑 OpenAI API 設定", expanded=False): |
| st.markdown("### 輸入你的 API Key") |
| |
| api_key_input = st.text_input( |
| "API Key", |
| type="password", |
| value=st.session_state.get("openai_api_key", ""), |
| help="從 https://platform.openai.com/api/keys 取得" |
| ) |
| |
| if api_key_input: |
| st.session_state.openai_api_key = api_key_input |
| st.success("✅ API Key 已設定") |
|
|
| st.sidebar.markdown("---") |
|
|
| |
| |
| |
| if page == "🏠 首頁": |
| st.title("🏥 多階段CKD疾病進展機器學習預測模型") |
| |
| st.markdown(""" |
| ## 🎯 AI 驅動的腎臟病風險評估與臨床決策支持 |
| |
| 基於馬可夫連鎖模型的 CKD 進展預測,整合機器學習和 ChatGPT 智能對話 |
| """) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("✨ 核心功能") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("📊 預測功能", "完整", "7 轉移") |
| with col2: |
| st.metric("🤖 AI 對話", "實時", "個性化") |
| with col3: |
| st.metric("📈 批量處理", "CSV/Excel", "自動") |
| with col4: |
| st.metric("💾 數據記憶", "自動", "智能") |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🔬 KDIGO CKD 分類(eGFR × 蛋白尿)") |
| |
| st.markdown(""" |
| 系統使用 KDIGO 標準將 CKD 分為 **5 個風險等級**: |
| |
| | eGFR 分級 | 正常 (A1) | 微量 (A2) | ≥1+ (A3) | |
| |---------|---------|---------|---------| |
| | G1 (≥90) | **Low** | Moderate | High | |
| | G2 (60-89) | **Low** | Moderate | High | |
| | G3a (45-59) | Moderate | High | **VeryHigh** | |
| | G3b (30-44) | High | **VeryHigh** | VeryHigh | |
| | G4 (15-29) | **VeryHigh** | VeryHigh | VeryHigh | |
| | G5 (<15) | **VeryHigh** | VeryHigh | VeryHigh | |
| |
| > 📌 **關鍵概念:** |
| > - **eGFR**:估計腎臟過濾能力(mL/min/1.73m²) |
| > - **蛋白尿**:A1=正常(-),A2=微量(+/-),A3=≥1+ |
| > - 同一 eGFR 等級下,蛋白尿越多,風險越高 |
| """) |
| |
| st.markdown("---") |
| st.subheader("📋 CKD 5個狀態分類") |
| |
| states_visual = pd.DataFrame({ |
| '狀態': ['Low', 'Moderate', 'High', 'VeryHigh', 'Dialysis'], |
| '定義': ['eGFR≥90', 'G2-3a或有蛋白尿', 'G3b', 'G4-5或有蛋白尿', '已進入透析'], |
| '風險': [10, 25, 50, 75, 95], |
| }) |
| |
| fig_states = go.Figure() |
| |
| fig_states.add_trace(go.Bar( |
| x=states_visual['狀態'], |
| y=states_visual['風險'], |
| marker=dict( |
| color=['green', 'yellow', 'orange', 'red', 'darkred'], |
| line=dict(color='black', width=2) |
| ), |
| text=states_visual['定義'], |
| textposition='outside', |
| showlegend=False |
| )) |
| |
| fig_states.update_layout( |
| title="CKD 狀態與相對風險程度", |
| xaxis_title="CKD 狀態", |
| yaxis_title="相對風險", |
| height=350, |
| template='plotly_white' |
| ) |
| |
| st.plotly_chart(fig_states, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("🔄 CKD 狀態轉移路徑") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown(""" |
| ### 前進轉移(惡化) |
| |
| 🔴 **進展方向:** |
| - Low → Moderate |
| - Moderate → High |
| - High → VeryHigh |
| - VeryHigh → Dialysis |
| |
| 危險因素:年齡、血糖、血壓、尿酸 |
| """) |
| |
| with col2: |
| st.markdown(""" |
| ### 後退轉移(改善) |
| |
| 🟢 **改善方向:** |
| - Moderate → Low |
| - High → Moderate |
| - VeryHigh → High |
| |
| 保護因素:血糖控制、血壓控制、蛋白質限制 |
| """) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🚀 四步快速開始") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.markdown(""" |
| ### ① 輸入數據 |
| 👤 **單患者預測** |
| |
| 輸入: |
| - 年齡、性別 |
| - 血壓、血糖 |
| - 腎功能、蛋白尿 |
| """) |
| |
| with col2: |
| st.markdown(""" |
| ### ② AI 預測 |
| 🔮 **自動計算** |
| |
| 獲得: |
| - 5年洗腎風險 |
| - 10年洗腎風險 |
| - 風險分層 |
| - 轉移速率 |
| """) |
| |
| with col3: |
| st.markdown(""" |
| ### ③ 智能對話 |
| 💬 **AI 諮詢** |
| |
| ChatGPT: |
| - 記住患者數據 |
| - 個性化回答 |
| - 臨床建議 |
| """) |
| |
| with col4: |
| st.markdown(""" |
| ### ④ 決策支持 |
| 📊 **臨床應用** |
| |
| 支持: |
| - 隨訪計劃 |
| - 轉介決策 |
| - 患者教育 |
| """) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🎨 風險分層與臨床決策") |
| |
| risk_data = pd.DataFrame({ |
| '風險等級': ['🟢 低風險', '🟡 中等風險', '🟠 高風險', '🔴 極高風險'], |
| '5年洗腎風險': ['< 5%', '5-15%', '15-30%', '> 30%'], |
| '建議隨訪頻率': ['年 1 次', '半年 1 次', '每季 1 次', '每月 1 次'], |
| '臨床行動': ['常規管理', '強化血壓血糖控制', '準備透析通路評估', '準備透析治療'] |
| }) |
| |
| st.dataframe(risk_data, use_container_width=True, hide_index=True) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🔬 模型特性與驗證") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.success(""" |
| ### 📊 訓練數據規模 |
| |
| ✅ **樣本數**:21,756 轉移段 |
| ✅ **患者數**:5,455 人 |
| ✅ **隨訪期**:平均 3.5 年 |
| ✅ **完整率**:99.2% |
| """) |
| |
| with col2: |
| st.info(""" |
| ### 🧮 模型結構 |
| |
| 📌 **狀態**:5 個 |
| 📌 **轉移**:7 種(前進 4 + 後退 3) |
| 📌 **協變數**:10 個 |
| 📌 **參數**:77 個 |
| """) |
| |
| with col3: |
| st.warning(""" |
| ### ⚙️ 技術細節 |
| |
| 🔧 **方法**:最大似然估計 |
| 🔧 **優化**:L-BFGS-B |
| 🔧 **驗證**:Hessian 反演 |
| 🔧 **變數**:標準化 |
| """) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("📊 10 個預測協變數") |
| |
| covariates_info = pd.DataFrame({ |
| '協變數': ['age_at_screening', 'hi_UA', 'RBC', 'PDH_HP', 'GENDER', 'sbp', 'waist', 'WBC', 'GLUCOSE', 'EDU_high'], |
| '中文名稱': ['年齡', '高尿酸', '紅血球', '高血壓病史', '性別', '收縮壓', '腰圍', '白血球', '血糖', '高教育'], |
| '變數類型': ['連續', '二元', '連續', '二元', '二元', '連續', '連續', '連續', '連續', '二元'], |
| '臨床意義': ['年紀越大風險越高', '尿酸升高增加風險', '貧血增加風險', '高血壓增加風險', '性別差異', '血壓升高增加風險', '肥胖增加風險', '感染風險', '血糖升高增加風險', '教育程度保護'] |
| }) |
| |
| st.dataframe(covariates_info, use_container_width=True, hide_index=True) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🤖 智能 AI 對話功能") |
| |
| st.markdown(""" |
| ### 💡 三個特點 |
| |
| **1️⃣ 自動記憶患者數據** |
| - 你在「單患者預測」輸入的所有信息,系統自動保存 |
| - 無需重複輸入 |
| |
| **2️⃣ 個性化智能回答** |
| - ChatGPT 知道患者的年齡、血糖、洗腎風險等具體數據 |
| - 提供的建議是針對這位患者,不是通用建議 |
| |
| **3️⃣ 持續對話支持** |
| - 保留完整對話記錄 |
| - 支持追問和深入討論 |
| |
| ### 📝 可以問的問題範例 |
| |
| - 「這位患者的 5 年洗腎風險為什麼是 72.5%?」 |
| - 「他/她應該怎樣控制血糖?」 |
| - 「需要準備透析通路嗎?」 |
| - 「下次隨訪應該檢查什麼項目?」 |
| - 「有飲食建議嗎?」 |
| - 「什麼時候應該轉介給腎臟科?」 |
| """) |
| |
| st.info( |
| "**✨ 開始使用:**\n\n" |
| "1. 👉 點左側「👤 單患者預測」\n" |
| "2. 👉 輸入患者信息並點「🔮 預測」\n" |
| "3. 👉 再點「💬 AI 諮詢」開始對話!\n\n" |
| "系統會自動記住這位患者的所有信息 🎉" |
| ) |
|
|
| |
| |
| |
| elif page == "👤 單患者預測": |
| st.title("👤 單患者 CKD 風險預測") |
| |
| predictor = load_model() |
| if predictor is None: |
| st.stop() |
| |
| |
| if "prediction_results" not in st.session_state: |
| st.session_state.prediction_results = None |
| |
| with st.form("patient_form"): |
| st.subheader("📋 患者信息與協變數") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| patient_id = st.text_input("患者ID", value="P001") |
| patient_name = st.text_input("患者姓名", value="") |
| |
| with col2: |
| age = st.slider("年齡", 20, 100, 60) |
| gender = st.radio("性別", ["女 (0)", "男 (1)"], horizontal=True) |
| gender_val = 0 if gender == "女 (0)" else 1 |
| |
| with col3: |
| st.markdown("### 腎功能指標") |
| egfr = st.number_input("eGFR (mL/min/1.73m²)", 5, 120, 60) |
| pro_coded = st.selectbox( |
| "尿蛋白", |
| ["陰性 (0)", "微量 (1)", "1+ (2)", "2+ (3)", "3+ (4)", "4+ (5)"], |
| index=0 |
| ) |
| pro_coded_val = int(pro_coded.split("(")[1].strip(")")) |
| |
| |
| def judge_ckd_state(egfr, pro_coded): |
| """ |
| 根據 KDIGO 標準判斷 CKD 狀態 |
| |
| 蛋白尿分類: |
| - A1: 正常 (-) → pro_coded < 1 |
| - A2: 微量 (+/-) → pro_coded == 1 |
| - A3: ≥1+ → pro_coded >= 2 |
| """ |
| |
| |
| if pro_coded < 1: |
| albuminuria = "A1" |
| elif pro_coded == 1: |
| albuminuria = "A2" |
| else: |
| albuminuria = "A3" |
| |
| |
| if egfr >= 90: |
| gfr_grade = "G1" |
| if albuminuria == "A1": |
| state_idx, state_name = 0, "Low" |
| elif albuminuria == "A2": |
| state_idx, state_name = 1, "Moderate" |
| else: |
| state_idx, state_name = 2, "High" |
| elif egfr >= 60: |
| gfr_grade = "G2" |
| if albuminuria == "A1": |
| state_idx, state_name = 0, "Low" |
| elif albuminuria == "A2": |
| state_idx, state_name = 1, "Moderate" |
| else: |
| state_idx, state_name = 2, "High" |
| elif egfr >= 45: |
| gfr_grade = "G3a" |
| if albuminuria == "A1": |
| state_idx, state_name = 1, "Moderate" |
| elif albuminuria == "A2": |
| state_idx, state_name = 2, "High" |
| else: |
| state_idx, state_name = 3, "VeryHigh" |
| elif egfr >= 30: |
| gfr_grade = "G3b" |
| if albuminuria == "A1": |
| state_idx, state_name = 2, "High" |
| else: |
| state_idx, state_name = 3, "VeryHigh" |
| elif egfr >= 15: |
| gfr_grade = "G4" |
| state_idx, state_name = 3, "VeryHigh" |
| else: |
| gfr_grade = "G5" |
| state_idx, state_name = 3, "VeryHigh" |
| |
| return state_idx, state_name, gfr_grade, albuminuria |
| |
| state_idx, state_name, gfr_grade, albuminuria = judge_ckd_state(egfr, pro_coded_val) |
| st.info(f"✅ **自動判斷狀態:{state_name}**\n\n**KDIGO 分類:** {gfr_grade} {albuminuria}\n**eGFR:** {egfr} mL/min/1.73m²\n**尿蛋白:** {pro_coded.split('(')[0].strip()}") |
| |
| st.markdown("---") |
| st.subheader("🔬 生化檢驗與測量") |
| |
| col1, col2, col3, col4, col5 = st.columns(5) |
| |
| with col1: |
| hi_ua = st.selectbox("高尿酸", ["否 (0)", "是 (1)"], index=0) |
| hi_ua_val = int(hi_ua.split("(")[1].strip(")")) |
| |
| with col2: |
| rbc = st.number_input("紅血球 (RBC)", 2.0, 8.0, 4.5, 0.1) |
| |
| with col3: |
| sbp = st.number_input("收縮壓", 80, 200, 130) |
| |
| with col4: |
| wbc = st.number_input("白血球 (WBC)", 2.0, 15.0, 7.0, 0.1) |
| |
| with col5: |
| glucose = st.number_input("血糖", 50, 300, 110) |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| waist = st.number_input("腰圍", 60, 150, 85) |
| |
| with col2: |
| pdh_hp = st.selectbox("高血壓病史", ["否 (0)", "是 (1)"], index=0) |
| pdh_hp_val = int(pdh_hp.split("(")[1].strip(")")) |
| |
| with col3: |
| edu_high = st.selectbox("高教育程度", ["否 (0)", "是 (1)"], index=0) |
| edu_high_val = int(edu_high.split("(")[1].strip(")")) |
| |
| with col4: |
| st.empty() |
| |
| st.markdown("---") |
| submitted = st.form_submit_button("🔮 預測", use_container_width=True) |
| |
| if submitted: |
| covariates = { |
| 'age_at_screening': age, |
| 'hi_UA': hi_ua_val, |
| 'RBC': rbc, |
| 'PDH_HP': pdh_hp_val, |
| 'GENDER': gender_val, |
| 'sbp': sbp, |
| 'waist': waist, |
| 'WBC': wbc, |
| 'GLUCOSE': glucose, |
| 'EDU_high': edu_high_val, |
| } |
| |
| pred_5y = predictor.predict(covariates, start_state=state_idx, years=5) |
| pred_10y = predictor.predict(covariates, start_state=state_idx, years=10) |
| |
| |
| st.session_state.prediction_results = { |
| 'covariates': covariates, |
| 'pred_5y': pred_5y, |
| 'pred_10y': pred_10y, |
| 'state_idx': state_idx, |
| 'age': age, |
| 'gender_val': gender_val, |
| 'sbp': sbp, |
| 'glucose': glucose, |
| 'waist': waist, |
| 'rbc': rbc, |
| 'wbc': wbc, |
| 'hi_ua_val': hi_ua_val, |
| 'pdh_hp_val': pdh_hp_val, |
| 'edu_high_val': edu_high_val, |
| 'patient_id': patient_id, |
| 'patient_name': patient_name |
| } |
| |
| |
| if st.session_state.prediction_results is not None: |
| results = st.session_state.prediction_results |
| covariates = results['covariates'] |
| pred_5y = results['pred_5y'] |
| pred_10y = results['pred_10y'] |
| state_idx = results['state_idx'] |
| age = results['age'] |
| gender_val = results['gender_val'] |
| sbp = results['sbp'] |
| glucose = results['glucose'] |
| waist = results['waist'] |
| rbc = results['rbc'] |
| wbc = results['wbc'] |
| hi_ua_val = results['hi_ua_val'] |
| pdh_hp_val = results['pdh_hp_val'] |
| edu_high_val = results['edu_high_val'] |
| |
| dial_5y = pred_5y['final_probs']['Dialysis'] * 100 |
| dial_10y = pred_10y['final_probs']['Dialysis'] * 100 |
| |
| if dial_5y < 5: |
| risk_level = "🟢 低風險" |
| elif dial_5y < 15: |
| risk_level = "🟡 中等風險" |
| elif dial_5y < 30: |
| risk_level = "🟠 高風險" |
| else: |
| risk_level = "🔴 極高風險" |
| |
| |
| st.session_state.current_patient_data = { |
| 'patient_id': results.get('patient_id', patient_id), |
| 'patient_name': results.get('patient_name', patient_name), |
| 'age': age, |
| 'gender': gender_val, |
| 'gender_text': "男" if gender_val == 1 else "女", |
| |
| 'egfr': egfr, |
| 'pro_coded': pro_coded_val, |
| 'pro_text': pro_coded.split("(")[0].strip(), |
| |
| 'state': state_name, |
| 'state_idx': state_idx, |
| 'gfr_grade': gfr_grade, |
| 'albuminuria': albuminuria, |
| 'kdigo_class': f"{gfr_grade} {albuminuria}", |
| |
| 'hi_ua': hi_ua_val, |
| 'rbc': rbc, |
| 'pdh_hp': pdh_hp_val, |
| 'pdh_hp_text': "是" if pdh_hp_val == 1 else "否", |
| 'sbp': sbp, |
| 'waist': waist, |
| 'wbc': wbc, |
| 'glucose': glucose, |
| 'edu_high': edu_high_val, |
| 'edu_high_text': "高" if edu_high_val == 1 else "一般", |
| |
| 'dial_5y': dial_5y, |
| 'dial_10y': dial_10y, |
| 'risk_level': risk_level |
| } |
| |
| st.success("✅ 預測完成!") |
| |
| st.subheader("📊 預測結果") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.metric("當前狀態", st.session_state.current_patient_data['state']) |
| with col2: |
| st.metric("洗腎風險 (5年)", f"{dial_5y:.1f}%") |
| with col3: |
| st.metric("洗腎風險 (10年)", f"{dial_10y:.1f}%") |
| with col4: |
| st.metric("風險分層", risk_level) |
| |
| st.markdown("---") |
| |
| st.subheader("📈 轉移概率預測") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| states = ["Low", "Moderate", "High", "VeryHigh", "Dialysis"] |
| probs_5y = [pred_5y['final_probs'][s] * 100 for s in states] |
| |
| fig_5y = go.Figure(data=[ |
| go.Bar(x=states, y=probs_5y, marker_color=['green', 'yellow', 'orange', 'red', 'darkred']) |
| ]) |
| fig_5y.update_layout(title="5年狀態預測概率", xaxis_title="狀態", yaxis_title="概率 (%)", height=400, showlegend=False) |
| fig_5y.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_5y, use_container_width=True) |
| |
| with col2: |
| probs_10y = [pred_10y['final_probs'][s] * 100 for s in states] |
| |
| fig_10y = go.Figure(data=[ |
| go.Bar(x=states, y=probs_10y, marker_color=['green', 'yellow', 'orange', 'red', 'darkred']) |
| ]) |
| fig_10y.update_layout(title="10年狀態預測概率", xaxis_title="狀態", yaxis_title="概率 (%)", height=400, showlegend=False) |
| fig_10y.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_10y, use_container_width=True) |
| |
| st.markdown("---") |
| st.success("✅ 患者數據已保存!現在可以去「💬 AI 諮詢」頁面跟 ChatGPT 討論這位患者!") |
| |
| st.markdown("---") |
| |
| |
| st.subheader("💊 哪些因素會影響洗腎風險?") |
| |
| q_params = predictor.q_params |
| h4_idx = 3 |
| if len(q_params) > h4_idx: |
| h4_params = q_params.iloc[h4_idx] |
| cov_names = ['age_at_screening', 'hi_UA', 'RBC', 'PDH_HP', 'GENDER', 'sbp', 'waist', 'WBC', 'GLUCOSE', 'EDU_high'] |
| cov_labels = ['年齡', '高尿酸', '紅血球', '高血壓病史', '性別', '收縮壓', '腰圍', '白血球', '血糖', '教育程度'] |
| |
| |
| factor_explanations = { |
| '年齡': '年紀越大,腎臟功能衰退風險越高', |
| '高尿酸': '尿酸升高會加重腎臟損傷,增加風險', |
| '紅血球': '貧血(紅血球低)會加重腎臟缺氧,增加風險', |
| '高血壓病史': '長期高血壓直接損傷腎臟,增加風險', |
| '性別': '男性患者風險相對較高', |
| '收縮壓': '血壓越高,對腎臟損傷越大,增加風險', |
| '腰圍': '腹部肥胖增加腎臟負擔,增加風險', |
| '白血球': '白血球低可能代表免疫功能差,增加風險;但過高可能代表感染,也會增加風險', |
| '血糖': '血糖升高會損傷腎小球,增加風險', |
| '教育程度': '教育程度高的患者自我管理能力強,能降低風險' |
| } |
| |
| betas = [h4_params[f'beta_{c}'] for c in cov_names] |
| |
| |
| impact_list = [] |
| for label, beta in zip(cov_labels, betas): |
| if beta > 0: |
| impact = "🔴 增加風險" |
| direction = "升高" |
| else: |
| impact = "🟢 降低風險" |
| direction = "升高" |
| |
| impact_list.append({ |
| '健康指標': label, |
| '影響': impact, |
| '影響程度': abs(beta), |
| '詳細說明': factor_explanations.get(label, ''), |
| '方向': direction |
| }) |
| |
| impact_df = pd.DataFrame(impact_list).sort_values('影響程度', ascending=False) |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown("### 🔴 會增加洗腎風險的因素") |
| increase = impact_df[impact_df['影響'] == "🔴 增加風險"][['健康指標', '詳細說明', '影響程度']].head(5) |
| for idx, row in increase.iterrows(): |
| st.markdown(f""" |
| **{row['健康指標']}** (影響程度:{row['影響程度']:.2f}) |
| - {row['詳細說明']} |
| """) |
| |
| with col2: |
| st.markdown("### 🟢 會降低洗腎風險的因素") |
| decrease = impact_df[impact_df['影響'] == "🟢 降低風險"][['健康指標', '詳細說明', '影響程度']].head(5) |
| for idx, row in decrease.iterrows(): |
| st.markdown(f""" |
| **{row['健康指標']}** (保護程度:{row['影響程度']:.2f}) |
| - {row['詳細說明']} |
| """) |
| |
| |
| fig_beta = px.bar( |
| impact_df.head(10), |
| x='影響程度', |
| y='健康指標', |
| color='影響', |
| color_discrete_map={'🔴 增加風險': '#ff6b6b', '🟢 降低風險': '#51cf66'}, |
| title='各項健康指標對洗腎風險的影響程度', |
| height=400, |
| orientation='h', |
| labels={'健康指標': '', '影響程度': '影響強度'}, |
| hover_data=['詳細說明'] |
| ) |
| fig_beta.update_layout(showlegend=False) |
| st.plotly_chart(fig_beta, use_container_width=True) |
| |
| st.info( |
| "💡 **怎麼理解這個圖?**\n\n" |
| "• 🔴 **紅色柱子** = 這個因素會增加洗腎風險(數值越高越危險)\n" |
| "• 🟢 **綠色柱子** = 這個因素會降低洗腎風險(需要維持在良好狀態)\n" |
| "• **柱子越長** = 影響越大\n\n" |
| "例如:\n" |
| "- 白血球低 → 增加風險(需要提升白血球)\n" |
| "- 紅血球低 → 增加風險(需要治療貧血)\n" |
| "- 年齡增加 → 增加風險(無法改變,但可以強化其他管理)" |
| ) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("📉 時間序列預測曲線") |
| |
| times_5y = pred_5y['times'] |
| probs_traj = pred_5y['probs'] |
| |
| fig_traj = go.Figure() |
| colors = ['green', 'yellow', 'orange', 'red', 'darkred'] |
| |
| for i, state in enumerate(states): |
| fig_traj.add_trace(go.Scatter( |
| x=times_5y, |
| y=probs_traj[:, i] * 100, |
| mode='lines', |
| name=state, |
| line=dict(color=colors[i], width=2) |
| )) |
| |
| fig_traj.update_layout( |
| title="5年轉移概率時間序列", |
| xaxis_title="時間 (年)", |
| yaxis_title="概率 (%)", |
| hovermode='x unified', |
| height=450 |
| ) |
| st.plotly_chart(fig_traj, use_container_width=True) |
| |
| st.markdown("---") |
| |
| |
| with st.expander("⚙️ 技術詳情 - 轉移速率", expanded=False): |
| st.markdown("_僅供研究人員參考_") |
| |
| Q = pred_5y['Q'] |
| hazard_names = [ |
| 'h1: Low→Moderate', |
| 'h2: Moderate→High', |
| 'h3: High→VeryHigh', |
| 'h4: VeryHigh→Dialysis', |
| 'b1: Moderate→Low', |
| 'b2: High→Moderate', |
| 'b3: VeryHigh→High' |
| ] |
| hazard_info = [] |
| |
| hazards = [(0,1), (1,2), (2,3), (3,4), (1,0), (2,1), (3,2)] |
| for i, (from_s, to_s) in enumerate(hazards): |
| rate = Q[from_s, to_s] |
| hazard_info.append({ |
| 'Hazard': hazard_names[i], |
| '速率 (/年)': f"{rate:.6f}", |
| '倒數 (年)': f"{1/rate:.2f}" if rate > 0 else "∞" |
| }) |
| |
| hazard_df = pd.DataFrame(hazard_info) |
| st.dataframe(hazard_df, use_container_width=True, hide_index=True) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("💊 臨床決策建議") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| if dial_5y < 5: |
| st.info("✅ **低風險** - 常規隨訪\n\n每年檢查一次,注意生活方式改善") |
| elif dial_5y < 15: |
| st.warning("🟡 **中等風險** - 密集隨訪\n\n建議半年檢查一次,強化營養諮詢和血壓控制") |
| elif dial_5y < 30: |
| st.error("🟠 **高風險** - 每季隨訪\n\n需要密集監測,準備洗腎通路評估") |
| else: |
| st.error("🔴 **極高風險** - 每月隨訪\n\n立即準備透析,考慮先制性介入") |
| |
| with col2: |
| st.info( |
| "📋 **建議檢驗項目:**\n\n" |
| "• 血清肌酐 + eGFR\n" |
| "• 尿蛋白/肌酐比\n" |
| "• 電解質 (Na, K, Ca, P)\n" |
| "• 血紅素 + 鐵代謝\n" |
| "• 血糖 + 糖化血紅素" |
| ) |
| |
| st.markdown("---") |
| |
| |
| st.subheader("🎯 介入效果模擬") |
| |
| st.markdown(""" |
| **自訂參數調整,模擬介入效果** |
| |
| 調整下方參數,系統會自動重新預測洗腎風險,看看介入後會改善多少! |
| """) |
| |
| |
| intervention_covariates = covariates.copy() |
| |
| |
| st.markdown("#### 📊 可介入參數調整") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| |
| with col1: |
| st.markdown("**血管與代謝**") |
| sbp_adjusted = st.number_input( |
| "收縮壓 (mmHg)", |
| min_value=80.0, |
| max_value=200.0, |
| value=float(sbp), |
| step=1.0, |
| key="sbp_int", |
| help="💊 可通過藥物或生活方式改變" |
| ) |
| intervention_covariates['sbp'] = sbp_adjusted |
| |
| glucose_adjusted = st.number_input( |
| "血糖 (mg/dL)", |
| min_value=50.0, |
| max_value=300.0, |
| value=float(glucose), |
| step=1.0, |
| key="glucose_int", |
| help="💊 可通過藥物或飲食改變" |
| ) |
| intervention_covariates['GLUCOSE'] = glucose_adjusted |
| |
| with col2: |
| st.markdown("**身體組成**") |
| waist_adjusted = st.number_input( |
| "腰圍 (cm)", |
| min_value=60.0, |
| max_value=150.0, |
| value=float(waist), |
| step=0.5, |
| key="waist_int", |
| help="💪 可通過減重改變" |
| ) |
| intervention_covariates['waist'] = waist_adjusted |
| |
| rbc_adjusted = st.number_input( |
| "紅血球 (RBC)", |
| min_value=2.0, |
| max_value=8.0, |
| value=float(rbc), |
| step=0.1, |
| key="rbc_int", |
| help="💊 可通過治療貧血改變" |
| ) |
| intervention_covariates['RBC'] = rbc_adjusted |
| |
| with col3: |
| st.markdown("**血球與代謝**") |
| wbc_adjusted = st.number_input( |
| "白血球 (WBC)", |
| min_value=2.0, |
| max_value=15.0, |
| value=float(wbc), |
| step=0.1, |
| key="wbc_int", |
| help="💊 可通過感染控制改變" |
| ) |
| intervention_covariates['WBC'] = wbc_adjusted |
| |
| hi_ua_adjusted = st.selectbox( |
| "高尿酸", |
| ["否 (0)", "是 (1)"], |
| index=hi_ua_val, |
| key="hi_ua_int", |
| help="💊 可通過藥物改變" |
| ) |
| intervention_covariates['hi_UA'] = int(hi_ua_adjusted.split("(")[1].strip(")")) |
| |
| |
| st.markdown("---") |
| st.markdown("#### 🫘 腎功能指標調整(介入目標)") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| egfr_adjusted = st.number_input( |
| "eGFR (mL/min/1.73m²)", |
| min_value=5.0, |
| max_value=120.0, |
| value=float(egfr), |
| step=1.0, |
| key="egfr_int", |
| help="🎯 介入目標:改善腎功能。藥物(如 ACEi/ARB)、控制血壓血糖可能改善。" |
| ) |
| |
| with col2: |
| pro_adjusted = st.selectbox( |
| "尿蛋白", |
| ["陰性 (0)", "微量 (1)", "1+ (2)", "2+ (3)", "3+ (4)", "4+ (5)"], |
| index=min(pro_coded_val, 5), |
| key="pro_int", |
| help="🎯 介入目標:減少尿蛋白。血壓控制、ACEi/ARB、減重是關鍵。" |
| ) |
| pro_adjusted_val = int(pro_adjusted.split("(")[1].strip(")")) |
| |
| |
| new_state_idx, new_state_name, new_gfr_grade, new_albuminuria = judge_ckd_state_kdigo(egfr_adjusted, pro_adjusted_val) |
| |
| |
| st.markdown("---") |
| st.markdown("#### 📊 CKD 狀態變化預測") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.info(f""" |
| **當前狀態** |
| {state_name} |
| |
| **{gfr_grade} {albuminuria}** |
| """) |
| |
| with col2: |
| st.markdown("**→**") |
| st.write("") |
| |
| with col3: |
| if new_state_idx != state_idx: |
| st.success(f""" |
| **介入後狀態** |
| {new_state_name} |
| |
| **{new_gfr_grade} {new_albuminuria}** |
| """) |
| else: |
| st.info(f""" |
| **介入後狀態** |
| {new_state_name} |
| |
| **{new_gfr_grade} {new_albuminuria}** |
| """) |
| |
| with col4: |
| if new_state_idx < state_idx: |
| st.success(f"✅ 進展降級\n{state_name} → {new_state_name}") |
| elif new_state_idx > state_idx: |
| st.error(f"⚠️ 進展升級\n{state_name} → {new_state_name}") |
| else: |
| st.info(f"➡️ 狀態不變\n{state_name}") |
| |
| |
| st.markdown("---") |
| st.markdown("#### 📋 患者基本信息(無法改變)") |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("年齡", f"{age:.0f} 歲") |
| intervention_covariates['age_at_screening'] = age |
| with col2: |
| st.metric("性別", "男" if gender_val == 1 else "女") |
| intervention_covariates['GENDER'] = gender_val |
| with col3: |
| st.metric("高血壓病史", "是" if pdh_hp_val == 1 else "否") |
| intervention_covariates['PDH_HP'] = pdh_hp_val |
| with col4: |
| st.metric("教育程度", "高" if edu_high_val == 1 else "一般") |
| intervention_covariates['EDU_high'] = edu_high_val |
| |
| if st.button("🎯 模擬介入效果", use_container_width=True, key="simulate_btn"): |
| |
| pred_5y_int = predictor.predict(intervention_covariates, start_state=new_state_idx, years=5) |
| pred_10y_int = predictor.predict(intervention_covariates, start_state=new_state_idx, years=10) |
| |
| dial_5y_int = pred_5y_int['final_probs']['Dialysis'] * 100 |
| dial_10y_int = pred_10y_int['final_probs']['Dialysis'] * 100 |
| |
| |
| changes = {} |
| change_descriptions = [] |
| |
| |
| if egfr_adjusted != egfr: |
| change = egfr_adjusted - egfr |
| changes['eGFR'] = { |
| '原始': egfr, |
| '介入後': egfr_adjusted, |
| '變化': change |
| } |
| direction = "改善" if change > 0 else "惡化" |
| change_descriptions.append(f"🫘 eGFR {direction} {abs(change):.0f} (從 {egfr:.0f} 到 {egfr_adjusted:.0f} mL/min/1.73m²)") |
| |
| if pro_adjusted_val != pro_coded_val: |
| changes['尿蛋白'] = { |
| '原始': pro_coded_val, |
| '介入後': pro_adjusted_val, |
| '變化': pro_adjusted_val - pro_coded_val |
| } |
| direction = "減少" if pro_adjusted_val < pro_coded_val else "增加" |
| change_descriptions.append(f"🫘 尿蛋白{direction}: {pro_coded.split('(')[0].strip()} → {pro_adjusted.split('(')[0].strip()}") |
| |
| if new_state_idx != state_idx: |
| change_descriptions.append(f"🔄 CKD 狀態進展: {state_name} → {new_state_name}") |
| |
| if intervention_covariates['sbp'] != covariates['sbp']: |
| change = intervention_covariates['sbp'] - covariates['sbp'] |
| changes['收縮壓'] = { |
| '原始': covariates['sbp'], |
| '介入後': intervention_covariates['sbp'], |
| '變化': change |
| } |
| change_descriptions.append(f"降低收縮壓 {abs(change):.0f} mmHg (從 {covariates['sbp']:.0f} 到 {intervention_covariates['sbp']:.0f})") |
| |
| if intervention_covariates['GLUCOSE'] != covariates['GLUCOSE']: |
| change = intervention_covariates['GLUCOSE'] - covariates['GLUCOSE'] |
| changes['血糖'] = { |
| '原始': covariates['GLUCOSE'], |
| '介入後': intervention_covariates['GLUCOSE'], |
| '變化': change |
| } |
| change_descriptions.append(f"降低血糖 {abs(change):.0f} mg/dL (從 {covariates['GLUCOSE']:.0f} 到 {intervention_covariates['GLUCOSE']:.0f})") |
| |
| if intervention_covariates['waist'] != covariates['waist']: |
| change = intervention_covariates['waist'] - covariates['waist'] |
| changes['腰圍'] = { |
| '原始': covariates['waist'], |
| '介入後': intervention_covariates['waist'], |
| '變化': change |
| } |
| change_descriptions.append(f"減少腰圍 {abs(change):.1f} cm (從 {covariates['waist']:.0f} 到 {intervention_covariates['waist']:.0f})") |
| |
| if intervention_covariates['RBC'] != covariates['RBC']: |
| change = intervention_covariates['RBC'] - covariates['RBC'] |
| changes['紅血球'] = { |
| '原始': covariates['RBC'], |
| '介入後': intervention_covariates['RBC'], |
| '變化': change |
| } |
| change_descriptions.append(f"提升紅血球 {abs(change):.1f} (從 {covariates['RBC']:.1f} 到 {intervention_covariates['RBC']:.1f})") |
| |
| if intervention_covariates['WBC'] != covariates['WBC']: |
| change = intervention_covariates['WBC'] - covariates['WBC'] |
| changes['白血球'] = { |
| '原始': covariates['WBC'], |
| '介入後': intervention_covariates['WBC'], |
| '變化': change |
| } |
| change_descriptions.append(f"調整白血球 {abs(change):.1f} (從 {covariates['WBC']:.1f} 到 {intervention_covariates['WBC']:.1f})") |
| |
| if intervention_covariates['hi_UA'] != covariates['hi_UA']: |
| changes['高尿酸'] = { |
| '原始': '是' if covariates['hi_UA'] == 1 else '否', |
| '介入後': '是' if intervention_covariates['hi_UA'] == 1 else '否', |
| } |
| change_descriptions.append(f"高尿酸狀態變為 {'是' if intervention_covariates['hi_UA'] == 1 else '否'}") |
| |
| |
| st.session_state.intervention_results = { |
| 'changes': changes, |
| 'change_descriptions': change_descriptions, |
| 'original_dial_5y': dial_5y, |
| 'original_dial_10y': dial_10y, |
| 'intervention_dial_5y': dial_5y_int, |
| 'intervention_dial_10y': dial_10y_int, |
| 'improvement_5y': dial_5y - dial_5y_int, |
| 'improvement_10y': dial_10y - dial_10y_int, |
| 'improvement_5y_percent': ((dial_5y - dial_5y_int)/dial_5y*100) if dial_5y > 0 else 0, |
| 'improvement_10y_percent': ((dial_10y - dial_10y_int)/dial_10y*100) if dial_10y > 0 else 0, |
| } |
| |
| st.markdown("---") |
| st.subheader("📊 介入效果對比") |
| |
| |
| comparison_data = pd.DataFrame({ |
| '指標': ['5年洗腎風險', '10年洗腎風險'], |
| '原始風險': [f"{dial_5y:.1f}%", f"{dial_10y:.1f}%"], |
| '介入後': [f"{dial_5y_int:.1f}%", f"{dial_10y_int:.1f}%"], |
| '改善幅度': [f"{dial_5y - dial_5y_int:.1f}%", f"{dial_10y - dial_10y_int:.1f}%"], |
| '改善百分比': [f"{((dial_5y - dial_5y_int)/dial_5y*100):.1f}%" if dial_5y > 0 else "N/A", |
| f"{((dial_10y - dial_10y_int)/dial_10y*100):.1f}%" if dial_10y > 0 else "N/A"] |
| }) |
| |
| st.dataframe(comparison_data, use_container_width=True, hide_index=True) |
| |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| fig_comp_5y = go.Figure(data=[ |
| go.Bar(name='原始風險', x=['5年洗腎風險'], y=[dial_5y], marker_color='#ff6b6b'), |
| go.Bar(name='介入後', x=['5年洗腎風險'], y=[dial_5y_int], marker_color='#51cf66') |
| ]) |
| fig_comp_5y.update_layout( |
| title="5年洗腎風險對比", |
| yaxis_title="風險 (%)", |
| barmode='group', |
| height=400 |
| ) |
| st.plotly_chart(fig_comp_5y, use_container_width=True) |
| |
| with col2: |
| fig_comp_10y = go.Figure(data=[ |
| go.Bar(name='原始風險', x=['10年洗腎風險'], y=[dial_10y], marker_color='#ff6b6b'), |
| go.Bar(name='介入後', x=['10年洗腎風險'], y=[dial_10y_int], marker_color='#51cf66') |
| ]) |
| fig_comp_10y.update_layout( |
| title="10年洗腎風險對比", |
| yaxis_title="風險 (%)", |
| barmode='group', |
| height=400 |
| ) |
| st.plotly_chart(fig_comp_10y, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("💡 介入評估") |
| |
| improvement_5y = dial_5y - dial_5y_int |
| improvement_10y = dial_10y - dial_10y_int |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| if improvement_5y > 10: |
| st.success(f"✅ **顯著改善** - 5年風險下降 {improvement_5y:.1f}%") |
| elif improvement_5y > 5: |
| st.info(f"🟢 **中度改善** - 5年風險下降 {improvement_5y:.1f}%") |
| elif improvement_5y > 0: |
| st.info(f"🟡 **輕微改善** - 5年風險下降 {improvement_5y:.1f}%") |
| else: |
| st.warning(f"⚠️ **未見改善** - 5年風險無變化或升高") |
| |
| with col2: |
| if improvement_10y > 10: |
| st.success(f"✅ **顯著改善** - 10年風險下降 {improvement_10y:.1f}%") |
| elif improvement_10y > 5: |
| st.info(f"🟢 **中度改善** - 10年風險下降 {improvement_10y:.1f}%") |
| elif improvement_10y > 0: |
| st.info(f"🟡 **輕微改善** - 10年風險下降 {improvement_10y:.1f}%") |
| else: |
| st.warning(f"⚠️ **未見改善** - 10年風險無變化或升高") |
| |
| st.info(""" |
| **💡 解讀提示:** |
| |
| - 改善幅度 > 10% = 這個介入策略效果很好 |
| - 改善幅度 5-10% = 這個介入策略有效果 |
| - 改善幅度 < 5% = 這個參數對風險影響較小 |
| |
| 可以試試調整不同參數,找出最有效的介入組合! |
| """) |
|
|
| |
| |
| |
| elif page == "👥 多患者對比": |
| st.title("👥 多患者對比分析") |
| |
| predictor = load_model() |
| if predictor is None: |
| st.stop() |
| |
| st.info("👇 輸入多個患者進行對比。最多 5 個患者。") |
| |
| patient_count = st.number_input("患者個數", 2, 5, 2) |
| patients_data = [] |
| |
| with st.form("compare_form"): |
| for i in range(patient_count): |
| st.subheader(f"患者 {i+1}") |
| |
| col1, col2, col3, col4, col5 = st.columns(5) |
| with col1: |
| p_id = st.text_input(f"ID", f"P{i+1:03d}", key=f"p_id_{i}") |
| with col2: |
| p_age = st.number_input(f"年齡", 20, 100, 60, key=f"p_age_{i}") |
| with col3: |
| p_gender = st.selectbox(f"性別", ["女", "男"], key=f"p_gender_{i}") |
| p_gender_val = 0 if p_gender == "女" else 1 |
| with col4: |
| p_state = st.selectbox(f"狀態", ["Low", "Moderate", "High", "VeryHigh"], key=f"p_state_{i}") |
| p_state_idx = ["Low", "Moderate", "High", "VeryHigh"].index(p_state) |
| with col5: |
| p_hi_ua = st.selectbox(f"高尿酸", ["否", "是"], key=f"p_hi_ua_{i}") |
| p_hi_ua_val = 0 if p_hi_ua == "否" else 1 |
| |
| col1, col2, col3, col4, col5 = st.columns(5) |
| with col1: |
| p_rbc = st.number_input(f"RBC", 2.0, 8.0, 4.5, 0.1, key=f"p_rbc_{i}") |
| with col2: |
| p_pdh = st.selectbox(f"高血壓", ["否", "是"], key=f"p_pdh_{i}") |
| p_pdh_val = 0 if p_pdh == "否" else 1 |
| with col3: |
| p_sbp = st.number_input(f"收縮壓", 80, 200, 130, key=f"p_sbp_{i}") |
| with col4: |
| p_waist = st.number_input(f"腰圍", 60, 150, 85, key=f"p_waist_{i}") |
| with col5: |
| p_wbc = st.number_input(f"WBC", 2.0, 15.0, 7.0, 0.1, key=f"p_wbc_{i}") |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| p_glucose = st.number_input(f"血糖", 50, 300, 110, key=f"p_glucose_{i}") |
| with col2: |
| p_edu = st.selectbox(f"高教育", ["否", "是"], key=f"p_edu_{i}") |
| p_edu_val = 0 if p_edu == "否" else 1 |
| |
| patients_data.append({ |
| 'id': p_id, |
| 'covariates': { |
| 'age_at_screening': p_age, |
| 'hi_UA': p_hi_ua_val, |
| 'RBC': p_rbc, |
| 'PDH_HP': p_pdh_val, |
| 'GENDER': p_gender_val, |
| 'sbp': p_sbp, |
| 'waist': p_waist, |
| 'WBC': p_wbc, |
| 'GLUCOSE': p_glucose, |
| 'EDU_high': p_edu_val, |
| }, |
| 'state_idx': p_state_idx |
| }) |
| st.markdown("---") |
| |
| submitted = st.form_submit_button("🔄 對比分析", use_container_width=True) |
| |
| if submitted: |
| results = [] |
| for p_data in patients_data: |
| pred_5y = predictor.predict(p_data['covariates'], start_state=p_data['state_idx'], years=5) |
| dial_5y = pred_5y['final_probs']['Dialysis'] * 100 |
| dial_10y = predictor.predict(p_data['covariates'], start_state=p_data['state_idx'], years=10)['final_probs']['Dialysis'] * 100 |
| |
| results.append({ |
| '患者ID': p_data['id'], |
| '5年洗腎風險': f"{dial_5y:.1f}%", |
| '10年洗腎風險': f"{dial_10y:.1f}%", |
| '風險排序': dial_5y |
| }) |
| |
| results_df = pd.DataFrame(results).sort_values('風險排序', ascending=False) |
| results_df['排名'] = range(1, len(results_df) + 1) |
| |
| st.subheader("📊 對比結果") |
| st.dataframe(results_df[['排名', '患者ID', '5年洗腎風險', '10年洗腎風險']], use_container_width=True, hide_index=True) |
| |
| fig_compare = px.bar( |
| results_df.sort_values('風險排序'), |
| x='患者ID', |
| y='風險排序', |
| title='患者洗腎風險對比 (5年)', |
| labels={'風險排序': '洗腎風險 (%)'}, |
| color='風險排序', |
| color_continuous_scale='Reds' |
| ) |
| st.plotly_chart(fig_compare, use_container_width=True) |
|
|
| |
| |
| |
| elif page == "📊 患者追蹤": |
| st.title("📊 患者縱向追蹤") |
|
|
| |
| if "tracking_results" not in st.session_state: |
| st.session_state.tracking_results = None |
| |
| predictor = load_model() |
| if predictor is None: |
| st.stop() |
| |
| st.info("📋 追蹤同一患者的多次檢驗,查看風險變化趨勢") |
| |
| |
| st.subheader("👤 患者基本信息") |
| col1, col2 = st.columns(2) |
| with col1: |
| patient_id = st.text_input("患者ID", value="P001") |
| with col2: |
| patient_name = st.text_input("患者姓名", value="") |
| |
| |
| st.markdown("---") |
| st.subheader("📋 患者固定信息(不變)") |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| age = st.number_input("年齡", 20, 100, 60) |
| with col2: |
| gender = st.radio("性別", ["女 (0)", "男 (1)"], horizontal=True) |
| gender_val = 0 if gender == "女 (0)" else 1 |
| with col3: |
| pdh_hp = st.selectbox("高血壓病史", ["否 (0)", "是 (1)"], index=0) |
| pdh_hp_val = int(pdh_hp.split("(")[1].strip(")")) |
| with col4: |
| edu_high = st.selectbox("高教育程度", ["否 (0)", "是 (1)"], index=0) |
| edu_high_val = int(edu_high.split("(")[1].strip(")")) |
| |
| st.markdown("---") |
| st.subheader("📊 多次檢驗數據") |
| |
| st.markdown("**輸入患者的每次檢驗數據(可輸入多次):**") |
| |
| |
| if "visit_data" not in st.session_state: |
| st.session_state.visit_data = [] |
| |
| st.subheader("📊 檢驗數據輸入") |
| |
| |
| col1, col2, col3, col4, col5 = st.columns(5) |
| |
| with col1: |
| visit_date = st.date_input("檢驗日期", key="visit_date_input") |
| with col2: |
| visit_egfr = st.number_input("eGFR (mL/min/1.73m²)", 5, 120, 60, key="visit_egfr") |
| with col3: |
| visit_pro_coded = st.selectbox( |
| "尿蛋白", |
| ["陰性 (0)", "微量 (1)", "1+ (2)", "2+ (3)", "3+ (4)", "4+ (5)"], |
| index=0, |
| key="visit_pro" |
| ) |
| visit_pro_val = int(visit_pro_coded.split("(")[1].strip(")")) |
| |
| with col4: |
| st.empty() |
| |
| with col5: |
| st.empty() |
| |
| st.markdown("---") |
| st.subheader("🩺 代謝指標") |
| |
| |
| col1, col2, col3, col4, col5, col6, col7 = st.columns(7) |
| |
| with col1: |
| visit_sbp = st.number_input("收縮壓 (mmHg)", 80, 200, 130, key="visit_sbp") |
| with col2: |
| visit_glucose = st.number_input("血糖 (mg/dL)", 50, 300, 110, key="visit_glucose") |
| with col3: |
| visit_waist = st.number_input("腰圍 (cm)", 60, 150, 85, key="visit_waist") |
| with col4: |
| visit_rbc = st.number_input("紅血球 (RBC)", 2.0, 8.0, 4.5, 0.1, key="visit_rbc") |
| with col5: |
| visit_wbc = st.number_input("白血球 (WBC)", 2.0, 15.0, 7.0, 0.1, key="visit_wbc") |
| with col6: |
| visit_hi_ua = st.selectbox("高尿酸", ["否", "是"], index=0, key="visit_hi_ua") |
| visit_hi_ua_val = 0 if visit_hi_ua == "否" else 1 |
| with col7: |
| st.empty() |
| |
| |
| state_idx, state_name, gfr_grade, albuminuria = judge_ckd_state_kdigo(visit_egfr, visit_pro_val) |
| |
| |
| st.markdown("---") |
| st.markdown("### 📋 當前 CKD 狀態判斷(KDIGO)") |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("eGFR", f"{visit_egfr:.0f}", "mL/min/1.73m²") |
| with col2: |
| st.metric("尿蛋白", visit_pro_coded.split("(")[0].strip()) |
| with col3: |
| st.info(f""" |
| **GFR 等級** |
| {gfr_grade} |
| |
| **蛋白尿等級** |
| {albuminuria} |
| """) |
| with col4: |
| st.success(f""" |
| **判斷狀態** |
| {state_name} |
| |
| **KDIGO:{gfr_grade} {albuminuria}** |
| """) |
| |
| st.markdown("---") |
| |
| col1, col2 = st.columns([1, 5]) |
| with col1: |
| if st.button("➕ 新增檢驗", use_container_width=True): |
| st.session_state.visit_data.append({ |
| 'date': visit_date, |
| 'date_str': visit_date.strftime("%Y-%m-%d"), |
| 'egfr': visit_egfr, |
| 'pro_coded': visit_pro_val, |
| 'pro_text': visit_pro_coded.split("(")[0].strip(), |
| 'state_idx': state_idx, |
| 'state_name': state_name, |
| 'gfr_grade': gfr_grade, |
| 'albuminuria': albuminuria, |
| 'kdigo_class': f"{gfr_grade} {albuminuria}", |
| 'sbp': visit_sbp, |
| 'glucose': visit_glucose, |
| 'waist': visit_waist, |
| 'rbc': visit_rbc, |
| 'wbc': visit_wbc, |
| 'hi_ua': visit_hi_ua_val, |
| '5年風險': 0.0, |
| '10年風險': 0.0 |
| }) |
| st.success(f"✅ 已新增檢驗數據!({gfr_grade} {albuminuria} - {state_name})") |
| |
| with col2: |
| if st.button("🗑️ 清除所有數據", use_container_width=True): |
| st.session_state.visit_data = [] |
| st.info("已清除所有檢驗數據") |
| |
| |
| if st.session_state.visit_data: |
| st.markdown("---") |
| st.subheader("✅ 已輸入的檢驗數據") |
| |
| visits_df = pd.DataFrame([ |
| { |
| '檢驗日期': v['date_str'], |
| 'eGFR': f"{v['egfr']:.0f}", |
| '尿蛋白': v['pro_text'], |
| 'KDIGO': v['kdigo_class'], |
| 'CKD狀態': v['state_name'], |
| '收縮壓': f"{v['sbp']:.0f}", |
| '血糖': f"{v['glucose']:.0f}", |
| '腰圍': f"{v['waist']:.0f}", |
| '高尿酸': "是" if v['hi_ua'] == 1 else "否" |
| } |
| for v in sorted(st.session_state.visit_data, key=lambda x: x['date']) |
| ]) |
| st.dataframe(visits_df, use_container_width=True, hide_index=True) |
| |
| |
| st.markdown("---") |
| if st.button("📊 進行追蹤預測", use_container_width=True, key="predict_tracking"): |
| st.subheader("📈 患者風險變化趨勢") |
| |
| |
| tracking_results = [] |
| states = ["Low", "Moderate", "High", "VeryHigh", "Dialysis"] |
| |
| for visit in sorted(st.session_state.visit_data, key=lambda x: x['date']): |
| covariates = { |
| 'age_at_screening': age, |
| 'hi_UA': visit['hi_ua'], |
| 'RBC': visit['rbc'], |
| 'PDH_HP': pdh_hp_val, |
| 'GENDER': gender_val, |
| 'sbp': visit['sbp'], |
| 'waist': visit['waist'], |
| 'WBC': visit['wbc'], |
| 'GLUCOSE': visit['glucose'], |
| 'EDU_high': edu_high_val, |
| } |
| |
| |
| state_idx = visit['state_idx'] |
| state_name = visit['state_name'] |
| |
| pred_5y = predictor.predict(covariates, start_state=state_idx, years=5) |
| pred_10y = predictor.predict(covariates, start_state=state_idx, years=10) |
| |
| dial_5y = pred_5y['final_probs']['Dialysis'] * 100 |
| dial_10y = pred_10y['final_probs']['Dialysis'] * 100 |
| |
| tracking_results.append({ |
| '日期': visit['date'], |
| '日期_str': visit['date'].strftime("%Y-%m-%d"), |
| |
| 'eGFR': visit['egfr'], |
| '尿蛋白': visit['pro_text'], |
| 'KDIGO': visit['kdigo_class'], |
| 'CKD狀態': visit['state_name'], |
| |
| '5年風險': dial_5y, |
| '10年風險': dial_10y, |
| |
| '收縮壓': visit['sbp'], |
| '血糖': visit['glucose'], |
| '腰圍': visit['waist'] |
| }) |
| |
| |
| results_df = pd.DataFrame([ |
| { |
| '檢驗日期': r['日期_str'], |
| '5年洗腎風險': f"{r['5年風險']:.1f}%", |
| '10年洗腎風險': f"{r['10年風險']:.1f}%", |
| '收縮壓': f"{r['收縮壓']:.0f}", |
| '血糖': f"{r['血糖']:.0f}", |
| '腰圍': f"{r['腰圍']:.0f}" |
| } |
| for r in tracking_results |
| ]) |
| |
| st.dataframe(results_df, use_container_width=True, hide_index=True) |
| |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| fig_5y = go.Figure() |
| fig_5y.add_trace(go.Scatter( |
| x=[r['日期_str'] for r in tracking_results], |
| y=[r['5年風險'] for r in tracking_results], |
| mode='lines+markers', |
| name='5年洗腎風險', |
| line=dict(color='#ff6b6b', width=3), |
| marker=dict(size=10) |
| )) |
| fig_5y.update_layout( |
| title="5年洗腎風險變化趨勢", |
| xaxis_title="檢驗日期", |
| yaxis_title="風險 (%)", |
| height=400, |
| hovermode='x unified' |
| ) |
| fig_5y.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_5y, use_container_width=True) |
| |
| with col2: |
| fig_10y = go.Figure() |
| fig_10y.add_trace(go.Scatter( |
| x=[r['日期_str'] for r in tracking_results], |
| y=[r['10年風險'] for r in tracking_results], |
| mode='lines+markers', |
| name='10年洗腎風險', |
| line=dict(color='#ff8c00', width=3), |
| marker=dict(size=10) |
| )) |
| fig_10y.update_layout( |
| title="10年洗腎風險變化趨勢", |
| xaxis_title="檢驗日期", |
| yaxis_title="風險 (%)", |
| height=400, |
| hovermode='x unified' |
| ) |
| fig_10y.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_10y, use_container_width=True) |
| |
| |
| st.markdown("---") |
| st.subheader("📊 健康指標變化趨勢") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| fig_sbp = go.Figure() |
| fig_sbp.add_trace(go.Scatter( |
| x=[r['日期_str'] for r in tracking_results], |
| y=[r['收縮壓'] for r in tracking_results], |
| mode='lines+markers', |
| name='收縮壓', |
| line=dict(color='#1f77b4', width=2), |
| marker=dict(size=8) |
| )) |
| fig_sbp.update_layout( |
| title="收縮壓變化", |
| xaxis_title="日期", |
| yaxis_title="mmHg", |
| height=350 |
| ) |
| fig_sbp.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_sbp, use_container_width=True) |
| |
| with col2: |
| fig_glucose = go.Figure() |
| fig_glucose.add_trace(go.Scatter( |
| x=[r['日期_str'] for r in tracking_results], |
| y=[r['血糖'] for r in tracking_results], |
| mode='lines+markers', |
| name='血糖', |
| line=dict(color='#ff7f0e', width=2), |
| marker=dict(size=8) |
| )) |
| fig_glucose.update_layout( |
| title="血糖變化", |
| xaxis_title="日期", |
| yaxis_title="mg/dL", |
| height=350 |
| ) |
| fig_glucose.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_glucose, use_container_width=True) |
| |
| with col3: |
| fig_waist = go.Figure() |
| fig_waist.add_trace(go.Scatter( |
| x=[r['日期_str'] for r in tracking_results], |
| y=[r['腰圍'] for r in tracking_results], |
| mode='lines+markers', |
| name='腰圍', |
| line=dict(color='#2ca02c', width=2), |
| marker=dict(size=8) |
| )) |
| fig_waist.update_layout( |
| title="腰圍變化", |
| xaxis_title="日期", |
| yaxis_title="cm", |
| height=350 |
| ) |
| fig_waist.update_xaxes(tickformat="%Y-%m-%d") |
| st.plotly_chart(fig_waist, use_container_width=True) |
| |
| |
| st.session_state.tracking_results = tracking_results |
|
|
| |
| st.markdown("---") |
| st.subheader("🎯 長期改善評估") |
| |
| if len(tracking_results) >= 2: |
| first = tracking_results[0] |
| last = tracking_results[-1] |
| |
| improvement_5y = first['5年風險'] - last['5年風險'] |
| improvement_10y = first['10年風險'] - last['10年風險'] |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| if improvement_5y > 0: |
| st.success(f"✅ 5年風險改善\n\n{first['5年風險']:.1f}% → {last['5年風險']:.1f}%\n降低 {improvement_5y:.1f}%") |
| else: |
| st.warning(f"⚠️ 5年風險惡化\n\n{first['5年風險']:.1f}% → {last['5年風險']:.1f}%\n升高 {abs(improvement_5y):.1f}%") |
| |
| with col2: |
| if improvement_10y > 0: |
| st.success(f"✅ 10年風險改善\n\n{first['10年風險']:.1f}% → {last['10年風險']:.1f}%\n降低 {improvement_10y:.1f}%") |
| else: |
| st.warning(f"⚠️ 10年風險惡化\n\n{first['10年風險']:.1f}% → {last['10年風險']:.1f}%\n升高 {abs(improvement_10y):.1f}%") |
| |
| with col3: |
| st.info(f""" |
| 📊 **追蹤期間:** |
| {first['日期'].strftime("%Y-%m-%d")} 到 {last['日期'].strftime("%Y-%m-%d")} |
| (共 {(last['日期'] - first['日期']).days} 天) |
| |
| ✨ 如果風險下降,說明患者的管理有效! |
| """) |
| |
| |
| st.markdown("---") |
| st.subheader("🔄 介入時長有效性對比分析") |
| |
| if st.session_state.tracking_results and len(st.session_state.tracking_results) >= 2: |
| tracking_results = st.session_state.tracking_results |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown("**選擇介入前的時間點**") |
| before_idx = st.slider("介入前", 0, len(tracking_results)-2, 0, key="before_idx") |
| |
| with col2: |
| st.markdown("**選擇介入後的時間點**") |
| after_idx = st.slider("介入後", before_idx+1, len(tracking_results)-1, len(tracking_results)-1, key="after_idx") |
| |
| if st.button("📊 分析介入效果", use_container_width=True): |
| before = tracking_results[before_idx] |
| after = tracking_results[after_idx] |
| |
| |
| days_diff = (after['日期'] - before['日期']).days |
| weeks_diff = days_diff / 7 |
| months_diff = days_diff / 30 |
| |
| |
| sbp_change = after['收縮壓'] - before['收縮壓'] |
| glucose_change = after['血糖'] - before['血糖'] |
| waist_change = after['腰圍'] - before['腰圍'] |
| dial_5y_change = after['5年風險'] - before['5年風險'] |
| dial_10y_change = after['10年風險'] - before['10年風險'] |
| |
| |
| sbp_per_week = sbp_change / weeks_diff if weeks_diff > 0 else 0 |
| dial_5y_per_month = dial_5y_change / months_diff if months_diff > 0 else 0 |
| |
| |
| st.markdown("---") |
| st.markdown("### 📈 介入效果分析結果") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric( |
| "介入時長", |
| f"{days_diff} 天", |
| f"約 {weeks_diff:.1f} 週\n約 {months_diff:.1f} 個月" |
| ) |
| |
| with col2: |
| st.metric( |
| "5年洗腎風險", |
| f"{after['5年風險']:.1f}%", |
| f"{dial_5y_change:+.1f}%", |
| delta_color="inverse" |
| ) |
| |
| with col3: |
| st.metric( |
| "10年洗腎風險", |
| f"{after['10年風險']:.1f}%", |
| f"{dial_10y_change:+.1f}%", |
| delta_color="inverse" |
| ) |
| |
| with col4: |
| st.metric( |
| "收縮壓變化", |
| f"{after['收縮壓']:.0f} mmHg", |
| f"{sbp_change:+.0f} mmHg" |
| ) |
| |
| |
| st.markdown("---") |
| st.markdown("#### 📋 詳細指標對比") |
| |
| comparison_df = pd.DataFrame({ |
| '指標': ['日期', 'CKD 狀態', 'eGFR', '尿蛋白', '收縮壓', '血糖', '腰圍', '5年風險', '10年風險'], |
| '介入前': [ |
| before['日期'].strftime("%Y-%m-%d"), |
| before.get('CKD狀態', ''), |
| f"{before.get('eGFR', 0):.0f}", |
| before.get('尿蛋白', ''), |
| f"{before['收縮壓']:.0f} mmHg", |
| f"{before['血糖']:.0f} mg/dL", |
| f"{before['腰圍']:.0f} cm", |
| f"{before['5年風險']:.1f}%", |
| f"{before['10年風險']:.1f}%" |
| ], |
| '介入後': [ |
| after['日期'].strftime("%Y-%m-%d"), |
| after.get('CKD狀態', ''), |
| f"{after.get('eGFR', 0):.0f}", |
| after.get('尿蛋白', ''), |
| f"{after['收縮壓']:.0f} mmHg", |
| f"{after['血糖']:.0f} mg/dL", |
| f"{after['腰圍']:.0f} cm", |
| f"{after['5年風險']:.1f}%", |
| f"{after['10年風險']:.1f}%" |
| ], |
| '變化': [ |
| '', |
| '', |
| '', |
| '', |
| f"{sbp_change:+.0f}", |
| f"{glucose_change:+.0f}", |
| f"{waist_change:+.0f}", |
| f"{dial_5y_change:+.1f}", |
| f"{dial_10y_change:+.1f}" |
| ] |
| }) |
| |
| st.dataframe(comparison_df, use_container_width=True, hide_index=True) |
| |
| |
| st.markdown("---") |
| st.markdown("#### 🎯 介入效果評估") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown(f""" |
| **改善速度:** |
| - 收縮壓:每週平均 {sbp_per_week:+.1f} mmHg |
| - 5年風險:每月平均 {dial_5y_per_month:+.2f}% |
| |
| **評估:** |
| """) |
| if dial_5y_change < -1.0: |
| st.success("✅ 效果顯著 - 風險明顯下降!") |
| elif dial_5y_change < 0: |
| st.info("🟡 效果溫和 - 風險有所改善") |
| else: |
| st.warning("⚠️ 效果不佳 - 風險未改善,建議調整治療方案") |
| |
| with col2: |
| |
| fig_comparison = go.Figure() |
| |
| fig_comparison.add_trace(go.Bar( |
| name='介入前', |
| x=['5年風險', '10年風險'], |
| y=[before['5年風險'], before['10年風險']], |
| marker_color='rgba(255, 127, 14, 0.7)' |
| )) |
| |
| fig_comparison.add_trace(go.Bar( |
| name='介入後', |
| x=['5年風險', '10年風險'], |
| y=[after['5年風險'], after['10年風險']], |
| marker_color='rgba(44, 160, 44, 0.7)' |
| )) |
| |
| fig_comparison.update_layout( |
| title=f"洗腎風險對比({days_diff}天介入)", |
| xaxis_title="時間點", |
| yaxis_title="風險 (%)", |
| barmode='group', |
| height=400, |
| showlegend=True |
| ) |
| |
| st.plotly_chart(fig_comparison, use_container_width=True) |
| else: |
| st.info("💡 需要至少 2 次檢驗數據才能進行對比分析") |
| else: |
| st.info("👇 請在上方輸入至少一次檢驗數據,然後點「📊 進行追蹤預測」") |
|
|
| |
| |
| |
| elif page == "💬 AI 諮詢": |
| st.title("💬 CKD AI 諮詢") |
| |
| st.markdown(""" |
| 👋 **歡迎來到 CKD AI 諮詢!** |
| |
| 💡 **智能功能:** 系統會自動記住你在「單患者預測」中輸入的患者信息, |
| ChatGPT 會根據這位患者的具體數據回答你的問題! |
| """) |
| |
| st.markdown("---") |
| |
| api_key = st.session_state.get("openai_api_key") |
| |
| if not api_key: |
| st.warning("⚠️ 需要 OpenAI API Key") |
| st.info("請在左側邊欄「🔑 OpenAI API 設定」中輸入你的 API Key") |
| else: |
| st.success("✅ API Key 已設定") |
| |
| has_patient_data = st.session_state.current_patient_data is not None |
| has_intervention = st.session_state.intervention_results is not None |
| |
| if has_patient_data: |
| patient_data = st.session_state.current_patient_data |
| st.info( |
| f"""✅ **已載入患者數據:** |
| |
| 患者 ID:{patient_data['patient_id']} | 年齡:{patient_data['age']:.0f} 歲 | 性別:{'男' if patient_data['gender'] == 1 else '女'} |
| 當前狀態:{patient_data['state']} | 5年洗腎風險:{patient_data['dial_5y']:.1f}% | 風險分層:{patient_data['risk_level']}""" |
| ) |
| |
| |
| if has_intervention: |
| intervention = st.session_state.intervention_results |
| st.success(f""" |
| ✅ **已完成介入效果分析:** |
| |
| 介入方案: |
| {chr(10).join(['• ' + desc for desc in intervention['change_descriptions']])} |
| |
| 改善效果: |
| • 5年風險:{intervention['original_dial_5y']:.1f}% → {intervention['intervention_dial_5y']:.1f}% ⬇️ 降低 {intervention['improvement_5y']:.1f}% ({intervention['improvement_5y_percent']:.1f}%) |
| • 10年風險:{intervention['original_dial_10y']:.1f}% → {intervention['intervention_dial_10y']:.1f}% ⬇️ 降低 {intervention['improvement_10y']:.1f}% ({intervention['improvement_10y_percent']:.1f}%) |
| """) |
| else: |
| st.warning( |
| "⚠️ **尚未載入患者數據**\n\n" |
| "請先去「👤 單患者預測」輸入患者信息,點擊「🔮 預測」後,系統會自動記住患者信息!" |
| ) |
| |
| st.markdown("---") |
| st.markdown("### 📝 對話記錄") |
| |
| for message in st.session_state.messages: |
| with st.chat_message(message["role"]): |
| st.markdown(message["content"]) |
| |
| user_input = st.chat_input("輸入你的問題...") |
| |
| if user_input: |
| st.session_state.messages.append({"role": "user", "content": user_input}) |
| |
| with st.chat_message("user"): |
| st.markdown(user_input) |
| |
| with st.chat_message("assistant"): |
| with st.spinner("🤖 ChatGPT 思考中..."): |
| try: |
| client = OpenAI(api_key=api_key) |
| |
| |
| patient_context = "" |
| intervention_context = "" |
| |
| if has_patient_data: |
| p = st.session_state.current_patient_data |
| patient_context = f""" |
| 【當前患者信息】 |
| - 患者ID:{p['patient_id']} |
| - 年齡:{p['age']:.0f} 歲,性別:{p['gender_text']} |
| |
| 【腎功能狀態】 |
| - eGFR:{p['egfr']:.0f} mL/min/1.73m²({p['gfr_grade']}) |
| - 尿蛋白:{p['pro_text']}({p['albuminuria']}) |
| - KDIGO 分類:{p['kdigo_class']} |
| - CKD 阶段:{p['state']} |
| - 風險分層:{p['risk_level']} |
| |
| 【臨床指標】 |
| - 血壓:{p['sbp']:.0f} mmHg,血糖:{p['glucose']:.0f} mg/dL,腰圍:{p['waist']:.0f} cm |
| - 紅血球:{p['rbc']:.1f},白血球:{p['wbc']:.1f} |
| - 高尿酸:{'是' if p['hi_ua'] == 1 else '否'} |
| - 高血壓病史:{p['pdh_hp_text']},教育程度:{p['edu_high_text']} |
| |
| 【風險評估】 |
| - 5年洗腎概率:{p['dial_5y']:.1f}% |
| - 10年洗腎概率:{p['dial_10y']:.1f}% |
| """ |
| |
| |
| tracking_context = "" |
| if st.session_state.visit_data and len(st.session_state.visit_data) > 0: |
| tracking_context = """ |
| 【患者追蹤記錄(多次檢驗)】 |
| """ |
| sorted_visits = sorted(st.session_state.visit_data, key=lambda x: x['date']) |
| for i, visit in enumerate(sorted_visits, 1): |
| visit_date_str = visit['date'].strftime("%Y-%m-%d") |
| tracking_context += f""" |
| 檢驗 #{i}({visit_date_str}): |
| - 收縮壓:{visit['sbp']:.0f} mmHg |
| - 血糖:{visit['glucose']:.0f} mg/dL |
| - 腰圍:{visit['waist']:.0f} cm |
| - 紅血球:{visit['rbc']:.1f} |
| - 白血球:{visit['wbc']:.1f} |
| - 高尿酸:{'是' if visit['hi_ua'] == 1 else '否'} |
| """ |
| if len(sorted_visits) > 1: |
| first = sorted_visits[0] |
| last = sorted_visits[-1] |
| tracking_context += f""" |
| 長期變化趨勢({first['date'].strftime("%Y-%m-%d")} 到 {last['date'].strftime("%Y-%m-%d")}): |
| - 收縮壓變化:{first['sbp']:.0f} → {last['sbp']:.0f} mmHg |
| - 血糖變化:{first['glucose']:.0f} → {last['glucose']:.0f} mg/dL |
| - 腰圍變化:{first['waist']:.0f} → {last['waist']:.0f} cm |
| """ |
| |
| patient_context += tracking_context |
| |
| if has_intervention: |
| inter = st.session_state.intervention_results |
| intervention_context = f""" |
| 【已完成的介入效果分析】 |
| 患者做過的介入調整: |
| {chr(10).join(['- ' + desc for desc in inter['change_descriptions']])} |
| |
| 介入效果: |
| - 5年洗腎風險:從 {inter['original_dial_5y']:.1f}% 降低到 {inter['intervention_dial_5y']:.1f}%,改善 {inter['improvement_5y']:.1f}% ({inter['improvement_5y_percent']:.1f}%) |
| - 10年洗腎風險:從 {inter['original_dial_10y']:.1f}% 降低到 {inter['intervention_dial_10y']:.1f}%,改善 {inter['improvement_10y']:.1f}% ({inter['improvement_10y_percent']:.1f}%) |
| |
| 請根據患者做過的介入調整,給出具體的臨床建議和實踐方案,包括: |
| 1. 達到這些改變需要的具體措施 |
| 2. 可能的治療方案和藥物 |
| 3. 生活方式的改變建議 |
| 4. 預期的效果時間表 |
| """ |
| |
| system_prompt = f"""你是一位經驗豐富的腎臟科醫生,專門提供 CKD(慢性腎臟病)相關的專業建議。 |
| |
| {patient_context if patient_context else "(暫無患者數據,提供一般性建議)"} |
| |
| {intervention_context if intervention_context else ""} |
| |
| 請根據上述患者信息(如果有),用簡潔、清楚、患者友善的語言回答問題。 |
| 避免過度醫學術語,提供實際可行的建議。""" |
| |
| messages_for_api = [{"role": "system", "content": system_prompt}] |
| |
| recent_messages = st.session_state.messages[-10:] |
| for msg in recent_messages: |
| messages_for_api.append(msg) |
| |
| response = client.chat.completions.create( |
| model="gpt-4", |
| messages=messages_for_api, |
| temperature=0.7, |
| max_tokens=1500 |
| ) |
| |
| assistant_message = response.choices[0].message.content |
| st.session_state.messages.append({"role": "assistant", "content": assistant_message}) |
| st.markdown(assistant_message) |
| |
| except Exception as e: |
| st.error(f"❌ 錯誤:{str(e)}") |
| elif page == "📋 患者記錄": |
| st.title("📋 患者記錄管理") |
| |
| |
| st.markdown("### 🔑 API Key 管理") |
| |
| col1, col2 = st.columns([4, 1]) |
| with col1: |
| api_key_input = st.text_input( |
| "輸入並保存你的 OpenAI API Key", |
| type="password", |
| value=st.session_state.get("openai_api_key", ""), |
| help="從 https://platform.openai.com/api/keys 取得" |
| ) |
| with col2: |
| if st.button("💾 保存", use_container_width=True): |
| if api_key_input: |
| st.session_state.openai_api_key = api_key_input |
| st.success("✅ API Key 已保存!") |
| else: |
| st.warning("⚠️ 請輸入有效的 API Key") |
| |
| if st.session_state.get("openai_api_key"): |
| st.success("✅ 已保存 API Key(之後無需重新輸入)") |
| |
| st.markdown("---") |
| |
| |
| st.markdown("### 👤 當前患者記錄") |
| |
| if st.session_state.current_patient_data: |
| patient = st.session_state.current_patient_data |
| |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("患者ID", patient['patient_id'], patient.get('patient_name', ''), delta_color="off") |
| with col2: |
| st.metric("年齡", f"{patient['age']:.0f} 歲", patient['gender_text']) |
| with col3: |
| st.metric("CKD 狀態", patient['state'], patient['risk_level']) |
| with col4: |
| st.metric("KDIGO 分類", patient['kdigo_class'], "") |
| |
| |
| st.markdown("---") |
| st.markdown("#### 🫘 腎功能指標") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.info(f""" |
| **eGFR** |
| {patient['egfr']:.0f} mL/min/1.73m² |
| |
| **GFR 等級:** {patient['gfr_grade']} |
| """) |
| |
| with col2: |
| st.warning(f""" |
| **尿蛋白** |
| {patient['pro_text']} |
| |
| **蛋白尿等級:** {patient['albuminuria']} |
| """) |
| |
| with col3: |
| st.success(f""" |
| **CKD 阶段** |
| {patient['state']} 期 |
| |
| **KDIGO:** {patient['kdigo_class']} |
| """) |
| |
| |
| st.markdown("---") |
| st.markdown("#### 📊 詳細臨床信息") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.info(f""" |
| **血壓** |
| {patient['sbp']:.0f} mmHg |
| |
| **血糖** |
| {patient['glucose']:.0f} mg/dL |
| """) |
| |
| with col2: |
| st.info(f""" |
| **紅血球** |
| {patient['rbc']:.1f} |
| |
| **白血球** |
| {patient['wbc']:.1f} |
| """) |
| |
| with col3: |
| st.info(f""" |
| **腰圍** |
| {patient['waist']:.0f} cm |
| |
| **高尿酸** |
| {patient.get('hi_ua', 0) == 1 and '是' or '否'} |
| """) |
| |
| with col4: |
| st.info(f""" |
| **高血壓病史** |
| {patient['pdh_hp_text']} |
| |
| **教育程度** |
| {patient['edu_high_text']} |
| """) |
| |
| |
| st.markdown("---") |
| st.markdown("#### ⚠️ 洗腎風險評估") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| dial_5y = patient['dial_5y'] |
| if dial_5y < 5: |
| st.success(f"**5年洗腎風險**\n\n{dial_5y:.1f}% 🟢 低風險") |
| elif dial_5y < 15: |
| st.warning(f"**5年洗腎風險**\n\n{dial_5y:.1f}% 🟡 中等風險") |
| elif dial_5y < 30: |
| st.error(f"**5年洗腎風險**\n\n{dial_5y:.1f}% 🟠 高風險") |
| else: |
| st.error(f"**5年洗腎風險**\n\n{dial_5y:.1f}% 🔴 極高風險") |
| |
| with col2: |
| dial_10y = patient['dial_10y'] |
| if dial_10y < 5: |
| st.success(f"**10年洗腎風險**\n\n{dial_10y:.1f}% 🟢 低風險") |
| elif dial_10y < 15: |
| st.warning(f"**10年洗腎風險**\n\n{dial_10y:.1f}% 🟡 中等風險") |
| elif dial_10y < 30: |
| st.error(f"**10年洗腎風險**\n\n{dial_10y:.1f}% 🟠 高風險") |
| else: |
| st.error(f"**10年洗腎風險**\n\n{dial_10y:.1f}% 🔴 極高風險") |
| |
| |
| if st.session_state.intervention_results is not None: |
| st.markdown("---") |
| st.markdown("#### 🎯 最近的介入效果分析") |
| |
| inter = st.session_state.intervention_results |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown(f""" |
| **介入方案:** |
| {chr(10).join(['- ' + desc for desc in inter['change_descriptions'][:3]])} |
| {'- ...' if len(inter['change_descriptions']) > 3 else ''} |
| """) |
| |
| with col2: |
| improvement_5y = inter['improvement_5y'] |
| improvement_10y = inter['improvement_10y'] |
| |
| if improvement_5y > 10: |
| st.success(f"✅ **顯著改善** (5年)\n降低 {improvement_5y:.1f}%") |
| elif improvement_5y > 5: |
| st.info(f"🟢 **中度改善** (5年)\n降低 {improvement_5y:.1f}%") |
| elif improvement_5y > 0: |
| st.info(f"🟡 **輕微改善** (5年)\n降低 {improvement_5y:.1f}%") |
| else: |
| st.warning(f"⚠️ **無改善** (5年)") |
| |
| |
| st.markdown("---") |
| st.markdown("#### 🔧 操作") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| if st.button("💬 去AI諮詢", use_container_width=True, key="go_ai"): |
| st.info("👉 點左側邊欄「💬 AI 諮詢」開始對話!") |
| |
| with col2: |
| if st.button("👤 重新預測", use_container_width=True, key="new_predict"): |
| st.info("👉 點左側邊欄「👤 單患者預測」重新輸入數據!") |
| |
| with col3: |
| if st.button("🗑️ 清除記錄", use_container_width=True, key="clear_patient"): |
| st.session_state.current_patient_data = None |
| st.session_state.intervention_results = None |
| st.success("✅ 患者記錄已清除") |
| st.rerun() |
| |
| else: |
| st.info(""" |
| 📭 **尚未加載患者記錄** |
| |
| 👉 要建立患者記錄,請: |
| 1. 點左側邊欄「👤 單患者預測」 |
| 2. 輸入患者信息 |
| 3. 點「🔮 預測」 |
| 4. 患者記錄會自動保存在這裡 |
| """) |
|
|
| st.markdown("---") |
| st.markdown("<p style='text-align: center; color: gray;'>© 2026 多階段CKD疾病進展機器學習預測模型 | 完整最終版</p>", unsafe_allow_html=True) |