Heart Disease Prediction ML Pipeline

Build an end-to-end ML pipeline to predict heart disease from clinical data.

Introduction

30 Second Summary

Heart disease is the leading cause of death worldwide, and early detection can save lives. Machine learning models can analyze patient data to flag high-risk individuals before symptoms become critical.

In this project, you will build a complete machine learning pipeline in Google Colab that takes raw patient clinical data and produces a trained, tuned classifier that predicts heart disease.

What You'll Build

When you run the final cell of your notebook, you can feed in a patient's clinical measurements and get back a heart disease prediction with a confidence percentage.

By the end of this project, you'll have:

  • A trained Random Forest model that you can test with patient data and see predictions with confidence scores printed in your notebook.
  • A documented Colab notebook walking through exploratory data analysis, preprocessing, baseline training, and hyperparameter tuning that you can share as a portfolio piece.
  • Visual proof (ROC curves, confusion matrices, feature importance charts) showing your tuned model catches more disease cases than the baseline.
  • Secret Mission: Add SHAP explainability to show which features drive each individual prediction.

Are there any prerequisites?

You need a Kaggle account (free) to download the dataset, and a Google account for Colab and Drive. Basic Python knowledge (variables, functions, loops) is helpful but not required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the goal before any hands-on work begins.

Set Up and Load the Data

Before you can explore any data, you need the dataset accessible from your notebook environment. Mounting Google Drive in Google Colab lets you persist files across sessions so you never lose your work.

In this step, you will download the Cleveland Heart Disease dataset from Kaggle, upload it to Google Drive, create a Colab notebook, and load the data into a pandas DataFrame.

In this step, get ready to:
  • Download the Heart Disease Cleveland UCI dataset from Kaggle and upload it to Google Drive.
  • Create a new Colab notebook, mount Drive, and import all required libraries.
  • Load the dataset and verify its structure, shape, and target column.
Download the dataset from Kaggle

The Cleveland Heart Disease dataset contains 303 patient records with 13 clinical features. You will download it manually from Kaggle and place it in your Google Drive so Colab can access it.

  • Open the dataset page in your browser:
  • https://www.kaggle.com/datasets/cherngs/heart-disease-cleveland-uci
  • Sign in to your Kaggle account if prompted.
  • Click the Download button near the top of the dataset page to download the ZIP file.
  • Extract the ZIP file on your computer to get heart_cleveland_upload.csv.
  • Open Google Drive in your browser at drive.google.com.
  • Drag and drop heart_cleveland_upload.csv into the root of your My Drive folder.

Why upload to Google Drive?

Colab sessions reset when you disconnect. Files stored in Google Drive persist between sessions, so you can always reload your dataset without re-downloading it from Kaggle.

  • Confirm the file appears in your Drive root folder. You should see heart_cleveland_upload.csv listed in My Drive.

Don't see the Download button on Kaggle?

Make sure you are signed in to your Kaggle account. The download button only appears for authenticated users.

If the page shows a reCAPTCHA challenge, complete it and the page will load normally.

help me download the heart disease dataset from Kaggle

Create a Colab notebook and mount Google Drive

Google Colab is a free, cloud-hosted Jupyter notebook environment. It comes with all the data science libraries you need pre-installed.

  • Open Google Colab in your browser at colab.research.google.com.
  • Click File in the top menu bar, then select New notebook in Drive.
  • Click the notebook name at the top-left (it will say something like Untitled0.ipynb) and rename it to Heart_Disease_ML_Pipeline.

Your notebook is ready. Now mount Google Drive so the notebook can access your dataset file.

  • Paste the following code into the first cell and run it by pressing Shift+Enter:
# === CELL 1: Mount Google Drive ===
from google.colab import drive
drive.mount('/content/drive')

What does this code do?

drive.mount('/content/drive') connects your Google Drive to the Colab file system. After mounting, everything in your Drive is accessible at the path /content/drive/MyDrive/.

  • Colab will open a pop-up asking you to sign in and grant access to your Google Drive. Follow the prompts and click Allow.

You should see Mounted at /content/drive printed below the cell.

Not seeing the Drive mount prompt?

Make sure you are signed in to the same Google account in both Colab and Drive. If the cell hangs, click Runtime then Restart runtime and try again.

help me mount Google Drive in Colab

Next, import all the libraries you will use throughout this project.

  • Add a new code cell below and paste the following imports:
# === CELL 2: Import Libraries ===
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (accuracy_score, classification_report,
                             confusion_matrix, roc_auc_score, roc_curve)
from joblib import dump

print("pandas:", pd.__version__)
print("numpy:", np.__version__)
import sklearn
print("scikit-learn:", sklearn.__version__)

