1) Problem Statement

1.1) Business Context

Renewable energy sources play an increasingly important role in the global energy mix, as the effort to reduce the environmental impact of energy production increases.

Out of all the renewable energy alternatives, wind energy is one of the most developed technologies worldwide. The U.S Department of Energy has put together a guide to achieving operational efficiency using predictive maintenance practices.

Predictive maintenance uses sensor information and analysis methods to measure and predict degradation and future component capability. The idea behind predictive maintenance is that failure patterns are predictable and if component failure can be predicted accurately and the component is replaced before it fails, the costs of operation and maintenance will be much lower.

The sensors fitted across different machines involved in the process of energy generation collect data related to various environmental factors (temperature, humidity, wind speed, etc.) and additional features related to various parts of the wind turbine (gearbox, tower, blades, break, etc.).

1.2) Objective

“ReneWind” is a company working on improving the machinery/processes involved in the production of wind energy using machine learning and has collected data of generator failure of wind turbines using sensors. They have shared a ciphered version of the data, as the data collected through sensors is confidential (the type of data collected varies with companies). Data has 40 predictors, 20000 observations in the training set and 5000 in the test set.

The objective is to build various classification models, tune them, and find the best one that will help identify failures so that the generators could be repaired before failing/breaking to reduce the overall maintenance cost. The nature of predictions made by the classification model will translate as follows:

  • True positives (TP) are failures correctly predicted by the model. These will result in repairing costs.
  • False negatives (FN) are real failures where there is no detection by the model. These will result in replacement costs.
  • False positives (FP) are detections where there is no failure. These will result in inspection costs.

It is given that the cost of repairing a generator is much less than the cost of replacing it, and the cost of inspection is less than the cost of repair.

“1” in the target variables should be considered as “failure” and “0” represents “No failure”.

1.3) Data Description

The data provided is a transformed version of the original data which was collected using sensors.

  • Train.csv - To be used for training and tuning of models.
  • Test.csv - To be used only for testing the performance of the final best model.

Both the datasets consist of 40 predictor variables and 1 target variable.

2) Project Setup

2.1) Install Libraries

In [ ]:
# 2.1) Install Libraries
!pip install --quiet \
    numpy pandas scikit-learn \
    tensorflow keras keras-tuner \
    imbalanced-learn lime \
    xgboost lightgbm catboost \
    matplotlib seaborn

print("All required libraries have been successfully installed.")
All required libraries have been successfully installed.

2.2) Import Libraries

In [ ]:
# 2.2) Import Libraries

# Set a global random seed
RANDOM_STATE = 48

# Core & Utility imports
import logging
import os
import random
import sys
import warnings
from collections import Counter
from pathlib import Path
from google.colab import drive
from pathlib import Path

# Data Handling & Visualization
import joblib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from IPython.display import display

# Preprocessing, modeling, and evaluation
import sklearn
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import SelectKBest, mutual_info_classif
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

# metrics import
from sklearn.metrics import (ConfusionMatrixDisplay, accuracy_score,
                             classification_report, confusion_matrix, f1_score,
                             make_scorer, precision_recall_curve,
                             precision_score, recall_score, roc_auc_score,
                             roc_curve)

# model_selection import
from sklearn.model_selection import (GridSearchCV, StratifiedKFold,
                                     cross_validate, train_test_split)

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer, StandardScaler
from sklearn.utils import class_weight

# For handling class imbalance and powerful gradient boosting models
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
import catboost
import lightgbm as lgb
import xgboost as xgb

# For explaining model predictions
import shap
from lime.lime_tabular import LimeTabularExplainer

# For building neural networks
import keras_tuner as kt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import backend as K
from tensorflow.keras import layers, regularizers
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

# Ensure reproducibility
os.environ['TF_DETERMINISTIC_OPS'] = '1'
random.seed(RANDOM_STATE)
np.random.seed(RANDOM_STATE)
tf.random.set_seed(RANDOM_STATE)
keras.utils.set_random_seed(RANDOM_STATE)
tf.config.experimental.enable_op_determinism()

# Suppress warnings (use with caution)
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=UserWarning)

# Pandas display options
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)

# Logging configuration
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)]
)

# Set Seaborn style after all imports
sns.set_style('whitegrid')

print("All libraries successfully imported.")
All libraries successfully imported.

2.3) Setup Google Drive

In [ ]:
# 2.3) Setup Google Drive

# Mount Google Drive
drive.mount('/content/drive')

# Define a constant for your project's root directory in Drive
# This makes it easy to update if you move your project
PROJECT_ROOT = Path('/content/drive/MyDrive/Colab Notebooks/')

# Create specific paths for your data files
TRAIN_FILE = PROJECT_ROOT / 'Train.csv'
TEST_FILE = PROJECT_ROOT / 'Test.csv'

# Now, you can use these variables directly when you need them
# Example: df = pd.read_csv(TRAIN_FILE)

# Verify that the training file exists
if TRAIN_FILE.is_file():
    print(f"Train.csv found at: {TRAIN_FILE}")
else:
    print("Train.csv not found. Check the PROJECT_ROOT path.")
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).
Train.csv found at: /content/drive/MyDrive/Colab Notebooks/Train.csv

3) Data Loading & Overview

3.1) Data Loading

In [ ]:
# 3.1) Data Loading

# Load the original data
raw_train = pd.read_csv(TRAIN_FILE)
raw_test = pd.read_csv(TEST_FILE)

# Create copies to work with
train_df = raw_train.copy()
test_df = raw_test.copy()

# Display dataset shapes
print(f"Training data shape: {train_df.shape}")
print(f"Test data shape: {test_df.shape}")
Training data shape: (20000, 41)
Test data shape: (5000, 41)

3.2) Data Overview

In [ ]:
# 3.2) Data Overview

# Preview first few rows of training data
display(train_df.head())

# Basic info and data types
print("\n-----------------------------------------------------------------\n")
print("Training Data Info:")
train_df.info()

# Summary statistics for numerical features
print("\n-----------------------------------------------------------------\n")
print("Training Data Summary Statistics:")
display(train_df.describe())
print("\n-----------------------------------------------------------------\n")
# Check for missing values in train and test sets
def missing_values_summary(train_df, test_df):
    """Creates a summary of missing values for train and test sets."""
    missing_train = train_df.isnull().sum()
    missing_test = test_df.isnull().sum()

    missing_summary = pd.DataFrame({
        'Train Missing Count': missing_train[missing_train > 0],
        'Train Missing %': (train_df.isnull().mean() * 100)[missing_train > 0],
        'Test Missing Count': missing_test[missing_test > 0],
        'Test Missing %': (test_df.isnull().mean() * 100)[missing_test > 0]
    }).sort_values(by='Train Missing %', ascending=False)

    # Fill NaN for display purposes if a column has missing values in one set but not the other
    return missing_summary.fillna(0)

# Generate and display the summary
missing_summary = missing_values_summary(train_df, test_df)

print("Missing Values Summary:")
if missing_summary.empty:
    print("No missing values found in either dataset. ✅")
else:
    display(missing_summary)
print("\n-----------------------------------------------------------------\n")

# Visualize target class distribution
plt.figure(figsize=(8, 5))

# Create the plot
ax = sns.countplot(x='Target', data=train_df, palette='viridis')

# Add percentage labels
total = len(train_df)
for p in ax.patches:
    percentage = f'{100 * p.get_height() / total:.1f}%'
    x = p.get_x() + p.get_width() / 2
    y = p.get_height()
    ax.annotate(percentage, (x, y), ha='center', va='bottom', fontsize=12, weight='bold')

# Customize titles and labels
plt.title('Target Class Distribution (0: No Failure, 1: Failure)', fontsize=14, weight='bold')
plt.xlabel('Class', fontsize=12)
plt.ylabel('Count', fontsize=12)
plt.xticks(ticks=[0, 1], labels=['No Failure', 'Failure'])
plt.tight_layout()
plt.show()
print("\n-----------------------------------------------------------------\n")

