| | |
| | import streamlit as st |
| | import pandas as pd |
| | import numpy as np |
| | from huggingface_hub import hf_hub_download |
| | import joblib |
| | import io |
| |
|
| | |
| | st.set_page_config( |
| | page_title="Engine Predictive Maintenance", |
| | page_icon="🛠️", |
| | layout="wide" |
| | ) |
| |
|
| | st.title("🛠️ Smart Engine Predictive Maintenance App") |
| | st.markdown(""" |
| | This application predicts whether an engine is **Faulty (maintenance required)** or **Normal** |
| | based on sensor readings. |
| | |
| | **Target:** |
| | - **0 = Normal** |
| | - **1 = Faulty** |
| | |
| | **Note:** The model expects engineered features, so the app computes the same feature engineering |
| | used during training to ensure schema consistency. |
| | """) |
| |
|
| | |
| | MODEL_REPO_ID = "simnid/predictive-maintenance-model" |
| | MODEL_FILENAME = "best_predictive_maintenance_model.joblib" |
| |
|
| | |
| | DATA_REPO_ID = "simnid/predictive-engine-maintenance-dataset" |
| | BULK_TEST_FILENAME = "bulk_test_sample.csv" |
| |
|
| | RAW_COLS = [ |
| | "Engine rpm", |
| | "Lub oil pressure", |
| | "Fuel pressure", |
| | "Coolant pressure", |
| | "lub oil temp", |
| | "Coolant temp" |
| | ] |
| |
|
| | ENGINEERED_COLS = [ |
| | "RPM_FuelPressure_Ratio", |
| | "Power_Index", |
| | "Thermal_Pressure_Index", |
| | "Mech_Cooling_Balance", |
| | "Pressure_Coordination", |
| | "Low_Oil_Pressure_Flag", |
| | "High_Coolant_Temp_Flag", |
| | "Low_RPM_Flag" |
| | ] |
| |
|
| | FINAL_FEATURE_ORDER = RAW_COLS + ENGINEERED_COLS |
| |
|
| | |
| | def add_engineered_features(df: pd.DataFrame) -> pd.DataFrame: |
| | df = df.copy() |
| |
|
| | |
| | missing = [c for c in RAW_COLS if c not in df.columns] |
| | if missing: |
| | raise ValueError(f"Missing required columns: {missing}") |
| |
|
| | |
| | for c in RAW_COLS: |
| | df[c] = pd.to_numeric(df[c], errors="coerce") |
| |
|
| | if df[RAW_COLS].isnull().any().any(): |
| | bad_cols = df[RAW_COLS].columns[df[RAW_COLS].isnull().any()].tolist() |
| | raise ValueError(f"Non-numeric / missing values detected in: {bad_cols}") |
| |
|
| | |
| | df["RPM_FuelPressure_Ratio"] = df["Engine rpm"] / (df["Fuel pressure"] + 1e-5) |
| | df["Power_Index"] = (df["Engine rpm"] * df["Fuel pressure"]) / 1000 |
| |
|
| | |
| | df["Thermal_Pressure_Index"] = df["Coolant temp"] / (df["Fuel pressure"] + 1e-5) |
| | df["Mech_Cooling_Balance"] = ( |
| | (df["Engine rpm"] + df["Lub oil pressure"]) - |
| | (df["Coolant temp"] + df["Coolant pressure"]) |
| | ) |
| | df["Pressure_Coordination"] = df["Fuel pressure"] - df["Coolant pressure"] |
| |
|
| | |
| | df["Low_Oil_Pressure_Flag"] = (df["Lub oil pressure"] < 1.5).astype(int) |
| | df["High_Coolant_Temp_Flag"] = (df["Coolant temp"] > 100).astype(int) |
| | df["Low_RPM_Flag"] = (df["Engine rpm"] < 600).astype(int) |
| |
|
| | return df[FINAL_FEATURE_ORDER] |
| |
|
| | |
| | @st.cache_resource |
| | def load_model(): |
| | try: |
| | model_path = hf_hub_download( |
| | repo_id=MODEL_REPO_ID, |
| | filename=MODEL_FILENAME, |
| | repo_type="model" |
| | ) |
| | return joblib.load(model_path) |
| | except Exception as e: |
| | st.error(f"Error loading model from Hugging Face: {e}") |
| | return None |
| |
|
| | model = load_model() |
| | if model is None: |
| | st.warning("Model could not be loaded. Please verify model repo + filename.") |
| | st.stop() |
| |
|
| |
|
| | |
| | with st.sidebar: |
| | st.header("About This Model") |
| | st.markdown(""" |
| | **Model Details** |
| | - **Model Type:** Gradient Boosting Classifier |
| | - **Optimization Objective:** Maximize recall for faulty engines (minimize missed failures) |
| | - **Artifact Source:** Hugging Face Model Hub |
| | |
| | **Why Recall Matters** |
| | A false negative means a failure was missed, leading to downtime, safety risks, and costly repairs. |
| | """) |
| |
|
| | st.subheader("Production Metrics (Reference)") |
| | st.metric("Recall (Faulty)", "0.84") |
| | st.metric("ROC-AUC", "0.70") |
| | st.metric("PR-AUC", "0.80") |
| |
|
| | st.markdown("---") |
| | st.subheader("Decision Threshold") |
| | threshold = st.slider( |
| | "Classification Threshold (Faulty if P ≥ threshold)", |
| | min_value=0.05, max_value=0.95, value=0.50, step=0.01 |
| | ) |
| | st.caption("Lower threshold → higher recall (fewer missed failures), but more false alarms.") |
| |
|
| |
|
| | |
| | tab1, tab2 = st.tabs(["🔎 Single Prediction", "📦 Bulk Prediction"]) |
| |
|
| |
|
| | |
| | with tab1: |
| | st.subheader("Engine Sensor Inputs") |
| |
|
| | c1, c2, c3 = st.columns(3) |
| |
|
| | with c1: |
| | engine_rpm = st.number_input("Engine rpm", min_value=0.0, value=700.0, step=1.0) |
| | lub_oil_pressure = st.number_input("Lub oil pressure", min_value=0.0, value=2.50, step=0.01) |
| |
|
| | with c2: |
| | fuel_pressure = st.number_input("Fuel pressure", min_value=0.0, value=12.00, step=0.01) |
| | coolant_pressure = st.number_input("Coolant pressure", min_value=0.0, value=2.50, step=0.01) |
| |
|
| | with c3: |
| | lub_oil_temp = st.number_input("lub oil temp", min_value=0.0, value=80.0, step=0.1) |
| | coolant_temp = st.number_input("Coolant temp", min_value=0.0, value=85.0, step=0.1) |
| |
|
| | raw_input_df = pd.DataFrame([{ |
| | "Engine rpm": engine_rpm, |
| | "Lub oil pressure": lub_oil_pressure, |
| | "Fuel pressure": fuel_pressure, |
| | "Coolant pressure": coolant_pressure, |
| | "lub oil temp": lub_oil_temp, |
| | "Coolant temp": coolant_temp |
| | }]) |
| |
|
| | try: |
| | feature_df = add_engineered_features(raw_input_df) |
| | except Exception as e: |
| | st.error(f"Feature engineering failed: {e}") |
| | st.stop() |
| |
|
| | with st.expander("View engineered input dataframe"): |
| | st.dataframe(feature_df) |
| | csv = feature_df.to_csv(index=False).encode("utf-8") |
| | st.download_button("Download Engineered Input CSV", csv, "engine_input_features.csv", "text/csv") |
| |
|
| | st.subheader("Prediction Output") |
| |
|
| | if st.button("Predict Engine Condition", type="primary", use_container_width=True): |
| | try: |
| | proba_faulty = None |
| | if hasattr(model, "predict_proba"): |
| | proba_faulty = float(model.predict_proba(feature_df)[0][1]) |
| |
|
| | |
| | if proba_faulty is not None: |
| | pred_class = int(proba_faulty >= threshold) |
| | else: |
| | pred_class = int(model.predict(feature_df)[0]) |
| |
|
| | colA, colB = st.columns(2) |
| |
|
| | with colA: |
| | if pred_class == 1: |
| | st.error("⚠️ Prediction: FAULTY (Maintenance Recommended)") |
| | else: |
| | st.success("✅ Prediction: NORMAL (No Immediate Maintenance Required)") |
| |
|
| | with colB: |
| | if proba_faulty is not None: |
| | st.metric("Probability of Faulty (Class 1)", f"{proba_faulty*100:.1f}%") |
| | st.progress(int(proba_faulty * 100)) |
| | else: |
| | st.info("Probability score unavailable (model does not support predict_proba).") |
| |
|
| | except Exception as e: |
| | st.error(f"Prediction failed: {e}") |
| |
|
| | |
| | with tab2: |
| | st.subheader("Bulk CSV Prediction") |
| |
|
| | st.markdown(""" |
| | Upload a CSV containing **raw sensor columns only**: |
| | |
| | - Engine rpm |
| | - Lub oil pressure |
| | - Fuel pressure |
| | - Coolant pressure |
| | - lub oil temp |
| | - Coolant temp |
| | |
| | The app will automatically engineer features and return: |
| | - `Predicted_Class` (0/1) |
| | - `Faulty_Probability` (if available) |
| | """) |
| |
|
| | |
| | @st.cache_resource |
| | def load_bulk_sample(): |
| | try: |
| | path = hf_hub_download( |
| | repo_id=DATA_REPO_ID, |
| | filename=BULK_TEST_FILENAME, |
| | repo_type="dataset" |
| | ) |
| | return pd.read_csv(path) |
| | except Exception: |
| | return None |
| |
|
| | sample_df = load_bulk_sample() |
| | if sample_df is not None: |
| | with st.expander("Preview bulk sample from Hugging Face"): |
| | st.dataframe(sample_df.head()) |
| |
|
| | uploaded_file = st.file_uploader("Upload CSV for bulk prediction", type=["csv"]) |
| |
|
| | bulk_df = None |
| | if uploaded_file is not None: |
| | bulk_df = pd.read_csv(uploaded_file) |
| | elif sample_df is not None: |
| | bulk_df = sample_df.copy() |
| |
|
| | if bulk_df is not None: |
| | st.markdown("✅ Bulk data loaded.") |
| | st.dataframe(bulk_df.head()) |
| |
|
| | if st.button("Run Bulk Prediction", use_container_width=True): |
| | try: |
| | |
| | missing = [c for c in RAW_COLS if c not in bulk_df.columns] |
| | if missing: |
| | st.error(f"Missing required columns: {missing}") |
| | st.stop() |
| |
|
| | bulk_features = add_engineered_features(bulk_df[RAW_COLS]) |
| |
|
| | |
| | preds = model.predict(bulk_features).astype(int) |
| |
|
| | if hasattr(model, "predict_proba"): |
| | probs = model.predict_proba(bulk_features)[:, 1] |
| | else: |
| | probs = np.full(shape=(len(bulk_features),), fill_value=np.nan) |
| |
|
| | |
| | if hasattr(model, "predict_proba"): |
| | preds = (probs >= threshold).astype(int) |
| |
|
| | out = bulk_df.copy() |
| | out["Predicted_Class"] = preds |
| | out["Faulty_Probability"] = probs |
| |
|
| | st.success("Bulk predictions completed.") |
| | st.dataframe(out.head(50)) |
| |
|
| | out_csv = out.to_csv(index=False).encode("utf-8") |
| | st.download_button( |
| | "Download Bulk Predictions CSV", |
| | out_csv, |
| | "bulk_engine_predictions.csv", |
| | "text/csv" |
| | ) |
| |
|
| | except Exception as e: |
| | st.error(f"Bulk prediction failed: {e}") |
| |
|
| |
|
| | |
| | st.markdown("---") |
| | st.caption("Predictive Maintenance | Gradient Boosting + Streamlit + Hugging Face Model Hub") |
| |
|