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"""
{label}
{pct:.1f}%
""" 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"""
{rec['icon']} {rec['title']}
{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)