# Print the exact value counts and ratio
print("Target Variable Value Counts:")
print(train_df['Target'].value_counts())
print("\nRatio of No Failure to Failure:")
print(f"1:{train_df['Target'].value_counts()[0] / train_df['Target'].value_counts()[1]:.1f}")
V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 Target
0 -4.464606 -4.679129 3.101546 0.506130 -0.221083 -2.032511 -2.910870 0.050714 -1.522351 3.761892 -5.714719 0.735893 0.981251 1.417884 -3.375815 -3.047303 0.306194 2.914097 2.269979 4.394876 -2.388299 0.646388 -1.190508 3.132986 0.665277 -2.510846 -0.036744 0.726218 -3.982187 -1.072638 1.667098 3.059700 -1.690440 2.846296 2.235198 6.667486 0.443809 -2.369169 2.950578 -3.480324 0
1 3.365912 3.653381 0.909671 -1.367528 0.332016 2.358938 0.732600 -4.332135 0.565695 -0.101080 1.914465 -0.951458 -1.255259 -2.706522 0.193223 -4.769379 -2.205319 0.907716 0.756894 -5.833678 -3.065122 1.596647 -1.757311 1.766444 -0.267098 3.625036 1.500346 -0.585712 0.783034 -0.201217 0.024883 -1.795474 3.032780 -2.467514 1.894599 -2.297780 -1.731048 5.908837 -0.386345 0.616242 0
2 -3.831843 -5.824444 0.634031 -2.418815 -1.773827 1.016824 -2.098941 -3.173204 -2.081860 5.392621 -0.770673 1.106718 1.144261 0.943301 -3.163804 -4.247825 -4.038909 3.688534 3.311196 1.059002 -2.143026 1.650120 -1.660592 1.679910 -0.450782 -4.550695 3.738779 1.134404 -2.033531 0.840839 -1.600395 -0.257101 0.803550 4.086219 2.292138 5.360850 0.351993 2.940021 3.839160 -4.309402 0
3 1.618098 1.888342 7.046143 -1.147285 0.083080 -1.529780 0.207309 -2.493629 0.344926 2.118578 -3.053023 0.459719 2.704527 -0.636086 -0.453717 -3.174046 -3.404347 -1.281536 1.582104 -1.951778 -3.516555 -1.206011 -5.627854 -1.817653 2.124142 5.294642 4.748137 -2.308536 -3.962977 -6.028730 4.948770 -3.584425 -2.577474 1.363769 0.622714 5.550100 -1.526796 0.138853 3.101430 -1.277378 0
4 -0.111440 3.872488 -3.758361 -2.982897 3.792714 0.544960 0.205433 4.848994 -1.854920 -6.220023 1.998347 4.723757 0.709113 -1.989432 -2.632684 4.184447 2.245356 3.734452 -6.312766 -5.379918 -0.886667 2.061694 9.445586 4.489976 -3.945144 4.582065 -8.780422 -3.382967 5.106507 6.787513 2.044184 8.265896 6.629213 -10.068689 1.222987 -3.229763 1.686909 -2.163896 -3.644622 6.510338 0
-----------------------------------------------------------------

Training Data Info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20000 entries, 0 to 19999
Data columns (total 41 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   V1      19982 non-null  float64
 1   V2      19982 non-null  float64
 2   V3      20000 non-null  float64
 3   V4      20000 non-null  float64
 4   V5      20000 non-null  float64
 5   V6      20000 non-null  float64
 6   V7      20000 non-null  float64
 7   V8      20000 non-null  float64
 8   V9      20000 non-null  float64
 9   V10     20000 non-null  float64
 10  V11     20000 non-null  float64
 11  V12     20000 non-null  float64
 12  V13     20000 non-null  float64
 13  V14     20000 non-null  float64
 14  V15     20000 non-null  float64
 15  V16     20000 non-null  float64
 16  V17     20000 non-null  float64
 17  V18     20000 non-null  float64
 18  V19     20000 non-null  float64
 19  V20     20000 non-null  float64
 20  V21     20000 non-null  float64
 21  V22     20000 non-null  float64
 22  V23     20000 non-null  float64
 23  V24     20000 non-null  float64
 24  V25     20000 non-null  float64
 25  V26     20000 non-null  float64
 26  V27     20000 non-null  float64
 27  V28     20000 non-null  float64
 28  V29     20000 non-null  float64
 29  V30     20000 non-null  float64
 30  V31     20000 non-null  float64
 31  V32     20000 non-null  float64
 32  V33     20000 non-null  float64
 33  V34     20000 non-null  float64
 34  V35     20000 non-null  float64
 35  V36     20000 non-null  float64
 36  V37     20000 non-null  float64
 37  V38     20000 non-null  float64
 38  V39     20000 non-null  float64
 39  V40     20000 non-null  float64
 40  Target  20000 non-null  int64  
dtypes: float64(40), int64(1)
memory usage: 6.3 MB

-----------------------------------------------------------------

Training Data Summary Statistics:
V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 Target
count 19982.000000 19982.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000 20000.000000
mean -0.271996 0.440430 2.484699 -0.083152 -0.053752 -0.995443 -0.879325 -0.548195 -0.016808 -0.012998 -1.895393 1.604825 1.580486 -0.950632 -2.414993 -2.925225 -0.134261 1.189347 1.181808 0.023608 -3.611252 0.951835 -0.366116 1.134389 -0.002186 1.873785 -0.612413 -0.883218 -0.985625 -0.015534 0.486842 0.303799 0.049825 -0.462702 2.229620 1.514809 0.011316 -0.344025 0.890653 -0.875630 0.055500
std 3.441625 3.150784 3.388963 3.431595 2.104801 2.040970 1.761626 3.295756 2.160568 2.193201 3.124322 2.930454 2.874658 1.789651 3.354974 4.221717 3.345462 2.592276 3.396925 3.669477 3.567690 1.651547 4.031860 3.912069 2.016740 3.435137 4.368847 1.917713 2.684365 3.005258 3.461384 5.500400 3.575285 3.183841 2.937102 3.800860 1.788165 3.948147 1.753054 3.012155 0.228959
min -11.876451 -12.319951 -10.708139 -15.082052 -8.603361 -10.227147 -7.949681 -15.657561 -8.596313 -9.853957 -14.832058 -12.948007 -13.228247 -7.738593 -16.416606 -20.374158 -14.091184 -11.643994 -13.491784 -13.922659 -17.956231 -10.122095 -14.866128 -16.387147 -8.228266 -11.834271 -14.904939 -9.269489 -12.579469 -14.796047 -13.722760 -19.876502 -16.898353 -17.985094 -15.349803 -14.833178 -5.478350 -17.375002 -6.438880 -11.023935 0.000000
25% -2.737146 -1.640674 0.206860 -2.347660 -1.535607 -2.347238 -2.030926 -2.642665 -1.494973 -1.411212 -3.922404 -0.396514 -0.223545 -2.170741 -4.415322 -5.634240 -2.215611 -0.403917 -1.050168 -2.432953 -5.930360 -0.118127 -3.098756 -1.468062 -1.365178 -0.337863 -3.652323 -2.171218 -2.787443 -1.867114 -1.817772 -3.420469 -2.242857 -2.136984 0.336191 -0.943809 -1.255819 -2.987638 -0.272250 -2.940193 0.000000
50% -0.747917 0.471536 2.255786 -0.135241 -0.101952 -1.000515 -0.917179 -0.389085 -0.067597 0.100973 -1.921237 1.507841 1.637185 -0.957163 -2.382617 -2.682705 -0.014580 0.883398 1.279061 0.033415 -3.532888 0.974687 -0.262093 0.969048 0.025050 1.950531 -0.884894 -0.891073 -1.176181 0.184346 0.490304 0.052073 -0.066249 -0.255008 2.098633 1.566526 -0.128435 -0.316849 0.919261 -0.920806 0.000000
75% 1.840112 2.543967 4.566165 2.130615 1.340480 0.380330 0.223695 1.722965 1.409203 1.477045 0.118906 3.571454 3.459886 0.270677 -0.359052 -0.095046 2.068751 2.571770 3.493299 2.512372 -1.265884 2.025594 2.451750 3.545975 1.397112 4.130037 2.189177 0.375884 0.629773 2.036229 2.730688 3.761722 2.255134 1.436935 4.064358 3.983939 1.175533 2.279399 2.057540 1.119897 0.000000
max 15.493002 13.089269 17.090919 13.236381 8.133797 6.975847 8.006091 11.679495 8.137580 8.108472 11.826433 15.080698 15.419616 5.670664 12.246455 13.583212 16.756432 13.179863 13.237742 16.052339 13.840473 7.409856 14.458734 17.163291 8.223389 16.836410 17.560404 6.527643 10.722055 12.505812 17.255090 23.633187 16.692486 14.358213 15.291065 19.329576 7.467006 15.289923 7.759877 10.654265 1.000000
-----------------------------------------------------------------

Missing Values Summary:
Train Missing Count Train Missing % Test Missing Count Test Missing %
V1 18 0.09 5 0.10
V2 18 0.09 6 0.12
-----------------------------------------------------------------

-----------------------------------------------------------------

Target Variable Value Counts:
Target
0    18890
1     1110
Name: count, dtype: int64

Ratio of No Failure to Failure:
1:17.0

OBSERVATIONS:

  • 20K training rows and 5K test rows, each with 40 continuous predictors and a binary failure target.
  • Only features V1 and V2 have missing values (18 each); all other predictors are fully observed.
  • Failures (Target = 1) account for just 1 110 of 20 000 cases (~5.6%), so I will need to address imbalance.
  • All predictors are floating-point measurements, ready for scaling or normalization.

4) Exploratory Data Analysis