What does this code do?

  • pandas and numpy handle data loading and numerical operations.
  • matplotlib and seaborn create visualizations (heatmaps, distribution plots, ROC curves).
  • sklearn provides all the machine learning tools: model training, evaluation metrics, preprocessing, and cross-validation.
  • joblib saves trained models to disk as .joblib files, which is more efficient than pickle for large NumPy arrays.
  • Run the cell. You should see version numbers printed for pandas, numpy, and scikit-learn.

All libraries are pre-installed

Google Colab comes with pandas, numpy, matplotlib, seaborn, and scikit-learn pre-installed. You do not need to run any pip install commands for the main pipeline.

Now load the dataset from Drive into a pandas DataFrame.

  • Add a new code cell and paste the following:
# === CELL 3: Load Dataset ===
df = pd.read_csv('/content/drive/MyDrive/heart_cleveland_upload.csv')
print(f"Dataset shape: {df.shape}")
df.head()

What does this code do?

pd.read_csv() reads the CSV file from your mounted Google Drive into a DataFrame called df.

df.shape prints the number of rows and columns. df.head() displays the first 5 rows so you can visually confirm the data loaded correctly.

Before you run this cell, what do you think the shape will be? The dataset has 303 patient records and 14 columns (13 features plus the target).

  • Run the cell.

You should see Dataset shape: (303, 14) and a preview of the first 5 rows with columns like age, sex, cp, trestbps, and chol.

Seeing a FileNotFoundError?

Double-check that heart_cleveland_upload.csv is in the root of your My Drive folder, not inside a subfolder.

If you placed the file in a subfolder, update the path in the code to match. For example: /content/drive/MyDrive/YourFolder/heart_cleveland_upload.csv.

help me fix a FileNotFoundError when loading a CSV from Google Drive in Colab

Verify the dataset structure

You have confirmed the dataset loaded. Now inspect its structure to confirm there are no missing values and the target column is correct.

  • Add a new code cell and paste the following:
# === CELL 4: Dataset Overview ===
print("=== Dataset Info ===")
print(df.dtypes)
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nBasic statistics:")
df.describe()

What does this code do?

  • df.dtypes shows the data type of each column (int64, float64).
  • df.isnull().sum() counts missing values per column. You want all zeros here.
  • df.describe() prints summary statistics (mean, std, min, max) for every numeric column.
  • Run the cell. Confirm that all 14 columns have 0 missing values.

Finally, verify the target column and check class balance.

  • Add one more code cell and paste this:
# === CELL 5: Verify Target Column ===
TARGET = 'condition'
print(f"Target column: {TARGET}")
print(f"Target distribution:\n{df[TARGET].value_counts()}")
print(f"\nClass balance: {df[TARGET].value_counts(normalize=True).round(3).to_dict()}")

What does this code do?

The TARGET variable stores the name of the column you are trying to predict. In this dataset, condition is the target: 0 means no heart disease, 1 means heart disease is present.

value_counts() shows how many patients fall in each class. The normalize=True variant shows proportions so you can quickly see if the classes are balanced.

  • Run the cell. You should see the target distribution showing roughly 160 patients with disease (1) and 137 without (0), indicating a reasonably balanced dataset.

✔️ Awesome, I've got everything!

Great work. Make sure all five cells ran without errors and you can see the target distribution output.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm you have the following five cells in order:

  • Cell 1: Mount Google Drive (drive.mount('/content/drive')).
  • Cell 2: Import Libraries (all pandas, numpy, matplotlib, seaborn, sklearn, and joblib imports, plus version prints).
  • Cell 3: Load Dataset (pd.read_csv(...) from Drive, prints shape, shows df.head()).
  • Cell 4: Dataset Overview (dtypes, missing values, describe).
  • Cell 5: Verify Target Column (TARGET = 'condition', value counts, class balance).

Each cell should have run successfully with no errors. If any cell shows an error, re-read the troubleshooting callouts above for that specific cell.

Your data is loaded and verified. Next up, you will explore the data visually to understand which features correlate most strongly with heart disease.

Explore the Data

Your dataset is loaded and you have confirmed its shape and structure.

You cannot build a good model without understanding your data first. Exploratory Data Analysis reveals patterns, outliers, and relationships that directly inform how you preprocess features and which models will work best.

In this step, get ready to:
  • Create a correlation heatmap to identify which features relate most to heart disease.
  • Visualize target and feature distributions to spot patterns between disease and no-disease groups.
Create a correlation heatmap

A correlation heatmap shows how strongly each feature relates to every other feature, including the target. This helps you spot which clinical measurements are most predictive of heart disease before training any model.

  • Create a new cell in your notebook below Cell 5 and add the following code:
# === CELL 6: Correlation Heatmap ===
plt.figure(figsize=(12, 8))
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap='RdBu_r', center=0, fmt='.2f')
plt.title('Feature Correlation Heatmap')
plt.tight_layout()
plt.show()

What does this code do?

df.corr(numeric_only=True) calculates the Pearson correlation coefficient between every pair of numeric columns.

sns.heatmap() renders those values as a colour-coded grid. Values close to +1 (dark red) mean strong positive correlation, values close to -1 (dark blue) mean strong negative correlation, and values near 0 mean little linear relationship.

  • Run the cell.
  • You should see a colour-coded grid of all 14 features. Look at the condition row (the bottom row) to identify which features have the strongest correlation with heart disease.

The features thalach (max heart rate), cp (chest pain type), and oldpeak (ST depression) show the strongest correlations with the target.

Don't see the heatmap?

Make sure you ran all previous cells first (Cells 1 through 5). The heatmap depends on df being loaded and seaborn being imported.

If you see a NameError, go back to Cell 2 and re-run your imports.

Help me debug my correlation heatmap

Visualize target and feature distributions

Correlations tell you which features matter, but distributions tell you how they differ between disease and no-disease groups. A feature where the two groups clearly separate makes a strong signal for your model.

  • Create a new cell below Cell 6 and add this code to plot the target distribution and age breakdown:
# === CELL 7: Target and Feature Distributions ===
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x=TARGET, data=df, ax=axes[0])
axes[0].set_title('Heart Disease Distribution')
axes[0].set_xticklabels(['No Disease (0)', 'Disease (1)'])

sns.histplot(data=df, x='age', hue=TARGET, kde=True, ax=axes[1])
axes[1].set_title('Age Distribution by Heart Disease Status')

plt.tight_layout()
plt.show()

What does this code do?

  • The left plot is a count plot showing how many patients fall into each target class (0 = no disease, 1 = disease). This tells you whether the dataset is balanced or skewed toward one class.
  • The right plot is a histogram of age, split by target. Overlapping distributions mean age alone is not a strong separator.
  • Run Cell 7.
  • You should see two side-by-side plots. The left plot shows the class balance (roughly even between disease and no-disease). The right plot shows age distributions overlapping, meaning age alone does not cleanly separate the groups.

Seeing overlapping or cut-off labels?

If x-axis labels overlap, check that plt.tight_layout() is present before plt.show(). This adjusts spacing so plots do not collide.

Help me fix my Colab chart layout

Now let's look at the features that showed strong correlations in the heatmap. Before you run the next cell, think about which features you expect to show the clearest separation between disease and no-disease groups based on what the heatmap revealed.

  • Create a new cell below Cell 7 and add this code to plot key feature distributions:
# === CELL 8: Key Feature Distributions ===
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

sns.boxplot(x=TARGET, y='thalach', data=df, ax=axes[0, 0])
axes[0, 0].set_title('Max Heart Rate by Condition')

sns.boxplot(x=TARGET, y='oldpeak', data=df, ax=axes[0, 1])
axes[0, 1].set_title('ST Depression by Condition')

sns.countplot(x='cp', hue=TARGET, data=df, ax=axes[1, 0])
axes[1, 0].set_title('Chest Pain Type by Condition')

sns.boxplot(x=TARGET, y='chol', data=df, ax=axes[1, 1])
axes[1, 1].set_title('Cholesterol by Condition')

plt.tight_layout()
plt.show()

What does this code do?

  • Box plots compare the spread and median of a numeric feature between the two groups. When the boxes barely overlap, the feature is a strong discriminator.
  • thalach (max heart rate) typically shows disease patients with higher heart rates.
  • oldpeak (ST depression during exercise) tends to be higher in disease patients.
  • cp (chest pain type) uses a count plot because it is categorical. Certain pain types cluster more with disease.
  • Run Cell 8.
  • You should see a 2x2 grid of charts. Look for features where the two groups (0 and 1) sit at clearly different levels.

The thalach and oldpeak box plots show clear separation between disease and no-disease groups. The cp count plot shows that certain chest pain types strongly associate with disease presence. Cholesterol shows more overlap, making it a weaker signal on its own.

Charts look blank or cramped?

Check that you are using the exact TARGET variable (set in Cell 5) in the x=TARGET argument. If you renamed the variable or typed the column name as a raw string, Seaborn may not find the column.

