Tariq349's picture
Update app.py
bff2edc verified
Raw
History Blame Contribute Delete
18.9 kB
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 = """
<div style='margin-top: 15px;'>
<h4 style='color: var(--primary-cyan); margin-bottom: 12px; font-size: 1.15rem; font-weight: 600;'>Local Feature Contribution Rankings</h4>
<div style='display: flex; flex-direction: column; gap: 10px;'>
"""
for item in sorted_impacts:
feat = item['feature']
status = item['status']
sig = item['sig']
color = item['color']
explanation_html += f"""
<div style='background: rgba(255,255,255,0.02); border-left: 4px solid {color}; padding: 8px 12px; border-radius: 4px;'>
<div style='display: flex; justify-content: space-between; font-size: 0.95rem;'>
<span style='font-weight: 500; color: #f8fafc;'>{feat}</span>
<span style='color: {color}; font-weight: 600;'>{status}</span>
</div>
<div style='font-size: 0.85rem; color: #94a3b8; margin-top: 2px;'>{sig}</div>
</div>
"""
explanation_html += """
</div>
</div>
"""
return explanation_html
# -------------------------------------------------------------------------
# Core Prediction Routine
# -------------------------------------------------------------------------
def predict_water_quality(ammonia, bod, do, orthophosphate, ph, temp, nitrogen, nitrate):
try:
inputs = [ammonia, bod, do, orthophosphate, ph, temp, nitrogen, nitrate]
# Validation & Null Imputation
cleaned_inputs = []
for i, val in enumerate(inputs):
# Check for empty string, None or NaN
if val is None or str(val).strip() == "" or (isinstance(val, float) and np.isnan(val)):
cleaned_inputs.append(MEDIANS[i]) # Impute with median
else:
try:
cleaned_inputs.append(float(val))
except ValueError:
# Graceful validation error
error_html = f"""
<div style='text-align: center; padding: 15px; border-radius: 8px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3);'>
<h4 style='color: #ef4444; margin: 0;'>⚠️ Input Validation Error</h4>
<p style='color: #cbd5e1; margin: 5px 0 0 0; font-size: 0.95rem;'>
'{val}' is not a valid number for <b>{FEATURE_NAMES[i]}</b>. Please review your inputs.
</p>
</div>
"""
return error_html, None, "assets/poor.jpg", "<div style='color:#ef4444;'>Validation failed. Please enter numeric values.</div>"
# Scale parameters using robust scaler
inputs_df = pd.DataFrame([cleaned_inputs], columns=FEATURE_NAMES)
scaled_inputs = scaler.transform(inputs_df)
# Run prediction
probs = model.predict_proba(scaled_inputs)[0]
pred_class = int(np.argmax(probs))
confidence = probs[pred_class] * 100
class_name = CLASSES[pred_class]
image_path = f"assets/{class_name.lower()}.jpg"
# Color-coded badge
badge_html = f"""
<div style='text-align: center; padding: 12px; border-radius: 12px; background: rgba(255,255,255,0.03); border: 1px solid var(--glass-border);'>
<span class='badge badge-{class_name.lower()}'>{class_name}</span>
<p style='font-size: 1.2rem; margin-top: 12px; color: var(--text-primary); margin-bottom: 0;'>
Model Confidence: <span style='color: var(--primary-cyan); font-weight: bold;'>{confidence:.2f}%</span>
</p>
</div>
"""
# Probability Chart
prob_chart = plot_probabilities(probs)
# Feature Explanations
explanation_html = explain_prediction(cleaned_inputs)
return badge_html, prob_chart, image_path, explanation_html
except Exception as e:
import traceback
error_details = traceback.format_exc()
err_html = f"""
<div style='text-align: center; padding: 15px; border-radius: 8px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3);'>
<h4 style='color: #ef4444; margin: 0;'>⚠️ Prediction Execution Failed</h4>
<p style='color: #cbd5e1; margin: 5px 0 0 0; font-size: 0.9rem;'>{str(e)}</p>
</div>
"""
return err_html, None, "assets/poor.jpg", f"<pre style='color:#ef4444; font-size: 0.85rem;'>{error_details}</pre>"
# 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("""
<h1 style='margin: 0; font-size: 2.2rem; background: linear-gradient(90deg, #06b6d4, #3b82f6); -webkit-background-clip: text; -webkit-text-fill-color: transparent;'>
Water Quality Prediction Dashboard
</h1>
<p style='margin: 5px 0 0 0; color: var(--text-secondary); font-size: 1.05rem;'>
Evaluate ecosystem health, predict contamination risk, and analyze feature distributions using the pre-trained LightGBM classification model.
</p>
""")
# ----- مین گرڈ (Main Grid) -----
with gr.Row():
# بائیں پینل (Inputs)
with gr.Column(scale=5, elem_classes=["glass-panel"]):
gr.HTML("<h3 style='color: var(--primary-cyan); border-bottom: 1px solid var(--glass-border); padding-bottom: 8px; margin-top: 0;'>Water Metrics Configurator</h3>")
with gr.Tabs():
with gr.TabItem("🎛️ Guided Sliders"):
s_ammonia = gr.Slider(minimum=0.0, maximum=10.0, value=0.066, step=0.001, label="Ammonia (mg/l) [Normal Median: 0.066]")
s_bod = gr.Slider(minimum=0.0, maximum=30.0, value=2.7, step=0.01, label="Biochemical Oxygen Demand (mg/l) [Normal Median: 2.7]")
s_do = gr.Slider(minimum=0.0, maximum=20.0, value=10.2, step=0.01, label="Dissolved Oxygen (mg/l) [Normal Median: 10.2]")
s_orthophosphate = gr.Slider(minimum=0.0, maximum=5.0, value=0.144, step=0.001, label="Orthophosphate (mg/l) [Normal Median: 0.144]")
s_ph = gr.Slider(minimum=0.0, maximum=14.0, value=7.78, step=0.01, label="pH (ph units) [Normal Median: 7.78]")
s_temp = gr.Slider(minimum=-5.0, maximum=45.0, value=11.46, step=0.01, label="Temperature (cel) [Normal Median: 11.46]")
s_nitrogen = gr.Slider(minimum=0.0, maximum=25.0, value=4.98, step=0.01, label="Nitrogen (mg/l) [Normal Median: 4.98]")
s_nitrate = gr.Slider(minimum=0.0, maximum=20.0, value=4.5, step=0.01, label="Nitrate (mg/l) [Normal Median: 4.5]")
with gr.TabItem("📝 Precision Inputs (Overrides Sliders)"):
gr.HTML("<p style='font-size:0.85rem; color:var(--text-muted); margin-bottom: 12px;'>Enter precise float values below. Any non-empty textbox here overrides its corresponding slider above.</p>")
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("<h3 style='color: var(--primary-cyan); margin-top: 0; border-bottom: 1px solid var(--glass-border); padding-bottom: 8px;'>Prediction Summary Card</h3>")
out_badge = gr.HTML(value="""
<div style='text-align: center; padding: 12px; border-radius: 12px; background: rgba(255,255,255,0.02); border: 1px solid var(--glass-border);'>
<span style='color: var(--text-muted); font-size: 1.1rem;'>Adjust parameters and click Predict</span>
</div>
""")
out_image = gr.Image(value="assets/good.jpg", label="Ecosystem Visualisation", height=200, interactive=False)
with gr.Column(elem_classes=["glass-panel", "top-margin"]):
with gr.Tabs():
with gr.TabItem("📊 Probability Distribution"):
out_chart = gr.Plot(label="Confidence Distribution")
with gr.TabItem("🔍 Explainable AI (XAI)"):
out_explanation = gr.HTML(value="<p style='color:var(--text-muted); font-size: 0.95rem;'>Click predict to run the explainable AI analysis.</p>")
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)