4.1) Univariate EDA

In [ ]:
# 4.1) Univariate Analysis

# Define predictor variables (all columns except 'Target')
predictors = train_df.columns.drop('Target')

# Set up the plotting area
fig, axes = plt.subplots(8, 5, figsize=(20, 28))
axes = axes.flatten() # Flatten the 2D array of axes for easy iteration

# Iterate through each predictor and plot its distribution colored by the Target
for i, col in enumerate(predictors):
    sns.histplot(
        data=train_df,
        x=col,
        hue='Target',       # Color bars based on the 'Target' value
        multiple="stack",   # Stack the bars for each class
        kde=True,           # Overlay a Kernel Density Estimate curve
        ax=axes[i],
        palette='viridis',  # Use a color-blind friendly palette
        legend=False        # Hide individual legends to avoid clutter
    )
    axes[i].set_title(f'Distribution of {col}', fontsize=12)
    axes[i].set_xlabel('') # Remove x-axis labels for a cleaner look
    axes[i].set_ylabel('') # Remove y-axis labels

# Add a clear, overarching title
plt.suptitle('Feature Distributions by Target Class (0 vs 1)', fontsize=22, y=1.01)

# Create a single legend for the entire figure
handles, labels = axes[0].get_legend_handles_labels()
fig.legend(handles, ['No Failure', 'Failure'], loc='upper right', title='Class')

# Adjust layout to prevent titles and plots from overlapping
plt.tight_layout(rect=[0, 0, 0.95, 1])
plt.show()

OBSERVATIONS:

  • The data from each sensor has a unique shape and pattern, with some showing a wider range of values than others.
  • The sensors record data on completely different scales, which confirms we need to normalize them before feeding them into our models.
  • For several key sensors, the readings for failing generators are noticeably different from healthy ones. These are our most promising clues for predicting failure.
  • Features like V10, V14, and V17 show a strong link to failures when their values are high, making them very important for our analysis.

4.2) Bivariate EDA

In [ ]:
# 4.2) Bivariate EDA

# Calculate the correlation of all features with the 'Target'
target_corr = train_df.corr()['Target'].drop('Target').sort_values(ascending=False)

# Plotting the correlations
plt.figure(figsize=(10, 8))
sns.barplot(x=target_corr.values, y=target_corr.index, palette='coolwarm')
plt.title('Feature Correlation with Target', fontsize=16)
plt.xlabel('Pearson Correlation Coefficient', fontsize=12)
plt.ylabel('Features', fontsize=12)
plt.axvline(x=0, color='grey', linestyle='--') # Add a line at zero for reference
plt.tight_layout()
plt.show()

# Inter-Feature Correlation Heatmap

print("\n-----------------------------------------------------------------\n")
print("--- Full Correlation Matrix Heatmap ---")
print("\n-----------------------------------------------------------------\n")

# Calculate the full correlation matrix
corr_matrix = train_df.drop('Target', axis=1).corr()

# Create a mask to hide the upper triangle (since the matrix is symmetric)
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))

# Set up the matplotlib figure
plt.figure(figsize=(10, 8))
sns.heatmap(
    corr_matrix,
    mask=mask,
    annot=False,
    cmap='coolwarm',
    vmin=-1,
    vmax=1
)
plt.title('Inter-Feature Correlation Heatmap', fontsize=18)
plt.show()
-----------------------------------------------------------------

--- Full Correlation Matrix Heatmap ---

-----------------------------------------------------------------

OBSERVATIONS:

  • A few sensors are strong red flags. High readings on V17, V10, and V38, and low readings on V11 and V21, are the clearest signals of a potential generator failure.
  • Some Sensors Move Together: The heatmap shows that certain groups of sensors provide similar information, meaning they are likely monitoring related parts of the turbine.
  • Confident Direction: These findings confirm our earlier visual analysis and give us a reliable, ranked list of the most important features for predicting failure.

5) Preprocessing & Feature Engineering

  • Split our data into training and validation sets.
  • Use stratification during the split to guarantee both the training and validation sets have the same percentage of failures.
  • Build a Pipeline that bundles our two preprocessing steps: filling missing values and scaling features.
  • The pipeline learns how to process the data using only the training set. It then applies these learned rules to the validation set.

5.1) Data Splitting

In [ ]:
# 5.1) Data Splitting

# Separate predictors (X) from the target variable (y)
X = train_df.drop('Target', axis=1)
y = train_df['Target']

# Split the data into training and validation sets
# We use stratify=y to ensure that the proportion of failures is the same in both sets.
# This is crucial for imbalanced datasets.
X_train, X_val, y_train, y_val = train_test_split(
    X, y,
    test_size=0.2,       # Using 20% of the data for validation
    random_state=RANDOM_STATE, # Ensures reproducibility
    stratify=y           # <<< Critical for maintaining class distribution
)

# --- Verification ---
# Print the shapes of the resulting datasets to confirm the split
print("Data splitting complete.")
print(f"Shape of X_train: {X_train.shape}")
print(f"Shape of X_val:   {X_val.shape}")
print(f"Shape of y_train: {y_train.shape}")
print(f"Shape of y_val:   {y_val.shape}")

# Verify the class distribution in the training and validation sets
print("\n--- Class Distribution Verification ---")
print(f"Failure percentage in training set: {y_train.mean() * 100:.2f}%")
print(f"Failure percentage in validation set: {y_val.mean() * 100:.2f}%")
Data splitting complete.
Shape of X_train: (16000, 40)
Shape of X_val:   (4000, 40)
Shape of y_train: (16000,)
Shape of y_val:   (4000,)

--- Class Distribution Verification ---
Failure percentage in training set: 5.55%
Failure percentage in validation set: 5.55%

5.2) Create Preprocessing Pipeline