Help me debug my distribution charts

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook (Colab auto-saves, but you can press Cmd+S or Ctrl+S to be sure).

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm you have these three cells in order after Cell 5:

  • Cell 6: Correlation Heatmap (uses sns.heatmap with df.corr()).
  • Cell 7: Target and Feature Distributions (two subplots: a count plot of TARGET and a histogram of age).
  • Cell 8: Key Feature Distributions (four subplots: box plots for thalach, oldpeak, chol, and a count plot for cp).

Each cell should produce a visible chart when run.

You now know which features carry the strongest signal for predicting heart disease. Next up, you will preprocess the data so these features are ready for a machine learning model to learn from.

Preprocess the Data

Your EDA revealed clear patterns in the data. Features like thalach and cp show strong relationships with heart disease. Now it's time to prepare this data for machine learning.

Raw clinical features are on wildly different scales. Age is measured in years while cholesterol is in mg/dl. Algorithms like Logistic Regression are sensitive to these differences, so without proper preprocessing your model will perform poorly.

In this step, get ready to:
  • Separate features from the target column and split into training and test sets.
  • Scale features with StandardScaler so all measurements are on the same playing field.
Separate features from target and split the data

Machine learning models need their inputs (features) and outputs (target) as separate arrays. You also need to hold back some data for testing so you can evaluate the model on data it has never seen.

  • Create a new code cell in your notebook and separate the features from the target by running this code:
# === CELL 9: Separate Features and Target ===
X = df.drop(TARGET, axis=1)
y = df[TARGET]

print(f"Features shape: {X.shape}")
print(f"Target shape: {y.shape}")
print(f"\nFeature columns: {list(X.columns)}")

What does this code do?

  • X = df.drop(TARGET, axis=1) creates a DataFrame with all columns except the target. These 13 columns are the measurements the model will learn from.
  • y = df[TARGET] extracts just the condition column. This is the label the model will try to predict (0 or 1).
  • Run the cell. You should see Features shape: (303, 13) and Target shape: (303,) confirming the split worked.

Now you need to split this data into a training set and a test set. The model learns from the training set and gets evaluated on the test set it has never seen.

  • Create a new cell and perform the train-test split by running this code:
# === CELL 10: Train-Test Split ===
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")
print(f"\nTraining target distribution:\n{y_train.value_counts()}")
print(f"\nTest target distribution:\n{y_test.value_counts()}")

What does this code do?

  • test_size=0.2 holds back 20% of the data (61 samples) for testing. The remaining 80% (242 samples) is used for training.
  • random_state=42 locks the random split so you get the same results every time you run the notebook.
  • stratify=y ensures both training and test sets have the same proportion of disease vs. no-disease cases as the original data.
  • Run the cell. You should see Training set: 242 samples and Test set: 61 samples printed in the output.
  • Confirm that the class proportions in training and test distributions look similar to each other.

Seeing different sample counts?

If your training set is not 242 samples, check that test_size=0.2 is set correctly. Also verify your dataset loaded with 303 rows in the earlier cell.

help me debug my train-test split sample counts

Scale features with StandardScaler

Right now, age ranges from about 29 to 77, while cholesterol ranges from 126 to 564. Feature scaling transforms every feature to have mean 0 and standard deviation 1, putting all measurements on the same scale.

  • Create a new cell and apply StandardScaler by running this code:
# === CELL 11: Feature Scaling ===
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print("Feature scaling complete.")
print(f"Training mean (first 3 features): {X_train_scaled.mean(axis=0)[:3].round(4)}")
print(f"Training std (first 3 features): {X_train_scaled.std(axis=0)[:3].round(4)}")

Why fit only on training data?

Notice that fit_transform is called on X_train but only transform is called on X_test. This prevents data leakage.

If you fit the scaler on the test data too, the model indirectly "sees" test data during training. The scaler learns the mean and standard deviation from training data only, then applies those same numbers to the test set.

Before you run this cell, what do you expect the training mean values to be for properly scaled data?

  • Run the cell to check your prediction.

You should see the training mean values at approximately 0 (very small numbers like -0.0000) and standard deviation values at approximately 1.0000. That confirms the scaling worked correctly.

Seeing unexpected values?

If the mean is not close to 0 or std is not close to 1, make sure you used fit_transform on X_train (not X_test). Also check that you printed X_train_scaled stats, not X_test_scaled.

help me fix my StandardScaler output

✔️ Awesome, I've got everything!

Great. Double check you have three new cells (Cells 9, 10, and 11) saved in your notebook.

ⓧ I'd like to double check the full code

Scroll up through your notebook and confirm these three cells are present in order:

  • Cell 9: Separate Features and Target - creates X and y from your DataFrame.
  • Cell 10: Train-Test Split - splits into X_train, X_test, y_train, y_test with 80/20 stratified split.
  • Cell 11: Feature Scaling - creates X_train_scaled and X_test_scaled using StandardScaler fitted on training data only.

