smartTranscend's picture
Update app.py
f0b1f62 verified
Raw
History Blame Contribute Delete
90.2 kB
# -*- 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("""
<style>
.metric-card {
background-color: #f0f2f6;
padding: 20px;
border-radius: 10px;
margin: 10px 0;
}
</style>
""", 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("<p style='text-align: center; color: gray;'>© 2026 多階段CKD疾病進展機器學習預測模型 | 完整最終版</p>", unsafe_allow_html=True)