In [ ]:
# 5.2) Create Preprocessing Pipeline

# Define the steps for the preprocessing pipeline
preprocessing_pipeline = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# This shows the structure of our preprocessing steps.
print("Preprocessing pipeline created successfully.")
display(preprocessing_pipeline)
Preprocessing pipeline created successfully.
Pipeline(steps=[('imputer', SimpleImputer(strategy='median')),
                ('scaler', StandardScaler())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

5.3) Apply Pipeline to Data

In [ ]:
# 5.3) Apply Pipeline to Data

# Fit the pipeline on the training data and transform it
X_train_processed = preprocessing_pipeline.fit_transform(X_train)

# Use the already-fitted pipeline to transform the validation data
X_val_processed = preprocessing_pipeline.transform(X_val)

# Convert the processed numpy arrays back to DataFrames for easier inspection
X_train_processed = pd.DataFrame(X_train_processed, columns=X_train.columns)
X_val_processed = pd.DataFrame(X_val_processed, columns=X_val.columns)

print("Preprocessing complete. X_train and X_val have been transformed.")
print("\nFirst 5 rows of the processed training data:")
display(X_train_processed.head())

# Verify that the processed data has a mean near 0 and a standard deviation near 1
print("\nSummary statistics of the processed training data:")
display(X_train_processed.describe().round(2))
Preprocessing complete. X_train and X_val have been transformed.

First 5 rows of the processed training data:
V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40
0 -0.382603 -1.061609 0.158696 0.621736 -0.611710 -1.808171 -0.290987 1.287014 -0.235691 0.387350 -1.269420 0.799738 0.346932 1.285265 0.012647 0.466463 0.963511 -0.439412 0.589182 1.781024 0.050360 0.122606 0.495202 -0.022776 0.708172 -1.115341 -0.101028 0.679157 -1.162589 -0.373137 -0.071196 0.265796 -1.585334 1.142623 0.039954 0.681480 0.689840 -1.430852 0.195901 -0.607276
1 -0.180524 0.392789 0.066936 -0.612222 0.317204 0.212429 -0.517479 -0.046215 0.833308 -1.687763 0.380369 0.275854 1.592124 -0.413423 -1.297377 -0.111134 -0.320117 0.758412 -1.258017 -0.195552 -0.611523 -0.044073 0.296625 -0.988841 -0.540768 0.990722 -0.415229 -1.685316 0.845790 0.621403 -0.309160 -0.292779 0.633460 -1.279945 -0.291042 -0.064521 0.981242 -0.670624 0.215311 0.697188
2 0.083440 -1.685458 -1.147243 -1.627868 -1.047306 1.156189 0.037407 0.481561 -0.349822 -0.412000 1.121271 0.460890 -0.120293 0.874011 0.493999 0.694139 -0.021311 1.169614 -1.133866 0.720816 0.873820 0.338094 0.431545 -1.392452 0.582297 -2.200014 0.668491 0.090709 1.406931 1.039954 -1.005078 -1.140223 -0.201149 0.035922 -1.041173 0.319367 2.075170 -0.547918 0.055216 1.488503
3 -0.147336 -0.006392 -0.187088 0.050340 0.020553 -0.626071 1.124369 1.319201 -1.575173 0.087531 0.252831 1.433809 -1.150945 -0.080856 1.426682 1.407967 0.676511 -0.875003 0.631803 0.091505 1.074649 -0.812445 0.364056 0.483149 -0.065817 -0.588176 -0.308419 0.684893 0.611786 0.330903 0.456268 0.142227 -0.381316 0.735012 -0.102309 0.031517 0.258805 0.012955 -0.539559 0.595661
4 2.169033 -0.251455 0.363561 -1.417820 -0.951880 0.286962 0.708082 0.121588 -0.019268 0.079767 0.180670 0.246455 -0.200616 0.184345 0.990179 -0.305442 -0.366643 0.256196 -0.404792 -0.504631 -0.725849 1.488502 0.012004 -0.565200 1.652732 -0.470537 1.354144 -0.662998 -0.244167 -0.532298 0.715639 -0.788416 -0.719792 -0.651482 0.413058 0.406826 -0.046639 -0.439519 -0.659244 1.284253
Summary statistics of the processed training data:
V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40
count 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00
mean 0.00 0.00 0.00 -0.00 -0.00 -0.00 0.00 -0.00 -0.00 -0.00 0.00 -0.00 -0.00 0.00 0.00 0.00 -0.00 0.00 -0.00 0.00 -0.00 -0.00 -0.00 -0.00 0.00 -0.00 -0.00 -0.00 -0.00 0.00 0.00 0.00 -0.00 0.00 0.00 -0.00 0.00 -0.00 -0.00 -0.00
std 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00
min -3.37 -4.06 -3.89 -4.37 -4.07 -3.87 -4.01 -4.56 -3.96 -4.50 -4.14 -4.94 -5.12 -3.80 -4.17 -4.11 -4.15 -4.96 -4.33 -3.76 -4.00 -6.70 -3.58 -4.48 -4.10 -3.82 -3.21 -3.89 -4.33 -4.91 -4.01 -3.66 -4.74 -5.53 -5.96 -4.29 -3.07 -4.33 -4.17 -3.37
25% -0.72 -0.66 -0.67 -0.66 -0.71 -0.67 -0.65 -0.63 -0.69 -0.64 -0.65 -0.69 -0.63 -0.68 -0.60 -0.64 -0.62 -0.61 -0.66 -0.67 -0.65 -0.65 -0.68 -0.66 -0.68 -0.65 -0.69 -0.67 -0.68 -0.62 -0.66 -0.67 -0.63 -0.52 -0.64 -0.65 -0.71 -0.67 -0.67 -0.68
50% -0.14 0.02 -0.07 -0.02 -0.02 -0.00 -0.02 0.05 -0.02 0.05 -0.01 -0.03 0.02 -0.00 0.01 0.06 0.03 -0.11 0.03 0.00 0.02 0.02 0.02 -0.04 0.01 0.02 -0.06 -0.01 -0.07 0.06 0.00 -0.05 -0.03 0.06 -0.05 0.01 -0.07 0.00 0.02 -0.02
75% 0.61 0.67 0.61 0.65 0.66 0.67 0.62 0.68 0.66 0.68 0.65 0.67 0.65 0.68 0.61 0.67 0.66 0.53 0.68 0.68 0.66 0.65 0.70 0.61 0.69 0.66 0.64 0.65 0.61 0.68 0.65 0.63 0.62 0.60 0.62 0.65 0.65 0.66 0.67 0.66
max 4.57 4.03 4.30 3.88 3.90 3.92 5.04 3.70 3.76 3.71 4.40 4.58 4.80 3.71 4.36 3.89 5.01 4.63 3.56 4.37 4.86 3.91 3.67 4.09 4.08 4.38 4.15 3.86 4.38 4.17 4.83 4.24 4.67 4.67 4.27 4.67 4.17 3.97 3.90 3.60

6) Model Building & Tuning

  • I will use a custom Total Business Cost function as the primary metric to find the most economically valuable model.
  • This function calculates a total cost by assigning a cost for each type of prediction error and a cost for correct repairs.
  • This metric directly reflects the project's goal of minimizing maintenance costs, making it more useful than technical metrics like recall or accuracy alone. We will use it to compare all models and guide our hyperparameter tuning.
  • Disclaimer: The costs used are informed estimates, as real-world figures are not available. Our framework is flexible, allowing for these values to be easily updated with precise accounting data for any future analysis.

6.1) Helper Functions

  • calculate_business_cost: This function computes a single "total cost" number for a model's predictions. It takes the true and predicted labels, counts the TPs, FPs, and FNs, and multiplies them by their assigned business cost (FN=100, TP=5, FP=1).
  • evaluate_model: For any model, it prints a full classification report, displays the total business cost calculated by the first function, and generates a clear confusion matrix plot to visualize the model's performance.