Your data is now preprocessed and ready for modeling. Next up, you'll train a baseline model and see exactly where it falls short on heart disease predictions.

Train a Baseline Model

Your data is preprocessed, scaled, and split into training and test sets. Now it is time to see how well a simple model can predict heart disease from these features.

A baseline model establishes what "good enough" looks like before you invest time in tuning. You will train a Logistic Regression classifier and evaluate its predictions to see where it succeeds and where it falls short.

In this step, get ready to:
  • Train a Logistic Regression model and evaluate its accuracy and ROC-AUC score.
  • Visualize the confusion matrix to identify where the model makes mistakes.
  • Discover why accuracy alone is not enough for healthcare predictions.
Train Logistic Regression

Logistic Regression is the standard starting point for binary classification problems. It is fast to train, easy to interpret, and gives you a clear reference point to compare future models against.

  • Create a new code cell in your Colab notebook and add the following code to train the baseline model:
# === CELL 12: Baseline - Logistic Regression ===
lr_model = LogisticRegression(random_state=42, max_iter=1000)
lr_model.fit(X_train_scaled, y_train)

y_pred_lr = lr_model.predict(X_test_scaled)
y_prob_lr = lr_model.predict_proba(X_test_scaled)[:, 1]

print("=== Logistic Regression (Baseline) ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_lr):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_lr)}")

What does this code do?

  • LogisticRegression(random_state=42, max_iter=1000) creates the model. random_state=42 ensures reproducible results. max_iter=1000 gives the optimizer enough iterations to converge.
  • predict() gives hard class labels (0 or 1). predict_proba()[:, 1] gives the probability of disease, which is needed for ROC-AUC.
  • ROC-AUC measures how well the model ranks patients by disease risk across all possible thresholds. It is more informative than accuracy alone for healthcare problems.
  • Run the cell.

You should see accuracy around 0.85 and a ROC-AUC score, plus a full classification report showing precision, recall, and f1-score for each class.

What is ROC-AUC?

ROC-AUC stands for "Receiver Operating Characteristic - Area Under Curve". It measures how well the model distinguishes between disease and no-disease patients across every possible decision threshold.

A score of 1.0 means perfect separation. A score of 0.5 means the model is no better than random guessing. In healthcare, ROC-AUC is preferred over accuracy because it accounts for the tradeoff between catching disease cases and avoiding false alarms.

Seeing a ConvergenceWarning?

If you see a warning about convergence, make sure max_iter=1000 is set in your LogisticRegression call. The default of 100 iterations is sometimes not enough for this dataset.

Help me debug my Logistic Regression training

Visualize baseline performance

The accuracy looks decent at around 85%. But in healthcare, the real question is: how many sick patients does the model miss? A confusion matrix breaks down predictions into four categories so you can see exactly where the model goes wrong.

Before you run this next cell, think about it: do you expect the model to miss any disease cases, given that accuracy was around 85%?

  • Create a new code cell and add this code to plot the confusion matrix:
# === CELL 13: Baseline Confusion Matrix ===
cm_lr = confusion_matrix(y_test, y_pred_lr)
plt.figure(figsize=(6, 5))
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues',
            xticklabels=['No Disease', 'Disease'],
            yticklabels=['No Disease', 'Disease'])
plt.title('Logistic Regression - Confusion Matrix')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()

print(f"\nFalse Negatives (missed disease cases): {cm_lr[1][0]}")
print("In healthcare, missing a disease case is dangerous!")
print("Can we reduce these false negatives with a better model?")

What does this code do?

  • confusion_matrix(y_test, y_pred_lr) creates a 2x2 grid comparing actual labels against predicted labels.
  • The heatmap makes the counts visual. The bottom-left cell is the critical one: patients who actually have disease but were predicted as healthy.
  • cm_lr[1][0] accesses that bottom-left cell directly. These are the false negatives.
  • Run the cell.

You should see the confusion matrix heatmap and a printed count of false negatives. These are patients who actually have heart disease but the model told them they are healthy.

Why are false negatives dangerous in healthcare?

A false negative means the model says "no disease" when the patient actually has heart disease. That patient might not receive treatment they urgently need.

This is the gap in the baseline. Overall accuracy of 85% sounds solid, but every false negative is a missed diagnosis. In the next step, you will tune a Random Forest model specifically to reduce these dangerous misses.

Confusion matrix not displaying?

Make sure you ran Cell 12 first. The variables y_pred_lr and y_test must exist before you can compute the confusion matrix. If you restarted the runtime, re-run all cells from the top.

Help me fix my confusion matrix visualization

✔️ Awesome, I've got everything!

