# app.py - Gradio app to serve a saved sklearn pipeline (loan_pipeline.pkl) # Robust, commented, and production-friendly. import joblib import pandas as pd import gradio as gr import traceback from typing import Tuple, Dict, Any MODEL_PATH = "loan_pipeline.pkl" # file must be in the same repo root # -------------------------- # Load model safely (with helpful error message) # -------------------------- try: model = joblib.load(MODEL_PATH) except Exception as e: # If the model fails to load (common: sklearn version mismatch), # store the exception so the UI can display an informative error. model = None load_error = traceback.format_exc() else: load_error = None # -------------------------- # Utility: safe conversion helper # -------------------------- def safe_cast(value: Any, to_type, default=None): """ Try to cast `value` to `to_type`. If fails, return `default`. Keeps app from crashing when users type bad input. """ try: return to_type(value) except Exception: return default # -------------------------- # Prediction function # -------------------------- def predict_loan(age, income, employment, credit_score, dependents, loan_amount, purpose, existing_debt, marital_status, education_level, default_history) -> Tuple[str, Dict[str, float]]: """ Build one-row DataFrame from inputs, run model.predict & predict_proba, then return (decision_text, probabilities_map). Return value shape matches Gradio outputs: (Textbox, Label). """ # If model failed to load, return error message if model is None: # Return error text + empty label mapping return ("ERROR: Model load failed. See server logs.", {"ERROR": 1.0}) # Coerce numeric inputs safely age_i = safe_cast(age, int, None) income_f = safe_cast(income, float, None) credit_score_i = safe_cast(credit_score, int, None) dependents_i = safe_cast(dependents, int, None) loan_amount_f = safe_cast(loan_amount, float, None) existing_debt_f = safe_cast(existing_debt, float, None) default_history_i = safe_cast(default_history, int, None) # Basic validation: ensure required numeric fields are present missing_inputs = [] for name, val in [ ("age", age_i), ("income", income_f), ("credit_score", credit_score_i), ("dependents", dependents_i), ("loan_amount", loan_amount_f), ("existing_debt", existing_debt_f), ("default_history", default_history_i) ]: if val is None: missing_inputs.append(name) if missing_inputs: message = f"Invalid or missing numeric inputs: {', '.join(missing_inputs)}" # Return a user-friendly error message plus a label map so Gradio doesn't crash. return (f"INPUT ERROR: {message}", {"ERROR": 1.0}) # Build the row dict exactly matching training column names row = { 'age': age_i, 'income': income_f, 'employment': employment, 'credit_score': credit_score_i, 'dependents': dependents_i, 'loan_amount': loan_amount_f, 'purpose': purpose, 'existing_debt': existing_debt_f, 'marital_status': marital_status, 'education_level': education_level, 'default_history': default_history_i } # Create DataFrame (single-row) - pipeline expects DataFrame with same columns try: df = pd.DataFrame([row]) except Exception as e: return (f"ERROR building DataFrame: {e}", {"ERROR": 1.0}) # Run model prediction inside try/except to catch runtime issues try: pred = int(model.predict(df)[0]) # 0 or 1 proba = model.predict_proba(df)[0] # [prob_no, prob_yes] except Exception as e: # If predict fails, return readable error tb = traceback.format_exc() return (f"PREDICTION ERROR: {str(e)}", {"ERROR": 1.0}) # Create human-friendly outputs decision = "YES" if pred == 1 else "NO" probs_map = {"NO": round(float(proba[0]), 4), "YES": round(float(proba[1]), 4)} return (f"Decision: {decision}", probs_map) # -------------------------- # Build Gradio UI # -------------------------- title = "Loan Approval Demo" description = ( "Enter applicant details and get a prediction from the saved pipeline.\n\n" "If the model fails to load or predict, a helpful error message will appear here." ) # Input widgets. Keep values and types aligned with training data. inputs = [ gr.Number(label="age", value=30, precision=0), gr.Number(label="income", value=50000.0), gr.Dropdown(label="employment", choices=["employed","self-employed","unemployed","contract"], value="employed"), gr.Number(label="credit_score", value=650, precision=0), gr.Number(label="dependents", value=0, precision=0), gr.Number(label="loan_amount", value=100000.0), gr.Dropdown(label="purpose", choices=["home","car","education","personal","business"], value="personal"), gr.Number(label="existing_debt", value=0.0), gr.Dropdown(label="marital_status", choices=["single","married","divorced","widowed"], value="single"), gr.Dropdown(label="education_level", choices=["other","highschool","bachelor","master","phd"], value="bachelor"), gr.Dropdown(label="default_history", choices=[0,1], value=0) ] # Outputs: Textbox for readable decision + Label for probabilities outputs = [ gr.Textbox(label="Decision"), gr.Label(num_top_classes=2, label="Probabilities") ] iface = gr.Interface( fn=predict_loan, inputs=inputs, outputs=outputs, title=title, description=description, ) # -------------------------- # Launch the app (only when run as script) # -------------------------- if __name__ == "__main__": iface.launch()