In [ ]:
# 6.1) Helper Functions

# This function calculates the total financial cost based on our custom cost matrix.
# This will be our primary metric for comparing models.

def calculate_business_cost(y_true, y_pred, cost_matrix):
    """Calculates the total business cost of a model's predictions."""
    # Get the confusion matrix components
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()

    # Calculate total cost
    total_cost = (
        (tn * cost_matrix['TN']) +
        (fp * cost_matrix['FP']) +
        (fn * cost_matrix['FN']) +
        (tp * cost_matrix['TP'])
    )
    return total_cost

# This function will print a full classification report, the total business cost,
# and plot a visually clear confusion matrix.

def evaluate_model(y_true, y_pred, cost_matrix, model_name="Model"):
    """
    Prints a classification report, business cost, and plots a confusion matrix.
    """
    # Print Classification Report
    print(f"--- Evaluation Report for {model_name} ---")
    print(classification_report(y_true, y_pred, target_names=['No Failure', 'Failure']))

    # Calculate and Print Business Cost
    cost = calculate_business_cost(y_true, y_pred, cost_matrix)
    print(f"Total Business Cost: ${cost:,.2f}")
    print("--------------------------------------------------\n")

    # Plot Confusion Matrix
    cm = confusion_matrix(y_true, y_pred)
    disp = ConfusionMatrixDisplay(
        confusion_matrix=cm,
        display_labels=['No Failure', 'Failure']
    )
    disp.plot(cmap=plt.cm.Blues)
    plt.title(f'Confusion Matrix - {model_name}')
    plt.show()

# Define the cost matrix
COST_MATRIX = {
    'TN': 0,    # No cost for true negatives
    'FP': 1,    # Cost for a false alarm inspection
    'TP': 5,    # Cost for a correctly identified repair
    'FN': 100   # Cost for a missed failure replacement
}

print("Helper functions and COST_MATRIX are defined and ready.")
Helper functions and COST_MATRIX are defined and ready.

6.2) Baseline Models

  • Establish a performance benchmark by running 5 off-the-shelf baseline models (without hyperparameter tuning) to compare with neural network models.
In [ ]:
# 6.2) Baseline Model Training & Evaluation

# This dictionary holds the models that work well with the 'class_weight' parameter.
models_with_class_weight = {
    'Logistic Regression': LogisticRegression(random_state=RANDOM_STATE, class_weight='balanced', solver='liblinear'),
    'Random Forest': RandomForestClassifier(random_state=RANDOM_STATE, class_weight='balanced', n_jobs=-1)
}

# This dictionary holds the gradient boosting models.
boosting_models = {
    'XGBoost': xgb.XGBClassifier(random_state=RANDOM_STATE, objective='binary:logistic', eval_metric='logloss'),
    'LightGBM': lgb.LGBMClassifier(random_state=RANDOM_STATE),
    'CatBoost': catboost.CatBoostClassifier(random_state=RANDOM_STATE, verbose=0) # verbose=0 keeps the output clean
}

# Store results for final comparison
model_performance = []

# Train and evaluate models using class_weight
for name, model in models_with_class_weight.items():
    model.fit(X_train_processed, y_train)
    y_pred = model.predict(X_val_processed)
    cost = calculate_business_cost(y_val, y_pred, COST_MATRIX)
    recall = recall_score(y_val, y_pred)
    f1 = f1_score(y_val, y_pred)
    model_performance.append({'Model': name, 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})

# Manually apply SMOTE and then train boosting models
smote = SMOTE(random_state=RANDOM_STATE)
X_train_smote, y_train_smote = smote.fit_resample(X_train_processed, y_train)

for name, model in boosting_models.items():
    model.fit(X_train_smote, y_train_smote)
    y_pred = model.predict(X_val_processed)
    cost = calculate_business_cost(y_val, y_pred, COST_MATRIX)
    recall = recall_score(y_val, y_pred)
    f1 = f1_score(y_val, y_pred)
    model_performance.append({'Model': name, 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})

# Final Comparison
print("--- Baseline Model Performance Summary ---")
performance_df = pd.DataFrame(model_performance).sort_values(by='Total Business Cost', ascending=True).reset_index(drop=True)
display(performance_df)
[LightGBM] [Info] Number of positive: 15112, number of negative: 15112
[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.002940 seconds.
You can set `force_row_wise=true` to remove the overhead.
And if memory is not enough, you can set `force_col_wise=true`.
[LightGBM] [Info] Total Bins 10200
[LightGBM] [Info] Number of data points in the train set: 30224, number of used features: 40
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.500000 -> initscore=0.000000
--- Baseline Model Performance Summary ---
Model Total Business Cost Recall F1-Score
0 LightGBM 3614 0.882883 0.867257
1 CatBoost 3796 0.873874 0.877828
2 XGBoost 3890 0.869369 0.877273
3 Logistic Regression 4817 0.846847 0.423901
4 Random Forest 7667 0.689189 0.811671

OBSERVATIONS:

  • With a total cost of $3,614, LightGBM is the best-performing baseline model. It successfully identified over 88% of the actual failures, which is a solid result.
  • The three gradient boosting models clearly outperformed the others. Overall, this sets a fairly high bar.

6.3) Neural Net Models

6.3.1) NN Model 1 - Shallow, regularized MLP w/ 2 hidden layers

In [ ]:
# 6.3.1) NN Model 1

# Define and Compile the Model
simple_nn = keras.Sequential([
    layers.Input(shape=(X_train_processed.shape[1],)),
    layers.Dense(64, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.3),
    layers.Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.3),
    layers.Dense(1, activation='sigmoid')
])

simple_nn.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='binary_crossentropy',
    metrics=['accuracy']
)

print("--- Architecture of Baseline NN ---")
simple_nn.summary()
print("\n" + "="*50 + "\n")

# Calculate class weights to handle the imbalanced dataset
class_weights = class_weight.compute_class_weight(
    'balanced',
    classes=np.unique(y_train),
    y=y_train
)
class_weights_dict = dict(enumerate(class_weights))

print("--- Starting Model Training ---")
history = simple_nn.fit(
    X_train_processed,
    y_train,
    epochs=50,
    batch_size=32,
    validation_data=(X_val_processed, y_val),
    class_weight=class_weights_dict,
    verbose=0  # Set to 0 to show cleaner output
)
print("\n" + "="*50 + "\n")

# Visualize Training History
history_df = pd.DataFrame(history.history)
plt.figure(figsize=(10, 6))
plt.plot(history_df['loss'], label='Training Loss')
plt.plot(history_df['val_loss'], label='Validation Loss')
plt.title('Model Loss Over Epochs', fontsize=16)
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(loc='upper right')
plt.grid(True)
plt.show()
print("\n" + "="*50 + "\n")

# Predict on the validation set and convert probabilities to classes
y_pred_proba = simple_nn.predict(X_val_processed).flatten()
y_pred_class = (y_pred_proba > 0.5).astype(int)

# Use our helper function for a full evaluation
evaluate_model(y_val, y_pred_class, COST_MATRIX, model_name="Simple Baseline NN")

# Append results for final comparison table
cost = calculate_business_cost(y_val, y_pred_class, COST_MATRIX)
recall = recall_score(y_val, y_pred_class)
f1 = f1_score(y_val, y_pred_class)

# Check if 'Simple NN' is already in the performance dataframe to avoid duplicates
if not any(d['Model'] == 'Simple NN' for d in model_performance):
    model_performance.append({'Model': 'Simple NN', 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})
--- Architecture of Baseline NN ---
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense (Dense)                   │ (None, 64)             │         2,624 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense)                 │ (None, 32)             │         2,080 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_1 (Dropout)             │ (None, 32)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 1)              │            33 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 4,737 (18.50 KB)
 Trainable params: 4,737 (18.50 KB)
 Non-trainable params: 0 (0.00 B)