Great. Double check you have both Cell 12 and Cell 13 in your notebook, and that the confusion matrix is visible in the output.

ⓧ I'd like to double check the full code

Here is Cell 12 in full:

# === CELL 12: Baseline - Logistic Regression ===
lr_model = LogisticRegression(random_state=42, max_iter=1000)
lr_model.fit(X_train_scaled, y_train)

y_pred_lr = lr_model.predict(X_test_scaled)
y_prob_lr = lr_model.predict_proba(X_test_scaled)[:, 1]

print("=== Logistic Regression (Baseline) ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_lr):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_lr)}")

And here is Cell 13:

# === CELL 13: Baseline Confusion Matrix ===
cm_lr = confusion_matrix(y_test, y_pred_lr)
plt.figure(figsize=(6, 5))
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues',
            xticklabels=['No Disease', 'Disease'],
            yticklabels=['No Disease', 'Disease'])
plt.title('Logistic Regression - Confusion Matrix')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()

print(f"\nFalse Negatives (missed disease cases): {cm_lr[1][0]}")
print("In healthcare, missing a disease case is dangerous!")
print("Can we reduce these false negatives with a better model?")

Your baseline is trained and its weakness is clear: decent overall accuracy, but it misses real disease cases. In the next step, you will tune a Random Forest model with cross-validation to close that gap and reduce those dangerous false negatives.

Tune, Evaluate, and Export the Final Model

Your baseline Logistic Regression achieves decent accuracy, but it misses real disease cases. In healthcare, a false negative means a patient with heart disease walks away without treatment.

In this step, you will use GridSearchCV to systematically search for the best Random Forest configuration. You will compare it against the baseline, prove it reduces missed diagnoses, and export the final model for deployment in Project 2.

In this step, get ready to:
  • Tune a Random Forest classifier with GridSearchCV to optimize ROC-AUC.
  • Compare the tuned model against the baseline using ROC curves and confusion matrices.
  • Export the final model and scaler to Google Drive as .joblib files.
Run hyperparameter tuning with GridSearchCV

Instead of guessing which Random Forest settings work best, GridSearchCV tests every combination from a parameter grid and picks the winner using cross-validation.

  • Create a new code cell in your Colab notebook (below your baseline confusion matrix cell) and paste the following code:
# === CELL 14: Hyperparameter Tuning ===
param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [3, 5, 7, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=cv,
    scoring='roc_auc',
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train_scaled, y_train)

print(f"\nBest Parameters: {grid_search.best_params_}")
print(f"Best Cross-Validation ROC-AUC: {grid_search.best_score_:.4f}")

What does this code do?

The param_grid defines 108 combinations of hyperparameters to try (3 x 4 x 3 x 3). Each combination gets tested with 5-fold stratified cross-validation, so the grid search runs 540 model fits total.

Scoring on roc_auc means the search optimizes for the model's ability to rank patients correctly across all classification thresholds. This is more informative than accuracy alone for medical prediction.

  • Run the cell. This takes about 1-2 minutes on Colab's free tier.
  • You should see progress output ("Fitting 5 folds for each of 108 candidates...") followed by the best parameters and best cross-validation ROC-AUC score.

Cell taking too long or crashing?

  • Make sure you ran all previous cells in order. If your Colab session disconnected, re-run from Cell 1.
  • Check that X_train_scaled exists by running X_train_scaled.shape in a temporary cell. You should see (242, 13).
  • If the runtime crashed, click **Runtime** then **Restart and run all** to re-execute from scratch.

Why is my GridSearchCV failing or timing out?

Evaluate the tuned model and compare against the baseline

Now you will see whether the tuned Random Forest actually improves on the baseline. You will evaluate its metrics, visualize its confusion matrix, compare ROC curves, and check which features matter most.

  • Create a new cell and paste this code to generate predictions and print metrics:
# === CELL 15: Final Model Evaluation ===
best_model = grid_search.best_estimator_

y_pred_best = best_model.predict(X_test_scaled)
y_prob_best = best_model.predict_proba(X_test_scaled)[:, 1]

print("=== Tuned Random Forest (Final Model) ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_best):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_best)}")

What does this code do?

The best_estimator_ attribute gives you the actual trained model with the best hyperparameters found by the grid search. You then generate predictions and probability scores on the held-out test set to measure real performance.

  • Run the cell. You should see accuracy, ROC-AUC, and a full classification report printed.
  • Compare the ROC-AUC to your baseline from Step 4. The tuned model's score should be higher.
  • Create the next cell and paste this code to plot the tuned model's confusion matrix:
# === CELL 16: Final Confusion Matrix ===
cm_best = confusion_matrix(y_test, y_pred_best)
plt.figure(figsize=(6, 5))
sns.heatmap(cm_best, annot=True, fmt='d', cmap='Greens',
            xticklabels=['No Disease', 'Disease'],
            yticklabels=['No Disease', 'Disease'])
plt.title('Tuned Random Forest - Confusion Matrix')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()

print(f"\nFalse Negatives (missed disease cases): {cm_best[1][0]}")
print(f"Baseline false negatives were: {cm_lr[1][0]}")

What does this code do?

This plots the confusion matrix using a green color map (to distinguish it from the blue baseline). The printed comparison at the end shows you exactly how many fewer disease cases the tuned model misses compared to the Logistic Regression baseline.

  • Run the cell. You should see a green heatmap and a printed comparison showing fewer false negatives than the baseline.

Before you run the next cell, think about this: if you plot both models' ROC curves on the same chart, which curve do you expect to sit higher?

  • Create the next cell and paste this code to plot both ROC curves side by side:
# === CELL 17: ROC Curve Comparison ===
plt.figure(figsize=(8, 6))

fpr_lr, tpr_lr, _ = roc_curve(y_test, y_prob_lr)
fpr_best, tpr_best, _ = roc_curve(y_test, y_prob_best)

plt.plot(fpr_lr, tpr_lr, label=f'Logistic Regression (AUC={roc_auc_score(y_test, y_prob_lr):.3f})')
plt.plot(fpr_best, tpr_best, label=f'Tuned Random Forest (AUC={roc_auc_score(y_test, y_prob_best):.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Chance')

plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

What does this code do?

An ROC curve plots how well a model separates positive and negative cases at every possible threshold. A curve that hugs the top-left corner is better. The dashed diagonal line represents random chance (AUC = 0.5).

By plotting both models on the same chart, you get visual proof of which model ranks patients more accurately. The legend shows the exact AUC for each.

  • Run the cell. You should see the tuned Random Forest's curve sitting above the Logistic Regression curve, confirming a higher AUC.
  • Create the next cell and paste this code to visualize which features the model relies on most:
# === CELL 18: Feature Importance ===
feature_importance = pd.DataFrame({
    'feature': X.columns,
    'importance': best_model.feature_importances_
}).sort_values('importance', ascending=False)

plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=feature_importance)
plt.title('Feature Importance - Tuned Random Forest')
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.tight_layout()
plt.show()

print("Top 5 most important features:")
print(feature_importance.head().to_string(index=False))

What does this code do?

Random Forest models track how much each feature contributes to reducing prediction error across all trees. The bar chart ranks features by importance, so you can see which clinical measurements (like max heart rate or chest pain type) drive predictions the most.

  • Run the cell. You should see a horizontal bar chart with the most important features at the top, plus a printed list of the top 5.

Charts not displaying?

  • Make sure you ran the Cell 15 code first. Cells 16-18 depend on best_model, y_pred_best, and y_prob_best existing.
  • If you see a NameError, the variable was not created. Re-run the cells above in order.

Why are my evaluation charts not showing in Colab?

Export the model and scaler to Google Drive

Your tuned model exists only in memory right now. If the Colab session disconnects, it disappears. You need to persist both the model and the scaler as files that Project 2 (the FastAPI API) can load.

  • Create a new cell and paste this code to save both artifacts to Google Drive:
# === CELL 19: Export Model to Google Drive ===
model_path = '/content/drive/MyDrive/heart_disease_model.joblib'
scaler_path = '/content/drive/MyDrive/heart_disease_scaler.joblib'

dump(best_model, model_path)
dump(scaler, scaler_path)

print(f"Model saved to: {model_path}")
print(f"Scaler saved to: {scaler_path}")
print("\nThese files are ready for Project 2 (FastAPI deployment)!")

Why save the scaler too?

The model was trained on scaled data. When new patient data arrives in production, it needs to be scaled the same way before prediction. Saving the fitted StandardScaler ensures the API in Project 2 transforms incoming data identically to how training data was transformed.

  • Run the cell. You should see both file paths printed, confirming the model and scaler are saved to your Google Drive.

Before you run the final test, do you expect the saved model to produce the same prediction as the in-memory model, or could something go wrong during export?

  • Create a final cell and paste this code to verify the exported model works:
# === CELL 20: Quick Prediction Test ===
sample_patient = X_test_scaled[0].reshape(1, -1)
prediction = best_model.predict(sample_patient)
probability = best_model.predict_proba(sample_patient)

print(f"Sample prediction: {'Heart Disease' if prediction[0] == 1 else 'No Heart Disease'}")
print(f"Confidence: {probability[0][prediction[0]]:.2%}")
print(f"Actual label: {'Heart Disease' if y_test.iloc[0] == 1 else 'No Heart Disease'}")

