import os from pathlib import Path import joblib import numpy as np import pandas as pd import streamlit as st from huggingface_hub import hf_hub_download HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "saranka85/predictive-maintenance-random-forest") HF_MODEL_FILENAME = os.getenv("HF_MODEL_FILENAME", "model.joblib") HF_TOKEN = os.getenv("HF_TOKEN") or None PREDICTION_LOG_DIR = Path(os.getenv("PREDICTION_LOG_DIR", "/tmp/prediction_logs")) PREDICTION_LOG_PATH = PREDICTION_LOG_DIR / "prediction_inputs.csv" CLASS_LABELS = { 0: os.getenv("ENGINE_CLASS_0_LABEL", "Requires Maintenance"), 1: os.getenv("ENGINE_CLASS_1_LABEL", "Operating Normally"), } MODEL_FEATURES = [ "engine_rpm", "lub_oil_pressure", "fuel_pressure", "coolant_pressure", "lub_oil_temp", "coolant_temp", "temperature_difference", "mean_temperature", "mean_pressure", "pressure_range", "lub_oil_pressure_per_1000_rpm", "fuel_pressure_per_1000_rpm", "rpm_fuel_pressure_interaction", ] @st.cache_resource def load_model(): """Load the saved model from the Hugging Face Model Hub.""" model_path = hf_hub_download( repo_id=HF_MODEL_REPO, filename=HF_MODEL_FILENAME, token=HF_TOKEN, ) return joblib.load(model_path) def create_engine_features(raw_features: pd.DataFrame) -> pd.DataFrame: """Create the engineered features used during model training.""" engineered = raw_features.copy() pressure_columns = ["lub_oil_pressure", "fuel_pressure", "coolant_pressure"] temperature_columns = ["lub_oil_temp", "coolant_temp"] engineered["temperature_difference"] = ( engineered["coolant_temp"] - engineered["lub_oil_temp"] ) engineered["mean_temperature"] = engineered[temperature_columns].mean(axis=1) engineered["mean_pressure"] = engineered[pressure_columns].mean(axis=1) engineered["pressure_range"] = ( engineered[pressure_columns].max(axis=1) - engineered[pressure_columns].min(axis=1) ) rpm_denominator = engineered["engine_rpm"].clip(lower=1) engineered["lub_oil_pressure_per_1000_rpm"] = ( engineered["lub_oil_pressure"] * 1000 / rpm_denominator ) engineered["fuel_pressure_per_1000_rpm"] = ( engineered["fuel_pressure"] * 1000 / rpm_denominator ) engineered["rpm_fuel_pressure_interaction"] = ( engineered["engine_rpm"] * engineered["fuel_pressure"] ) engineered = engineered[MODEL_FEATURES] if not np.isfinite(engineered.to_numpy(dtype=float)).all(): raise ValueError("Input values created non-finite engineered features.") return engineered def save_input_dataframe(input_dataframe: pd.DataFrame) -> None: """Save submitted inputs into a runtime CSV for traceability.""" PREDICTION_LOG_DIR.mkdir(parents=True, exist_ok=True) input_dataframe.to_csv( PREDICTION_LOG_PATH, mode="a", header=not PREDICTION_LOG_PATH.exists(), index=False, ) def predict_engine_condition(input_dataframe: pd.DataFrame): """Save inputs, engineer features, load the model, and predict engine condition.""" save_input_dataframe(input_dataframe) model_input = create_engine_features(input_dataframe) model = load_model() predicted_class = int(model.predict(model_input)[0]) predicted_status = CLASS_LABELS.get(predicted_class, f"Class {predicted_class}") if hasattr(model, "predict_proba"): probabilities = model.predict_proba(model_input)[0] probability_output = { CLASS_LABELS.get(int(class_label), f"Class {int(class_label)}"): float(probability) for class_label, probability in zip(model.classes_, probabilities) } else: probability_output = {predicted_status: 1.0} result_summary = { "Engine status": predicted_status, "Predicted engine_condition class": predicted_class, "Model repository": HF_MODEL_REPO, "Saved input file": str(PREDICTION_LOG_PATH), } return probability_output, model_input, result_summary st.set_page_config( page_title="Predictive Maintenance Engine Classifier", page_icon="🔧", layout="wide", ) st.title("Predictive Maintenance Engine Classifier") st.write( "Enter raw engine sensor readings. The app saves the inputs into a DataFrame, " "creates the engineered training features, loads the saved Random Forest model " "from Hugging Face Model Hub, and classifies whether the engine requires " "maintenance or is operating normally." ) with st.sidebar: st.header("Model Configuration") st.write(f"Model repository: `{HF_MODEL_REPO}`") st.write(f"Model file: `{HF_MODEL_FILENAME}`") st.write("Class label mapping:") st.write(f"`0` → **{CLASS_LABELS[0]}**") st.write(f"`1` → **{CLASS_LABELS[1]}**") col1, col2, col3 = st.columns(3) with col1: engine_rpm = st.number_input("Engine RPM", min_value=0.0, value=746.0, step=1.0) coolant_pressure = st.number_input( "Coolant Pressure", min_value=0.0, value=2.3, step=0.1 ) with col2: lub_oil_pressure = st.number_input( "Lub Oil Pressure", min_value=0.0, value=3.3, step=0.1 ) lub_oil_temp = st.number_input("Lub Oil Temp", value=77.6, step=0.1) with col3: fuel_pressure = st.number_input("Fuel Pressure", min_value=0.0, value=6.4, step=0.1) coolant_temp = st.number_input("Coolant Temp", value=78.4, step=0.1) input_dataframe = pd.DataFrame( [{ "engine_rpm": float(engine_rpm), "lub_oil_pressure": float(lub_oil_pressure), "fuel_pressure": float(fuel_pressure), "coolant_pressure": float(coolant_pressure), "lub_oil_temp": float(lub_oil_temp), "coolant_temp": float(coolant_temp), }] ) st.subheader("Submitted Input DataFrame") st.dataframe(input_dataframe, use_container_width=True) if st.button("Predict Engine Condition", type="primary"): try: probability_output, model_input, result_summary = predict_engine_condition( input_dataframe ) engine_status = result_summary["Engine status"] predicted_class = result_summary["Predicted engine_condition class"] st.subheader("Engine Maintenance Decision") if engine_status == "Requires Maintenance": st.error(f"🔴 {engine_status}") elif engine_status == "Operating Normally": st.success(f"🟢 {engine_status}") else: st.info(f"Predicted status: {engine_status}") st.caption(f"Raw model class: engine_condition = {predicted_class}") st.subheader("Prediction Summary") st.json(result_summary) st.subheader("Prediction Probabilities") st.dataframe( pd.DataFrame([probability_output]), use_container_width=True, ) st.subheader("Model Input DataFrame") st.dataframe(model_input, use_container_width=True) except Exception as error: st.error(f"Prediction failed: {error}")