CoolFace
Apppublic

AnubhaParashar/Predictive_Preventive_Maintenance_for_Generator_Reliability

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py1384 linesDownload Raw Back to root
1import os2import io3import math4import zipfile5from pathlib import Path6from typing import Dict, Optional, Tuple7from urllib.parse import quote8 9import numpy as np10import pandas as pd11import plotly.express as px12import plotly.graph_objects as go13import streamlit as st14from sklearn.compose import ColumnTransformer15from sklearn.pipeline import Pipeline16from sklearn.preprocessing import OneHotEncoder, StandardScaler17from sklearn.impute import SimpleImputer18from sklearn.model_selection import train_test_split19from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix20from sklearn.linear_model import LogisticRegression21from sklearn.ensemble import RandomForestClassifier22 23 24APP_DIR = Path(__file__).resolve().parent25DEFAULT_DATA_DIR = APP_DIR / "data" / "processed"26OUTPUT_FIG_DIR = APP_DIR / "outputs" / "figures"27OUTPUT_TABLE_DIR = APP_DIR / "outputs" / "tables"28RAW_DATA_DIR = APP_DIR / "data" / "raw"29DEFAULT_RAW_TELEMETRY_PATH = RAW_DATA_DIR / "generator_telemetry_with_labels.csv"30 31# Hugging Face Spaces can store larger files through Xet/LFS. In some Docker builds,32# the local file may appear as a tiny pointer file instead of the real CSV.33# This fallback lets the app fetch the resolved file from the Space repo when needed.34HF_SPACE_ID = os.getenv(35    "HF_SPACE_ID",36    "AnubhaParashar/Predictive_Preventive_Maintenance_for_Generator_Reliability",37)38HF_RESOLVE_BASE = f"https://huggingface.co/spaces/{HF_SPACE_ID}/resolve/main"39EXTERNAL_RAW_TELEMETRY_PATH = DEFAULT_RAW_TELEMETRY_PATH40 41 42# Snowflake Streamlit does not support page_title/page_icon in st.set_page_config.43# Keep layout only and continue safely if the runtime ignores this call.44try:45    st.set_page_config(layout="wide", initial_sidebar_state="expanded")46except Exception:47    pass48 49 50# -----------------------------51# Utility helpers52# -----------------------------53def money(x):54    try:55        if pd.isna(x):56            return "$0"57        return f"${float(x):,.0f}"58    except Exception:59        return "$0"60 61 62def num(x, decimals=0):63    try:64        if pd.isna(x):65            return "0"66        return f"{float(x):,.{decimals}f}"67    except Exception:68        return "0"69 70 71def pct(x, decimals=1):72    try:73        if pd.isna(x):74            return "0%"75        return f"{float(x) * 100:.{decimals}f}%"76    except Exception:77        return "0%"78 79 80def _looks_like_xet_or_lfs_pointer(df: pd.DataFrame) -> bool:81    """Detect a Git LFS / Hugging Face Xet pointer that was read as a tiny CSV."""82    if df is None or df.empty:83        return False84    if df.shape[0] > 5 or df.shape[1] > 2:85        return False86    text = " ".join([str(x) for x in list(df.columns) + df.astype(str).values.flatten().tolist()])87    text = text.lower()88    return (89        "git-lfs.github.com/spec" in text90        or "version https://git-lfs" in text91        or "xet" in text and "sha256" in text92        or "oid sha256" in text93    )94 95 96def _hf_resolve_url_for_path(path: Path) -> str:97    """Build a Hugging Face /resolve/main URL for a local repo-relative path."""98    try:99        rel = path.resolve().relative_to(APP_DIR.resolve()).as_posix()100    except Exception:101        rel = path.as_posix().lstrip("/")102    return f"{HF_RESOLVE_BASE}/{quote(rel)}"103 104 105def read_csv_safely(path_or_buffer) -> pd.DataFrame:106    """Read CSV and recover automatically if Hugging Face gives a Xet/LFS pointer file."""107    df = pd.read_csv(path_or_buffer)108 109    # If a local repo file is only a pointer, fetch the actual resolved file from Hugging Face.110    if isinstance(path_or_buffer, (str, Path)):111        path = Path(path_or_buffer)112        if _looks_like_xet_or_lfs_pointer(df):113            resolved_url = _hf_resolve_url_for_path(path)114            return pd.read_csv(resolved_url)115 116    return df117 118 119def parse_date_cols(df: pd.DataFrame, cols) -> pd.DataFrame:120    out = df.copy()121    for c in cols:122        if c in out.columns:123            out[c] = pd.to_datetime(out[c], errors="coerce")124    return out125 126 127@st.cache_data(show_spinner=False)128def load_default_tables() -> Dict[str, pd.DataFrame]:129    tables = {}130    files = {131        "asset_master": "asset_master.csv",132        "pm_events": "pm_events.csv",133        "failure_events": "failure_events.csv",134        "business_impact": "business_impact.csv",135        "pm_failure_linked": "pm_failure_linked.csv",136        "telemetry_weekly": "telemetry_weekly.csv",137    }138    for key, filename in files.items():139        path = DEFAULT_DATA_DIR / filename140        if path.exists():141            tables[key] = read_csv_safely(path)142        else:143            tables[key] = pd.DataFrame()144 145    tables["asset_master"] = parse_date_cols(tables["asset_master"], ["install_date"])146    tables["pm_events"] = parse_date_cols(tables["pm_events"], ["scheduled_date", "completed_date", "pm_date"])147    tables["failure_events"] = parse_date_cols(148        tables["failure_events"], ["failure_date", "ticket_open_date", "ticket_close_date"]149    )150    tables["business_impact"] = parse_date_cols(tables["business_impact"], ["event_date"])151    tables["pm_failure_linked"] = parse_date_cols(tables["pm_failure_linked"], ["pm_date", "next_failure_date"])152    tables["telemetry_weekly"] = parse_date_cols(tables["telemetry_weekly"], ["timestamp"])153    return tables154 155 156@st.cache_data(show_spinner=False)157def load_default_raw_telemetry() -> Tuple[pd.DataFrame, str]:158    """Load the packaged raw telemetry file used by the demo app."""159    if DEFAULT_RAW_TELEMETRY_PATH.exists():160        df = read_csv_safely(DEFAULT_RAW_TELEMETRY_PATH)161        return parse_date_cols(df, ["timestamp"]), "Packaged demo file: data/raw/generator_telemetry_with_labels.csv"162    return pd.DataFrame(), "No packaged raw telemetry file found"163 164 165def dataframe_profile(df: pd.DataFrame, source_path: str) -> pd.DataFrame:166    if df.empty:167        return pd.DataFrame({"Metric": ["Source path", "Rows", "Columns"], "Value": [source_path, "0", "0"]})168    values = {169        "Source path": source_path,170        "Rows": f"{len(df):,}",171        "Columns": f"{len(df.columns):,}",172        "Date range": "Not available",173        "Duplicate rows": f"{int(df.duplicated().sum()):,}",174        "Missing values": f"{int(df.isna().sum().sum()):,}",175    }176    if "timestamp" in df.columns:177        ts = pd.to_datetime(df["timestamp"], errors="coerce")178        if ts.notna().any():179            values["Date range"] = f"{ts.min().date()} to {ts.max().date()}"180    return pd.DataFrame({"Metric": list(values.keys()), "Value": list(values.values())})181 182 183def enrich_algorithm_output(raw_df: pd.DataFrame) -> pd.DataFrame:184    """Create a transparent demo algorithm output from raw telemetry columns."""185    if raw_df.empty:186        return pd.DataFrame()187    out = raw_df.copy()188    if "timestamp" in out.columns:189        out["timestamp"] = pd.to_datetime(out["timestamp"], errors="coerce")190    for c in ["anomaly_score", "days_since_last_pm", "failure_within_30d", "failure_within_14d"]:191        if c in out.columns:192            out[c] = pd.to_numeric(out[c], errors="coerce")193    if "anomaly_score" not in out.columns:194        numeric_cols = out.select_dtypes(include=[np.number]).columns.tolist()195        if numeric_cols:196            z = out[numeric_cols].fillna(out[numeric_cols].median(numeric_only=True))197            out["anomaly_score"] = ((z - z.mean()) / z.std(ddof=0)).abs().mean(axis=1).rank(pct=True) * 100198        else:199            out["anomaly_score"] = 0200    conditions = [201        out["anomaly_score"] >= 80,202        out["anomaly_score"] >= 60,203        out["anomaly_score"] >= 40,204    ]205    choices = ["Critical", "High", "Watch"]206    out["algorithm_risk_band"] = np.select(conditions, choices, default="Low")207    pm_age = pd.to_numeric(out.get("days_since_last_pm", pd.Series(0, index=out.index)), errors="coerce").fillna(0)208    out["algorithm_failure_risk_score"] = np.clip((out["anomaly_score"].fillna(0) * 0.75) + np.minimum(pm_age, 180) / 180 * 25, 0, 100).round(2)209    out["algorithm_predicted_failure_within_30d"] = (out["algorithm_failure_risk_score"] >= 70).astype(int)210    out["recommended_action"] = np.select(211        [out["algorithm_risk_band"].eq("Critical"), out["algorithm_risk_band"].eq("High"), out["algorithm_risk_band"].eq("Watch")],212        ["Immediate inspection / corrective work order", "Schedule PM within 7 days", "Monitor and plan PM",],213        default="Normal monitoring",214    )215    sort_cols = [c for c in ["timestamp", "algorithm_failure_risk_score"] if c in out.columns]216    if sort_cols:217        out = out.sort_values(sort_cols, ascending=[False, False] if len(sort_cols) == 2 else False)218    return out219 220 221def summarize_algorithm_by_asset(algo_df: pd.DataFrame) -> pd.DataFrame:222    if algo_df.empty or "asset_id" not in algo_df.columns:223        return pd.DataFrame()224    df = algo_df.copy()225    if "timestamp" in df.columns:226        df = df.sort_values("timestamp")227    latest = df.groupby("asset_id", as_index=False).tail(1).copy()228    keep_cols = [c for c in ["asset_id", "timestamp", "model", "region", "criticality", "environment_type", "days_since_last_pm", "anomaly_score", "algorithm_failure_risk_score", "algorithm_risk_band", "algorithm_predicted_failure_within_30d", "recommended_action"] if c in latest.columns]229    return latest[keep_cols].sort_values("algorithm_failure_risk_score", ascending=False)230 231 232 233# -----------------------------234# Machine Learning helpers235# -----------------------------236@st.cache_data(show_spinner=False)237def prepare_ml_frame(raw_df: pd.DataFrame, target_col: str = "failure_within_30d", max_rows: int = 90000):238    """Prepare telemetry data for supervised failure-risk modeling."""239    if raw_df.empty or target_col not in raw_df.columns:240        return pd.DataFrame(), [], []241 242    use_cols = [243        "asset_id", "age_years", "days_since_last_pm", "runtime_hours_week", "avg_load_pct", "oil_temp_c",244        "coolant_temp_c", "battery_voltage", "vibration_mm_s", "fuel_rate_lph", "alarm_count", "anomaly_score",245        "model", "region", "criticality", "environment_type", target_col, "timestamp"246    ]247    use_cols = [c for c in use_cols if c in raw_df.columns]248    df = raw_df[use_cols].copy().dropna(subset=[target_col])249    if "timestamp" in df.columns:250        df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")251        df = df.sort_values("timestamp")252 253    # Keep the app fast in Snowflake by training on a balanced sample.254    if len(df) > max_rows:255        pos = df[df[target_col] == 1]256        neg = df[df[target_col] == 0]257        n_pos = min(len(pos), max_rows // 3)258        n_neg = max_rows - n_pos259        df = pd.concat([260            pos.sample(n=n_pos, random_state=42) if len(pos) > n_pos else pos,261            neg.sample(n=n_neg, random_state=42) if len(neg) > n_neg else neg,262        ], axis=0).sample(frac=1, random_state=42)263 264    feature_cols = [c for c in df.columns if c not in [target_col, "timestamp"]]265    numeric_cols = [c for c in feature_cols if pd.api.types.is_numeric_dtype(df[c])]266    categorical_cols = [c for c in feature_cols if c not in numeric_cols]267    return df, numeric_cols, categorical_cols268 269 270@st.cache_resource(show_spinner=False)271def train_failure_models(df: pd.DataFrame, numeric_cols, categorical_cols, target_col: str = "failure_within_30d"):272    """Train baseline and advanced ML classifiers for near-term failure prediction."""273    X = df[numeric_cols + categorical_cols]274    y = df[target_col].astype(int)275    X_train, X_test, y_train, y_test = train_test_split(276        X, y, test_size=0.25, random_state=42, stratify=y277    )278 279    numeric_transformer = Pipeline(steps=[280        ("imputer", SimpleImputer(strategy="median")),281        ("scaler", StandardScaler()),282    ])283    categorical_transformer = Pipeline(steps=[284        ("imputer", SimpleImputer(strategy="most_frequent")),285        ("onehot", OneHotEncoder(handle_unknown="ignore")),286    ])287    preprocessor = ColumnTransformer(288        transformers=[289            ("num", numeric_transformer, numeric_cols),290            ("cat", categorical_transformer, categorical_cols),291        ]292    )293 294    models = {295        "Logistic Regression baseline": LogisticRegression(max_iter=500, class_weight="balanced"),296        "Random Forest advanced model": RandomForestClassifier(297            n_estimators=160,298            max_depth=12,299            min_samples_leaf=5,300            random_state=42,301            n_jobs=-1,302            class_weight="balanced_subsample",303        ),304    }305 306    results = {}307    for name, model in models.items():308        pipe = Pipeline(steps=[("preprocessor", preprocessor), ("model", model)])309        pipe.fit(X_train, y_train)310        proba = pipe.predict_proba(X_test)[:, 1]311        pred = (proba >= 0.5).astype(int)312        results[name] = {313            "pipeline": pipe,314            "accuracy": accuracy_score(y_test, pred),315            "precision": precision_score(y_test, pred, zero_division=0),316            "recall": recall_score(y_test, pred, zero_division=0),317            "f1": f1_score(y_test, pred, zero_division=0),318            "roc_auc": roc_auc_score(y_test, proba),319            "confusion_matrix": confusion_matrix(y_test, pred),320        }321    return results322 323 324def get_ml_feature_importance(pipeline, model_name: str, top_n: int = 15) -> pd.DataFrame:325    preprocessor = pipeline.named_steps["preprocessor"]326    feature_names = preprocessor.get_feature_names_out()327    model = pipeline.named_steps["model"]328    if "Logistic" in model_name:329        values = np.abs(model.coef_[0])330    else:331        values = model.feature_importances_332    out = pd.DataFrame({"feature": feature_names, "importance": values})333    out["feature"] = out["feature"].str.replace("num__", "", regex=False).str.replace("cat__", "", regex=False)334    return out.sort_values("importance", ascending=False).head(top_n)335 336 337def score_latest_asset_risk(telemetry_df: pd.DataFrame, pipeline) -> pd.DataFrame:338    latest = telemetry_df.copy()339    if latest.empty or "asset_id" not in latest.columns:340        return pd.DataFrame()341    if "timestamp" in latest.columns:342        latest["timestamp"] = pd.to_datetime(latest["timestamp"], errors="coerce")343        latest = latest.sort_values("timestamp").groupby("asset_id", as_index=False).tail(1)344 345    feature_cols = [346        "asset_id", "age_years", "days_since_last_pm", "runtime_hours_week", "avg_load_pct", "oil_temp_c",347        "coolant_temp_c", "battery_voltage", "vibration_mm_s", "fuel_rate_lph", "alarm_count", "anomaly_score",348        "model", "region", "criticality", "environment_type"349    ]350    feature_cols = [c for c in feature_cols if c in latest.columns]351    latest = latest.copy()352    latest["ml_predicted_failure_risk"] = pipeline.predict_proba(latest[feature_cols])[:, 1]353    latest["ml_risk_band"] = pd.cut(354        latest["ml_predicted_failure_risk"],355        bins=[-0.01, 0.30, 0.60, 0.80, 1.0],356        labels=["Low", "Medium", "High", "Critical"],357    )358    latest["ml_recommended_action"] = np.select(359        [360            latest["ml_predicted_failure_risk"] >= 0.80,361            latest["ml_predicted_failure_risk"] >= 0.60,362            latest["ml_predicted_failure_risk"] >= 0.30,363        ],364        [365            "Immediate inspection / PM within 3 days",366            "Schedule PM within 7 days",367            "Watch list / inspect soon",368        ],369        default="Normal monitoring",370    )371    keep = [c for c in [372        "asset_id", "timestamp", "model", "region", "criticality", "days_since_last_pm",373        "anomaly_score", "battery_voltage", "vibration_mm_s", "alarm_count",374        "ml_predicted_failure_risk", "ml_risk_band", "ml_recommended_action"375    ] if c in latest.columns]376    return latest[keep].sort_values("ml_predicted_failure_risk", ascending=False)377 378 379def build_prescriptive_pm_plan(ml_risk_df: pd.DataFrame, linked_df: pd.DataFrame, business_df: pd.DataFrame) -> pd.DataFrame:380    if ml_risk_df.empty:381        return pd.DataFrame()382    plan = ml_risk_df.copy()383    if not linked_df.empty and {"asset_id", "days_to_next_failure"}.issubset(linked_df.columns):384        hist = linked_df.copy()385        hist["days_to_next_failure"] = pd.to_numeric(hist["days_to_next_failure"], errors="coerce")386        hist = hist.groupby("asset_id", as_index=False)["days_to_next_failure"].median()387        hist = hist.rename(columns={"days_to_next_failure": "historical_median_days_to_failure"})388        plan = plan.merge(hist, on="asset_id", how="left")389 390    avg_failure_cost = 0.0391    if not business_df.empty:392        if "repair_cost" in business_df.columns:393            avg_failure_cost += pd.to_numeric(business_df["repair_cost"], errors="coerce").fillna(0).mean()394        if "truck_roll_cost" in business_df.columns:395            avg_failure_cost += pd.to_numeric(business_df["truck_roll_cost"], errors="coerce").fillna(0).mean()396    plan["estimated_failure_cost_exposure"] = avg_failure_cost397 398    plan["prescriptive_pm_action"] = np.select(399        [400            plan["ml_predicted_failure_risk"] >= 0.80,401            (plan["ml_predicted_failure_risk"] >= 0.60) | (pd.to_numeric(plan.get("days_since_last_pm", 0), errors="coerce").fillna(0) > 120),402            plan["ml_predicted_failure_risk"] >= 0.30,403        ],404        [405            "Create urgent work order and inspect immediately",406            "Plan PM in next 7 days and review alarms / vibration",407            "Increase monitoring and inspect in next PM window",408        ],409        default="Maintain current PM cycle",410    )411    plan["priority_rank"] = plan["ml_predicted_failure_risk"].rank(method="dense", ascending=False).astype(int)412    return plan.sort_values("priority_rank")413 414def identify_uploaded_zip(uploaded_zip) -> Dict[str, pd.DataFrame]:415    """Accepts a zip containing CSVs with expected names."""416    tables = {}417    if uploaded_zip is None:418        return tables419 420    expected = {421        "asset_master": "asset_master.csv",422        "pm_events": "pm_events.csv",423        "failure_events": "failure_events.csv",424        "business_impact": "business_impact.csv",425        "pm_failure_linked": "pm_failure_linked.csv",426        "telemetry_weekly": "telemetry_weekly.csv",427    }428 429    with zipfile.ZipFile(uploaded_zip) as z:430        names = z.namelist()431        for key, filename in expected.items():432            match = [n for n in names if n.endswith(filename)]433            if match:434                with z.open(match[0]) as f:435                    tables[key] = pd.read_csv(f)436    return tables437 438 439def build_pm_failure_linked(pm_df: pd.DataFrame, failure_df: pd.DataFrame) -> pd.DataFrame:440    required_pm = {"pm_event_id", "asset_id", "pm_date"}441    required_fail = {"failure_event_id", "asset_id", "failure_date"}442 443    if pm_df.empty or failure_df.empty:444        return pd.DataFrame()445    if not required_pm.issubset(pm_df.columns) or not required_fail.issubset(failure_df.columns):446        return pd.DataFrame()447 448    pm = parse_date_cols(pm_df, ["pm_date"]).copy()449    failures = parse_date_cols(failure_df, ["failure_date"]).copy()450 451    failure_groups = {}452    for asset_id, g in failures.dropna(subset=["failure_date"]).sort_values("failure_date").groupby("asset_id"):453        failure_groups[asset_id] = g.reset_index(drop=True)454 455    rows = []456    for _, row in pm.dropna(subset=["pm_date"]).iterrows():457        asset_id = row["asset_id"]458        pm_date = row["pm_date"]459        next_fail = None460        if asset_id in failure_groups:461            g = failure_groups[asset_id]462            pos = np.searchsorted(g["failure_date"].values.astype("datetime64[ns]"), np.datetime64(pm_date), side="right")463            if pos < len(g):464                next_fail = g.iloc[int(pos)]465 466        if next_fail is not None:467            days = (next_fail["failure_date"] - pm_date).days468            rows.append({469                "pm_event_id": row.get("pm_event_id"),470                "asset_id": asset_id,471                "pm_date": pm_date,472                "next_failure_event_id": next_fail.get("failure_event_id"),473                "next_failure_date": next_fail.get("failure_date"),474                "days_to_next_failure": days,475                "failure_found_flag": 1,476                "ontime_flag": row.get("ontime_flag", np.nan),477                "delay_days": row.get("delay_days", np.nan),478                "pm_total_cost": row.get("total_cost", np.nan),479                "failure_total_cost": next_fail.get("total_cost", np.nan),480                "failure_category": next_fail.get("failure_category", "Unknown"),481            })482        else:483            rows.append({484                "pm_event_id": row.get("pm_event_id"),485                "asset_id": asset_id,486                "pm_date": pm_date,487                "next_failure_event_id": None,488                "next_failure_date": pd.NaT,489                "days_to_next_failure": np.nan,490                "failure_found_flag": 0,491                "ontime_flag": row.get("ontime_flag", np.nan),492                "delay_days": row.get("delay_days", np.nan),493                "pm_total_cost": row.get("total_cost", np.nan),494                "failure_total_cost": np.nan,495                "failure_category": None,496            })497 498    return pd.DataFrame(rows)499 500 501def apply_asset_filters(tables: Dict[str, pd.DataFrame], regions, models, criticalities) -> Dict[str, pd.DataFrame]:502    assets = tables.get("asset_master", pd.DataFrame()).copy()503    if assets.empty:504        return tables505 506    mask = pd.Series(True, index=assets.index)507    if regions and "region" in assets.columns:508        mask &= assets["region"].isin(regions)509    if models and "model" in assets.columns:510        mask &= assets["model"].isin(models)511    if criticalities and "criticality" in assets.columns:512        mask &= assets["criticality"].isin(criticalities)513 514    keep_assets = set(assets.loc[mask, "asset_id"].astype(str))515    out = {}516    for key, df in tables.items():517        if isinstance(df, pd.DataFrame) and not df.empty and "asset_id" in df.columns:518            out[key] = df[df["asset_id"].astype(str).isin(keep_assets)].copy()519        else:520            out[key] = df.copy() if isinstance(df, pd.DataFrame) else df521    return out522 523 524def survival_curve(linked: pd.DataFrame) -> pd.DataFrame:525    if linked.empty or "days_to_next_failure" not in linked.columns:526        return pd.DataFrame(columns=["days", "failure_free_probability"])527    d = linked.loc[linked["days_to_next_failure"].notna(), "days_to_next_failure"].astype(float)528    if d.empty:529        return pd.DataFrame(columns=["days", "failure_free_probability"])530    days_grid = np.arange(0, max(30, int(d.max()) + 30), 30)531    n = len(d)532    vals = []533    for day in days_grid:534        vals.append({535            "days": int(day),536            "failure_free_probability": float((d > day).sum() / n)537        })538    return pd.DataFrame(vals)539 540 541def make_download_zip(tables: Dict[str, pd.DataFrame]) -> bytes:542    mem = io.BytesIO()543    with zipfile.ZipFile(mem, "w", compression=zipfile.ZIP_DEFLATED) as z:544        for name, df in tables.items():545            if isinstance(df, pd.DataFrame) and not df.empty:546                z.writestr(f"{name}.csv", df.to_csv(index=False))547    mem.seek(0)548    return mem.read()549 550 551def metric_card(label, value, help_text=None):552    st.metric(label, value, help=help_text)553 554 555# -----------------------------556# Sidebar: data loading557# -----------------------------558st.sidebar.title("⚙️ Dashboard Controls")559 560st.sidebar.markdown(561    """562    **Data mode**563    - Default mode loads the included generator PM demo dataset.564    - Upload mode lets you override one or more CSVs.565    """566)567 568data_mode = st.sidebar.radio(569    "Choose data source",570    ["Use included default dataset", "Upload my own CSVs / ZIP"],571    index=0,572)573 574tables = load_default_tables()575raw_telemetry_df, raw_telemetry_source_path = load_default_raw_telemetry()576 577if data_mode == "Upload my own CSVs / ZIP":578    st.sidebar.info("Upload a ZIP with expected CSV names, or upload individual CSVs below. Missing files fall back to default data.")579 580    uploaded_zip = st.sidebar.file_uploader(581        "Optional: upload full data ZIP",582        type=["zip"],583        help="ZIP may contain asset_master.csv, pm_events.csv, failure_events.csv, telemetry_weekly.csv, business_impact.csv, pm_failure_linked.csv",584    )585    try:586        zip_tables = identify_uploaded_zip(uploaded_zip)587        for k, v in zip_tables.items():588            tables[k] = v589    except Exception as e:590        st.sidebar.error(f"Could not read ZIP: {e}")591 592    upload_specs = {593        "asset_master": "asset_master.csv",594        "pm_events": "pm_events.csv",595        "failure_events": "failure_events.csv",596        "business_impact": "business_impact.csv",597        "pm_failure_linked": "pm_failure_linked.csv",598        "telemetry_weekly": "telemetry_weekly.csv",599    }600 601    with st.sidebar.expander("Upload individual CSVs"):602        for key, label in upload_specs.items():603            f = st.file_uploader(label, type=["csv"], key=f"upload_{key}")604            if f is not None:605                try:606                    tables[key] = pd.read_csv(f)607                except Exception as e:608                    st.error(f"Could not read {label}: {e}")609 610    with st.sidebar.expander("Upload raw telemetry CSV"):611        raw_file = st.file_uploader(612            "generator_telemetry_with_labels.csv",613            type=["csv"],614            key="upload_raw_telemetry",615            help="Optional raw telemetry file before algorithm enrichment. If omitted, dashboard uses the WSL path when found, otherwise the included package file.",616        )617        if raw_file is not None:618            try:619                raw_telemetry_df = parse_date_cols(pd.read_csv(raw_file), ["timestamp"])620                raw_telemetry_source_path = "Uploaded file: generator_telemetry_with_labels.csv"621            except Exception as e:622                st.error(f"Could not read raw telemetry CSV: {e}")623# Parse dates after custom upload.624tables["asset_master"] = parse_date_cols(tables.get("asset_master", pd.DataFrame()), ["install_date"])625tables["pm_events"] = parse_date_cols(tables.get("pm_events", pd.DataFrame()), ["scheduled_date", "completed_date", "pm_date"])626tables["failure_events"] = parse_date_cols(tables.get("failure_events", pd.DataFrame()), ["failure_date", "ticket_open_date", "ticket_close_date"])627tables["business_impact"] = parse_date_cols(tables.get("business_impact", pd.DataFrame()), ["event_date"])628tables["pm_failure_linked"] = parse_date_cols(tables.get("pm_failure_linked", pd.DataFrame()), ["pm_date", "next_failure_date"])629tables["telemetry_weekly"] = parse_date_cols(tables.get("telemetry_weekly", pd.DataFrame()), ["timestamp"])630raw_telemetry_df = parse_date_cols(raw_telemetry_df, ["timestamp"])631algorithm_output_df = enrich_algorithm_output(raw_telemetry_df)632algorithm_asset_summary_df = summarize_algorithm_by_asset(algorithm_output_df)633 634# If the user uploaded PM/failure but not linked, rebuild it.635if tables.get("pm_failure_linked", pd.DataFrame()).empty and not tables.get("pm_events", pd.DataFrame()).empty and not tables.get("failure_events", pd.DataFrame()).empty:636    tables["pm_failure_linked"] = build_pm_failure_linked(tables["pm_events"], tables["failure_events"])637 638# Also allow forced rebuild.639if st.sidebar.button("Rebuild PM → Failure Links"):640    tables["pm_failure_linked"] = build_pm_failure_linked(tables["pm_events"], tables["failure_events"])641    st.sidebar.success("Rebuilt pm_failure_linked from PM and failure tables.")642 643assets = tables.get("asset_master", pd.DataFrame())644regions = []645models = []646criticalities = []647if not assets.empty:648    if "region" in assets.columns:649        all_regions = sorted([x for x in assets["region"].dropna().unique().tolist()])650        regions = st.sidebar.multiselect("Filter region", all_regions, default=all_regions)651    if "model" in assets.columns:652        all_models = sorted([x for x in assets["model"].dropna().unique().tolist()])653        models = st.sidebar.multiselect("Filter model", all_models, default=all_models)654    if "criticality" in assets.columns:655        all_criticality = sorted([x for x in assets["criticality"].dropna().unique().tolist()])656        criticalities = st.sidebar.multiselect("Filter criticality", all_criticality, default=all_criticality)657 658sample_limit = st.sidebar.slider("Telemetry chart sample size", 1_000, 50_000, 10_000, step=1_000)659 660filtered = apply_asset_filters(tables, regions, models, criticalities)661 662asset_df = filtered.get("asset_master", pd.DataFrame())663pm_df = filtered.get("pm_events", pd.DataFrame())664failure_df = filtered.get("failure_events", pd.DataFrame())665business_df = filtered.get("business_impact", pd.DataFrame())666linked_df = filtered.get("pm_failure_linked", pd.DataFrame())667telemetry_df = filtered.get("telemetry_weekly", pd.DataFrame())668 669# -----------------------------670# Header671# -----------------------------672st.title("⚙️ Generator Preventive Maintenance Reliability Dashboard")673st.caption("Upload your own maintenance data or use the included demo dataset to analyze PM effectiveness, failure risk, and business impact.")674 675with st.expander("Expected CSV inputs", expanded=False):676    st.markdown(677        """678        **Recommended CSVs**679        - `asset_master.csv`: one row per generator/asset680        - `pm_events.csv`: preventive maintenance events681        - `failure_events.csv`: unplanned repair/failure tickets682        - `pm_failure_linked.csv`: optional; dashboard can rebuild it683        - `telemetry_weekly.csv`: optional sensor/risk history684        - `generator_telemetry_with_labels.csv`: optional ML training table with `failure_within_14d` and `failure_within_30d`685        - `business_impact.csv`: optional downtime, repair cost, customer impact686 687        **Minimum required for PM-to-failure analysis**688        - `pm_events.csv` with `pm_event_id`, `asset_id`, `pm_date`689        - `failure_events.csv` with `failure_event_id`, `asset_id`, `failure_date`690        """691    )692 693# -----------------------------694# Tabs695# -----------------------------696tabs = st.tabs([697    "Problem & Solution",698    "Dataset & Raw → Algorithm",699    "Executive KPIs",700    "PM Effectiveness",701    "Failures & Cost",702    "Telemetry Risk",703    "Predictive ML & PM Strategy",704    "Saved Outputs",705    "Data Explorer & Export",706])707 708 709# -----------------------------710# Problem & Solution tab711# -----------------------------712with tabs[0]:713    st.header("Problem Statement")714    st.markdown(715        """716        Telecom and field-service teams need to know whether **preventive maintenance is actually preventing generator failures**.717 718        The key business question is:719 720        > After a PM is completed, how long does a generator typically run before the next failure or repair ticket?721 722        Without this, teams cannot confidently answer:723        - Which PMs are effective?724        - Which assets are becoming risky?725        - How much downtime and cost can be avoided?726        - Which regions, models, or environments need more attention?727        """728    )729 730    st.header("Solution Provided")731    st.markdown(732        """733        This dashboard converts maintenance records into a reliability workflow:734 735        1. **Load data**  736           Use the included generator demo dataset or upload your own CSVs.737 738        2. **Link PM to next failure**  739           For every PM event, the dashboard finds the first later failure ticket for the same asset.740 741        3. **Measure PM effectiveness**  742           It calculates `days_to_next_failure`, failure-free rate, and on-time vs delayed PM comparisons.743 744        4. **Analyze failure cost and downtime**  745           It summarizes repair cost, truck-roll cost, downtime, SLA impact, and customer impact.746 747        5. **Train ML failure-risk models**  748           Logistic Regression and Random Forest predict whether a generator may fail within 14 or 30 days.749 750        6. **Prescribe PM actions**  751           ML risk scores are converted into priority bands and recommended maintenance actions.752 753        7. **Export outputs**  754           KPI tables and processed datasets can be downloaded for Power BI, Excel, Streamlit sharing, or a client deck.755        """756    )757 758    st.subheader("Architecture")759    st.code(760        """761PM Events + Failure Tickets + Asset Master762          │763          ▼764PM → Next Failure Linker765          │766          ├── PM effectiveness KPIs767          ├── Failure-free survival curve768          ├── On-time vs delayed PM comparison769          ├── Failure cost and downtime view770          └── Telemetry risk overlay771        """,772        language="text",773    )774 775    st.subheader("Dataset currently loaded")776    c1, c2, c3, c4, c5 = st.columns(5)777    c1.metric("Assets", f"{len(asset_df):,}")778    c2.metric("PM events", f"{len(pm_df):,}")779    c3.metric("Failure tickets", f"{len(failure_df):,}")780    c4.metric("Telemetry rows", f"{len(telemetry_df):,}")781    c5.metric("Linked PM rows", f"{len(linked_df):,}")782 783 784# -----------------------------785# -----------------------------786# Dataset and Raw → Algorithm tab787# -----------------------------788with tabs[1]:789    st.header("Dataset Used: Raw → Algorithm Output")790    st.markdown(791        """792        This page explains exactly what data is being used, where it is expected in WSL, what the raw file looks like before the algorithm, and what the algorithm produces after enrichment.793 794        **Packaged raw dataset used by the app:**795        """796    )797    st.code("data/raw/generator_telemetry_with_labels.csv", language="text")798 799    st.info(800        f"Currently loaded raw telemetry source: {raw_telemetry_source_path}\n\n"801        "The dashboard uses the packaged demo raw file. You can replace it by uploading your own raw telemetry CSV from the sidebar."802    )803 804    st.subheader("1) Raw dataset profile before algorithm")805    st.dataframe(dataframe_profile(raw_telemetry_df, raw_telemetry_source_path), use_container_width=True)806 807    if raw_telemetry_df.empty:808        st.warning("Raw telemetry dataset is not available. Place generator_telemetry_with_labels.csv at the WSL path above or upload it from the sidebar.")809    else:810        c1, c2 = st.columns([2, 1])811        with c1:812            st.markdown("**Raw telemetry preview**")813            st.dataframe(raw_telemetry_df.head(1000), use_container_width=True)814        with c2:815            missing = raw_telemetry_df.isna().sum().sort_values(ascending=False).head(12).reset_index()816            missing.columns = ["column", "missing_count"]817            fig = px.bar(missing, x="missing_count", y="column", orientation="h", title="Raw missing values by column")818            st.plotly_chart(fig, use_container_width=True)819 820        st.markdown("**Raw signal visualizations before algorithm**")821        c3, c4 = st.columns(2)822        with c3:823            if "anomaly_score" in raw_telemetry_df.columns:824                fig = px.histogram(raw_telemetry_df, x="anomaly_score", nbins=50, title="Raw anomaly score distribution")825                st.plotly_chart(fig, use_container_width=True)826            elif "runtime_hours_week" in raw_telemetry_df.columns:827                fig = px.histogram(raw_telemetry_df, x="runtime_hours_week", nbins=50, title="Raw weekly runtime distribution")828                st.plotly_chart(fig, use_container_width=True)829        with c4:830            signal_cols = [c for c in ["oil_temp_c", "coolant_temp_c", "battery_voltage", "vibration_mm_s", "fuel_rate_lph", "runtime_hours_week"] if c in raw_telemetry_df.columns]831            if signal_cols and "timestamp" in raw_telemetry_df.columns:832                sample_asset = raw_telemetry_df["asset_id"].iloc[0] if "asset_id" in raw_telemetry_df.columns else None833                signal = signal_cols[0]834                plot_df = raw_telemetry_df[raw_telemetry_df["asset_id"].eq(sample_asset)].copy() if sample_asset and "asset_id" in raw_telemetry_df.columns else raw_telemetry_df.head(200)835                fig = px.line(plot_df.sort_values("timestamp"), x="timestamp", y=signal, title=f"Raw signal trend example: {signal}")836                st.plotly_chart(fig, use_container_width=True)837 838        st.subheader("2) Algorithm applied")839        st.markdown(840            """841            The dashboard creates a transparent demo algorithm layer from the raw telemetry:842 843            - Uses `anomaly_score` as the main health signal.844            - Adds PM-age pressure using `days_since_last_pm`.845            - Produces `algorithm_failure_risk_score` on a 0–100 scale.846            - Converts risk score into `Low`, `Watch`, `High`, or `Critical` bands.847            - Adds `algorithm_predicted_failure_within_30d` and a recommended maintenance action.848 849            This is intentionally explainable for a business demo. When real client data is available, this layer can be replaced by a trained classifier, survival model, or remaining-useful-life model.850            """851        )852 853        st.code(854            """855algorithm_failure_risk_score = 0.75 * anomaly_score + 0.25 * normalized_days_since_last_pm856risk bands:857  Low      < 40858  Watch    40–59859  High     60–79860  Critical >= 80861prediction:862  predicted_failure_within_30d = 1 when risk_score >= 70863            """.strip(),864            language="text",865        )866 867        st.subheader("3) After algorithm: enriched output")868        if algorithm_output_df.empty:869            st.warning("Algorithm output is empty because no raw telemetry was loaded.")870        else:871            c5, c6, c7, c8 = st.columns(4)872            c5.metric("Raw rows processed", f"{len(raw_telemetry_df):,}")873            c6.metric("Algorithm output rows", f"{len(algorithm_output_df):,}")874            c7.metric("Assets scored", f"{algorithm_output_df['asset_id'].nunique():,}" if "asset_id" in algorithm_output_df.columns else "0")875            c8.metric("Predicted 30-day failures", f"{int(algorithm_output_df.get('algorithm_predicted_failure_within_30d', pd.Series(dtype=int)).sum()):,}")876 877            c9, c10 = st.columns(2)878            with c9:879                risk_counts = algorithm_output_df["algorithm_risk_band"].value_counts().reindex(["Low", "Watch", "High", "Critical"]).dropna().reset_index()880                risk_counts.columns = ["risk_band", "row_count"]881                fig = px.bar(risk_counts, x="risk_band", y="row_count", title="After algorithm: risk-band distribution")882                st.plotly_chart(fig, use_container_width=True)883            with c10:884                if {"region", "algorithm_predicted_failure_within_30d"}.issubset(algorithm_output_df.columns):885                    region_risk = algorithm_output_df.groupby("region", as_index=False)["algorithm_predicted_failure_within_30d"].sum()886                    fig = px.bar(region_risk, x="region", y="algorithm_predicted_failure_within_30d", title="Predicted 30-day failures by region")887                    st.plotly_chart(fig, use_container_width=True)888 889            st.markdown("**Algorithm output preview**")890            output_cols = [c for c in ["asset_id", "timestamp", "model", "region", "criticality", "days_since_last_pm", "anomaly_score", "algorithm_failure_risk_score", "algorithm_risk_band", "algorithm_predicted_failure_within_30d", "recommended_action"] if c in algorithm_output_df.columns]891            st.dataframe(algorithm_output_df[output_cols].head(2000), use_container_width=True)892 893            st.markdown("**Latest asset-level output after algorithm**")894            st.dataframe(algorithm_asset_summary_df.head(500), use_container_width=True)895 896            st.download_button(897                "Download algorithm_output.csv",898                data=algorithm_output_df.to_csv(index=False),899                file_name="algorithm_output.csv",900                mime="text/csv",901            )902            st.download_button(903                "Download latest_asset_risk_summary.csv",904                data=algorithm_asset_summary_df.to_csv(index=False),905                file_name="latest_asset_risk_summary.csv",906                mime="text/csv",907            )908 909    st.subheader("4) WSL commands to run dashboard")910    st.code(911        """912cd "local-user-path-hidden/OneDrive - Pearce Services, LLC/onedrive_ubuntu/project/Predictive_Preventive_Maintenance_for_Generator_Reliability"913pip install -r requirements.txt914streamlit run app.py915        """.strip(),916        language="bash",917    )918 919# Executive KPIs tab920# -----------------------------921with tabs[2]:922    st.header("Executive KPIs")923 924    valid_days = pd.Series(dtype=float)925    if not linked_df.empty and "days_to_next_failure" in linked_df.columns:926        valid_days = pd.to_numeric(linked_df["days_to_next_failure"], errors="coerce").dropna()927 928    avg_days = valid_days.mean() if not valid_days.empty else np.nan929    median_days = valid_days.median() if not valid_days.empty else np.nan930    fail_30 = (valid_days <= 30).mean() if not valid_days.empty else np.nan931    fail_90 = (valid_days <= 90).mean() if not valid_days.empty else np.nan932 933    total_downtime = pd.to_numeric(failure_df.get("downtime_hours", pd.Series(dtype=float)), errors="coerce").sum()934    total_failure_cost = pd.to_numeric(failure_df.get("total_cost", pd.Series(dtype=float)), errors="coerce").sum()935 936    total_revenue_loss = 0937    total_customers = 0938    if not business_df.empty:939        total_revenue_loss = pd.to_numeric(business_df.get("estimated_revenue_loss", pd.Series(dtype=float)), errors="coerce").sum()940        total_customers = pd.to_numeric(business_df.get("estimated_customers_impacted", pd.Series(dtype=float)), errors="coerce").sum()941 942    c1, c2, c3, c4 = st.columns(4)943    c1.metric("Avg days PM → failure", num(avg_days, 1))944    c2.metric("Median days PM → failure", num(median_days, 1))945    c3.metric("Failure within 30 days", pct(fail_30))946    c4.metric("Failure within 90 days", pct(fail_90))947 948    c5, c6, c7, c8 = st.columns(4)949    c5.metric("Total downtime hours", num(total_downtime, 1))950    c6.metric("Failure repair cost", money(total_failure_cost))951    c7.metric("Estimated revenue loss", money(total_revenue_loss))952    c8.metric("Customer impact count", num(total_customers, 0))953 954    st.divider()955 956    st.subheader("Executive interpretation")957    st.markdown(958        f"""959        - The loaded data contains **{len(asset_df):,} assets**, **{len(pm_df):,} PM events**, and **{len(failure_df):,} failure/repair tickets**.960        - The average observed time from PM to the next failure is **{num(avg_days, 1)} days**.961        - **{pct(fail_30)}** of linked PM events are followed by a failure within 30 days.962        - The failure records represent approximately **{num(total_downtime, 1)} downtime hours** and **{money(total_failure_cost)}** in repair cost.963        """964    )965 966    if not linked_df.empty and "failure_found_flag" in linked_df.columns:967        status_counts = linked_df["failure_found_flag"].value_counts().rename(index={0: "No later failure observed", 1: "Later failure observed"})968        fig = px.pie(969            values=status_counts.values,970            names=status_counts.index,971            title="Linked PM events with later failure observed",972            hole=0.35,973        )974        st.plotly_chart(fig, use_container_width=True)975 976 977# -----------------------------978# PM Effectiveness tab979# -----------------------------980with tabs[3]:981    st.header("Preventive Maintenance Effectiveness")982 983    if linked_df.empty or "days_to_next_failure" not in linked_df.columns:984        st.warning("No PM-to-failure linked data found. Upload PM and failure CSVs, then click 'Rebuild PM → Failure Links'.")985    else:986        linked_plot = linked_df.copy()987        linked_plot["days_to_next_failure"] = pd.to_numeric(linked_plot["days_to_next_failure"], errors="coerce")988 989        c1, c2 = st.columns(2)990 991        with c1:992            fig = px.histogram(993                linked_plot.dropna(subset=["days_to_next_failure"]),994                x="days_to_next_failure",995                nbins=50,996                title="Distribution: Days from PM to next failure",997                labels={"days_to_next_failure": "Days to next failure"},998            )999            st.plotly_chart(fig, use_container_width=True)1000 1001        with c2:1002            surv = survival_curve(linked_plot)1003            if not surv.empty:1004                fig = px.line(1005                    surv,1006                    x="days",1007                    y="failure_free_probability",1008                    markers=True,1009                    title="Failure-free probability after PM",1010                    labels={"days": "Days after PM", "failure_free_probability": "Failure-free probability"},1011                )1012                fig.update_yaxes(tickformat=".0%")1013                st.plotly_chart(fig, use_container_width=True)1014 1015        c3, c4 = st.columns(2)1016        with c3:1017            if "ontime_flag" in linked_plot.columns:1018                temp = linked_plot.dropna(subset=["ontime_flag", "days_to_next_failure"]).copy()1019                if not temp.empty:1020                    temp["PM status"] = temp["ontime_flag"].map({1: "On-time PM", 0: "Delayed PM"}).fillna("Unknown")1021                    fig = px.box(1022                        temp,1023                        x="PM status",1024                        y="days_to_next_failure",1025                        points="outliers",1026                        title="On-time vs delayed PM: Days to next failure",1027                    )1028                    st.plotly_chart(fig, use_container_width=True)1029 1030        with c4:1031            if "delay_days" in linked_plot.columns:1032                temp = linked_plot.dropna(subset=["delay_days", "days_to_next_failure"]).copy()1033                if not temp.empty:1034                    fig = px.scatter(1035                        temp.sample(min(len(temp), 5000), random_state=42),1036                        x="delay_days",1037                        y="days_to_next_failure",1038                        trendline="ols",1039                        title="PM delay vs days to next failure",1040                        labels={"delay_days": "PM delay days", "days_to_next_failure": "Days to next failure"},1041                    )1042                    st.plotly_chart(fig, use_container_width=True)1043 1044        st.subheader("PM effectiveness data")1045        st.dataframe(linked_plot.head(1000), use_container_width=True)1046 1047 1048# -----------------------------1049# Failures & Cost tab1050# -----------------------------1051with tabs[4]:1052    st.header("Failure Patterns, Cost, and Downtime")1053 1054    if failure_df.empty:1055        st.warning("No failure_events.csv data found.")1056    else:1057        c1, c2 = st.columns(2)1058 1059        with c1:1060            if "failure_category" in failure_df.columns:1061                cat = failure_df["failure_category"].fillna("Unknown").value_counts().reset_index()1062                cat.columns = ["failure_category", "count"]1063                fig = px.bar(cat, x="failure_category", y="count", title="Failures by category")1064                st.plotly_chart(fig, use_container_width=True)1065 1066        with c2:1067            if "severity" in failure_df.columns:1068                sev = failure_df["severity"].fillna("Unknown").value_counts().reset_index()1069                sev.columns = ["severity", "count"]1070                fig = px.bar(sev, x="severity", y="count", title="Failures by severity")1071                st.plotly_chart(fig, use_container_width=True)1072 1073        c3, c4 = st.columns(2)1074 1075        with c3:1076            if {"region", "total_cost"}.issubset(failure_df.columns):1077                temp = failure_df.copy()1078                temp["total_cost"] = pd.to_numeric(temp["total_cost"], errors="coerce")1079                reg = temp.groupby("region", as_index=False)["total_cost"].sum()1080                fig = px.bar(reg, x="region", y="total_cost", title="Repair cost by region")1081                st.plotly_chart(fig, use_container_width=True)1082 1083        with c4:1084            if {"model", "downtime_hours"}.issubset(failure_df.columns):1085                temp = failure_df.copy()1086                temp["downtime_hours"] = pd.to_numeric(temp["downtime_hours"], errors="coerce")1087                mod = temp.groupby("model", as_index=False)["downtime_hours"].sum().sort_values("downtime_hours", ascending=False)1088                fig = px.bar(mod, x="model", y="downtime_hours", title="Downtime hours by model")1089                st.plotly_chart(fig, use_container_width=True)1090 1091        if not business_df.empty:1092            st.subheader("Business impact")1093            b1, b2, b3 = st.columns(3)1094            b1.metric("Truck-roll cost", money(pd.to_numeric(business_df.get("truck_roll_cost", pd.Series(dtype=float)), errors="coerce").sum()))1095            b2.metric("Revenue loss", money(pd.to_numeric(business_df.get("estimated_revenue_loss", pd.Series(dtype=float)), errors="coerce").sum()))1096            b3.metric("SLA breach events", num(pd.to_numeric(business_df.get("sla_breach_flag", pd.Series(dtype=float)), errors="coerce").sum(), 0))1097 1098            if {"event_date", "estimated_revenue_loss"}.issubset(business_df.columns):1099                temp = business_df.copy()1100                temp["event_month"] = pd.to_datetime(temp["event_date"], errors="coerce").dt.to_period("M").astype(str)1101                temp["estimated_revenue_loss"] = pd.to_numeric(temp["estimated_revenue_loss"], errors="coerce")1102                month = temp.groupby("event_month", as_index=False)["estimated_revenue_loss"].sum()1103                fig = px.line(month, x="event_month", y="estimated_revenue_loss", markers=True, title="Estimated revenue loss over time")1104                st.plotly_chart(fig, use_container_width=True)1105 1106 1107# -----------------------------1108# Telemetry Risk tab1109# -----------------------------1110with tabs[5]:1111    st.header("Telemetry and Failure Risk")1112 1113    if telemetry_df.empty:1114        st.warning("No telemetry_weekly.csv data found.")1115    else:1116        tele = telemetry_df.copy()1117        for col in ["anomaly_score", "days_since_last_pm", "days_to_next_failure", "failure_within_30d"]:1118            if col in tele.columns:1119                tele[col] = pd.to_numeric(tele[col], errors="coerce")1120 1121        high_risk = pd.DataFrame()1122        if "anomaly_score" in tele.columns:1123            latest = tele.sort_values("timestamp").groupby("asset_id", as_index=False).tail(1)1124            high_risk = latest.sort_values("anomaly_score", ascending=False).head(25)1125 1126            c1, c2, c3 = st.columns(3)1127            c1.metric("Latest high-risk assets shown", f"{len(high_risk):,}")1128            c2.metric("Avg anomaly score", num(tele["anomaly_score"].mean(), 1))1129            if "failure_within_30d" in tele.columns:1130                c3.metric("Rows labeled failure within 30d", f"{int(tele['failure_within_30d'].sum()):,}")1131 1132        c1, c2 = st.columns(2)1133 1134        with c1:1135            if {"days_since_last_pm", "anomaly_score"}.issubset(tele.columns):1136                sample = tele.dropna(subset=["days_since_last_pm", "anomaly_score"])1137                sample = sample.sample(min(len(sample), sample_limit), random_state=42) if len(sample) > sample_limit else sample1138                color_col = "failure_within_30d" if "failure_within_30d" in sample.columns else None1139                fig = px.scatter(1140                    sample,1141                    x="days_since_last_pm",1142                    y="anomaly_score",1143                    color=color_col,1144                    hover_data=["asset_id"] if "asset_id" in sample.columns else None,1145                    title="Anomaly score vs days since last PM",1146                )1147                st.plotly_chart(fig, use_container_width=True)1148 1149        with c2:1150            if {"days_since_last_pm", "anomaly_score"}.issubset(tele.columns):1151                temp = tele.dropna(subset=["days_since_last_pm", "anomaly_score"]).copy()1152                temp["pm_age_bucket"] = pd.cut(temp["days_since_last_pm"], bins=[0, 30, 60, 90, 120, 180, 365, 10000])1153                bucket = temp.groupby("pm_age_bucket", observed=True, as_index=False)["anomaly_score"].mean()1154                bucket["pm_age_bucket"] = bucket["pm_age_bucket"].astype(str)1155                fig = px.bar(bucket, x="pm_age_bucket", y="anomaly_score", title="Average anomaly score by PM age bucket")1156                st.plotly_chart(fig, use_container_width=True)1157 1158        st.subheader("Top risky assets by latest anomaly score")1159        if not high_risk.empty:1160            st.dataframe(high_risk, use_container_width=True)1161 1162 1163 1164# -----------------------------1165# Predictive ML & PM Strategy tab1166# -----------------------------1167with tabs[6]:1168    st.header("Predictive ML and Prescriptive PM Strategy")1169    st.markdown(1170        """1171        This is the **machine-learning section** of the dashboard. It trains real supervised ML models on telemetry records,1172        compares their performance, scores each asset, and converts the score into maintenance actions.1173 1174        **Three-phase flow:**1175        1. **Phase 1 - Reliability analytics:** PM completed → next failure → days to next failure.1176        2. **Phase 2 - Predictive ML:** telemetry + PM recency → failure probability within 14 or 30 days.1177        3. **Phase 3 - Prescriptive PM:** failure probability + business impact → recommended maintenance action.1178        """1179    )1180 1181    # Prefer full raw telemetry with model/region/criticality if available. Otherwise use telemetry_weekly.1182    ml_source_df = raw_telemetry_df.copy() if not raw_telemetry_df.empty else telemetry_df.copy()1183    if ml_source_df.empty:1184        st.warning("No telemetry dataset found for ML training.")1185    else:1186        c1, c2, c3 = st.columns(3)1187        target_col = c1.selectbox("ML prediction target", ["failure_within_30d", "failure_within_14d"], index=0)1188        max_rows = c2.slider("Training sample size", min_value=20000, max_value=120000, value=90000, step=10000)1189        c3.metric("ML source rows", f"{len(ml_source_df):,}")1190 1191        st.subheader("ML inputs")1192        st.markdown(1193            """1194            The model uses telemetry and asset-context features including:1195            `days_since_last_pm`, `runtime_hours_week`, `avg_load_pct`, `oil_temp_c`, `coolant_temp_c`,1196            `battery_voltage`, `vibration_mm_s`, `fuel_rate_lph`, `alarm_count`, `anomaly_score`,1197            `model`, `region`, `criticality`, and `environment_type`.1198            """1199        )1200 

Showing the first 1,200 of 1384 lines. Download the file for the rest.