What does this code do?

This takes the first patient from the test set, runs it through the model, and prints the prediction alongside the actual diagnosis. The confidence percentage shows how certain the model is. If the prediction matches the actual label, your model is working correctly on this sample.

  • Run the cell. You should see a prediction (Heart Disease or No Heart Disease), a confidence percentage, and the actual label for comparison.

Seeing a FileNotFoundError on export?

  • Make sure Google Drive is still mounted. Run !ls /content/drive/MyDrive/ to confirm. If it errors, re-run your Cell 1 (drive mount) and try again.
  • If you see a permissions error, check that your Google account has write access to the Drive folder.

Why is my model export to Google Drive failing?

✔️ Awesome, I've got everything!

Great. Make sure your notebook is saved (Colab auto-saves, but you can press **Ctrl+S** to be sure).

ⓧ I'd like to double check the full code

Your notebook now has 20 cells. Scroll through and confirm these cells are present in order:

  • Cells 1-13: Drive mount, imports, data loading, EDA, preprocessing, and baseline (from previous steps).
  • Cell 14: Hyperparameter Tuning with GridSearchCV and the parameter grid.
  • Cell 15: Final model evaluation printing accuracy, ROC-AUC, and classification report.
  • Cell 16: Final confusion matrix (green heatmap) with false negatives comparison.
  • Cell 17: ROC curve comparison plotting both models on one chart.
  • Cell 18: Feature importance bar chart and top 5 printout.
  • Cell 19: Export model and scaler to Google Drive using dump().
  • Cell 20: Quick prediction test on one sample patient.

Your tuned Random Forest is now saved to Google Drive, ready to be loaded by a FastAPI service in Project 2. You have visual proof (ROC curves, confusion matrices, feature importance) that systematic tuning beat the baseline and reduced missed disease cases.

Secret mission

Add SHAP Explainability

Your model can predict heart disease, but can it explain why? In this bonus challenge, you'll add SHAP (SHapley Additive exPlanations) to your pipeline so that every prediction comes with a transparent breakdown of which features drove the decision and by how much.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on Google Colab's free tier with files stored on Google Drive, so there are no ongoing costs.

Resources you used:

  • Google Colab notebook (Heart_Disease_ML_Pipeline).
  • Google Drive files: heart_cleveland_upload.csv, heart_disease_model.joblib, heart_disease_scaler.joblib.
  • Kaggle account (free).
  • Google account (free).

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with Project 2 (FastAPI deployment) soon.

  • Your Google Colab notebook stays accessible from colab.research.google.com.
  • Your .joblib model files remain on Google Drive, ready for Project 2.
  • The dataset CSV stays on Google Drive for future reference.
  • Your Kaggle and Google accounts remain active at no cost.

Pause - I'll come back to this later

Since this project uses Google Colab (no running servers), there is nothing to pause. Your notebook and Drive files persist automatically with no cost.

  • Your Colab runtime disconnects automatically after inactivity. No manual shutdown needed.
  • All files on Google Drive remain stored at no charge (within the free 15 GB quota).
  • When you return, open the notebook from Colab and re-run cells from the top to restore your variables.

Delete - I don't want to use this again

Remove all project resources from Google Drive and Colab. Only do this if you are certain you do not need the trained model for Project 2.

  • Delete Google Drive files: open Google Drive, locate heart_cleveland_upload.csv, heart_disease_model.joblib, and heart_disease_scaler.joblib. Right-click each file and select Move to trash.
  • Empty the trash: in Google Drive, click Trash in the left sidebar, then click Empty trash to permanently remove the files.
  • Delete the Colab notebook: go to colab.research.google.com, find Heart_Disease_ML_Pipeline in your recent notebooks, right-click it, and select Remove.

That's a Wrap!

That's a Wrap!

Nice work! You've built a complete machine learning pipeline that takes raw patient clinical data and produces a trained, tuned heart disease classifier ready for deployment.

You've learned how to:

  • Built a complete ML pipeline in Google Colab from raw Kaggle clinical data to a trained heart disease classifier, including exploratory data analysis with correlation heatmaps and feature distribution charts.
  • Tuned a Random Forest with GridSearchCV and cross-validation that outperforms the Logistic Regression baseline on ROC-AUC and reduces dangerous false negatives (missed disease cases).
  • Exported the final model and scaler as .joblib files to Google Drive, ready for Project 2 where you'll serve predictions via a FastAPI API.
  • Secret Mission: Added SHAP explainability to reveal which clinical features drive each individual prediction, making your model interpretable for clinical review.

Ready to quiz yourself?