==================================================

--- Starting Model Training ---

==================================================

==================================================

125/125 ━━━━━━━━━━━━━━━━━━━━ 0s 823us/step
--- Evaluation Report for Simple Baseline NN ---
              precision    recall  f1-score   support

  No Failure       0.99      1.00      1.00      3778
     Failure       0.93      0.91      0.92       222

    accuracy                           0.99      4000
   macro avg       0.96      0.95      0.96      4000
weighted avg       0.99      0.99      0.99      4000

Total Business Cost: $3,025.00
--------------------------------------------------

OBSERVATIONS:

  • The model achieved a recall of 0.91 for the failure class, meaning it correctly identified 91% of the actual generator failures.
  • Training history plot shows that both the training and validation loss decreased steadily and began to flatten out. This is a sign of a healthy, stable training process without significant overfitting.
  • Most importantly, the model achieved a total cost of $3,025.

6.3.2) NN Model 2 - Deeper, more heavily regularized MLP w/ 3 hidden layers

In [ ]:
# 6.3.2) NN Model 2

# Define and Compile the Model
deeper_nn = keras.Sequential([
    layers.Input(shape=(X_train_processed.shape[1],)),
    layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(64, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(1, activation='sigmoid')
])

deeper_nn.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='binary_crossentropy',
    metrics=['accuracy']
)

print("--- Architecture of Deeper NN ---")
deeper_nn.summary()
print("\n" + "="*50 + "\n")

# use the same class weights as before to handle imbalance
print("--- Starting Model Training ---")
history_deep = deeper_nn.fit(
    X_train_processed,
    y_train,
    epochs=50,
    batch_size=32,
    validation_data=(X_val_processed, y_val),
    class_weight=class_weights_dict,
    verbose=0 # Set to 0 for a cleaner output
)
print("\n" + "="*50 + "\n")


# Visualize Training History
history_df_deep = pd.DataFrame(history_deep.history)
plt.figure(figsize=(10, 6))
plt.plot(history_df_deep['loss'], label='Training Loss')
plt.plot(history_df_deep['val_loss'], label='Validation Loss')
plt.title('Deeper NN: Model Loss Over Epochs', fontsize=16)
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(loc='upper right')
plt.grid(True)
plt.show()
print("\n" + "="*50 + "\n")


# Evaluate the Model
y_pred_proba_deep = deeper_nn.predict(X_val_processed).flatten()
y_pred_class_deep = (y_pred_proba_deep > 0.5).astype(int)

evaluate_model(y_val, y_pred_class_deep, COST_MATRIX, model_name="Deeper NN")

# Append results for final comparison table
cost = calculate_business_cost(y_val, y_pred_class_deep, COST_MATRIX)
recall = recall_score(y_val, y_pred_class_deep)
f1 = f1_score(y_val, y_pred_class_deep)
model_performance.append({'Model': 'Deeper NN', 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})
--- Architecture of Deeper NN ---
Model: "sequential_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense_3 (Dense)                 │ (None, 128)            │         5,248 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_2 (Dropout)             │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_4 (Dense)                 │ (None, 64)             │         8,256 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_3 (Dropout)             │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_5 (Dense)                 │ (None, 32)             │         2,080 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_4 (Dropout)             │ (None, 32)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_6 (Dense)                 │ (None, 1)              │            33 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 15,617 (61.00 KB)
 Trainable params: 15,617 (61.00 KB)
 Non-trainable params: 0 (0.00 B)
==================================================

--- Starting Model Training ---

==================================================

==================================================

125/125 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step  
--- Evaluation Report for Deeper NN ---
              precision    recall  f1-score   support

  No Failure       0.99      0.99      0.99      3778
     Failure       0.89      0.91      0.90       222

    accuracy                           0.99      4000
   macro avg       0.94      0.95      0.95      4000
weighted avg       0.99      0.99      0.99      4000

Total Business Cost: $2,940.00
--------------------------------------------------

OBSERVATIONS:

  • New best total cost of $2,940.
  • The model maintained an strong recall of 0.91 while only slightly decreasing precision to 0.89. This means we are still catching the vast majority of failures without a significant increase in false alarms.
  • The history plot shows another healthy learning curve.

6.4) Hyperparameter Tuning with KerasTuner

In [ ]:
# 6.4) Hyperparameter Tuning with KerasTuner

# Define the Model-Building Function
def build_model(hp):
    model = keras.Sequential()
    model.add(layers.Input(shape=(X_train_processed.shape[1],)))
    hp_units_1 = hp.Int('units_1', min_value=64, max_value=256, step=32)
    model.add(layers.Dense(units=hp_units_1, activation='relu', kernel_regularizer=regularizers.l2(0.001)))
    model.add(layers.Dropout(0.4))
    hp_units_2 = hp.Int('units_2', min_value=32, max_value=128, step=32)
    model.add(layers.Dense(units=hp_units_2, activation='relu', kernel_regularizer=regularizers.l2(0.001)))
    model.add(layers.Dropout(0.4))
    model.add(layers.Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.001)))
    model.add(layers.Dropout(0.4))
    model.add(layers.Dense(1, activation='sigmoid'))
    hp_learning_rate = hp.Choice('learning_rate', values=[1e-2, 1e-3, 1e-4])
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=hp_learning_rate),
        loss='binary_crossentropy',
        metrics=['accuracy']
    )
    return model

# Instantiate and Run the Tuner
tuner = kt.Hyperband(
    build_model,
    objective='val_loss',
    max_epochs=25,
    factor=3,
    directory='keras_tuner_dir',
    project_name='rene_wind_tuning',
    overwrite=True
)

stop_early = EarlyStopping(monitor='val_loss', patience=5)

print("--- Starting Hyperparameter Search ---")
tuner.search(
    X_train_processed,
    y_train,
    epochs=50,
    validation_data=(X_val_processed, y_val),
    class_weight=class_weights_dict,
    callbacks=[stop_early],
    verbose=1
)
print("--- Hyperparameter Search Complete ---\n")
print("\n" + "="*50 + "\n")

# Get the optimal hyperparameters
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
print("--- Optimal Hyperparameters Found ---")
print(f"Optimal units in first layer: {best_hps.get('units_1')}")
print(f"Optimal units in second layer: {best_hps.get('units_2')}")
print(f"Optimal learning rate: {best_hps.get('learning_rate')}\n")

# Build the model with the best HPs
tuned_nn = tuner.hypermodel.build(best_hps)

# Retrain the best model
print("--- Training the Final Tuned Model ---")
history_tuned = tuned_nn.fit(
    X_train_processed, y_train,
    epochs=50,
    validation_data=(X_val_processed, y_val),
    class_weight=class_weights_dict,
    callbacks=[stop_early],
    verbose=0
)
print("--- Final Tuned Model Training Complete ---\n")

# Evaluate the Final Tuned Model
print("--- Evaluating the Final Tuned Model ---")
y_pred_proba_tuned = tuned_nn.predict(X_val_processed).flatten()
y_pred_class_tuned = (y_pred_proba_tuned > 0.5).astype(int)

evaluate_model(y_val, y_pred_class_tuned, COST_MATRIX, model_name="Tuned NN")

# Append results for final comparison table
cost = calculate_business_cost(y_val, y_pred_class_tuned, COST_MATRIX)
recall = recall_score(y_val, y_pred_class_tuned)
f1 = f1_score(y_val, y_pred_class_tuned)
model_performance.append({'Model': 'Tuned NN', 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})

# Build and Train the Best Model

# Get the optimal hyperparameters found by the tuner
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]

print(f"""
--- Optimal Hyperparameters Found ---
Optimal units in first layer: {best_hps.get('units_1')}
Optimal units in second layer: {best_hps.get('units_2')}
Optimal learning rate: {best_hps.get('learning_rate')}
""")

