Spaces:
Sleeping
Sleeping
| """ | |
| EVB Prognosis Calculator - Production Gradio Application | |
| Integrates trained Random Forest model from research paper | |
| Research Paper: Rech MM, et al. Development and prospective validation of a machine learning | |
| model to predict mortality in cirrhosis with esophageal variceal bleeding. | |
| World J Hepatol 2025; In press | |
| Author: Matheus Machado Rech, MD | |
| Institution: Universidade de Caxias do Sul, Brazil | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| from datetime import datetime | |
| import json | |
| import math | |
| import joblib | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| # ============================================================================ | |
| # LOAD TRAINED MODEL | |
| # ============================================================================ | |
| import os | |
| # Try multiple locations for model files | |
| model_paths = [ | |
| 'calibrated_random_forest_model_updated_v1_1.joblib', # Same directory | |
| './calibrated_random_forest_model_updated_v1_1.joblib', | |
| '/mnt/user-data/outputs/calibrated_random_forest_model_updated_v1_1.joblib', # Claude outputs | |
| os.path.join(os.path.dirname(__file__), 'calibrated_random_forest_model_updated_v1_1.joblib'), | |
| ] | |
| preprocessor_paths = [ | |
| 'preprocessor_v1_0.joblib', | |
| './preprocessor_v1_0.joblib', | |
| '/mnt/user-data/outputs/preprocessor_v1_0.joblib', # Claude outputs | |
| os.path.join(os.path.dirname(__file__), 'preprocessor_v1_0.joblib'), | |
| ] | |
| MODEL = None | |
| PREPROCESSOR = None | |
| MODEL_LOADED = False | |
| for model_path in model_paths: | |
| if os.path.exists(model_path): | |
| try: | |
| MODEL = joblib.load(model_path) | |
| print(f"✓ Model loaded from: {model_path}") | |
| break | |
| except Exception as e: | |
| print(f"✗ Failed to load model from {model_path}: {e}") | |
| continue | |
| for prep_path in preprocessor_paths: | |
| if os.path.exists(prep_path): | |
| try: | |
| PREPROCESSOR = joblib.load(prep_path) | |
| print(f"✓ Preprocessor loaded from: {prep_path}") | |
| break | |
| except Exception as e: | |
| print(f"✗ Failed to load preprocessor from {prep_path}: {e}") | |
| continue | |
| if MODEL is not None and PREPROCESSOR is not None: | |
| MODEL_LOADED = True | |
| print("✓✓ Trained Random Forest model loaded successfully") | |
| else: | |
| print("⚠ Warning: Could not load trained model files") | |
| print(" Make sure calibrated_random_forest_model_updated_v1_1.joblib and preprocessor_v1_0.joblib") | |
| print(" are in the same directory as this script") | |
| print(" Falling back to heuristic model for demonstration") | |
| MODEL_LOADED = False | |
| # ============================================================================ | |
| # FEATURE PREPARATION FOR ML MODEL | |
| # ============================================================================ | |
| def prepare_features_for_model( | |
| age, sex, etiology, omeprazole, hrs, spironolactone, furosemide, | |
| beta_blocker, dialysis, pvt, ascites, hcc, encephalopathy, | |
| albumin, total_bili, inr, creatinine, platelet, ast, alt, | |
| hemoglobin, sodium, potassium, time_to_endo, varix_size, | |
| red_wall_marks, rupture_points, active_bleeding, terlipressin_dose | |
| ): | |
| """ | |
| Prepare features matching the 36-variable training dataset | |
| Features from paper (in order): | |
| Age, sex, cause, omeprazole, HRS, spironolactone, furosemide, beta-blocker, | |
| hemodialysis, PVT, ascites, HCC, encephalopathy, albumin, total bilirubin, | |
| direct bilirubin, PT, INR, creatinine, platelet, AST, ALT, hemoglobin, | |
| hematocrit, leucocytes, sodium, potassium, time-to-endoscopy, varix size, | |
| red wall marks, rupture points, active bleeding, therapy, terlipressin doses | |
| """ | |
| # Create feature dictionary | |
| features = { | |
| 'age': age, | |
| 'sex': 1 if sex == "Male" else 0, | |
| # Etiology (one-hot encoded) | |
| 'etiology_alcohol': 1 if etiology == "Alcohol" else 0, | |
| 'etiology_hcv': 1 if etiology == "HCV" else 0, | |
| 'etiology_hbv': 1 if etiology == "HBV" else 0, | |
| 'etiology_nash': 1 if etiology == "NASH" else 0, | |
| 'etiology_mixed': 1 if etiology == "Alcohol + HCV" else 0, | |
| 'etiology_other': 1 if etiology == "Other" else 0, | |
| # Medications | |
| 'omeprazole': 1 if omeprazole == "Yes" else 0, | |
| 'spironolactone': 1 if spironolactone == "Yes" else 0, | |
| 'furosemide': 1 if furosemide == "Yes" else 0, | |
| 'beta_blocker': 1 if beta_blocker == "Yes" else 0, | |
| # Clinical conditions | |
| 'hepatorenal_syndrome': 1 if hrs == "Yes" else 0, | |
| 'hemodialysis': 1 if dialysis == "Yes" else 0, | |
| 'portal_vein_thrombosis': 1 if pvt == "Yes" else 0, | |
| 'hepatocellular_carcinoma': 1 if hcc == "Yes" else 0, | |
| # Ascites (ordinal: 0=none, 1=mild, 2=severe) | |
| 'ascites': 0 if ascites == "None" else (1 if ascites == "Mild" else 2), | |
| # Encephalopathy (ordinal: 0=none, 1=mild, 2=severe) | |
| 'encephalopathy': 0 if encephalopathy == "None" else (1 if encephalopathy == "Mild" else 2), | |
| # Laboratory values | |
| 'albumin': albumin, | |
| 'total_bilirubin': total_bili, | |
| 'inr': inr, | |
| 'creatinine': creatinine, | |
| 'platelet': platelet, | |
| 'ast': ast, | |
| 'alt': alt, | |
| 'hemoglobin': hemoglobin, | |
| 'sodium': sodium, | |
| 'potassium': potassium, | |
| # Bleeding characteristics | |
| 'time_to_endoscopy': time_to_endo, | |
| 'varix_size': varix_size, # 1=small, 2=medium, 3=large | |
| 'red_wall_marks': 1 if red_wall_marks == "Yes" else 0, | |
| 'rupture_points': 1 if rupture_points == "Yes" else 0, | |
| 'active_bleeding': 1 if active_bleeding == "Yes" else 0, | |
| # Treatment | |
| 'terlipressin_dose': terlipressin_dose | |
| } | |
| return pd.DataFrame([features]) | |
| # ============================================================================ | |
| # ML MODEL PREDICTION | |
| # ============================================================================ | |
| def predict_with_ml_model(features_df): | |
| """ | |
| Use trained Random Forest model for prediction | |
| Returns: | |
| mortality_probability (float): 1-year mortality probability | |
| """ | |
| try: | |
| # Preprocess features | |
| X_processed = PREPROCESSOR.transform(features_df) | |
| # Get probability prediction | |
| mortality_prob = MODEL.predict_proba(X_processed)[0, 1] | |
| return float(mortality_prob) | |
| except Exception as e: | |
| print(f"Error in ML prediction: {e}") | |
| return None | |
| # ============================================================================ | |
| # CLINICAL SCORE CALCULATIONS | |
| # ============================================================================ | |
| def calculate_meld(bilirubin: float, inr: float, creatinine: float, dialysis: bool) -> int: | |
| """Calculate MELD score""" | |
| b = max(bilirubin, 1.0) | |
| i = max(inr, 1.0) | |
| c = 4.0 if dialysis else min(max(creatinine, 1.0), 4.0) | |
| meld = 3.78 * math.log(b) + 11.2 * math.log(i) + 9.57 * math.log(c) + 6.43 | |
| return max(min(round(meld), 40), 6) | |
| def calculate_meld_na(meld: int, sodium: float) -> int: | |
| """Calculate MELD-Na""" | |
| na = max(min(sodium, 137), 125) | |
| delta = 137 - na | |
| meld_na = meld + 1.32 * delta - (0.033 * meld * delta) | |
| return max(min(round(meld_na), 40), 6) | |
| def calculate_child_pugh(bilirubin: float, albumin: float, inr: float, | |
| ascites: str, encephalopathy: str) -> tuple: | |
| """Calculate Child-Pugh""" | |
| points = 0 | |
| points += 1 if bilirubin < 2 else (2 if bilirubin <= 3 else 3) | |
| points += 1 if albumin > 3.5 else (2 if albumin >= 2.8 else 3) | |
| points += 1 if inr < 1.7 else (2 if inr <= 2.3 else 3) | |
| ascites_map = {'none': 1, 'mild': 2, 'severe': 3} | |
| points += ascites_map.get(ascites, 1) | |
| enc_map = {'none': 1, 'mild': 2, 'severe': 3} | |
| points += enc_map.get(encephalopathy, 1) | |
| cp_class = 'A' if points <= 6 else ('B' if points <= 9 else 'C') | |
| return points, cp_class | |
| def calculate_albi(albumin: float, bilirubin: float) -> tuple: | |
| """Calculate ALBI""" | |
| score = (math.log10(bilirubin) * 0.66) + (albumin * -0.085) | |
| grade = '1' if score <= -2.60 else ('2' if score <= -1.39 else '3') | |
| return round(score, 2), grade | |
| # ============================================================================ | |
| # HEURISTIC FALLBACK (if ML model fails) | |
| # ============================================================================ | |
| def estimate_mortality_heuristic(meld_na, age, ascites, encephalopathy, | |
| active_bleeding, time_to_endo, hrs, cp_class): | |
| """Heuristic model as fallback""" | |
| base_risk = (meld_na - 6) / 34 * 0.65 | |
| age_weight = 0.08 if age > 65 else (0.12 if age > 75 else 0) | |
| ascites_weights = {'none': 0, 'mild': 0.06, 'severe': 0.12} | |
| enc_weights = {'none': 0, 'mild': 0.07, 'severe': 0.14} | |
| total_risk = ( | |
| base_risk + age_weight + | |
| ascites_weights.get(ascites, 0) + | |
| enc_weights.get(encephalopathy, 0) + | |
| (0.08 if active_bleeding == "Yes" else 0) + | |
| (min((time_to_endo - 12) / 36, 1) * 0.05 if time_to_endo > 12 else 0) + | |
| (0.12 if hrs == "Yes" else 0) + | |
| {'A': 0, 'B': 0.04, 'C': 0.08}.get(cp_class, 0) | |
| ) | |
| return max(min(total_risk, 0.98), 0.02) | |
| # ============================================================================ | |
| # MAIN CALCULATION FUNCTION | |
| # ============================================================================ | |
| def calculate_evb_risk( | |
| # Demographics | |
| age, sex, etiology, | |
| # Clinical | |
| ascites, encephalopathy, hrs, dialysis, pvt, hcc, | |
| # Medications | |
| omeprazole, spironolactone, furosemide, beta_blocker, | |
| # Lab values | |
| total_bili, albumin, inr, creatinine, sodium, potassium, | |
| platelet, ast, alt, hemoglobin, | |
| # Bleeding details | |
| active_bleeding, time_to_endo, varix_size, red_wall_marks, | |
| rupture_points, terlipressin_dose | |
| ): | |
| """Main calculation with ML model integration""" | |
| # Calculate clinical scores | |
| meld = calculate_meld(total_bili, inr, creatinine, dialysis == "Yes") | |
| meld_na = calculate_meld_na(meld, sodium) | |
| cp_score, cp_class = calculate_child_pugh(total_bili, albumin, inr, | |
| ascites.lower(), encephalopathy.lower()) | |
| albi_score, albi_grade = calculate_albi(albumin, total_bili) | |
| # Prepare features for ML model | |
| features_df = prepare_features_for_model( | |
| age, sex, etiology, omeprazole, hrs, spironolactone, furosemide, | |
| beta_blocker, dialysis, pvt, ascites, hcc, encephalopathy, | |
| albumin, total_bili, inr, creatinine, platelet, ast, alt, | |
| hemoglobin, sodium, potassium, time_to_endo, varix_size, | |
| red_wall_marks, rupture_points, active_bleeding, terlipressin_dose | |
| ) | |
| # Get mortality prediction | |
| if MODEL_LOADED: | |
| mortality_prob = predict_with_ml_model(features_df) | |
| model_type = "Trained Random Forest (AUC=0.91)" | |
| if mortality_prob is None: | |
| # Fallback if prediction fails | |
| mortality_prob = estimate_mortality_heuristic( | |
| meld_na, age, ascites.lower(), encephalopathy.lower(), | |
| active_bleeding, time_to_endo, hrs, cp_class | |
| ) | |
| model_type = "Heuristic Fallback" | |
| else: | |
| mortality_prob = estimate_mortality_heuristic( | |
| meld_na, age, ascites.lower(), encephalopathy.lower(), | |
| active_bleeding, time_to_endo, hrs, cp_class | |
| ) | |
| model_type = "Heuristic Model (Demo)" | |
| # Risk categorization | |
| if mortality_prob < 0.30: | |
| risk_category = "Low Risk" | |
| risk_color = "#00f2fe" | |
| bg_gradient = "rgba(0, 242, 254, 0.1)" | |
| elif mortality_prob < 0.60: | |
| risk_category = "Moderate Risk" | |
| risk_color = "#f9d423" | |
| bg_gradient = "rgba(249, 212, 35, 0.1)" | |
| else: | |
| risk_category = "High Risk" | |
| risk_color = "#ff4b2b" | |
| bg_gradient = "rgba(255, 75, 43, 0.1)" | |
| # Create result display | |
| results_html = f""" | |
| <div style='position: relative; padding: 40px; background: linear-gradient(135deg, {bg_gradient}, transparent); | |
| border: 2px solid rgba(255,255,255,0.1); border-radius: 24px; backdrop-filter: blur(20px); | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3);'> | |
| <div style='text-align: center; margin-bottom: 32px;'> | |
| <svg width="200" height="200" style='transform: rotate(-90deg);'> | |
| <circle cx="100" cy="100" r="90" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="12"/> | |
| <circle cx="100" cy="100" r="90" fill="none" stroke="{risk_color}" stroke-width="12" | |
| stroke-dasharray="{mortality_prob * 565} 565" stroke-linecap="round"/> | |
| </svg> | |
| <div style='margin-top: -140px;'> | |
| <div style='font-size: 3.5rem; font-weight: 300; color: {risk_color}; | |
| text-shadow: 0 0 20px {risk_color}50;'> | |
| {mortality_prob*100:.1f}% | |
| </div> | |
| <div style='font-size: 0.9rem; color: #999; margin-top: 8px; letter-spacing: 1px;'> | |
| 1-YEAR MORTALITY | |
| </div> | |
| <div style='font-size: 0.75rem; color: #666; margin-top: 4px;'> | |
| {model_type} | |
| </div> | |
| </div> | |
| </div> | |
| <div style='text-align: center;'> | |
| <span style='display: inline-block; padding: 12px 32px; background: {risk_color}; | |
| color: {"#000" if risk_category != "High Risk" else "#fff"}; | |
| border-radius: 24px; font-weight: 700; font-size: 1.1rem; | |
| letter-spacing: 1px; text-transform: uppercase; | |
| box-shadow: 0 4px 16px {risk_color}40;'> | |
| {risk_category} | |
| </span> | |
| </div> | |
| </div> | |
| """ | |
| # Scores table | |
| meld_mort = "<10%" if meld < 10 else ("10-19%" if meld < 20 else ("20-50%" if meld < 30 else ">50%")) | |
| scores_html = f""" | |
| <div style='margin-top: 24px; padding: 24px; background: rgba(255,255,255,0.02); | |
| border: 1px solid rgba(255,255,255,0.1); border-radius: 16px;'> | |
| <h3 style='color: #00d2ff; font-size: 0.85rem; text-transform: uppercase; | |
| letter-spacing: 2px; margin-bottom: 16px;'> | |
| 📊 Clinical Scores | |
| </h3> | |
| <table style='width: 100%; color: #fff; font-size: 0.9rem;'> | |
| <tr style='border-bottom: 1px solid rgba(255,255,255,0.05);'> | |
| <td style='padding: 12px 0; font-weight: 600;'>MELD</td> | |
| <td style='padding: 12px 0; color: #00d2ff; font-weight: 700; font-size: 1.2rem;'>{meld}</td> | |
| <td style='padding: 12px 0; color: #999; font-size: 0.85rem;'>3-month: {meld_mort}</td> | |
| </tr> | |
| <tr style='border-bottom: 1px solid rgba(255,255,255,0.05);'> | |
| <td style='padding: 12px 0; font-weight: 600;'>MELD-Na</td> | |
| <td style='padding: 12px 0; color: #00d2ff; font-weight: 700; font-size: 1.2rem;'>{meld_na}</td> | |
| <td style='padding: 12px 0; color: #999; font-size: 0.85rem;'>Sodium-adjusted</td> | |
| </tr> | |
| <tr style='border-bottom: 1px solid rgba(255,255,255,0.05);'> | |
| <td style='padding: 12px 0; font-weight: 600;'>Child-Pugh</td> | |
| <td style='padding: 12px 0; color: #92fe9d; font-weight: 700; font-size: 1.2rem;'>{cp_score} ({cp_class})</td> | |
| <td style='padding: 12px 0; color: #999; font-size: 0.85rem;'>{"Compensated" if cp_class == "A" else "Decompensated"}</td> | |
| </tr> | |
| <tr> | |
| <td style='padding: 12px 0; font-weight: 600;'>ALBI</td> | |
| <td style='padding: 12px 0; color: #f9d423; font-weight: 700; font-size: 1.2rem;'>{albi_score} (Grade {albi_grade})</td> | |
| <td style='padding: 12px 0; color: #999; font-size: 0.85rem;'>Liver function</td> | |
| </tr> | |
| </table> | |
| </div> | |
| """ | |
| # Export data | |
| export_data = { | |
| "timestamp": datetime.now().isoformat(), | |
| "model_used": model_type, | |
| "results": { | |
| "estimated_1yr_mortality": f"{mortality_prob*100:.1f}%", | |
| "risk_category": risk_category, | |
| "scores": {"MELD": meld, "MELD_Na": meld_na, "Child_Pugh": f"{cp_score} ({cp_class})", "ALBI": f"{albi_score} (Grade {albi_grade})"} | |
| } | |
| } | |
| return results_html, scores_html, json.dumps(export_data, indent=2) | |
| # ============================================================================ | |
| # PRESET SCENARIOS | |
| # ============================================================================ | |
| PRESETS = { | |
| "Compensated Cirrhosis (Child A)": { | |
| "age": 55, "total_bili": 1.2, "inr": 1.1, "creatinine": 0.9, | |
| "albumin": 3.8, "sodium": 140, "ascites": "None", "encephalopathy": "None", | |
| "active_bleeding": "No", "hrs": "No", "dialysis": "No", | |
| "time_to_endo": 8, "terlipressin_dose": 2, | |
| "platelet": 150, "ast": 45, "alt": 38, "hemoglobin": 13.5, "potassium": 4.2 | |
| }, | |
| "Decompensated Cirrhosis (Child B)": { | |
| "age": 62, "total_bili": 2.8, "inr": 1.6, "creatinine": 1.3, | |
| "albumin": 3.0, "sodium": 134, "ascites": "Mild", "encephalopathy": "None", | |
| "active_bleeding": "Yes", "hrs": "No", "dialysis": "No", | |
| "time_to_endo": 14, "terlipressin_dose": 2, | |
| "platelet": 95, "ast": 78, "alt": 62, "hemoglobin": 10.2, "potassium": 4.0 | |
| }, | |
| "Advanced Disease (Child C)": { | |
| "age": 58, "total_bili": 5.2, "inr": 2.4, "creatinine": 2.1, | |
| "albumin": 2.4, "sodium": 128, "ascites": "Severe", "encephalopathy": "Severe", | |
| "active_bleeding": "Yes", "hrs": "No", "dialysis": "No", | |
| "time_to_endo": 18, "terlipressin_dose": 4, | |
| "platelet": 62, "ast": 125, "alt": 95, "hemoglobin": 8.1, "potassium": 3.6 | |
| } | |
| } | |
| # ============================================================================ | |
| # GRADIO INTERFACE | |
| # ============================================================================ | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Rajdhani:wght@300;400;600;700&display=swap'); | |
| * { font-family: 'Rajdhani', sans-serif !important; } | |
| body { | |
| background: #0a0e14; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| background: transparent !important; | |
| } | |
| /* Vapor field animation */ | |
| @keyframes vaporMove { | |
| 0%, 100% { transform: translate(0, 0) scale(1); } | |
| 33% { transform: translate(-100px, -50px) scale(1.1); } | |
| 66% { transform: translate(50px, 100px) scale(0.9); } | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: | |
| radial-gradient(circle at 20% 50%, rgba(0, 210, 255, 0.15), transparent 25%), | |
| radial-gradient(circle at 80% 80%, rgba(146, 254, 157, 0.15), transparent 25%); | |
| animation: vaporMove 20s ease-in-out infinite; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .gradio-container { position: relative; z-index: 1; } | |
| .gr-box, .gr-form, .gr-input { | |
| background: rgba(255, 255, 255, 0.03) !important; | |
| backdrop-filter: blur(20px) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.1) !important; | |
| border-radius: 12px !important; | |
| } | |
| .gr-button-primary { | |
| background: linear-gradient(135deg, #00d2ff, #92fe9d) !important; | |
| border: none !important; | |
| font-weight: 700 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 2px !important; | |
| color: #000 !important; | |
| font-size: 1.1rem !important; | |
| padding: 16px 32px !important; | |
| box-shadow: 0 4px 20px rgba(0, 210, 255, 0.4) !important; | |
| } | |
| label { | |
| color: #fff !important; | |
| font-weight: 600 !important; | |
| } | |
| """ | |
| def create_app(): | |
| with gr.Blocks(css=custom_css, title="EVB Prognosis - ML Model", theme=gr.themes.Glass()) as app: | |
| # Header | |
| model_status = "✓ ML Model Active" if MODEL_LOADED else "⚠ Demo Mode" | |
| model_color = "#92fe9d" if MODEL_LOADED else "#f9d423" | |
| gr.HTML(f""" | |
| <div style='text-align: center; padding: 48px 0 32px 0;'> | |
| <div style='font-family: "Orbitron", sans-serif; font-size: 3.5rem; font-weight: 700; | |
| background: linear-gradient(135deg, #00d2ff 0%, #92fe9d 100%); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| letter-spacing: 4px; margin-bottom: 16px;'> | |
| EVB PROGNOSIS | |
| </div> | |
| <div style='font-size: 0.85rem; color: {model_color}; letter-spacing: 2px;'> | |
| {model_status} • Random Forest Classifier (AUC=0.91) | |
| </div> | |
| </div> | |
| """) | |
| # Disclaimer | |
| gr.HTML(""" | |
| <div style='margin: 0 auto 32px; max-width: 900px; padding: 20px; | |
| background: linear-gradient(135deg, rgba(255, 75, 43, 0.2), rgba(127, 29, 29, 0.3)); | |
| border: 2px solid #ff4b2b; border-radius: 16px; text-align: center;'> | |
| <div style='font-size: 1.2rem; color: #ff4b2b; font-weight: 700; margin-bottom: 8px;'> | |
| ⚠️ FOR EDUCATIONAL AND RESEARCH PURPOSES ONLY | |
| </div> | |
| <div style='font-size: 0.9rem; color: #ffb3b3;'> | |
| Not validated for clinical decision-making without physician supervision. | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| preset_dropdown = gr.Dropdown( | |
| choices=[""] + list(PRESETS.keys()), | |
| label="⚡ Load Clinical Scenario", | |
| value="" | |
| ) | |
| gr.Markdown("### 👤 Demographics") | |
| with gr.Row(): | |
| age = gr.Slider(18, 100, value=52, label="Age") | |
| sex = gr.Dropdown(["Male", "Female"], value="Male", label="Sex") | |
| etiology = gr.Dropdown( | |
| ["Alcohol", "HCV", "HBV", "NASH", "Alcohol + HCV", "Other"], | |
| value="Alcohol", label="Etiology" | |
| ) | |
| gr.Markdown("### 🏥 Clinical Status") | |
| with gr.Row(): | |
| ascites = gr.Dropdown(["None", "Mild", "Severe"], value="None", label="Ascites") | |
| encephalopathy = gr.Dropdown(["None", "Mild", "Severe"], value="None", label="Encephalopathy") | |
| with gr.Row(): | |
| hrs = gr.Dropdown(["No", "Yes"], value="No", label="Hepatorenal Syndrome") | |
| dialysis = gr.Dropdown(["No", "Yes"], value="No", label="Dialysis (≥2x/wk)") | |
| with gr.Row(): | |
| pvt = gr.Dropdown(["No", "Yes"], value="No", label="Portal Vein Thrombosis") | |
| hcc = gr.Dropdown(["No", "Yes"], value="No", label="Hepatocellular Carcinoma") | |
| gr.Markdown("### 💊 Medications") | |
| with gr.Row(): | |
| omeprazole = gr.Dropdown(["No", "Yes"], value="No", label="Omeprazole") | |
| spironolactone = gr.Dropdown(["No", "Yes"], value="No", label="Spironolactone") | |
| with gr.Row(): | |
| furosemide = gr.Dropdown(["No", "Yes"], value="No", label="Furosemide") | |
| beta_blocker = gr.Dropdown(["No", "Yes"], value="No", label="Beta-Blocker") | |
| gr.Markdown("### 🧪 Laboratory Values") | |
| with gr.Row(): | |
| total_bili = gr.Slider(0.1, 30, value=2.1, step=0.1, label="Total Bilirubin (mg/dL)") | |
| albumin = gr.Slider(1, 5, value=3.4, step=0.1, label="Albumin (g/dL)") | |
| with gr.Row(): | |
| inr = gr.Slider(0.5, 6, value=1.3, step=0.1, label="INR") | |
| creatinine = gr.Slider(0.1, 10, value=1.0, step=0.1, label="Creatinine (mg/dL)") | |
| with gr.Row(): | |
| sodium = gr.Slider(120, 150, value=138, label="Sodium (mEq/L)") | |
| potassium = gr.Slider(2.5, 6.5, value=4.0, step=0.1, label="Potassium (mEq/L)") | |
| with gr.Row(): | |
| platelet = gr.Slider(10, 500, value=150, label="Platelet (×10³/μL)") | |
| hemoglobin = gr.Slider(4, 20, value=12, step=0.1, label="Hemoglobin (g/dL)") | |
| with gr.Row(): | |
| ast = gr.Slider(10, 500, value=45, label="AST (U/L)") | |
| alt = gr.Slider(10, 500, value=38, label="ALT (U/L)") | |
| gr.Markdown("### 🩸 Bleeding Episode") | |
| with gr.Row(): | |
| active_bleeding = gr.Dropdown(["No", "Yes"], value="No", label="Active Bleeding") | |
| time_to_endo = gr.Number(value=12, label="Time to Endoscopy (hrs)") | |
| with gr.Row(): | |
| varix_size = gr.Slider(1, 3, value=2, step=1, label="Varix Size (1=small, 3=large)") | |
| terlipressin_dose = gr.Number(value=2, label="Terlipressin (mg)") | |
| with gr.Row(): | |
| red_wall_marks = gr.Dropdown(["No", "Yes"], value="No", label="Red Wall Marks") | |
| rupture_points = gr.Dropdown(["No", "Yes"], value="No", label="Rupture Points") | |
| with gr.Column(scale=1): | |
| calculate_btn = gr.Button("🔮 CALCULATE RISK", variant="primary", size="lg") | |
| results_display = gr.HTML() | |
| scores_display = gr.HTML() | |
| with gr.Accordion("📊 Export JSON", open=False): | |
| export_code = gr.Code(language="json") | |
| # Citation | |
| gr.HTML(""" | |
| <div style='text-align: center; margin-top: 48px; padding: 24px; border-top: 1px solid rgba(255,255,255,0.05);'> | |
| <div style='font-size: 0.85rem; color: #666;'> | |
| <strong style='color: #00d2ff;'>Research:</strong> Rech MM, et al. | |
| Development and prospective validation of a machine learning model to predict mortality | |
| in cirrhosis with esophageal variceal bleeding. <em>World J Hepatol</em> 2025; In press | |
| </div> | |
| </div> | |
| """) | |
| # Event handlers | |
| def load_preset(preset_name): | |
| if not preset_name or preset_name not in PRESETS: | |
| return [gr.update()] * 26 | |
| p = PRESETS[preset_name] | |
| return [ | |
| p.get("age", 52), p.get("sex", "Male"), p.get("etiology", "Alcohol"), | |
| p.get("ascites", "None"), p.get("encephalopathy", "None"), | |
| p.get("hrs", "No"), p.get("dialysis", "No"), p.get("pvt", "No"), p.get("hcc", "No"), | |
| p.get("omeprazole", "No"), p.get("spironolactone", "No"), | |
| p.get("furosemide", "No"), p.get("beta_blocker", "No"), | |
| p.get("total_bili", 2.1), p.get("albumin", 3.4), p.get("inr", 1.3), | |
| p.get("creatinine", 1.0), p.get("sodium", 138), p.get("potassium", 4.0), | |
| p.get("platelet", 150), p.get("hemoglobin", 12), p.get("ast", 45), p.get("alt", 38), | |
| p.get("active_bleeding", "No"), p.get("time_to_endo", 12), | |
| p.get("varix_size", 2), p.get("terlipressin_dose", 2), | |
| p.get("red_wall_marks", "No"), p.get("rupture_points", "No") | |
| ] | |
| preset_dropdown.change( | |
| load_preset, | |
| inputs=[preset_dropdown], | |
| outputs=[age, sex, etiology, ascites, encephalopathy, hrs, dialysis, pvt, hcc, | |
| omeprazole, spironolactone, furosemide, beta_blocker, | |
| total_bili, albumin, inr, creatinine, sodium, potassium, | |
| platelet, hemoglobin, ast, alt, active_bleeding, time_to_endo, | |
| varix_size, terlipressin_dose, red_wall_marks, rupture_points] | |
| ) | |
| calculate_btn.click( | |
| calculate_evb_risk, | |
| inputs=[age, sex, etiology, ascites, encephalopathy, hrs, dialysis, pvt, hcc, | |
| omeprazole, spironolactone, furosemide, beta_blocker, | |
| total_bili, albumin, inr, creatinine, sodium, potassium, | |
| platelet, hemoglobin, ast, alt, active_bleeding, time_to_endo, | |
| varix_size, terlipressin_dose, red_wall_marks, rupture_points], | |
| outputs=[results_display, scores_display, export_code] | |
| ) | |
| return app | |
| if __name__ == "__main__": | |
| app = create_app() | |
| app.launch() | |