smartTranscend/ckd-markov-predictor
0
1# -*- coding: utf-8 -*-2"""3CKD Markov 預測應用 - Streamlit 完整最終版4=======================================================================5功能完整版本:改進首頁 + 患者預測 + 智能AI對話6"""7 8import streamlit as st9import pandas as pd10import numpy as np11import plotly.graph_objects as go12import plotly.express as px13import os14from datetime import datetime15import json16from openai import OpenAI17 18# 自訂模塊19import sys20sys.path.insert(0, os.path.dirname(__file__))21 22from ckd_markov_final_predictor import CKDMarkovPredictor23 24# ============================================================25# 頁面配置26# ============================================================27st.set_page_config(28 page_title="CKD Markov 風險預測系統",29 page_icon="🏥",30 layout="wide",31 initial_sidebar_state="expanded"32)33 34st.markdown("""35 <style>36 .metric-card {37 background-color: #f0f2f6;38 padding: 20px;39 border-radius: 10px;40 margin: 10px 0;41 }42 </style>43""", unsafe_allow_html=True)44 45# ============================================================46# 初始化 session state47# ============================================================48if "messages" not in st.session_state:49 st.session_state.messages = []50 51if "current_patient_data" not in st.session_state:52 st.session_state.current_patient_data = None53 54if "intervention_results" not in st.session_state:55 st.session_state.intervention_results = None56 57if "prediction_results" not in st.session_state:58 st.session_state.prediction_results = None59 60if "visit_data" not in st.session_state:61 st.session_state.visit_data = []62 63# ============================================================64# 初始化模型65# ============================================================66@st.cache_resource67def load_model():68 """載入Markov模型"""69 theta_path = './10cov_theta.npy'70 q_path = './10cov_Q_params.csv'71 72 if os.path.exists(theta_path) and os.path.exists(q_path):73 return CKDMarkovPredictor(theta_path, q_path)74 else:75 st.error("⚠️ 模型文件未找到!")76 return None77 78# ============================================================79# 側邊欄 - 導航和設定80# ============================================================81def judge_ckd_state_kdigo(egfr, pro_coded):82 """83 根據 KDIGO 標準判斷 CKD 狀態84 85 蛋白尿分類:86 - A1: 正常 (-) → pro_coded < 187 - A2: 微量 (+/-) → pro_coded == 188 - A3: ≥1+ → pro_coded >= 289 """90 91 # 判斷蛋白尿分類92 if pro_coded < 1:93 albuminuria = "A1"94 elif pro_coded == 1:95 albuminuria = "A2"96 else:97 albuminuria = "A3"98 99 # 根據 KDIGO 表判斷狀態和等級100 if egfr >= 90:101 gfr_grade = "G1"102 if albuminuria == "A1":103 state_idx, state_name = 0, "Low"104 elif albuminuria == "A2":105 state_idx, state_name = 1, "Moderate"106 else:107 state_idx, state_name = 2, "High"108 elif egfr >= 60:109 gfr_grade = "G2"110 if albuminuria == "A1":111 state_idx, state_name = 0, "Low"112 elif albuminuria == "A2":113 state_idx, state_name = 1, "Moderate"114 else:115 state_idx, state_name = 2, "High"116 elif egfr >= 45:117 gfr_grade = "G3a"118 if albuminuria == "A1":119 state_idx, state_name = 1, "Moderate"120 elif albuminuria == "A2":121 state_idx, state_name = 2, "High"122 else:123 state_idx, state_name = 3, "VeryHigh"124 elif egfr >= 30:125 gfr_grade = "G3b"126 if albuminuria == "A1":127 state_idx, state_name = 2, "High"128 else:129 state_idx, state_name = 3, "VeryHigh"130 elif egfr >= 15:131 gfr_grade = "G4"132 state_idx, state_name = 3, "VeryHigh"133 else:134 gfr_grade = "G5"135 state_idx, state_name = 3, "VeryHigh"136 137 return state_idx, state_name, gfr_grade, albuminuria138 139 140st.sidebar.title("🏥 CKD Markov 風險預測")141st.sidebar.markdown("---")142 143page = st.sidebar.radio(144 "選擇功能:",145 ["🏠 首頁", "👤 單患者預測", "👥 多患者對比", "📊 患者追蹤", "💬 AI 諮詢", "📋 患者記錄"]146)147 148st.sidebar.markdown("---")149 150# API 設定部分151with st.sidebar.expander("🔑 OpenAI API 設定", expanded=False):152 st.markdown("### 輸入你的 API Key")153 154 api_key_input = st.text_input(155 "API Key",156 type="password",157 value=st.session_state.get("openai_api_key", ""),158 help="從 https://platform.openai.com/api/keys 取得"159 )160 161 if api_key_input:162 st.session_state.openai_api_key = api_key_input163 st.success("✅ API Key 已設定")164 165st.sidebar.markdown("---")166 167# ============================================================168# 頁面 1:首頁 - 改進版169# ============================================================170if page == "🏠 首頁":171 st.title("🏥 多階段CKD疾病進展機器學習預測模型")172 173 st.markdown("""174 ## 🎯 AI 驅動的腎臟病風險評估與臨床決策支持175 176 基於馬可夫連鎖模型的 CKD 進展預測,整合機器學習和 ChatGPT 智能對話177 """)178 179 st.markdown("---")180 181 # 系統功能概覽卡片182 st.subheader("✨ 核心功能")183 184 col1, col2, col3, col4 = st.columns(4)185 186 with col1:187 st.metric("📊 預測功能", "完整", "7 轉移")188 with col2:189 st.metric("🤖 AI 對話", "實時", "個性化")190 with col3:191 st.metric("📈 批量處理", "CSV/Excel", "自動")192 with col4:193 st.metric("💾 數據記憶", "自動", "智能")194 195 st.markdown("---")196 197 # KDIGO CKD 分類表198 st.subheader("🔬 KDIGO CKD 分類(eGFR × 蛋白尿)")199 200 st.markdown("""201 系統使用 KDIGO 標準將 CKD 分為 **5 個風險等級**:202 203 | eGFR 分級 | 正常 (A1) | 微量 (A2) | ≥1+ (A3) |204 |---------|---------|---------|---------|205 | G1 (≥90) | **Low** | Moderate | High |206 | G2 (60-89) | **Low** | Moderate | High |207 | G3a (45-59) | Moderate | High | **VeryHigh** |208 | G3b (30-44) | High | **VeryHigh** | VeryHigh |209 | G4 (15-29) | **VeryHigh** | VeryHigh | VeryHigh |210 | G5 (<15) | **VeryHigh** | VeryHigh | VeryHigh |211 212 > 📌 **關鍵概念:**213 > - **eGFR**:估計腎臟過濾能力(mL/min/1.73m²)214 > - **蛋白尿**:A1=正常(-),A2=微量(+/-),A3=≥1+215 > - 同一 eGFR 等級下,蛋白尿越多,風險越高216 """)217 218 st.markdown("---")219 st.subheader("📋 CKD 5個狀態分類")220 221 states_visual = pd.DataFrame({222 '狀態': ['Low', 'Moderate', 'High', 'VeryHigh', 'Dialysis'],223 '定義': ['eGFR≥90', 'G2-3a或有蛋白尿', 'G3b', 'G4-5或有蛋白尿', '已進入透析'],224 '風險': [10, 25, 50, 75, 95],225 })226 227 fig_states = go.Figure()228 229 fig_states.add_trace(go.Bar(230 x=states_visual['狀態'],231 y=states_visual['風險'],232 marker=dict(233 color=['green', 'yellow', 'orange', 'red', 'darkred'],234 line=dict(color='black', width=2)235 ),236 text=states_visual['定義'],237 textposition='outside',238 showlegend=False239 ))240 241 fig_states.update_layout(242 title="CKD 狀態與相對風險程度",243 xaxis_title="CKD 狀態",244 yaxis_title="相對風險",245 height=350,246 template='plotly_white'247 )248 249 st.plotly_chart(fig_states, use_container_width=True)250 251 # 狀態轉移圖252 st.markdown("---")253 st.subheader("🔄 CKD 狀態轉移路徑")254 255 col1, col2 = st.columns(2)256 257 with col1:258 st.markdown("""259 ### 前進轉移(惡化)260 261 🔴 **進展方向:**262 - Low → Moderate263 - Moderate → High264 - High → VeryHigh265 - VeryHigh → Dialysis266 267 危險因素:年齡、血糖、血壓、尿酸268 """)269 270 with col2:271 st.markdown("""272 ### 後退轉移(改善)273 274 🟢 **改善方向:**275 - Moderate → Low276 - High → Moderate277 - VeryHigh → High278 279 保護因素:血糖控制、血壓控制、蛋白質限制280 """)281 282 st.markdown("---")283 284 # 使用流程285 st.subheader("🚀 四步快速開始")286 287 col1, col2, col3, col4 = st.columns(4)288 289 with col1:290 st.markdown("""291 ### ① 輸入數據292 👤 **單患者預測**293 294 輸入:295 - 年齡、性別296 - 血壓、血糖297 - 腎功能、蛋白尿298 """)299 300 with col2:301 st.markdown("""302 ### ② AI 預測303 🔮 **自動計算**304 305 獲得:306 - 5年洗腎風險307 - 10年洗腎風險308 - 風險分層309 - 轉移速率310 """)311 312 with col3:313 st.markdown("""314 ### ③ 智能對話315 💬 **AI 諮詢**316 317 ChatGPT:318 - 記住患者數據319 - 個性化回答320 - 臨床建議321 """)322 323 with col4:324 st.markdown("""325 ### ④ 決策支持326 📊 **臨床應用**327 328 支持:329 - 隨訪計劃330 - 轉介決策331 - 患者教育332 """)333 334 st.markdown("---")335 336 # 風險分層表337 st.subheader("🎨 風險分層與臨床決策")338 339 risk_data = pd.DataFrame({340 '風險等級': ['🟢 低風險', '🟡 中等風險', '🟠 高風險', '🔴 極高風險'],341 '5年洗腎風險': ['< 5%', '5-15%', '15-30%', '> 30%'],342 '建議隨訪頻率': ['年 1 次', '半年 1 次', '每季 1 次', '每月 1 次'],343 '臨床行動': ['常規管理', '強化血壓血糖控制', '準備透析通路評估', '準備透析治療']344 })345 346 st.dataframe(risk_data, use_container_width=True, hide_index=True)347 348 st.markdown("---")349 350 # 模型特性351 st.subheader("🔬 模型特性與驗證")352 353 col1, col2, col3 = st.columns(3)354 355 with col1:356 st.success("""357 ### 📊 訓練數據規模358 359 ✅ **樣本數**:21,756 轉移段360 ✅ **患者數**:5,455 人361 ✅ **隨訪期**:平均 3.5 年362 ✅ **完整率**:99.2%363 """)364 365 with col2:366 st.info("""367 ### 🧮 模型結構368 369 📌 **狀態**:5 個370 📌 **轉移**:7 種(前進 4 + 後退 3)371 📌 **協變數**:10 個372 📌 **參數**:77 個373 """)374 375 with col3:376 st.warning("""377 ### ⚙️ 技術細節378 379 🔧 **方法**:最大似然估計380 🔧 **優化**:L-BFGS-B381 🔧 **驗證**:Hessian 反演382 🔧 **變數**:標準化383 """)384 385 st.markdown("---")386 387 # 10 個協變數說明388 st.subheader("📊 10 個預測協變數")389 390 covariates_info = pd.DataFrame({391 '協變數': ['age_at_screening', 'hi_UA', 'RBC', 'PDH_HP', 'GENDER', 'sbp', 'waist', 'WBC', 'GLUCOSE', 'EDU_high'],392 '中文名稱': ['年齡', '高尿酸', '紅血球', '高血壓病史', '性別', '收縮壓', '腰圍', '白血球', '血糖', '高教育'],393 '變數類型': ['連續', '二元', '連續', '二元', '二元', '連續', '連續', '連續', '連續', '二元'],394 '臨床意義': ['年紀越大風險越高', '尿酸升高增加風險', '貧血增加風險', '高血壓增加風險', '性別差異', '血壓升高增加風險', '肥胖增加風險', '感染風險', '血糖升高增加風險', '教育程度保護']395 })396 397 st.dataframe(covariates_info, use_container_width=True, hide_index=True)398 399 st.markdown("---")400 401 # 智能對話功能402 st.subheader("🤖 智能 AI 對話功能")403 404 st.markdown("""405 ### 💡 三個特點406 407 **1️⃣ 自動記憶患者數據**408 - 你在「單患者預測」輸入的所有信息,系統自動保存409 - 無需重複輸入410 411 **2️⃣ 個性化智能回答**412 - ChatGPT 知道患者的年齡、血糖、洗腎風險等具體數據413 - 提供的建議是針對這位患者,不是通用建議414 415 **3️⃣ 持續對話支持**416 - 保留完整對話記錄417 - 支持追問和深入討論418 419 ### 📝 可以問的問題範例420 421 - 「這位患者的 5 年洗腎風險為什麼是 72.5%?」422 - 「他/她應該怎樣控制血糖?」423 - 「需要準備透析通路嗎?」424 - 「下次隨訪應該檢查什麼項目?」425 - 「有飲食建議嗎?」426 - 「什麼時候應該轉介給腎臟科?」427 """)428 429 st.info(430 "**✨ 開始使用:**\n\n"431 "1. 👉 點左側「👤 單患者預測」\n"432 "2. 👉 輸入患者信息並點「🔮 預測」\n"433 "3. 👉 再點「💬 AI 諮詢」開始對話!\n\n"434 "系統會自動記住這位患者的所有信息 🎉"435 )436 437# ============================================================438# 頁面 2:單患者預測439# ============================================================440elif page == "👤 單患者預測":441 st.title("👤 單患者 CKD 風險預測")442 443 predictor = load_model()444 if predictor is None:445 st.stop()446 447 # 初始化session state用來保存預測結果448 if "prediction_results" not in st.session_state:449 st.session_state.prediction_results = None450 451 with st.form("patient_form"):452 st.subheader("📋 患者信息與協變數")453 454 col1, col2, col3 = st.columns(3)455 456 with col1:457 patient_id = st.text_input("患者ID", value="P001")458 patient_name = st.text_input("患者姓名", value="")459 460 with col2:461 age = st.slider("年齡", 20, 100, 60)462 gender = st.radio("性別", ["女 (0)", "男 (1)"], horizontal=True)463 gender_val = 0 if gender == "女 (0)" else 1464 465 with col3:466 st.markdown("### 腎功能指標")467 egfr = st.number_input("eGFR (mL/min/1.73m²)", 5, 120, 60)468 pro_coded = st.selectbox(469 "尿蛋白",470 ["陰性 (0)", "微量 (1)", "1+ (2)", "2+ (3)", "3+ (4)", "4+ (5)"],471 index=0472 )473 pro_coded_val = int(pro_coded.split("(")[1].strip(")"))474 475 # 自動判斷 CKD 狀態476 def judge_ckd_state(egfr, pro_coded):477 """478 根據 KDIGO 標準判斷 CKD 狀態479 480 蛋白尿分類:481 - A1: 正常 (-) → pro_coded < 1482 - A2: 微量 (+/-) → pro_coded == 1483 - A3: ≥1+ → pro_coded >= 2484 """485 486 # 判斷蛋白尿分類487 if pro_coded < 1:488 albuminuria = "A1"489 elif pro_coded == 1:490 albuminuria = "A2"491 else:492 albuminuria = "A3"493 494 # 根據 KDIGO 表判斷狀態和等級495 if egfr >= 90:496 gfr_grade = "G1"497 if albuminuria == "A1":498 state_idx, state_name = 0, "Low"499 elif albuminuria == "A2":500 state_idx, state_name = 1, "Moderate"501 else:502 state_idx, state_name = 2, "High"503 elif egfr >= 60:504 gfr_grade = "G2"505 if albuminuria == "A1":506 state_idx, state_name = 0, "Low"507 elif albuminuria == "A2":508 state_idx, state_name = 1, "Moderate"509 else:510 state_idx, state_name = 2, "High"511 elif egfr >= 45:512 gfr_grade = "G3a"513 if albuminuria == "A1":514 state_idx, state_name = 1, "Moderate"515 elif albuminuria == "A2":516 state_idx, state_name = 2, "High"517 else:518 state_idx, state_name = 3, "VeryHigh"519 elif egfr >= 30:520 gfr_grade = "G3b"521 if albuminuria == "A1":522 state_idx, state_name = 2, "High"523 else:524 state_idx, state_name = 3, "VeryHigh"525 elif egfr >= 15:526 gfr_grade = "G4"527 state_idx, state_name = 3, "VeryHigh"528 else:529 gfr_grade = "G5"530 state_idx, state_name = 3, "VeryHigh"531 532 return state_idx, state_name, gfr_grade, albuminuria533 534 state_idx, state_name, gfr_grade, albuminuria = judge_ckd_state(egfr, pro_coded_val)535 st.info(f"✅ **自動判斷狀態:{state_name}**\n\n**KDIGO 分類:** {gfr_grade} {albuminuria}\n**eGFR:** {egfr} mL/min/1.73m²\n**尿蛋白:** {pro_coded.split('(')[0].strip()}")536 537 st.markdown("---")538 st.subheader("🔬 生化檢驗與測量")539 540 col1, col2, col3, col4, col5 = st.columns(5)541 542 with col1:543 hi_ua = st.selectbox("高尿酸", ["否 (0)", "是 (1)"], index=0)544 hi_ua_val = int(hi_ua.split("(")[1].strip(")"))545 546 with col2:547 rbc = st.number_input("紅血球 (RBC)", 2.0, 8.0, 4.5, 0.1)548 549 with col3:550 sbp = st.number_input("收縮壓", 80, 200, 130)551 552 with col4:553 wbc = st.number_input("白血球 (WBC)", 2.0, 15.0, 7.0, 0.1)554 555 with col5:556 glucose = st.number_input("血糖", 50, 300, 110)557 558 col1, col2, col3, col4 = st.columns(4)559 560 with col1:561 waist = st.number_input("腰圍", 60, 150, 85)562 563 with col2:564 pdh_hp = st.selectbox("高血壓病史", ["否 (0)", "是 (1)"], index=0)565 pdh_hp_val = int(pdh_hp.split("(")[1].strip(")"))566 567 with col3:568 edu_high = st.selectbox("高教育程度", ["否 (0)", "是 (1)"], index=0)569 edu_high_val = int(edu_high.split("(")[1].strip(")"))570 571 with col4:572 st.empty()573 574 st.markdown("---")575 submitted = st.form_submit_button("🔮 預測", use_container_width=True)576 577 if submitted:578 covariates = {579 'age_at_screening': age,580 'hi_UA': hi_ua_val,581 'RBC': rbc,582 'PDH_HP': pdh_hp_val,583 'GENDER': gender_val,584 'sbp': sbp,585 'waist': waist,586 'WBC': wbc,587 'GLUCOSE': glucose,588 'EDU_high': edu_high_val,589 }590 591 pred_5y = predictor.predict(covariates, start_state=state_idx, years=5)592 pred_10y = predictor.predict(covariates, start_state=state_idx, years=10)593 594 # 保存所有結果到session_state595 st.session_state.prediction_results = {596 'covariates': covariates,597 'pred_5y': pred_5y,598 'pred_10y': pred_10y,599 'state_idx': state_idx,600 'age': age,601 'gender_val': gender_val,602 'sbp': sbp,603 'glucose': glucose,604 'waist': waist,605 'rbc': rbc,606 'wbc': wbc,607 'hi_ua_val': hi_ua_val,608 'pdh_hp_val': pdh_hp_val,609 'edu_high_val': edu_high_val,610 'patient_id': patient_id,611 'patient_name': patient_name612 }613 614 # 如果有保存的預測結果,就顯示615 if st.session_state.prediction_results is not None:616 results = st.session_state.prediction_results617 covariates = results['covariates']618 pred_5y = results['pred_5y']619 pred_10y = results['pred_10y']620 state_idx = results['state_idx']621 age = results['age']622 gender_val = results['gender_val']623 sbp = results['sbp']624 glucose = results['glucose']625 waist = results['waist']626 rbc = results['rbc']627 wbc = results['wbc']628 hi_ua_val = results['hi_ua_val']629 pdh_hp_val = results['pdh_hp_val']630 edu_high_val = results['edu_high_val']631 632 dial_5y = pred_5y['final_probs']['Dialysis'] * 100633 dial_10y = pred_10y['final_probs']['Dialysis'] * 100634 635 if dial_5y < 5:636 risk_level = "🟢 低風險"637 elif dial_5y < 15:638 risk_level = "🟡 中等風險"639 elif dial_5y < 30:640 risk_level = "🟠 高風險"641 else:642 risk_level = "🔴 極高風險"643 644 # 保存患者數據到 session state645 st.session_state.current_patient_data = {646 'patient_id': results.get('patient_id', patient_id),647 'patient_name': results.get('patient_name', patient_name),648 'age': age,649 'gender': gender_val,650 'gender_text': "男" if gender_val == 1 else "女",651 # 腎功能指標652 'egfr': egfr,653 'pro_coded': pro_coded_val,654 'pro_text': pro_coded.split("(")[0].strip(),655 # CKD 狀態656 'state': state_name,657 'state_idx': state_idx,658 'gfr_grade': gfr_grade,659 'albuminuria': albuminuria,660 'kdigo_class': f"{gfr_grade} {albuminuria}",661 # 臨床指標662 'hi_ua': hi_ua_val,663 'rbc': rbc,664 'pdh_hp': pdh_hp_val,665 'pdh_hp_text': "是" if pdh_hp_val == 1 else "否",666 'sbp': sbp,667 'waist': waist,668 'wbc': wbc,669 'glucose': glucose,670 'edu_high': edu_high_val,671 'edu_high_text': "高" if edu_high_val == 1 else "一般",672 # 風險評估673 'dial_5y': dial_5y,674 'dial_10y': dial_10y,675 'risk_level': risk_level676 }677 678 st.success("✅ 預測完成!")679 680 st.subheader("📊 預測結果")681 682 col1, col2, col3, col4 = st.columns(4)683 with col1:684 st.metric("當前狀態", st.session_state.current_patient_data['state'])685 with col2:686 st.metric("洗腎風險 (5年)", f"{dial_5y:.1f}%")687 with col3:688 st.metric("洗腎風險 (10年)", f"{dial_10y:.1f}%")689 with col4:690 st.metric("風險分層", risk_level)691 692 st.markdown("---")693 694 st.subheader("📈 轉移概率預測")695 696 col1, col2 = st.columns(2)697 698 with col1:699 states = ["Low", "Moderate", "High", "VeryHigh", "Dialysis"]700 probs_5y = [pred_5y['final_probs'][s] * 100 for s in states]701 702 fig_5y = go.Figure(data=[703 go.Bar(x=states, y=probs_5y, marker_color=['green', 'yellow', 'orange', 'red', 'darkred'])704 ])705 fig_5y.update_layout(title="5年狀態預測概率", xaxis_title="狀態", yaxis_title="概率 (%)", height=400, showlegend=False)706 fig_5y.update_xaxes(tickformat="%Y-%m-%d")707 st.plotly_chart(fig_5y, use_container_width=True)708 709 with col2:710 probs_10y = [pred_10y['final_probs'][s] * 100 for s in states]711 712 fig_10y = go.Figure(data=[713 go.Bar(x=states, y=probs_10y, marker_color=['green', 'yellow', 'orange', 'red', 'darkred'])714 ])715 fig_10y.update_layout(title="10年狀態預測概率", xaxis_title="狀態", yaxis_title="概率 (%)", height=400, showlegend=False)716 fig_10y.update_xaxes(tickformat="%Y-%m-%d")717 st.plotly_chart(fig_10y, use_container_width=True)718 719 st.markdown("---")720 st.success("✅ 患者數據已保存!現在可以去「💬 AI 諮詢」頁面跟 ChatGPT 討論這位患者!")721 722 st.markdown("---")723 724 # 協變數影響 - 民眾版本725 st.subheader("💊 哪些因素會影響洗腎風險?")726 727 q_params = predictor.q_params728 h4_idx = 3729 if len(q_params) > h4_idx:730 h4_params = q_params.iloc[h4_idx]731 cov_names = ['age_at_screening', 'hi_UA', 'RBC', 'PDH_HP', 'GENDER', 'sbp', 'waist', 'WBC', 'GLUCOSE', 'EDU_high']732 cov_labels = ['年齡', '高尿酸', '紅血球', '高血壓病史', '性別', '收縮壓', '腰圍', '白血球', '血糖', '教育程度']733 734 # 詳細說明735 factor_explanations = {736 '年齡': '年紀越大,腎臟功能衰退風險越高',737 '高尿酸': '尿酸升高會加重腎臟損傷,增加風險',738 '紅血球': '貧血(紅血球低)會加重腎臟缺氧,增加風險',739 '高血壓病史': '長期高血壓直接損傷腎臟,增加風險',740 '性別': '男性患者風險相對較高',741 '收縮壓': '血壓越高,對腎臟損傷越大,增加風險',742 '腰圍': '腹部肥胖增加腎臟負擔,增加風險',743 '白血球': '白血球低可能代表免疫功能差,增加風險;但過高可能代表感染,也會增加風險',744 '血糖': '血糖升高會損傷腎小球,增加風險',745 '教育程度': '教育程度高的患者自我管理能力強,能降低風險'746 }747 748 betas = [h4_params[f'beta_{c}'] for c in cov_names]749 750 # 建立易懂的資料框751 impact_list = []752 for label, beta in zip(cov_labels, betas):753 if beta > 0:754 impact = "🔴 增加風險"755 direction = "升高"756 else:757 impact = "🟢 降低風險"758 direction = "升高"759 760 impact_list.append({761 '健康指標': label,762 '影響': impact,763 '影響程度': abs(beta),764 '詳細說明': factor_explanations.get(label, ''),765 '方向': direction766 })767 768 impact_df = pd.DataFrame(impact_list).sort_values('影響程度', ascending=False)769 770 col1, col2 = st.columns(2)771 772 with col1:773 st.markdown("### 🔴 會增加洗腎風險的因素")774 increase = impact_df[impact_df['影響'] == "🔴 增加風險"][['健康指標', '詳細說明', '影響程度']].head(5)775 for idx, row in increase.iterrows():776 st.markdown(f"""777**{row['健康指標']}** (影響程度:{row['影響程度']:.2f})778- {row['詳細說明']}779 """)780 781 with col2:782 st.markdown("### 🟢 會降低洗腎風險的因素")783 decrease = impact_df[impact_df['影響'] == "🟢 降低風險"][['健康指標', '詳細說明', '影響程度']].head(5)784 for idx, row in decrease.iterrows():785 st.markdown(f"""786**{row['健康指標']}** (保護程度:{row['影響程度']:.2f})787- {row['詳細說明']}788 """)789 790 # 圖表791 fig_beta = px.bar(792 impact_df.head(10),793 x='影響程度',794 y='健康指標',795 color='影響',796 color_discrete_map={'🔴 增加風險': '#ff6b6b', '🟢 降低風險': '#51cf66'},797 title='各項健康指標對洗腎風險的影響程度',798 height=400,799 orientation='h',800 labels={'健康指標': '', '影響程度': '影響強度'},801 hover_data=['詳細說明']802 )803 fig_beta.update_layout(showlegend=False)804 st.plotly_chart(fig_beta, use_container_width=True)805 806 st.info(807 "💡 **怎麼理解這個圖?**\n\n"808 "• 🔴 **紅色柱子** = 這個因素會增加洗腎風險(數值越高越危險)\n"809 "• 🟢 **綠色柱子** = 這個因素會降低洗腎風險(需要維持在良好狀態)\n"810 "• **柱子越長** = 影響越大\n\n"811 "例如:\n"812 "- 白血球低 → 增加風險(需要提升白血球)\n"813 "- 紅血球低 → 增加風險(需要治療貧血)\n"814 "- 年齡增加 → 增加風險(無法改變,但可以強化其他管理)"815 )816 817 st.markdown("---")818 819 # 時間序列曲線820 st.subheader("📉 時間序列預測曲線")821 822 times_5y = pred_5y['times']823 probs_traj = pred_5y['probs']824 825 fig_traj = go.Figure()826 colors = ['green', 'yellow', 'orange', 'red', 'darkred']827 828 for i, state in enumerate(states):829 fig_traj.add_trace(go.Scatter(830 x=times_5y,831 y=probs_traj[:, i] * 100,832 mode='lines',833 name=state,834 line=dict(color=colors[i], width=2)835 ))836 837 fig_traj.update_layout(838 title="5年轉移概率時間序列",839 xaxis_title="時間 (年)",840 yaxis_title="概率 (%)",841 hovermode='x unified',842 height=450843 )844 st.plotly_chart(fig_traj, use_container_width=True)845 846 st.markdown("---")847 848 # 轉移速率表(摺疊)849 with st.expander("⚙️ 技術詳情 - 轉移速率", expanded=False):850 st.markdown("_僅供研究人員參考_")851 852 Q = pred_5y['Q']853 hazard_names = [854 'h1: Low→Moderate',855 'h2: Moderate→High',856 'h3: High→VeryHigh',857 'h4: VeryHigh→Dialysis',858 'b1: Moderate→Low',859 'b2: High→Moderate',860 'b3: VeryHigh→High'861 ]862 hazard_info = []863 864 hazards = [(0,1), (1,2), (2,3), (3,4), (1,0), (2,1), (3,2)]865 for i, (from_s, to_s) in enumerate(hazards):866 rate = Q[from_s, to_s]867 hazard_info.append({868 'Hazard': hazard_names[i],869 '速率 (/年)': f"{rate:.6f}",870 '倒數 (年)': f"{1/rate:.2f}" if rate > 0 else "∞"871 })872 873 hazard_df = pd.DataFrame(hazard_info)874 st.dataframe(hazard_df, use_container_width=True, hide_index=True)875 876 st.markdown("---")877 878 # 臨床決策建議879 st.subheader("💊 臨床決策建議")880 881 col1, col2 = st.columns(2)882 883 with col1:884 if dial_5y < 5:885 st.info("✅ **低風險** - 常規隨訪\n\n每年檢查一次,注意生活方式改善")886 elif dial_5y < 15:887 st.warning("🟡 **中等風險** - 密集隨訪\n\n建議半年檢查一次,強化營養諮詢和血壓控制")888 elif dial_5y < 30:889 st.error("🟠 **高風險** - 每季隨訪\n\n需要密集監測,準備洗腎通路評估")890 else:891 st.error("🔴 **極高風險** - 每月隨訪\n\n立即準備透析,考慮先制性介入")892 893 with col2:894 st.info(895 "📋 **建議檢驗項目:**\n\n"896 "• 血清肌酐 + eGFR\n"897 "• 尿蛋白/肌酐比\n"898 "• 電解質 (Na, K, Ca, P)\n"899 "• 血紅素 + 鐵代謝\n"900 "• 血糖 + 糖化血紅素"901 )902 903 st.markdown("---")904 905 # 介入效果模擬906 st.subheader("🎯 介入效果模擬")907 908 st.markdown("""909 **自訂參數調整,模擬介入效果**910 911 調整下方參數,系統會自動重新預測洗腎風險,看看介入後會改善多少!912 """)913 914 # 創建可調整的協變數副本915 intervention_covariates = covariates.copy()916 917 # 創建調整區域918 st.markdown("#### 📊 可介入參數調整")919 920 col1, col2, col3 = st.columns(3)921 922 # 只有可以通過介入改變的參數923 with col1:924 st.markdown("**血管與代謝**")925 sbp_adjusted = st.number_input(926 "收縮壓 (mmHg)",927 min_value=80.0,928 max_value=200.0,929 value=float(sbp),930 step=1.0,931 key="sbp_int",932 help="💊 可通過藥物或生活方式改變"933 )934 intervention_covariates['sbp'] = sbp_adjusted935 936 glucose_adjusted = st.number_input(937 "血糖 (mg/dL)",938 min_value=50.0,939 max_value=300.0,940 value=float(glucose),941 step=1.0,942 key="glucose_int",943 help="💊 可通過藥物或飲食改變"944 )945 intervention_covariates['GLUCOSE'] = glucose_adjusted946 947 with col2:948 st.markdown("**身體組成**")949 waist_adjusted = st.number_input(950 "腰圍 (cm)",951 min_value=60.0,952 max_value=150.0,953 value=float(waist),954 step=0.5,955 key="waist_int",956 help="💪 可通過減重改變"957 )958 intervention_covariates['waist'] = waist_adjusted959 960 rbc_adjusted = st.number_input(961 "紅血球 (RBC)",962 min_value=2.0,963 max_value=8.0,964 value=float(rbc),965 step=0.1,966 key="rbc_int",967 help="💊 可通過治療貧血改變"968 )969 intervention_covariates['RBC'] = rbc_adjusted970 971 with col3:972 st.markdown("**血球與代謝**")973 wbc_adjusted = st.number_input(974 "白血球 (WBC)",975 min_value=2.0,976 max_value=15.0,977 value=float(wbc),978 step=0.1,979 key="wbc_int",980 help="💊 可通過感染控制改變"981 )982 intervention_covariates['WBC'] = wbc_adjusted983 984 hi_ua_adjusted = st.selectbox(985 "高尿酸",986 ["否 (0)", "是 (1)"],987 index=hi_ua_val,988 key="hi_ua_int",989 help="💊 可通過藥物改變"990 )991 intervention_covariates['hi_UA'] = int(hi_ua_adjusted.split("(")[1].strip(")"))992 993 # 腎功能指標 - 可調整994 st.markdown("---")995 st.markdown("#### 🫘 腎功能指標調整(介入目標)")996 997 col1, col2 = st.columns(2)998 999 with col1:1000 egfr_adjusted = st.number_input(1001 "eGFR (mL/min/1.73m²)",1002 min_value=5.0,1003 max_value=120.0,1004 value=float(egfr),1005 step=1.0,1006 key="egfr_int",1007 help="🎯 介入目標:改善腎功能。藥物(如 ACEi/ARB)、控制血壓血糖可能改善。"1008 )1009 1010 with col2:1011 pro_adjusted = st.selectbox(1012 "尿蛋白",1013 ["陰性 (0)", "微量 (1)", "1+ (2)", "2+ (3)", "3+ (4)", "4+ (5)"],1014 index=min(pro_coded_val, 5),1015 key="pro_int",1016 help="🎯 介入目標:減少尿蛋白。血壓控制、ACEi/ARB、減重是關鍵。"1017 )1018 pro_adjusted_val = int(pro_adjusted.split("(")[1].strip(")"))1019 1020 # 自動重算 CKD 狀態1021 new_state_idx, new_state_name, new_gfr_grade, new_albuminuria = judge_ckd_state_kdigo(egfr_adjusted, pro_adjusted_val)1022 1023 # 顯示 CKD 狀態變化1024 st.markdown("---")1025 st.markdown("#### 📊 CKD 狀態變化預測")1026 1027 col1, col2, col3, col4 = st.columns(4)1028 1029 with col1:1030 st.info(f"""1031**當前狀態**1032{state_name}1033 1034**{gfr_grade} {albuminuria}**1035 """)1036 1037 with col2:1038 st.markdown("**→**")1039 st.write("") # 空白1040 1041 with col3:1042 if new_state_idx != state_idx:1043 st.success(f"""1044**介入後狀態**1045{new_state_name}1046 1047**{new_gfr_grade} {new_albuminuria}**1048 """)1049 else:1050 st.info(f"""1051**介入後狀態**1052{new_state_name}1053 1054**{new_gfr_grade} {new_albuminuria}**1055 """)1056 1057 with col4:1058 if new_state_idx < state_idx:1059 st.success(f"✅ 進展降級\n{state_name} → {new_state_name}")1060 elif new_state_idx > state_idx:1061 st.error(f"⚠️ 進展升級\n{state_name} → {new_state_name}")1062 else:1063 st.info(f"➡️ 狀態不變\n{state_name}")1064 1065 # 保持不變的參數(顯示但不可改)1066 st.markdown("---")1067 st.markdown("#### 📋 患者基本信息(無法改變)")1068 col1, col2, col3, col4 = st.columns(4)1069 1070 with col1:1071 st.metric("年齡", f"{age:.0f} 歲")1072 intervention_covariates['age_at_screening'] = age1073 with col2:1074 st.metric("性別", "男" if gender_val == 1 else "女")1075 intervention_covariates['GENDER'] = gender_val1076 with col3:1077 st.metric("高血壓病史", "是" if pdh_hp_val == 1 else "否")1078 intervention_covariates['PDH_HP'] = pdh_hp_val1079 with col4:1080 st.metric("教育程度", "高" if edu_high_val == 1 else "一般")1081 intervention_covariates['EDU_high'] = edu_high_val1082 # 模擬按鈕1083 if st.button("🎯 模擬介入效果", use_container_width=True, key="simulate_btn"):1084 # 使用新的 CKD 狀態進行預測(如果 eGFR/Pro 改變了)1085 pred_5y_int = predictor.predict(intervention_covariates, start_state=new_state_idx, years=5)1086 pred_10y_int = predictor.predict(intervention_covariates, start_state=new_state_idx, years=10)1087 1088 dial_5y_int = pred_5y_int['final_probs']['Dialysis'] * 1001089 dial_10y_int = pred_10y_int['final_probs']['Dialysis'] * 1001090 1091 # 計算改變1092 changes = {}1093 change_descriptions = []1094 1095 # 腎功能變化(最重要)1096 if egfr_adjusted != egfr:1097 change = egfr_adjusted - egfr1098 changes['eGFR'] = {1099 '原始': egfr,1100 '介入後': egfr_adjusted,1101 '變化': change1102 }1103 direction = "改善" if change > 0 else "惡化"1104 change_descriptions.append(f"🫘 eGFR {direction} {abs(change):.0f} (從 {egfr:.0f} 到 {egfr_adjusted:.0f} mL/min/1.73m²)")1105 1106 if pro_adjusted_val != pro_coded_val:1107 changes['尿蛋白'] = {1108 '原始': pro_coded_val,1109 '介入後': pro_adjusted_val,1110 '變化': pro_adjusted_val - pro_coded_val1111 }1112 direction = "減少" if pro_adjusted_val < pro_coded_val else "增加"1113 change_descriptions.append(f"🫘 尿蛋白{direction}: {pro_coded.split('(')[0].strip()} → {pro_adjusted.split('(')[0].strip()}")1114 1115 if new_state_idx != state_idx:1116 change_descriptions.append(f"🔄 CKD 狀態進展: {state_name} → {new_state_name}")1117 1118 if intervention_covariates['sbp'] != covariates['sbp']:1119 change = intervention_covariates['sbp'] - covariates['sbp']1120 changes['收縮壓'] = {1121 '原始': covariates['sbp'],1122 '介入後': intervention_covariates['sbp'],1123 '變化': change1124 }1125 change_descriptions.append(f"降低收縮壓 {abs(change):.0f} mmHg (從 {covariates['sbp']:.0f} 到 {intervention_covariates['sbp']:.0f})")1126 1127 if intervention_covariates['GLUCOSE'] != covariates['GLUCOSE']:1128 change = intervention_covariates['GLUCOSE'] - covariates['GLUCOSE']1129 changes['血糖'] = {1130 '原始': covariates['GLUCOSE'],1131 '介入後': intervention_covariates['GLUCOSE'],1132 '變化': change1133 }1134 change_descriptions.append(f"降低血糖 {abs(change):.0f} mg/dL (從 {covariates['GLUCOSE']:.0f} 到 {intervention_covariates['GLUCOSE']:.0f})")1135 1136 if intervention_covariates['waist'] != covariates['waist']:1137 change = intervention_covariates['waist'] - covariates['waist']1138 changes['腰圍'] = {1139 '原始': covariates['waist'],1140 '介入後': intervention_covariates['waist'],1141 '變化': change1142 }1143 change_descriptions.append(f"減少腰圍 {abs(change):.1f} cm (從 {covariates['waist']:.0f} 到 {intervention_covariates['waist']:.0f})")1144 1145 if intervention_covariates['RBC'] != covariates['RBC']:1146 change = intervention_covariates['RBC'] - covariates['RBC']1147 changes['紅血球'] = {1148 '原始': covariates['RBC'],1149 '介入後': intervention_covariates['RBC'],1150 '變化': change1151 }1152 change_descriptions.append(f"提升紅血球 {abs(change):.1f} (從 {covariates['RBC']:.1f} 到 {intervention_covariates['RBC']:.1f})")1153 1154 if intervention_covariates['WBC'] != covariates['WBC']:1155 change = intervention_covariates['WBC'] - covariates['WBC']1156 changes['白血球'] = {1157 '原始': covariates['WBC'],1158 '介入後': intervention_covariates['WBC'],1159 '變化': change1160 }1161 change_descriptions.append(f"調整白血球 {abs(change):.1f} (從 {covariates['WBC']:.1f} 到 {intervention_covariates['WBC']:.1f})")1162 1163 if intervention_covariates['hi_UA'] != covariates['hi_UA']:1164 changes['高尿酸'] = {1165 '原始': '是' if covariates['hi_UA'] == 1 else '否',1166 '介入後': '是' if intervention_covariates['hi_UA'] == 1 else '否',1167 }1168 change_descriptions.append(f"高尿酸狀態變為 {'是' if intervention_covariates['hi_UA'] == 1 else '否'}")1169 1170 # 保存介入結果到session_state1171 st.session_state.intervention_results = {1172 'changes': changes,1173 'change_descriptions': change_descriptions,1174 'original_dial_5y': dial_5y,1175 'original_dial_10y': dial_10y,1176 'intervention_dial_5y': dial_5y_int,1177 'intervention_dial_10y': dial_10y_int,1178 'improvement_5y': dial_5y - dial_5y_int,1179 'improvement_10y': dial_10y - dial_10y_int,1180 'improvement_5y_percent': ((dial_5y - dial_5y_int)/dial_5y*100) if dial_5y > 0 else 0,1181 'improvement_10y_percent': ((dial_10y - dial_10y_int)/dial_10y*100) if dial_10y > 0 else 0,1182 }1183 1184 st.markdown("---")1185 st.subheader("📊 介入效果對比")1186 1187 # 建立對比表1188 comparison_data = pd.DataFrame({1189 '指標': ['5年洗腎風險', '10年洗腎風險'],1190 '原始風險': [f"{dial_5y:.1f}%", f"{dial_10y:.1f}%"],1191 '介入後': [f"{dial_5y_int:.1f}%", f"{dial_10y_int:.1f}%"],1192 '改善幅度': [f"{dial_5y - dial_5y_int:.1f}%", f"{dial_10y - dial_10y_int:.1f}%"],1193 '改善百分比': [f"{((dial_5y - dial_5y_int)/dial_5y*100):.1f}%" if dial_5y > 0 else "N/A",1194 f"{((dial_10y - dial_10y_int)/dial_10y*100):.1f}%" if dial_10y > 0 else "N/A"]1195 })1196 1197 st.dataframe(comparison_data, use_container_width=True, hide_index=True)1198 1199 # 視覺化對比1200 col1, col2 = st.columns(2)