# Build the model with the optimal hyperparameters
tuned_nn = tuner.hypermodel.build(best_hps)

# Retrain this single best model on the data
print("\n--- Training the Final Tuned Model ---")
history_tuned = tuned_nn.fit(
    X_train_processed,
    y_train,
    epochs=50, # Train for a sufficient number of epochs
    validation_data=(X_val_processed, y_val),
    class_weight=class_weights_dict,
    callbacks=[stop_early], # Use early stopping to prevent overfitting
    verbose=0
)
Trial 30 Complete [00h 00m 42s]
val_loss: 0.19270648062229156

Best val_loss So Far: 0.17062073945999146
Total elapsed time: 00h 09m 31s
--- Hyperparameter Search Complete ---


==================================================

--- Optimal Hyperparameters Found ---
Optimal units in first layer: 160
Optimal units in second layer: 32
Optimal learning rate: 0.001

--- Training the Final Tuned Model ---
--- Final Tuned Model Training Complete ---

--- Evaluating the Final Tuned Model ---
125/125 ━━━━━━━━━━━━━━━━━━━━ 0s 884us/step
--- Evaluation Report for Tuned NN ---
              precision    recall  f1-score   support

  No Failure       0.99      0.99      0.99      3778
     Failure       0.89      0.91      0.90       222

    accuracy                           0.99      4000
   macro avg       0.94      0.95      0.95      4000
weighted avg       0.99      0.99      0.99      4000

Total Business Cost: $2,939.00
--------------------------------------------------

--- Optimal Hyperparameters Found ---
Optimal units in first layer: 160
Optimal units in second layer: 32
Optimal learning rate: 0.001


--- Training the Final Tuned Model ---

OBSERVATIONS:

  • Tuning brought some success, and brought total cost down to $2,939, a minor improvement.
  • Tuning shows the best-performing architecture used 160 neurons in the first hidden layer and 32 neurons in the second, with a learning rate of 0.001.
  • This tuned model maintained an excellent recall of 0.91. This ensures it continues to catch the vast majority of generator failures, which is the primary goal.

6.5) Threshold Optimization

In [ ]:
# 6.5) Threshold Optimization

# Get Predictions from the Tuned Model
y_pred_proba = tuned_nn.predict(X_val_processed).flatten()

# Find the Optimal Threshold
thresholds = np.arange(0.01, 1.0, 0.01)
costs = []
for t in thresholds:
    y_pred_class = (y_pred_proba > t).astype(int)
    costs.append(calculate_business_cost(y_val, y_pred_class, COST_MATRIX))

optimal_idx = np.argmin(costs)
optimal_threshold = thresholds[optimal_idx]
min_cost = costs[optimal_idx]

print(f"Optimal Threshold for Tuned Model: {optimal_threshold:.2f}")
print(f"Minimum possible business cost: ${min_cost:,.2f}")
print("\n" + "="*50 + "\n")

# Evaluate the Tuned Model with the Optimal Threshold
y_pred_optimal = (y_pred_proba > optimal_threshold).astype(int)
evaluate_model(y_val, y_pred_optimal, COST_MATRIX, model_name="Tuned NN with Optimal Threshold")

# Append results for final comparison table
cost = calculate_business_cost(y_val, y_pred_optimal, COST_MATRIX)
recall = recall_score(y_val, y_pred_optimal)
f1 = f1_score(y_val, y_pred_optimal)
model_performance.append({'Model': 'Tuned NN (Optimal Threshold)', 'Total Business Cost': cost, 'Recall': recall, 'F1-Score': f1})
125/125 ━━━━━━━━━━━━━━━━━━━━ 0s 889us/step
Optimal Threshold for Tuned Model: 0.24
Minimum possible business cost: $2,816.00

==================================================

--- Evaluation Report for Tuned NN with Optimal Threshold ---
              precision    recall  f1-score   support

  No Failure       1.00      0.98      0.99      3778
     Failure       0.69      0.92      0.79       222

    accuracy                           0.97      4000
   macro avg       0.84      0.95      0.89      4000
weighted avg       0.98      0.97      0.97      4000

Total Business Cost: $2,816.00
--------------------------------------------------

OBSERVATIONS:

  • Threshold optimization was effective. We brought cost down to $2,816 with an optimal decision threshold of 0.24.
  • Recall of 0.92 is strong.

7) Final Model Training & Evaluation

7.1) Train Final Model on Full Training Set

In [ ]:
# 7.1) Train Final Model on Full Training Set

# Prepare the Full Training Dat
X_full_train = raw_train.drop('Target', axis=1)
y_full_train = raw_train['Target']

# Use the same pipeline to process this full dataset.
X_full_train_processed = preprocessing_pipeline.fit_transform(X_full_train)

# Build the Final Model Architecture
print("--- Building the final champion model ---")
final_model = keras.Sequential([
    layers.Input(shape=(X_full_train_processed.shape[1],)),
    layers.Dense(160, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.001)),
    layers.Dropout(0.4),
    layers.Dense(1, activation='sigmoid')
])

final_model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='binary_crossentropy',
    metrics=['accuracy']
)

# Train the Model on All Data
print("--- Training the final model on all 20,000 training samples... ---")
final_model.fit(
    X_full_train_processed,
    y_full_train,
    epochs=40,
    batch_size=32,
    class_weight=class_weights_dict,
    verbose=0
)

print("--- Final Model is Trained and Ready for Evaluation ---")
--- Building the final champion model ---
--- Training the final model on all 20,000 training samples... ---
--- Final Model is Trained and Ready for Evaluation ---

7.2) Evaluate Final Model on Test Set

In [ ]:
# 7.2) Evaluate Final Model on Test Set

# Prepare the Test Data
X_test = raw_test.drop('Target', axis=1)
y_test = raw_test['Target']

# Use the pipeline that was already fitted on the full training data
X_test_processed = preprocessing_pipeline.transform(X_test)

# Make Predictions on the Test Set
print("--- Evaluating Final Model on Unseen Test Data ---")
y_pred_proba_final = final_model.predict(X_test_processed).flatten()

# Apply Optimal Threshold and Evaluate
optimal_threshold = 0.24
y_pred_final = (y_pred_proba_final > optimal_threshold).astype(int)

# Use our helper function for the final report
evaluate_model(y_test, y_pred_final, COST_MATRIX, model_name="Final Champion Model (on Test Set)")

# Display Final Model Comparison
print("\n--- Final Model Performance Comparison ---")
# This dataframe summarizes the performance of all models we tested on the validation set
performance_df = pd.DataFrame(model_performance).sort_values(by='Total Business Cost', ascending=True).reset_index(drop=True)
display(performance_df)
--- Evaluating Final Model on Unseen Test Data ---
157/157 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step
--- Evaluation Report for Final Champion Model (on Test Set) ---
              precision    recall  f1-score   support

  No Failure       0.99      0.98      0.99      4718
     Failure       0.73      0.88      0.80       282

    accuracy                           0.97      5000
   macro avg       0.86      0.93      0.89      5000
weighted avg       0.98      0.97      0.98      5000

Total Business Cost: $4,639.00
--------------------------------------------------

--- Final Model Performance Comparison ---
Model Total Business Cost Recall F1-Score
0 Tuned NN (Optimal Threshold) 2816 0.923423 0.791506
1 Tuned NN 2939 0.914414 0.904232
2 Deeper NN 2940 0.914414 0.902222
3 Simple NN 3025 0.909910 0.920273
4 LightGBM 3614 0.882883 0.867257
5 CatBoost 3796 0.873874 0.877828
6 XGBoost 3890 0.869369 0.877273
7 Logistic Regression 4817 0.846847 0.423901
8 Random Forest 7667 0.689189 0.811671

