import gradio as gr import joblib import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') # Headless backend for Matplotlib import matplotlib.pyplot as plt import seaborn as sns import os import warnings warnings.filterwarnings('ignore') # ------------------------------------------------------------------------- # Load Model and Scaler # ------------------------------------------------------------------------- MODEL_PATH = "lightgbm_split2_best_model2.pkl" SCALER_PATH = "robust_scaler.pkl" try: model = joblib.load(MODEL_PATH) scaler = joblib.load(SCALER_PATH) print("Model and Scaler loaded successfully!") except Exception as e: print(f"Error loading model or scaler: {e}") raise e # Feature Definitions FEATURE_NAMES = [ 'Ammonia (mg/l)', 'Biochemical Oxygen Demand (mg/l)', 'Dissolved Oxygen (mg/l)', 'Orthophosphate (mg/l)', 'pH (ph units)', 'Temperature (cel)', 'Nitrogen (mg/l)', 'Nitrate (mg/l)' ] # Medians and IQRs from the RobustScaler MEDIANS = [0.066, 2.7, 10.2, 0.144, 7.78, 11.46, 4.98, 4.5] IQRS = [0.438, 1.34, 0.93, 0.247, 0.39, 5.1, 6.01, 4.25] IMPORTANCES = [3614, 3008, 873, 3838, 959, 707, 1371, 602] CLASSES = ['Excellent', 'Good', 'Fair', 'Marginal', 'Poor'] # ------------------------------------------------------------------------- # Explainable AI & Charting Helpers # ------------------------------------------------------------------------- def plot_probabilities(probs): """Generates a vertical bar chart of prediction confidence by class.""" colors = ['#10b981', '#34d399', '#f59e0b', '#f97316', '#ef4444'] # Excellent -> Poor fig, ax = plt.subplots(figsize=(6, 4), facecolor='none') ax.set_facecolor('none') bars = ax.bar(CLASSES, probs * 100, color=colors, width=0.5, edgecolor='none') # Customize grid and spines ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_color('#cbd5e1') ax.spines['bottom'].set_color('#cbd5e1') ax.tick_params(colors='#cbd5e1', labelsize=10) ax.grid(axis='y', linestyle='--', alpha=0.15, color='#cbd5e1') # Values on top of bars for bar in bars: height = bar.get_height() ax.annotate(f'{height:.1f}%', xy=(bar.get_x() + bar.get_width() / 2, height), xytext=(0, 4), textcoords="offset points", ha='center', va='bottom', color='#f8fafc', fontsize=9, fontweight='bold') ax.set_title("Class Confidence (%)", color='#06b6d4', fontsize=12, fontweight='bold', pad=12) ax.set_ylabel("Probability (%)", color='#cbd5e1', fontsize=10) ax.set_ylim(0, 115) plt.tight_layout() return fig def plot_feature_importance(): """Generates a horizontal bar chart of global feature importances.""" sorted_idx = np.argsort(IMPORTANCES) sorted_features = [FEATURE_NAMES[i] for i in sorted_idx] sorted_importances = [IMPORTANCES[i] for i in sorted_idx] fig, ax = plt.subplots(figsize=(6, 4.2), facecolor='none') ax.set_facecolor('none') # Horizontal bar plot bars = ax.barh(sorted_features, sorted_importances, color='#3b82f6', edgecolor='none', height=0.55) # Customize grid and spines ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_color('#cbd5e1') ax.spines['bottom'].set_color('#cbd5e1') ax.tick_params(colors='#cbd5e1', labelsize=10) ax.grid(axis='x', linestyle='--', alpha=0.15, color='#cbd5e1') # Value annotations on the right of the bars for bar in bars: width = bar.get_width() ax.annotate(f' {int(width)}', xy=(width, bar.get_y() + bar.get_height() / 2), xytext=(3, 0), textcoords="offset points", ha='left', va='center', color='#cbd5e1', fontsize=9) ax.set_title("Global Feature Importance (LightGBM)", color='#06b6d4', fontsize=12, fontweight='bold', pad=12) ax.set_xlabel("Split Importance Score", color='#cbd5e1', fontsize=10) plt.tight_layout() return fig def explain_prediction(inputs): """Calculates local feature contribution rankings based on deviations from normal medians.""" impacts = [] for i in range(8): val = inputs[i] med = MEDIANS[i] iqr = IQRS[i] # Calculate scaled deviation (impact) if FEATURE_NAMES[i] == 'Dissolved Oxygen (mg/l)': # Low DO is harmful impact = (med - val) / iqr if val < med: status = f"Low ({val:.2f} vs normal {med:.2f} mg/l)" sig = "⚠️ Reduces aquatic life support" color = "#ef4444" else: status = f"Optimal ({val:.2f} mg/l)" sig = "✅ High oxygenation, excellent health" color = "#10b981" elif FEATURE_NAMES[i] == 'pH (ph units)': # Extreme pH is harmful impact = abs(val - med) / iqr if val < 6.5: status = f"Acidic ({val:.2f} vs neutral {med:.2f})" sig = "⚠️ Acidic conditions degrade WQI" color = "#ef4444" elif val > 8.5: status = f"Alkaline ({val:.2f} vs neutral {med:.2f})" sig = "⚠️ Alkaline levels can be toxic" color = "#ef4444" else: status = f"Optimal ({val:.2f})" sig = "✅ Balanced neutral pH" color = "#10b981" else: # High values of chemical pollutants are harmful impact = (val - med) / iqr if val > med: status = f"High ({val:.2f} vs normal {med:.2f} mg/l)" sig = f"⚠️ Pollutant elevation (x{val/med:.1f})" if med > 0 else "⚠️ Elevated level" color = "#ef4444" if impact > 1 else "#f97316" else: status = f"Low/Normal ({val:.2f} mg/l)" sig = "✅ Safe background concentration" color = "#10b981" impacts.append({ 'feature': FEATURE_NAMES[i], 'val': val, 'impact': impact, 'status': status, 'sig': sig, 'color': color }) # Sort features by impact score descending sorted_impacts = sorted(impacts, key=lambda x: x['impact'], reverse=True) explanation_html = """
'{val}' is not a valid number for {FEATURE_NAMES[i]}. Please review your inputs.
Model Confidence: {confidence:.2f}%
{str(e)}
{error_details}"
# Hybrid Switch/Merge function for Sliders & Custom Text overrides
def predict_hybrid(
s_ammonia, s_bod, s_do, s_orthophosphate, s_ph, s_temp, s_nitrogen, s_nitrate,
t_ammonia, t_bod, t_do, t_orthophosphate, t_ph, t_temp, t_nitrogen, t_nitrate
):
def resolve_val(text_val, slider_val):
if text_val is not None and str(text_val).strip() != "":
return text_val.strip()
return slider_val
ammonia = resolve_val(t_ammonia, s_ammonia)
bod = resolve_val(t_bod, s_bod)
do = resolve_val(t_do, s_do)
orthophosphate = resolve_val(t_orthophosphate, s_orthophosphate)
ph = resolve_val(t_ph, s_ph)
temp = resolve_val(t_temp, s_temp)
nitrogen = resolve_val(t_nitrogen, s_nitrogen)
nitrate = resolve_val(t_nitrate, s_nitrate)
return predict_water_quality(ammonia, bod, do, orthophosphate, ph, temp, nitrogen, nitrate)
# -------------------------------------------------------------------------
# Build Gradio Interface (Glassmorphic Dashboard Layout)
# -------------------------------------------------------------------------
with open("style.css", "r") as f:
css_styles = f.read()
with gr.Blocks(title="Water Quality Prediction Dashboard", css=css_styles) as demo:
# ----- ہیڈر (Header) -----
with gr.Row(elem_classes=["glass-panel"], variant="compact"):
with gr.Column(scale=1, min_width=100):
gr.Image("assets/logo.png", show_label=False, container=False, height=90, width=90, elem_classes=["floating-logo"])
with gr.Column(scale=8):
gr.HTML("""
Evaluate ecosystem health, predict contamination risk, and analyze feature distributions using the pre-trained LightGBM classification model.
""") # ----- مین گرڈ (Main Grid) ----- with gr.Row(): # بائیں پینل (Inputs) with gr.Column(scale=5, elem_classes=["glass-panel"]): gr.HTML("Enter precise float values below. Any non-empty textbox here overrides its corresponding slider above.
") t_ammonia = gr.Textbox(placeholder="E.g., 0.05 (Default: 0.066)", label="Ammonia (mg/l)") t_bod = gr.Textbox(placeholder="E.g., 2.50 (Default: 2.7)", label="Biochemical Oxygen Demand (mg/l)") t_do = gr.Textbox(placeholder="E.g., 10.5 (Default: 10.2)", label="Dissolved Oxygen (mg/l)") t_orthophosphate = gr.Textbox(placeholder="E.g., 0.12 (Default: 0.144)", label="Orthophosphate (mg/l)") t_ph = gr.Textbox(placeholder="E.g., 7.6 (Default: 7.78)", label="pH (ph units)") t_temp = gr.Textbox(placeholder="E.g., 12.0 (Default: 11.46)", label="Temperature (cel)") t_nitrogen = gr.Textbox(placeholder="E.g., 4.5 (Default: 4.98)", label="Nitrogen (mg/l)") t_nitrate = gr.Textbox(placeholder="E.g., 4.0 (Default: 4.5)", label="Nitrate (mg/l)") # دائیں پینل (Outputs) with gr.Column(scale=5): with gr.Column(elem_classes=["glass-panel"], variant="panel"): gr.HTML("Click predict to run the explainable AI analysis.
") with gr.TabItem("📈 Global Feature Importance"): gr.Plot(value=plot_feature_importance(), label="Model Feature Importance") # ----- بٹن (Buttons) ----- with gr.Row(elem_classes=["top-margin"]): btn_clear = gr.Button("🔄 Reset to Defaults", elem_classes=["clear-btn"]) btn_predict = gr.Button("⚡ Predict Water Quality", elem_classes=["action-btn"]) # ----- ایونٹس (Events) ----- input_list_sliders = [s_ammonia, s_bod, s_do, s_orthophosphate, s_ph, s_temp, s_nitrogen, s_nitrate] input_list_text = [t_ammonia, t_bod, t_do, t_orthophosphate, t_ph, t_temp, t_nitrogen, t_nitrate] output_list = [out_badge, out_chart, out_image, out_explanation] btn_predict.click( fn=predict_hybrid, inputs=input_list_sliders + input_list_text, outputs=output_list, api_name="predict" ) def reset_inputs(): return [0.066, 2.7, 10.2, 0.144, 7.78, 11.46, 4.98, 4.5] + [""] * 8 btn_clear.click( fn=reset_inputs, inputs=[], outputs=input_list_sliders + input_list_text ) # ------------------------------------------------------------------------- # Launch App # ------------------------------------------------------------------------- if __name__ == "__main__": demo.launch(server_name="127.0.0.1", server_port=7860)