# -*- coding: utf-8 -*- """ 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(""" """, unsafe_allow_html=True) # ============================================================ # 初始化 session state # ============================================================ 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" # 根據 KDIGO 表判斷狀態和等級 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("---") # API 設定部分 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("---") # ============================================================ # 頁面 1:首頁 - 改進版 # ============================================================ 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("---") # KDIGO CKD 分類表 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("---") # 10 個協變數說明 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" "系統會自動記住這位患者的所有信息 🎉" ) # ============================================================ # 頁面 2:單患者預測 # ============================================================ elif page == "👤 單患者預測": st.title("👤 單患者 CKD 風險預測") predictor = load_model() if predictor is None: st.stop() # 初始化session state用來保存預測結果 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(")")) # 自動判斷 CKD 狀態 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" # 根據 KDIGO 表判斷狀態和等級 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) # 保存所有結果到session_state 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 = "🔴 極高風險" # 保存患者數據到 session state 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(), # CKD 狀態 '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(")")) # 自動重算 CKD 狀態 new_state_idx, new_state_name, new_gfr_grade, new_albuminuria = judge_ckd_state_kdigo(egfr_adjusted, pro_adjusted_val) # 顯示 CKD 狀態變化 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"): # 使用新的 CKD 狀態進行預測(如果 eGFR/Pro 改變了) 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 '否'}") # 保存介入結果到session_state 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% = 這個參數對風險影響較小 可以試試調整不同參數,找出最有效的介入組合! """) # ============================================================ # 頁面 3:多患者對比 # ============================================================ 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) # ============================================================ # 頁面 4:患者縱向追蹤 # ============================================================ elif page == "📊 患者追蹤": st.title("📊 患者縱向追蹤") # ✅ 初始化 session_state 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() # 使用全局 KDIGO 函數判斷當前狀態 state_idx, state_name, gfr_grade, albuminuria = judge_ckd_state_kdigo(visit_egfr, visit_pro_val) # 顯示當前 CKD 狀態 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, } # 使用自動判斷的 CKD 狀態進行預測 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) # ✅ 保存到 session_state 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("👇 請在上方輸入至少一次檢驗數據,然後點「📊 進行追蹤預測」") # ============================================================ # 頁面 5:AI 諮詢 - 智能對話 # ============================================================ 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("📋 患者記錄管理") # API Key 管理 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 # 顯示基本信息 + KDIGO 分類 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("
© 2026 多階段CKD疾病進展機器學習預測模型 | 完整最終版
", unsafe_allow_html=True)