OBSERVATIONS

  • The final model performed well on the test data, successfully identifying 88% of all generator failures.
  • As expected, the model's performance was slightly lower on the test set than on the validation set. The final business cost of $4,639 reflects this.
  • This evaluation provides the most honest and reliable measure of how the model would perform in a real-world production environment. The results confirm that the model is a highly effective and valuable tool.

8) Feature Importance & Effects

8.1) Global Feature Importance

In [ ]:
# 8.1) Feature Importance

# Get the Baseline Score
y_pred_baseline = (final_model.predict(X_test_processed, verbose=0) > 0.29).astype(int)
baseline_accuracy = accuracy_score(y_test, y_pred_baseline)
print(f"Baseline Model Accuracy: {baseline_accuracy:.4f}")

# Manually Calculate Permutation Importance
print("\n--- Calculating Permutation Importance... ---")
importances = {}
for col in X_test.columns:
    # Create a copy of the processed test data
    X_test_permuted = X_test_processed.copy()

    # Get the index of the current column
    col_idx = X_test.columns.get_loc(col)

    # Shuffle the values in that single column
    np.random.shuffle(X_test_permuted[:, col_idx])

    # Get the new score with the shuffled data
    y_pred_permuted = (final_model.predict(X_test_permuted, verbose=0) > 0.29).astype(int)
    permuted_accuracy = accuracy_score(y_test, y_pred_permuted)

    # The importance is the drop in score
    feature_importance = baseline_accuracy - permuted_accuracy
    importances[col] = feature_importance

print("--- Calculation Complete ---")


# Organize and Plot the Results
perm_importance = pd.Series(importances).sort_values(ascending=False)

print("\n--- Model Feature Importance ---")
plt.figure(figsize=(10, 8))
sns.barplot(x=perm_importance.values, y=perm_importance.index, palette='viridis')
plt.title('Feature Importance', fontsize=16)
plt.xlabel('Performance Drop (Decrease in Accuracy Score)')
plt.ylabel('Feature')
plt.tight_layout()
plt.show()
Baseline Model Accuracy: 0.9786

--- Calculating Permutation Importance... ---
--- Calculation Complete ---

--- Model Feature Importance ---

OBSERVATIONS:

  • Top features identified here (V17, V10, V11, and V21) were identified as highly predictive during initial EDA, a strong sign that the model has learned the correct patterns from the data.
  • There is a distinct group of about 7-8 highly impactful features at the top. After that, there is a noticeable drop-off in importance. This suggests that the model's performance is driven by a core set of reliable sensor signals.
  • The model isn't relying on just one or two features. It has learned to weigh the input from several different sensors to make a decision, which makes it more robust and less likely to be thrown off by a single faulty reading.
  • The features at the bottom of the list have an importance score near zero. This confirms that they contribute very little to the model's predictive power, and we could likely build a simpler, faster model by excluding them without a significant loss in performance.

8.2) Visualizing Feature Effects

In [ ]:
# 8.2) Visualizing Feature Effects

# Select Top Features
top_features = perm_importance.head(6).index.tolist()

print("--- Manually Calculating and Plotting Partial Dependence... ---")
# Create a figure to hold all the subplots
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
axes = axes.flatten()

# Loop Through Each Top Feature
for i, feature_name in enumerate(top_features):
    # Get the integer index of the current feature
    feature_idx = X_test.columns.get_loc(feature_name)

    # Create a grid of 10 values for this feature, from its min to max in the test set
    feature_grid = np.linspace(
        X_test_processed[:, feature_idx].min(),
        X_test_processed[:, feature_idx].max(),
        num=10
    )

    # This will store the average predictions
    avg_preds = []

    # Calculate Average Prediction for Each Grid Value
    for grid_val in feature_grid:
        # Create a copy of the processed test data
        X_temp = X_test_processed.copy()

        # Replace the entire column for the current feature with the grid value
        X_temp[:, feature_idx] = grid_val

        # Get the model's predictions on this modified data
        preds = final_model.predict(X_temp, verbose=0)

        # Calculate the average prediction and store it
        avg_preds.append(preds.mean())

    # Plot the Results
    axes[i].plot(feature_grid, avg_preds, marker='o', linestyle='-')
    axes[i].set_title(f'Partial Dependence of {feature_name}')
    axes[i].set_xlabel('Feature Value')
    axes[i].set_ylabel('Avg. Predicted Probability')
    axes[i].grid(True)

# Finalize the Figure
fig.suptitle('Partial Dependence Plots for Top 6 Features', fontsize=20)
plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.show()
--- Manually Calculating and Plotting Partial Dependence... ---

OBSERVATIONS:

  • Plots reveal that the model has learned complex, non-linear patterns. For features like V12, V9, V22, and V31, the probability of failure is highest at low and high values, and lowest near the middle of their range. This suggests the model has learned that any significant deviation from a normal operating value for these sensors, in either direction, increases the risk of failure.
  • For features V10 and V14, the relationship is more straightforward. As the value of these features increases, the predicted probability of failure also increases steadily. This confirms they are strong positive predictors, which aligns perfectly with our earlier EDA and feature importance findings.

9) Conclusions & Business Recommendations

9.1) Project Summary & Final Model Performance

  • This project aimed to develop a predictive maintenance model to minimize operational costs for ReneWind by accurately identifying generator failures. Detailed EDA revealed a class imbalance and the necessity for feature scaling. To directly address the business goal, a custom cost function was defined that heavily penalized missed failures (False Negatives).
  • Strong benchmarks were set using five standard machine learning models, with LightGBM proving to be the best baseline. A series of neural network architectures were then developed, finding that a deep, regularized MLP as the most effective design. This champion architecture was further improved through systematic hyperparameter tuning and decision threshold optimization.
  • The final and best-performing model is the Tuned Neural Network with an optimized classification threshold of 0.29.
  • The model's strong performance, particularly its 88% recall on unseen data, confirms that it is a robust and valuable tool capable of generalizing well to new scenarios.

9.2) Recommendations for ReneWind

Based on the successful development and rigorous evaluation of the predictive maintenance model, we recommend the following actions to leverage this tool and maximize its value for ReneWind.

  • Models perform best when they are kept up-to-date. We recommend establishing a cycle to:
    • Monitor: Continuously track the model's performance on new data to ensure its accuracy does not degrade over time.
    • Retrain: Periodically retrain the model on a monthly or quarterly basis, incorporating new sensor data as it is collected. This will allow the model to adapt to any changes in equipment behavior or environmental conditions.
  • The model's performance is heavily influenced by the quality and quantity of the data it's trained on. While the current model is strong, its accuracy can be improved even further by gathering more examples of failure events. We recommend prioritizing the collection and labeling of data from any future generator failures to enhance the dataset for future retraining cycles.
  • The explainability plots are not just diagnostic tools; they are valuable operational assets. We recommend that when the model flags a turbine for inspection, the report should include an explanation showing which specific sensor readings drove the prediction. This will:

    • Build Trust: Help engineers and technicians trust the model's recommendations.
    • Speed Up Diagnostics: Guide maintenance teams on where to focus their initial inspection efforts.

By implementing these recommendations, ReneWind can fully capitalize on the predictive power of this model, leading to significant and sustained reductions in maintenance costs and operational downtime.

Export to HTML

In [ ]:
# Exporting to HTML
!jupyter nbconvert --to html "/content/drive/MyDrive/Colab Notebooks/Burns_Greg_ReneWind_Main_Project.ipynb"
[NbConvertApp] Converting notebook /content/drive/MyDrive/Colab Notebooks/Burns_Greg_ReneWind_Main_Project.ipynb to html
[NbConvertApp] WARNING | Alternative text is missing on 13 image(s).
[NbConvertApp] Writing 2259022 bytes to /content/drive/MyDrive/Colab Notebooks/Burns_Greg_ReneWind_Main_Project.html