{ "cells": [ { "cell_type": "markdown", "id": "75f568b1", "metadata": {}, "source": [ "# ✈️ SkyBridge Airlines: Predictive Maintenance Demo\n", "## A Complete BizML Lifecycle — From Business Problem to Deployment\n", "\n", "**Course:** Data Analytics & Data Mining \n", "**Built by:** Dr. Yaa\n", "**Use Case:** Predicting unscheduled engine maintenance within 30 flight cycles \n", "**Company:** SkyBridge Airlines (fictional) \n", "**Deployment:** Nightly batch scoring → Prioritized maintenance worklist\n", "\n", "---\n", "\n", "### What You'll Learn in This Demo\n", "\n", "This notebook walks through all **6 steps of the BizML lifecycle** using a realistic airline preventive maintenance scenario:\n", "\n", "| Step | BizML Phase | What We Do |\n", "|------|-------------|------------|\n", "| 1 | **Business Problem** | Define what we're predicting, who acts on it, and what \"success\" means |\n", "| 2 | **Data Pipeline** | Explore, clean, and engineer features from sensor + operational data |\n", "| 3 | **Model Building** | Train logistic regression, decision tree, and random forest models |\n", "| 4 | **Evaluation** | Compare models using accuracy, precision, recall, AUC, and the confusion matrix |\n", "| 5 | **Deployment** | Export the model to both a Python batch scorer and an Excel decision tool |\n", "| 6 | **Monitoring** | Set up drift detection and performance tracking |\n", "\n", "### The PAIR Framework for This Project\n", "\n", "| PAIR Element | Our Answer |\n", "|---|---|\n", "| **Prediction** | Which engines will require unscheduled maintenance within 30 flight cycles? |\n", "| **Action** | Route flagged engines to inspection during next scheduled ground time |\n", "| **Impact** | Reduce unscheduled AOG (Aircraft on Ground) events by 25%, saving ~$180K per avoided event |\n", "| **Risk** | False negative = missed failure (safety concern). False positive = unnecessary inspection ($8K cost) |\n", "\n", "> ⚠️ **Key Insight:** Because the cost of a missed failure ($180K+ plus safety risk) vastly exceeds the cost of an extra inspection ($8K), we'll tune our model to prioritize **recall** — catching as many true failures as possible, even if it means a few extra inspections.\n" ] }, { "cell_type": "markdown", "id": "e47ea5f0", "metadata": {}, "source": [ "---\n", "## 🔷 Step 1: Define the Business Problem & Project Scope\n", "\n", "### The Challenge\n", "\n", "SkyBridge Airlines operates a fleet of 60 narrowbody aircraft. When an engine requires **unscheduled maintenance**, the aircraft goes AOG (Aircraft on Ground). Each AOG event costs an average of **$180,000** in:\n", "- Flight cancellations and rebooking\n", "- Crew repositioning \n", "- Emergency parts and labor (2–3x scheduled rates)\n", "- Customer compensation and brand damage\n", "\n", "Last year, SkyBridge had **23 unscheduled engine events**, costing over **$4.1 million**.\n", "\n", "### The Opportunity\n", "\n", "The maintenance engineering team currently uses **time-based schedules** (inspect every N flight cycles) supplemented by manual review of sensor trends. They believe a predictive model could:\n", "1. Flag at-risk engines **before** they fail\n", "2. Prioritize inspection queues during scheduled maintenance windows\n", "3. Reduce unscheduled events by 25–40%\n", "\n", "### Project Scope Decisions\n", "\n", "| Decision | Our Choice | Why |\n", "|----------|-----------|-----|\n", "| **Prediction type** | Binary classification | \"Will this engine need unscheduled maintenance within 30 flight cycles?\" — Yes or No |\n", "| **Why not anomaly detection?** | Anomaly detection flags \"something weird\" but doesn't predict *when* or *what action* to take. We use anomaly counts as a *feature* instead. |\n", "| **Why not regression?** | Predicting exact cycle-to-failure requires sensor time series data we don't have. Binary classification matches the business action: inspect or don't inspect. |\n", "| **Deployment mode** | Nightly batch scoring | Maintenance decisions happen during ground time, not mid-flight. A nightly batch run fits the workflow. |\n", "| **Who uses the output?** | Maintenance Planning team (10 people) | They manage inspection schedules and need a prioritized worklist each morning. |\n", "| **Success metric** | ≥75% recall at ≤30% false positive rate | We'd rather inspect a healthy engine than miss a failing one. |\n", "\n", "### What Does \"Deployed\" Look Like?\n", "\n", "Every night at 11 PM:\n", "1. A Python script pulls the latest sensor and operational data\n", "2. The trained model scores every active engine\n", "3. Engines with predicted probability ≥ threshold get flagged\n", "4. A prioritized worklist (sorted by risk probability) lands in the Maintenance Planning team's inbox by 6 AM\n", "5. The team reviews the list and schedules inspections during the next available ground window\n" ] }, { "cell_type": "markdown", "id": "3ac92a0d", "metadata": {}, "source": [ "---\n", "## 🔷 Step 2: Data Pipeline — Explore, Clean, Engineer\n" ] }, { "cell_type": "code", "execution_count": null, "id": "8fbdead3", "metadata": {}, "outputs": [], "source": [ "# ---- Setup ----\n", "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.tree import DecisionTreeClassifier\n", "from sklearn.ensemble import RandomForestClassifier\n", "from sklearn.metrics import (\n", " classification_report, confusion_matrix, roc_auc_score,\n", " roc_curve, precision_recall_curve, ConfusionMatrixDisplay\n", ")\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# Set visual style\n", "plt.rcParams['figure.figsize'] = (10, 6)\n", "plt.rcParams['font.size'] = 12\n", "sns.set_style(\"whitegrid\")\n", "\n", "print(\"✅ All libraries loaded successfully!\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "9cb67aff", "metadata": {}, "outputs": [], "source": [ "# ---- Load the Dataset ----\n", "df = pd.read_csv(\"skybridge_engine_maintenance.csv\")\n", "print(f\"Dataset shape: {df.shape[0]:,} engines × {df.shape[1]} columns\")\n", "print(f\"\\nTarget distribution:\")\n", "print(df['needs_maintenance_30cyc'].value_counts(normalize=True).map('{:.1%}'.format))\n", "df.head()\n" ] }, { "cell_type": "markdown", "id": "3552f77a", "metadata": {}, "source": [ "### 2.1 Exploratory Data Analysis (EDA)\n", "\n", "Before building any model, we need to understand what our data looks like. Think of this like a doctor reviewing a patient's chart before making a diagnosis — you need the full picture first.\n", "\n", "**What we're looking for:**\n", "- How balanced is the target? (If 99% of engines are fine, we need special handling)\n", "- Which features seem related to maintenance needs?\n", "- Any missing values or weird outliers?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "cdde1354", "metadata": {}, "outputs": [], "source": [ "# ---- Data Overview ----\n", "print(\"=\" * 50)\n", "print(\"DATA QUALITY CHECK\")\n", "print(\"=\" * 50)\n", "print(f\"\\nTotal records: {len(df):,}\")\n", "print(f\"Missing values:\\n{df.isnull().sum()[df.isnull().sum() > 0]}\")\n", "if df.isnull().sum().sum() == 0:\n", " print(\" → None! Clean dataset. ✅\")\n", " \n", "print(f\"\\nDuplicates: {df.duplicated().sum()}\")\n", "print(f\"\\nTarget balance:\")\n", "target_counts = df['needs_maintenance_30cyc'].value_counts()\n", "print(f\" No maintenance needed: {target_counts[0]:,} ({target_counts[0]/len(df):.1%})\")\n", "print(f\" Maintenance needed: {target_counts[1]:,} ({target_counts[1]/len(df):.1%})\")\n", "print(f\"\\n → {'Moderately imbalanced — worth monitoring but workable.' if target_counts[1]/len(df) < 0.4 else 'Fairly balanced — good.'}\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "c3fcb2e0", "metadata": {}, "outputs": [], "source": [ "# ---- Descriptive Statistics ----\n", "numeric_cols = df.select_dtypes(include=[np.number]).columns.drop('needs_maintenance_30cyc')\n", "df[numeric_cols].describe().round(2)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "b1bd4707", "metadata": {}, "outputs": [], "source": [ "# ---- Target vs. Key Sensor Readings ----\n", "fig, axes = plt.subplots(2, 3, figsize=(16, 10))\n", "fig.suptitle(\"Sensor Readings: Maintenance Needed vs. Not Needed\", fontsize=14, fontweight='bold')\n", "\n", "sensor_cols = [\n", " 'avg_exhaust_temp_c', 'avg_vibration_level', 'oil_pressure_psi',\n", " 'fuel_flow_rate_lph', 'bleed_air_temp_c', 'anomaly_flags_30d'\n", "]\n", "labels = {0: 'No Maintenance', 1: 'Needs Maintenance'}\n", "colors = {0: '#5B9BD5', 1: '#E07B54'}\n", "\n", "for idx, col in enumerate(sensor_cols):\n", " ax = axes[idx // 3][idx % 3]\n", " for val in [0, 1]:\n", " subset = df[df['needs_maintenance_30cyc'] == val][col]\n", " ax.hist(subset, bins=30, alpha=0.6, label=labels[val], color=colors[val])\n", " ax.set_title(col.replace('_', ' ').title(), fontsize=11)\n", " ax.legend(fontsize=9)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"💡 What to notice: Engines needing maintenance tend to have higher exhaust temps,\")\n", "print(\" higher vibration, lower oil pressure, and more anomaly flags.\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "aab60a90", "metadata": {}, "outputs": [], "source": [ "# ---- Correlation with Target ----\n", "target_corr = df[numeric_cols].corrwith(df['needs_maintenance_30cyc']).sort_values(ascending=False)\n", "\n", "fig, ax = plt.subplots(figsize=(10, 6))\n", "colors = ['#E07B54' if v > 0 else '#5B9BD5' for v in target_corr.values]\n", "target_corr.plot(kind='barh', color=colors, ax=ax)\n", "ax.set_title(\"Feature Correlation with Maintenance Need\", fontweight='bold')\n", "ax.set_xlabel(\"Correlation Coefficient\")\n", "ax.axvline(x=0, color='gray', linestyle='--', alpha=0.5)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"💡 Positive correlation = higher values → more likely to need maintenance\")\n", "print(\" Negative correlation = lower values → more likely to need maintenance\")\n" ] }, { "cell_type": "markdown", "id": "efa70663", "metadata": {}, "source": [ "### 2.2 Data Preparation & Feature Engineering\n", "\n", "Now that we understand the data, let's get it model-ready. We need to:\n", "1. **Encode the categorical variable** (engine_type) — models need numbers, not text\n", "2. **Create interaction features** — sometimes two variables together tell a different story than either alone\n", "3. **Split into training and test sets** — critical: we NEVER let the model peek at test data during training\n", "4. **Scale the features** — logistic regression and some other models work better when features are on similar scales\n" ] }, { "cell_type": "code", "execution_count": null, "id": "6ec07e0d", "metadata": {}, "outputs": [], "source": [ "# ---- Feature Engineering ----\n", "\n", "# Drop ID columns (they're identifiers, not predictors)\n", "feature_df = df.drop(columns=['engine_id', 'aircraft_id'])\n", "\n", "# Encode engine_type\n", "le = LabelEncoder()\n", "feature_df['engine_type_encoded'] = le.fit_transform(feature_df['engine_type'])\n", "engine_type_mapping = dict(zip(le.classes_, le.transform(le.classes_)))\n", "print(\"Engine type encoding:\", engine_type_mapping)\n", "\n", "# Create interaction features (business-meaningful combinations)\n", "feature_df['temp_x_vibration'] = feature_df['avg_exhaust_temp_c'] * feature_df['avg_vibration_level']\n", "feature_df['cycles_x_age'] = feature_df['flight_cycles_since_overhaul'] * feature_df['component_age_years']\n", "feature_df['risk_score_manual'] = (\n", " feature_df['anomaly_flags_30d'] * 2 +\n", " feature_df['prior_unscheduled_events_12m'] * 3 +\n", " (feature_df['days_since_last_inspection'] > 90).astype(int) * 1\n", ")\n", "\n", "print(f\"\\n✅ Created 3 interaction features\")\n", "print(f\" Total features: {feature_df.shape[1] - 2}\") # minus target and engine_type string\n", "feature_df.head()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "2d5f158d", "metadata": {}, "outputs": [], "source": [ "# ---- Train/Test Split ----\n", "\n", "# Separate features (X) from target (y)\n", "drop_cols = ['needs_maintenance_30cyc', 'engine_type']\n", "X = feature_df.drop(columns=drop_cols)\n", "y = feature_df['needs_maintenance_30cyc']\n", "\n", "# 70/30 split with stratification (keeps the same target ratio in both sets)\n", "X_train, X_test, y_train, y_test = train_test_split(\n", " X, y, test_size=0.30, random_state=42, stratify=y\n", ")\n", "\n", "print(f\"Training set: {X_train.shape[0]:,} engines ({y_train.mean():.1%} positive)\")\n", "print(f\"Test set: {X_test.shape[0]:,} engines ({y_test.mean():.1%} positive)\")\n", "print(f\"\\n✅ Stratified split preserves the target balance in both sets.\")\n", "\n", "# Scale features (fit on training only, then transform both)\n", "scaler = StandardScaler()\n", "X_train_scaled = pd.DataFrame(scaler.fit_transform(X_train), columns=X_train.columns, index=X_train.index)\n", "X_test_scaled = pd.DataFrame(scaler.transform(X_test), columns=X_test.columns, index=X_test.index)\n", "\n", "print(f\"\\n✅ Features scaled using StandardScaler (mean=0, std=1)\")\n" ] }, { "cell_type": "markdown", "id": "187c4f5a", "metadata": {}, "source": [ "---\n", "## 🔷 Step 3: Model Building\n", "\n", "We'll train three different models and compare them. This is standard practice — no single algorithm wins every time, so we let the data decide.\n", "\n", "| Model | Why Include It? |\n", "|-------|----------------|\n", "| **Logistic Regression** | Simple baseline. Easy to explain to stakeholders. Shows which features matter most via coefficients. |\n", "| **Decision Tree** | Visual and intuitive. Creates IF-THEN rules the maintenance team can understand. |\n", "| **Random Forest** | Usually the strongest performer. Combines many trees to reduce overfitting. |\n", "\n", "> 🎯 **Remember:** We're optimizing for **recall** — catching as many true maintenance needs as possible. A missed engine failure is much more costly than an unnecessary inspection.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "5e689387", "metadata": {}, "outputs": [], "source": [ "# ---- Train Three Models ----\n", "\n", "models = {\n", " \"Logistic Regression\": LogisticRegression(max_iter=1000, random_state=42, class_weight='balanced'),\n", " \"Decision Tree\": DecisionTreeClassifier(max_depth=5, random_state=42, class_weight='balanced'),\n", " \"Random Forest\": RandomForestClassifier(n_estimators=100, max_depth=8, random_state=42, class_weight='balanced')\n", "}\n", "\n", "results = {}\n", "\n", "for name, model in models.items():\n", " # Logistic regression uses scaled features; tree-based models don't need scaling\n", " if \"Logistic\" in name:\n", " model.fit(X_train_scaled, y_train)\n", " y_pred = model.predict(X_test_scaled)\n", " y_prob = model.predict_proba(X_test_scaled)[:, 1]\n", " else:\n", " model.fit(X_train, y_train)\n", " y_pred = model.predict(X_test)\n", " y_prob = model.predict_proba(X_test)[:, 1]\n", " \n", " results[name] = {\n", " 'model': model,\n", " 'y_pred': y_pred,\n", " 'y_prob': y_prob,\n", " 'auc': roc_auc_score(y_test, y_prob)\n", " }\n", " print(f\"✅ {name} trained — AUC: {results[name]['auc']:.4f}\")\n" ] }, { "cell_type": "markdown", "id": "795ddba1", "metadata": {}, "source": [ "---\n", "## 🔷 Step 4: Model Evaluation\n", "\n", "This is where we answer: **Which model should we trust with real engine decisions?**\n", "\n", "We'll compare using multiple metrics because no single number tells the whole story:\n", "- **AUC** — How well does the model rank risky engines above safe ones? (Higher is better)\n", "- **Recall** — Of all engines that truly needed maintenance, how many did we catch? (Critical for safety)\n", "- **Precision** — Of all engines we flagged, how many actually needed maintenance? (Affects inspection costs)\n", "- **Confusion Matrix** — The full picture: true positives, false positives, true negatives, false negatives\n" ] }, { "cell_type": "code", "execution_count": null, "id": "9caf3f93", "metadata": {}, "outputs": [], "source": [ "# ---- ROC Curves ----\n", "fig, ax = plt.subplots(figsize=(8, 6))\n", "colors_roc = ['#5B9BD5', '#E07B54', '#6BAF6B']\n", "\n", "for (name, res), color in zip(results.items(), colors_roc):\n", " fpr, tpr, _ = roc_curve(y_test, res['y_prob'])\n", " ax.plot(fpr, tpr, label=f\"{name} (AUC = {res['auc']:.3f})\", color=color, linewidth=2)\n", "\n", "ax.plot([0, 1], [0, 1], 'k--', alpha=0.4, label='Random Guess (AUC = 0.500)')\n", "ax.set_xlabel(\"False Positive Rate (unnecessary inspections)\", fontsize=12)\n", "ax.set_ylabel(\"True Positive Rate (caught failures)\", fontsize=12)\n", "ax.set_title(\"ROC Curve Comparison: Which Model Ranks Risk Best?\", fontweight='bold')\n", "ax.legend(fontsize=11)\n", "plt.tight_layout()\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "5b86378b", "metadata": {}, "outputs": [], "source": [ "# ---- Classification Reports Side by Side ----\n", "print(\"=\" * 70)\n", "for name, res in results.items():\n", " print(f\"\\n📊 {name}\")\n", " print(\"-\" * 40)\n", " print(classification_report(y_test, res['y_pred'], target_names=['No Maint.', 'Maint. Needed']))\n", "print(\"=\" * 70)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "014b54f3", "metadata": {}, "outputs": [], "source": [ "# ---- Confusion Matrices ----\n", "fig, axes = plt.subplots(1, 3, figsize=(16, 5))\n", "\n", "for idx, (name, res) in enumerate(results.items()):\n", " ConfusionMatrixDisplay.from_predictions(\n", " y_test, res['y_pred'],\n", " display_labels=['No Maint.', 'Maint. Needed'],\n", " cmap='Blues', ax=axes[idx]\n", " )\n", " axes[idx].set_title(f\"{name}\", fontweight='bold')\n", "\n", "plt.suptitle(\"Confusion Matrices: Comparing All Three Models\", fontsize=14, fontweight='bold', y=1.02)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n💡 Reading the confusion matrix:\")\n", "print(\" Top-left = True Negatives (correctly said 'no maintenance needed')\")\n", "print(\" Top-right = False Positives (flagged for inspection but didn't need it)\")\n", "print(\" Bot-left = False Negatives (MISSED — needed maintenance but we didn't catch it) ← Most dangerous!\")\n", "print(\" Bot-right = True Positives (correctly caught a maintenance need) ← Our goal!\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "c0a4cfdc", "metadata": {}, "outputs": [], "source": [ "# ---- Model Selection Decision ----\n", "print(\"=\" * 60)\n", "print(\"MODEL SELECTION SUMMARY\")\n", "print(\"=\" * 60)\n", "\n", "# Find best model by AUC\n", "best_name = max(results, key=lambda k: results[k]['auc'])\n", "best_model = results[best_name]['model']\n", "best_auc = results[best_name]['auc']\n", "\n", "for name, res in results.items():\n", " cm = confusion_matrix(y_test, res['y_pred'])\n", " tn, fp, fn, tp = cm.ravel()\n", " recall = tp / (tp + fn) if (tp + fn) > 0 else 0\n", " precision = tp / (tp + fp) if (tp + fp) > 0 else 0\n", " marker = \" ← SELECTED ✅\" if name == best_name else \"\"\n", " print(f\"\\n {name}{marker}\")\n", " print(f\" AUC: {res['auc']:.4f}\")\n", " print(f\" Recall: {recall:.1%} (caught {tp} of {tp+fn} true maintenance needs)\")\n", " print(f\" Precision: {precision:.1%} ({fp} unnecessary inspections out of {tp+fp} flagged)\")\n", " print(f\" Missed: {fn} engines that needed maintenance\")\n", "\n", "print(f\"\\n🏆 Selected model: {best_name} (AUC = {best_auc:.4f})\")\n", "print(f\"\\n💡 Why? Best balance of catching failures (recall) while keeping false alarms manageable.\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a240a624", "metadata": {}, "outputs": [], "source": [ "# ---- Feature Importance (Random Forest) ----\n", "if 'Random Forest' in results:\n", " rf_model = results['Random Forest']['model']\n", " importances = pd.Series(rf_model.feature_importances_, index=X_train.columns).sort_values(ascending=True)\n", " \n", " fig, ax = plt.subplots(figsize=(10, 8))\n", " importances.plot(kind='barh', color='#5B9BD5', ax=ax)\n", " ax.set_title(\"Feature Importance: What Drives Maintenance Predictions?\", fontweight='bold')\n", " ax.set_xlabel(\"Importance Score\")\n", " plt.tight_layout()\n", " plt.show()\n", " \n", " print(\"\\n💡 Top 5 most important features:\")\n", " for feat, imp in importances.tail(5).items():\n", " print(f\" {feat.replace('_', ' ').title()}: {imp:.3f}\")\n", " print(\"\\n These are the sensor readings and operational factors the model relies on most.\")\n", " print(\" Maintenance engineers should pay special attention to engines with extreme values in these areas.\")\n" ] }, { "cell_type": "markdown", "id": "64aacd76", "metadata": {}, "source": [ "---\n", "## 🔷 Step 5: Deployment — Putting the Model to Work\n", "\n", "Here's where most analytics projects **die**. Remember the famous stat: **87% of ML projects never reach production**. We're going to be in the 13%.\n", "\n", "Our deployment strategy:\n", "1. **Python batch scorer** — Runs nightly, scores all engines, generates a worklist CSV\n", "2. **Excel decision tool** — For the Maintenance Planning team to review, override, and document decisions\n", "\n", "> 🧠 **The \"Train in Python, Deploy in Excel\" pattern:** We trained a sophisticated Random Forest in Python, but the people using the predictions every morning work in Excel. So we'll extract the model's logic and give them a tool they already know how to use.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "c52f90d8", "metadata": {}, "outputs": [], "source": [ "# ---- Export Model Coefficients for Deployment ----\n", "\n", "# For the Excel scorer, we'll use the Logistic Regression coefficients\n", "# (easier to implement in Excel formulas than a random forest)\n", "lr_model = results['Logistic Regression']['model']\n", "\n", "print(\"LOGISTIC REGRESSION COEFFICIENTS (for Excel deployment)\")\n", "print(\"=\" * 55)\n", "coef_df = pd.DataFrame({\n", " 'Feature': X_train.columns,\n", " 'Coefficient': lr_model.coef_[0],\n", " 'Abs_Importance': np.abs(lr_model.coef_[0])\n", "}).sort_values('Abs_Importance', ascending=False)\n", "\n", "print(f\"\\nIntercept: {lr_model.intercept_[0]:.4f}\")\n", "print(f\"\\nFeature coefficients:\")\n", "for _, row in coef_df.iterrows():\n", " direction = \"↑ risk\" if row['Coefficient'] > 0 else \"↓ risk\"\n", " print(f\" {row['Feature']:40s} {row['Coefficient']:+.4f} ({direction})\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "b67a7fdd", "metadata": {}, "outputs": [], "source": [ "# ---- Save Model Artifacts for Deployment ----\n", "# ---- heads-up: you'll get an error, see you can fix it :)\n", "\n", "import pickle\n", "\n", "# Save the Random Forest (primary model)\n", "with open(\"rf_model_skybridge.pkl\", 'wb') as f:\n", " pickle.dump(best_model, f)\n", "\n", "# Save the scaler\n", "with open(\"scaler_skybridge.pkl\", 'wb') as f:\n", " pickle.dump(scaler, f)\n", "\n", "# Save feature names\n", "with open(\"feature_names_skybridge.json\", 'w') as f:\n", " json.dump(list(X_train.columns), f)\n", "\n", "# Save logistic regression for Excel deployment\n", "lr_artifacts = {\n", " 'intercept': float(lr_model.intercept_[0]),\n", " 'coefficients': dict(zip(X_train.columns.tolist(), lr_model.coef_[0].tolist())),\n", " 'scaler_means': dict(zip(X_train.columns.tolist(), scaler.mean_.tolist())),\n", " 'scaler_stds': dict(zip(X_train.columns.tolist(), scaler.scale_.tolist())),\n", "}\n", "with open(\"lr_artifacts_skybridge.json\", 'w') as f:\n", " json.dump(lr_artifacts, f, indent=2)\n", "\n", "print(\"✅ Deployment artifacts saved:\")\n", "print(\" • rf_model_skybridge.pkl (Random Forest model)\")\n", "print(\" • scaler_skybridge.pkl (Feature scaler)\")\n", "print(\" • feature_names_skybridge.json (Feature list)\")\n", "print(\" • lr_artifacts_skybridge.json (LR coefficients for Excel)\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "f4bb90e0", "metadata": {}, "outputs": [], "source": [ "# ---- Simulate Batch Scoring (what the nightly job does) ----\n", "\n", "# In production, this would pull fresh data from the data warehouse\n", "# Here we simulate with 20 \"new\" engines\n", "new_engines = df.sample(20, random_state=99).copy()\n", "new_engine_ids = new_engines['engine_id'].values\n", "\n", "# Prepare features the same way we did for training\n", "new_features = new_engines.drop(columns=['engine_id', 'aircraft_id', 'engine_type', 'needs_maintenance_30cyc'])\n", "new_features['engine_type_encoded'] = le.transform(new_engines['engine_type'])\n", "new_features['temp_x_vibration'] = new_features['avg_exhaust_temp_c'] * new_features['avg_vibration_level']\n", "new_features['cycles_x_age'] = new_features['flight_cycles_since_overhaul'] * new_features['component_age_years']\n", "new_features['risk_score_manual'] = (\n", " new_features['anomaly_flags_30d'] * 2 +\n", " new_features['prior_unscheduled_events_12m'] * 3 +\n", " (new_features['days_since_last_inspection'] > 90).astype(int) * 1\n", ")\n", "\n", "# Reorder columns to match training\n", "new_features = new_features[X_train.columns]\n", "\n", "# Score with Random Forest\n", "risk_probs = best_model.predict_proba(new_features)[:, 1]\n", "\n", "# Create the maintenance worklist\n", "worklist = pd.DataFrame({\n", " 'engine_id': new_engine_ids,\n", " 'aircraft_id': new_engines['aircraft_id'].values,\n", " 'engine_type': new_engines['engine_type'].values,\n", " 'risk_probability': np.round(risk_probs, 4),\n", " 'risk_level': pd.cut(risk_probs, bins=[0, 0.3, 0.6, 1.0], labels=['LOW', 'MEDIUM', 'HIGH']),\n", " 'recommended_action': ['Schedule inspection' if p >= 0.5 else 'Monitor' for p in risk_probs]\n", "}).sort_values('risk_probability', ascending=False)\n", "\n", "print(\"=\" * 70)\n", "print(\"SKYBRIDGE AIRLINES — DAILY MAINTENANCE WORKLIST\")\n", "print(f\"Generated: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}\")\n", "print(\"=\" * 70)\n", "print(f\"\\nTotal engines scored: {len(worklist)}\")\n", "print(f\"Flagged for inspection: {(worklist['risk_probability'] >= 0.5).sum()}\")\n", "print(f\"\\n{worklist.to_string(index=False)}\")\n" ] }, { "cell_type": "markdown", "id": "8d18e849", "metadata": {}, "source": [ "---\n", "## 🔷 Step 6: Monitoring — Keeping the Model Honest\n", "\n", "Deploying a model isn't the finish line — it's the starting gun. Models degrade over time because:\n", "- **Data drift:** Sensor patterns change as engines age or new engine types enter the fleet\n", "- **Concept drift:** The relationship between readings and failures shifts (e.g., new maintenance procedures)\n", "- **Feedback loops:** If the model works well, fewer failures occur, which changes the training data distribution\n", "\n", "### Our Monitoring Plan\n", "\n", "| What We Monitor | How | Alert Threshold |\n", "|----------------|-----|-----------------|\n", "| **Prediction distribution** | Track daily % flagged for inspection | >40% or <10% flagged triggers review |\n", "| **Feature drift** | Compare rolling 30-day feature means to training means | >2 standard deviations from training mean |\n", "| **Model accuracy** | Compare predictions to actual outcomes (30-cycle lag) | AUC drops below 0.75 |\n", "| **Business impact** | Track unscheduled events per quarter | >8 events/quarter triggers model retrain |\n" ] }, { "cell_type": "code", "execution_count": null, "id": "634361d3", "metadata": {}, "outputs": [], "source": [ "# ---- Monitoring: Simulated Drift Detection ----\n", "\n", "# In production, you'd compare new data statistics to the training baseline\n", "training_stats = X_train.describe().loc[['mean', 'std']]\n", "\n", "print(\"TRAINING DATA BASELINE (for drift monitoring)\")\n", "print(\"=\" * 55)\n", "print(\"\\nFeature means and standard deviations at training time:\")\n", "print(training_stats.T.to_string())\n", "\n", "print(\"\\n💡 Monitoring rule: If any feature's rolling 30-day mean deviates\")\n", "print(\" by more than 2 standard deviations from the training mean,\")\n", "print(\" flag it for investigation.\")\n", "\n", "print(\"\\n\\n🎯 MONITORING DASHBOARD METRICS TO TRACK:\")\n", "print(\"-\" * 45)\n", "print(\" 1. Daily prediction distribution (% flagged)\")\n", "print(\" 2. Weekly feature drift scores\")\n", "print(\" 3. Monthly model accuracy vs actuals (30-cycle lag)\")\n", "print(\" 4. Quarterly business impact review\")\n", "print(\" 5. Annual model retrain cycle\")\n" ] }, { "cell_type": "markdown", "id": "518c8c41", "metadata": {}, "source": [ "---\n", "## 🏁 Summary: What We Built\n", "\n", "| BizML Step | What We Did | Key Deliverable |\n", "|-----------|-------------|-----------------|\n", "| **1. Business Problem** | Defined binary classification for 30-cycle maintenance prediction | PAIR framework + project scope |\n", "| **2. Data Pipeline** | EDA, feature engineering, train/test split, scaling | Clean, model-ready dataset |\n", "| **3. Model Building** | Trained Logistic Regression, Decision Tree, Random Forest | Three trained models |\n", "| **4. Evaluation** | Compared AUC, recall, precision, confusion matrices | Model selection rationale |\n", "| **5. Deployment** | Python batch scorer + Excel decision tool | Production-ready artifacts |\n", "| **6. Monitoring** | Drift detection + performance tracking plan | Monitoring dashboard specs |\n", "\n", "### 💰 Expected Business Impact\n", "\n", "- **Before:** 23 unscheduled events/year × $180K = **$4.1M annual cost**\n", "- **After (25% reduction):** ~17 events/year × $180K = **$3.1M** → **$1M+ annual savings**\n", "- **Additional value:** Improved safety, better maintenance scheduling, reduced crew disruption\n", "\n", "### 🎓 What This Means for Your Career\n", "\n", "If you can walk a business stakeholder through this entire lifecycle — from \"here's the problem\" to \"here's the deployed solution saving $1M/year\" — you're not just an analyst. You're a **strategic partner**. That's the difference between someone who runs models and someone who runs analytics programs.\n", "\n", "---\n", "*Demo created for GMBA 621 & FINC 332 — Predictive Analytics & Data Mining* \n", "*SkyBridge Airlines is a fictional company created for educational purposes.*\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.12" } }, "nbformat": 4, "nbformat_minor": 5 }