import streamlit as st
import numpy as np
import pandas as pd
import pickle
MODEL_PATH = "HantaVarNet.pkl"
@st.cache_resource
def load_model():
with open("HantaVarNet.pkl", "rb") as f:
package = pickle.load(f)
return package["model"] if isinstance(package, dict) else package
# Clinical knowledge: display labels, units, normal ranges
FEATURE_INFO = {
"Creatinine_mg/dL": ("Kreatinin", "mg/dL", 0.6, 1.2, "high"),
"ALT_U/L": ("ALT (SGPT)", "U/L", 7.0, 56.0, "high"),
"AST_U/L": ("AST (SGOT)", "U/L", 10.0, 40.0, "high"),
"BUN_mg/dL": ("BUN", "mg/dL", 7.0, 25.0, "high"),
"Platelet_Count_K/uL": ("Trombosit", "K/Β΅L", 150, 400, "low"),
}
# Exact thresholds
TREE_THRESHOLDS = {
"Creatinine_mg/dL": 1.35,
"ALT_U/L": 65.95,
"AST_U/L": 78.00,
"BUN_mg/dL": 39.05,
"Platelet_Count_K/uL": 146.14,
}
# Recommendation catalogue per feature
RECOMMENDATIONS = {
"Creatinine_mg/dL": {
"icon": "π«",
"title": "Evaluasi Fungsi Ginjal",
"color": "orange",
"items": [
"Perbanyak konsumsi air putih (min. 2 liter/hari) untuk mendukung ekskresi kreatinin.",
"Hindari penggunaan obat nefrotoksik (NSAID, aminoglikosida) tanpa pengawasan dokter.",
"Lakukan pemeriksaan urinalisis, GFR, dan USG ginjal untuk evaluasi menyeluruh.",
"Pertimbangkan konsultasi dokter spesialis nefrologi.",
],
},
"ALT_U/L": {
"icon": "π«",
"title": "Jaga Kesehatan Hati",
"color": "red",
"items": [
"Hindari konsumsi alkohol dan makanan tinggi lemak jenuh / makanan ultraproses.",
"Batasi obat-obatan hepatotoksik (parasetamol dosis tinggi, statin) tanpa resep.",
"Konsumsi sayuran hijau, buah-buahan, dan protein rendah lemak.",
"Lakukan pemeriksaan USG abdomen untuk menilai kondisi hati.",
],
},
"AST_U/L": {
"icon": "π¬",
"title": "Pemeriksaan Lanjutan Hati",
"color": "red",
"items": [
"Lakukan panel fungsi hati lengkap (LFT): ALT, AST, GGT, bilirubin, albumin.",
"Istirahat cukup dan hindari aktivitas fisik berat sementara waktu.",
"Monitor gejala seperti ikterus (kuning), mual, atau nyeri perut kanan atas.",
"Konsultasikan dengan dokter spesialis gastrohepatologi.",
],
},
"BUN_mg/dL": {
"icon": "π§",
"title": "Hidrasi & Evaluasi Ginjal",
"color": "orange",
"items": [
"Tingkatkan asupan cairan untuk membantu ekskresi urea oleh ginjal.",
"Kurangi sementara konsumsi protein hewani berlebih (daging merah, seafood).",
"Pantau output urin harian β penurunan volume bisa menjadi tanda perburukan.",
"Periksa rasio BUN/Kreatinin untuk membedakan penyebab pre-renal vs. renal.",
],
},
"Platelet_Count_K/uL": {
"icon": "π©Έ",
"title": "Pemeriksaan Darah Lanjutan",
"color": "purple",
"items": [
"Lakukan pemeriksaan darah lengkap (CBC) menyeluruh termasuk morfologi trombosit.",
"Hindari aspirin, ibuprofen, dan antikoagulan tanpa saran dokter.",
"Waspadai tanda perdarahan: lebam berlebih, mimisan, gusi berdarah, atau petekia.",
"Konsultasikan dengan dokter spesialis hematologi untuk evaluasi lebih lanjut.",
],
},
}
# CSS
PAGE_CSS = """
"""
ALL_SYMPTOMS = ["Abdominal Pain","Asymptomatic","Cough","Dyspnea",
"Fatigue","Fever","Headache","Missing","Muscle Aches"]
GENDER_OPTIONS = ["Male","Female"]
REGION_OPTIONS = ["Asia","Europe","North America","South America"]
EXPOSURE_OPTIONS = ["Agricultural","Camping","Dust Exposure",
"Occupational","Rodent Contact","Unknown"]
def build_feature_vector(age, symptom_count, wbc, platelet, alt, ast,
bun, creatinine, gender, region, exposure, symptoms):
num = {
"Age": age, "Symptom_Count": symptom_count,
"WBC_Count_K/uL": wbc, "Platelet_Count_K/uL": platelet,
"ALT_U/L": alt, "AST_U/L": ast,
"BUN_mg/dL": bun, "Creatinine_mg/dL": creatinine,
}
symp_vec = {s: int(s in symptoms) for s in ALL_SYMPTOMS}
gender_vec = {"Gender_F": int(gender=="Female"), "Gender_M": int(gender=="Male")}
region_vec = {
"Region_Asia": int(region=="Asia"),
"Region_Europe": int(region=="Europe"),
"Region_North America": int(region=="North America"),
"Region_South America": int(region=="South America"),
}
exposure_vec = {
"Exposure_Type_Agricultural": int(exposure=="Agricultural"),
"Exposure_Type_Camping": int(exposure=="Camping"),
"Exposure_Type_Dust Exposure": int(exposure=="Dust Exposure"),
"Exposure_Type_Occupational": int(exposure=="Occupational"),
"Exposure_Type_Rodent Contact": int(exposure=="Rodent Contact"),
"Exposure_Type_Unknown": int(exposure=="Unknown"),
}
return pd.DataFrame([{**num, **symp_vec, **gender_vec, **region_vec, **exposure_vec}])
def conf_bar(label, pct, cls):
return f"""
"""
def get_decision_path(model, X):
"""
Telusuri jalur node yang dilewati model untuk input X.
Hanya tampilkan node yang menggunakan fitur klinis utama (FEATURE_INFO).
"""
try:
tree_clf = model.estimator_
feat_names = list(X.columns)
tree_ = tree_clf.tree_
node_indicator = tree_clf.decision_path(X)
leaf_id = tree_clf.apply(X)
node_ids = node_indicator.indices[
node_indicator.indptr[0]:node_indicator.indptr[1]
]
steps = []
for node_id in node_ids:
if leaf_id[0] == node_id:
continue # skip leaf
feat_key = feat_names[tree_.feature[node_id]]
if feat_key not in FEATURE_INFO:
continue # skip non-clinical features
thresh = round(float(tree_.threshold[node_id]), 2)
val = round(float(X.iloc[0][feat_key]), 2)
went_left = val <= thresh
_, unit, _, _, direction = FEATURE_INFO[feat_key]
disp_name = FEATURE_INFO[feat_key][0]
# "is_risk": did this value cross toward the risk side?
is_risk = (not went_left) if direction == "high" else went_left
steps.append({
"name": disp_name,
"unit": unit,
"threshold": thresh,
"value": val,
"went_left": went_left,
"is_risk": is_risk,
})
return steps
except Exception:
return []
def get_triggered_features(creatinine, alt, ast, bun, platelet):
"""Return list of feature keys whose values cross the primary tree threshold."""
checks = {
"Creatinine_mg/dL": (creatinine, TREE_THRESHOLDS["Creatinine_mg/dL"], "high"),
"ALT_U/L": (alt, TREE_THRESHOLDS["ALT_U/L"], "high"),
"AST_U/L": (ast, TREE_THRESHOLDS["AST_U/L"], "high"),
"BUN_mg/dL": (bun, TREE_THRESHOLDS["BUN_mg/dL"], "high"),
"Platelet_Count_K/uL": (platelet, TREE_THRESHOLDS["Platelet_Count_K/uL"], "low"),
}
return [
feat for feat, (val, thresh, direction) in checks.items()
if (direction == "high" and val > thresh) or (direction == "low" and val < thresh)
]
def render_decision_path_html(steps):
if not steps:
return ""
html = 'Jalur Keputusan Model
'
for i, s in enumerate(steps):
cls = "triggered" if s["is_risk"] else "normal"
icon = "β οΈ" if s["is_risk"] else "β
"
op = "β€" if s["went_left"] else ">"
result_lbl = "MELEWATI AMBANG" if s["is_risk"] else "DALAM BATAS"
html += f"""
{icon}
{s['name']}
{s['value']} {s['unit']}
{op} {s['threshold']} {s['unit']}
{result_lbl}
"""
if i < len(steps) - 1:
html += '
'
html += "
"
return html
def render_param_chips_html(creatinine, alt, ast, bun, platelet, triggered):
params = [
("Creatinine_mg/dL", creatinine),
("ALT_U/L", alt),
("AST_U/L", ast),
("BUN_mg/dL", bun),
("Platelet_Count_K/uL", platelet),
]
chips = ""
for feat, val in params:
name, unit, _, _, _ = FEATURE_INFO[feat]
cls = "risk" if feat in triggered else "ok"
badge = "β οΈ" if feat in triggered else "β
"
chips += f'{badge} {name} {val} {unit}
'
return f'{chips}
'
def render_recommendations_html(triggered):
if not triggered:
return (
'β
'
'Semua parameter klinis utama berada dalam batas ambang tree model. '
'Tidak ada rekomendasi khusus yang diperlukan saat ini.
'
)
html = ""
for feat in triggered:
rec = RECOMMENDATIONS[feat]
items = "".join(f'{it}
' for it in rec["items"])
html += f"""
{items}
"""
return html
# Main page
def render(goto):
st.markdown(PAGE_CSS, unsafe_allow_html=True)
if st.button("β Kembali ke Home", key="back_var"):
goto("home")
st.markdown("""
""", unsafe_allow_html=True)
# Numerik
st.markdown('π Variabel Numerik', unsafe_allow_html=True)
c1, c2, c3, c4 = st.columns(4)
with c1: age = st.number_input("Age (tahun)", min_value=5, max_value=90, value=30, step=1)
with c2: symptom_count = st.number_input("Symptom Count", min_value=0, max_value=5, value=2, step=1)
with c3: wbc = st.number_input("WBC Count (K/uL)", min_value=1.0, max_value=20.0, value=7.0, step=0.1, format="%.1f")
with c4: platelet = st.number_input("Platelet Count (K/uL)", min_value=1.0, max_value=500.0, value=200.0, step=1.0, format="%.1f")
c5, c6, c7, c8 = st.columns(4)
with c5: alt = st.number_input("ALT (U/L)", min_value=5.0, max_value=300.0, value=35.0, step=1.0, format="%.1f")
with c6: ast = st.number_input("AST (U/L)", min_value=5.0, max_value=300.0, value=35.0, step=1.0, format="%.1f")
with c7: bun = st.number_input("BUN (mg/dL)", min_value=5.0, max_value=100.0, value=15.0, step=0.5, format="%.1f")
with c8: creatinine = st.number_input("Creatinine (mg/dL)", min_value=0.1, max_value=10.0, value=1.0, step=0.1, format="%.1f")
# Gejala
st.markdown('π€ Gejala Klinis', unsafe_allow_html=True)
symptoms = st.multiselect(
"Pilih semua gejala yang dialami pasien (boleh lebih dari satu)",
options=ALL_SYMPTOMS, default=[],
placeholder="Klik untuk memilih gejalaβ¦",
)
# Kategorikal
st.markdown('ποΈ Data Kategorikal', unsafe_allow_html=True)
cg, cr, ce = st.columns(3)
with cg: gender = st.selectbox("Gender", options=GENDER_OPTIONS)
with cr: region = st.selectbox("Region", options=REGION_OPTIONS)
with ce: exposure = st.selectbox("Exposure Type", options=EXPOSURE_OPTIONS)
# Predict
st.markdown("", unsafe_allow_html=True)
_, btn_col, _ = st.columns([1, 4, 1])
with btn_col:
predict_clicked = st.button("Jalankan Prediksi", key="predict_var", use_container_width=True)
if not predict_clicked:
return
# Load & predict
try:
model = load_model()
except FileNotFoundError:
st.error(f"Model tidak ditemukan di: {MODEL_PATH}")
return
X = build_feature_vector(age, symptom_count, wbc, platelet,
alt, ast, bun, creatinine,
gender, region, exposure, symptoms)
fn = None
if hasattr(model, "feature_names_in_"):
fn = model.feature_names_in_
elif hasattr(model, "estimator_") and hasattr(model.estimator_, "feature_names_in_"):
fn = model.estimator_.feature_names_in_
if fn is not None:
for col in fn:
if col not in X.columns:
X[col] = 0
X = X[fn]
pred = model.predict(X)[0]
proba = model.predict_proba(X)[0]
is_pos = int(pred) == 1
wrap_cls = "positive" if is_pos else "negative"
verd_cls = "pos" if is_pos else "neg"
verdict_txt = "POSITIF HANTAVIRUS" if is_pos else "NEGATIF HANTAVIRUS"
verdict_sub = (
"Pasien terindikasi terinfeksi Hantavirus. Segera lakukan konfirmasi laboratorium lebih lanjut."
if is_pos else
"Tidak terdeteksi indikasi infeksi Hantavirus berdasarkan data klinis yang diinput."
)
# 1. Prediction card
st.markdown(f"""
Hasil Prediksi Β· HantaVarNet (Decision Tree + Threshold Tuning)
{verdict_txt}
{verdict_sub}
Confidence Score
{conf_bar("Negatif", proba[0]*100, "neg")}
{conf_bar("Positif", proba[1]*100, "pos")}
""", unsafe_allow_html=True)
# 2. Decision path
st.markdown('πΏ Alasan Prediksi β Jalur Tree', unsafe_allow_html=True)
path_steps = get_decision_path(model, X)
if path_steps:
st.markdown(render_decision_path_html(path_steps), unsafe_allow_html=True)
else:
st.info("Jalur keputusan tidak tersedia untuk input ini.")
# 3. Parameter chips
triggered = get_triggered_features(creatinine, alt, ast, bun, platelet)
st.markdown('π Ringkasan Parameter', unsafe_allow_html=True)
st.markdown(render_param_chips_html(creatinine, alt, ast, bun, platelet, triggered), unsafe_allow_html=True)
# 4. Recommendations
st.markdown('π Rekomendasi Klinis', unsafe_allow_html=True)
st.markdown(render_recommendations_html(triggered), unsafe_allow_html=True)
# 5. Disclaimer
st.markdown("""
β οΈ Perhatian: Hasil prediksi dan rekomendasi ini bersifat indikatif dan
tidak menggantikan diagnosis klinis oleh tenaga medis profesional.
Rekomendasi dihasilkan berdasarkan aturan ambang batas dari Decision Tree yang ditraining,
bukan dari evaluasi medis langsung. Gunakan sebagai alat bantu skrining awal saja.
""", unsafe_allow_html=True)