Heart Disease Prediction ML Pipeline

Build a scikit-learn ML pipeline to predict heart disease from clinical data.

Introduction

30 Second Summary

Heart disease is the leading cause of death worldwide. Doctors assess risk using patient data like age, cholesterol, and blood pressure, but machine learning can spot patterns in this data faster and more consistently than manual review.

In this project, you will build a complete machine learning pipeline that predicts whether a patient has heart disease based on 13 clinical features. You'll load real medical data from Kaggle, clean it, train multiple models, and export the best one as a deployment-ready file.

What You'll Build

A Python-based ML pipeline in Google Colab that loads a real heart disease dataset, performs exploratory data analysis with visualizations, trains and compares classifiers using scikit-learn Pipelines, and exports the winning model.

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

  • A trained heart disease prediction model exported as a .joblib file ready for deployment.
  • A complete Google Colab notebook demonstrating a professional ML pipeline from raw data to evaluation.
  • Hands-on experience with scikit-learn Pipelines, ColumnTransformer, and model comparison techniques used in industry.
  • Secret Mission: Add hyperparameter tuning with GridSearchCV to squeeze more performance from your model.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, loops). No machine learning experience is required. All work happens in Google Colab, so no local Python setup is needed.

Set Up Kaggle and Download the Dataset

Every machine learning project starts with data. Your model can only learn patterns if you feed it real-world examples to study.

In this step, you will create a free Kaggle account, generate an API token, and use it to download a real heart disease dataset directly into your Google Colab notebook.

In this step, get ready to:
  • Create a Kaggle account and generate an API token.
  • Configure the Kaggle API inside Google Colab.
  • Download the heart disease dataset.
Create a Kaggle account and generate your API token

Kaggle is a platform where data scientists share datasets and compete in machine learning challenges. It also provides an API that lets you download datasets directly from code.

  • Go to kaggle.com and sign up for a free account (using your Google account is the fastest option).
  • After signing in, click your profile icon in the top-right corner.
  • Click Settings.
  • Scroll down to the API section.
  • Click Create New Token.
  • A file called kaggle.json downloads to your computer. Keep this file safe.

What is kaggle.json?

This file contains your Kaggle username and a secret API key. It lets your code authenticate with Kaggle's servers to download datasets without needing you to sign in manually every time.

Treat it like a password. Do not share it publicly or commit it to version control.

Configure the Kaggle API in Google Colab

Now you need to upload your API token to Colab so that the Kaggle CLI can authenticate when downloading data.

  • In Google Colab, click + New notebook to create a fresh notebook.
  • In the first code cell, upload your token and configure the API by running this code:
# Cell 1: Setup Kaggle API
from google.colab import files
files.upload()  # Upload kaggle.json

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

What does this code do?

  • files.upload() opens a file picker in your browser so you can select kaggle.json from your computer.
  • !mkdir -p ~/.kaggle creates the hidden folder where Kaggle looks for credentials.
  • !cp kaggle.json ~/.kaggle/ copies the uploaded file into that folder.
  • !chmod 600 ~/.kaggle/kaggle.json restricts read access to your user only, which Kaggle requires for security.
  • Run the cell by pressing Shift+Enter.
  • A file picker dialog appears. Select the kaggle.json file you downloaded earlier, then click Open.
  • You should see a progress bar followed by a confirmation that kaggle.json was uploaded successfully.

Don't see the file picker?

  • Make sure you ran the cell (Shift+Enter), not just clicked into it.
  • Some browsers block pop-ups. If the picker does not appear, check for a blocked pop-up notification in your address bar and allow it.
  • Confirm you are selecting the correct kaggle.json file. It is usually in your Downloads folder.

Why can't I upload my kaggle.json file?

Download the heart disease dataset

With authentication configured, you can now pull the dataset directly from Kaggle into your Colab environment.

  • Create a new code cell below the first one by clicking + Code.
  • Download and verify the dataset by running this code:
# Cell 2: Download dataset
!kaggle datasets download -d johnsmith88/heart-disease-dataset --unzip -p /content/data

import os
print(os.listdir('/content/data'))

What does this code do?

  • The kaggle datasets download command fetches the dataset from Kaggle's servers.
  • --unzip automatically extracts the downloaded ZIP archive.
  • -p /content/data saves the files into a /content/data folder inside Colab.
  • os.listdir('/content/data') prints the contents of that folder so you can confirm the download worked.
  • Run the cell. You should see ['heart.csv'] printed in the output.

Seeing a 403 or authentication error?

  • Make sure you ran Cell 1 first and the upload completed successfully.
  • Double-check that the file you uploaded is named exactly kaggle.json. Some browsers rename duplicate downloads (e.g. kaggle (1).json).
  • Try regenerating your token on Kaggle (Settings > API > Create New Token) and re-uploading the new file.

Why is my Kaggle dataset download failing?

✔️ Awesome, I've got everything!

Great. Double-check that your notebook is saved in Google Colab (it auto-saves, but you can press Ctrl+S to be sure).

ⓧ I'd like to double check the full code

Here is the complete code for both cells so far:

# Cell 1: Setup Kaggle API
from google.colab import files
files.upload()  # Upload kaggle.json

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

# Cell 2: Download dataset
!kaggle datasets download -d johnsmith88/heart-disease-dataset --unzip -p /content/data

import os
print(os.listdir('/content/data'))

Your data is ready. Next up, you will load this dataset into a DataFrame and explore its structure with visualizations.

Load and Explore the Data

Your dataset is downloaded and ready to go. Now it's time to look inside it and understand what you're working with.

Before training any model, data scientists perform Exploratory Data Analysis (EDA). EDA reveals the shape of your data, highlights relationships between features, and uncovers quality issues like duplicates or missing values.

In this step, get ready to:
  • Load the dataset into a pandas DataFrame and inspect its structure.
  • Visualize feature distributions and correlations.
  • Identify duplicate rows that need cleaning.
Load the dataset and inspect its structure

The first thing to do with any new dataset is load it into memory and check its dimensions. This tells you how many patients (rows) and features (columns) you have to work with.

  • In a new cell in your Colab notebook, import your data science libraries and load the CSV by running this code:
# Cell 3: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv('/content/data/heart.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names: {list(df.columns)}")
df.head()

What does this code do?

This cell imports four essential libraries. pandas handles tabular data. NumPy provides math operations. Matplotlib and Seaborn create visualizations.

It then loads heart.csv into a DataFrame called df and prints the shape (rows, columns), column names, and the first 5 rows so you can see what the data looks like.

  • Run the cell. You should see Dataset shape: (1025, 14) printed, followed by a list of 14 column names and a table showing the first 5 rows of patient data.

Seeing a FileNotFoundError?

Make sure you ran the Kaggle download cell from Step 1 in this same notebook session. If you restarted the runtime, re-run all previous cells first.

help me fix a FileNotFoundError when loading heart.csv in Google Colab

  • Now check the data types and look for missing values by running this in a new cell:
# Cell 4: Data info and statistics
df.info()
df.describe()
  • Run the cell. df.info() prints each column's data type and non-null count. df.describe() shows statistical summaries like mean, min, and max for each numeric column.
  • Confirm that all 14 columns show 1025 non-null values. This means the dataset has no missing values.
Visualize distributions and relationships

Numbers alone don't tell the full story. Visualizations help you spot patterns like class imbalance or strongly correlated features at a glance.

  • Plot the target variable distribution to see how many patients have heart disease versus those who don't. Run this in a new cell:
# Cell 5: Target distribution
sns.countplot(x='target', data=df)
plt.title('Heart Disease Distribution (1=Disease, 0=No Disease)')
plt.show()

What does this chart show?

The target column is what your model will predict. A value of 1 means the patient has heart disease. A value of 0 means they don't.

This countplot shows how many patients fall into each category. A roughly balanced split means the model won't be biased toward one class.

  • Run the cell. You should see a bar chart with two bars. One for class 0 (no disease) and one for class 1 (disease).

Not seeing a chart?

Make sure you ran Cell 3 first so that sns and plt are imported. Also check that you included plt.show() at the end of the cell.

help me display a seaborn countplot in Google Colab

  • Now create a correlation heatmap to see how features relate to each other. Run this in a new cell:
# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Feature Correlation Heatmap')
plt.tight_layout()
plt.show()

What is a correlation heatmap?

A correlation heatmap shows how strongly each pair of features moves together. Values range from -1 (perfect inverse relationship) to +1 (perfect positive relationship).

Features with strong correlations to target are likely good predictors of heart disease. Features correlated with each other might be redundant.

  • Run the cell. You should see a large colored grid with numbers in each square. Red squares indicate positive correlation. Blue squares indicate negative correlation.
Check for duplicates

Duplicate rows can inflate your model's confidence by letting it see the same patient multiple times during training. This is a critical data quality check before any model training.

  • Count the number of duplicate rows in the dataset by running this in a new cell:
# Cell 7: Check for duplicates
duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {duplicates}")
print(f"Unique rows: {df.shape[0] - duplicates}")

What does this code do?

The df.duplicated() method marks each row as True if it is an exact copy of an earlier row. Calling .sum() counts how many duplicates exist.

This dataset was inflated from 302 original patient records to 1,025 rows by repeating records. You will remove these duplicates in the next step to avoid training on repeated data.

  • Run the cell. You should see a significant number of duplicates printed (over 700 rows are copies).

Seeing 0 duplicates?

Double-check that you loaded heart.csv from the correct path: /content/data/heart.csv. If you loaded a different file, the duplicate count will differ.

help me check why my duplicate count is zero for the heart disease dataset

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the full notebook code up to this point. Compare your cells against this:

# Cell 1: Setup Kaggle API
from google.colab import files
files.upload()  # Upload kaggle.json

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

# Cell 2: Download dataset
!kaggle datasets download -d johnsmith88/heart-disease-dataset --unzip -p /content/data

import os
print(os.listdir('/content/data'))

# Cell 3: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv('/content/data/heart.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names: {list(df.columns)}")
df.head()

# Cell 4: Data info and statistics
df.info()
df.describe()

# Cell 5: Target distribution
sns.countplot(x='target', data=df)
plt.title('Heart Disease Distribution (1=Disease, 0=No Disease)')
plt.show()

# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Feature Correlation Heatmap')
plt.tight_layout()
plt.show()

# Cell 7: Check for duplicates
duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {duplicates}")
print(f"Unique rows: {df.shape[0] - duplicates}")

You now have a clear picture of what's inside this dataset. Next up, you'll clean those duplicates out and prepare the features for model training.

Clean the Data and Prepare Features

In the previous step, you discovered that the dataset has a significant number of duplicate rows hiding inside it. Raw data almost never arrives ready for a machine learning model.

Before training a model, you need to clean the data and split it into the right format. In this step, you will remove duplicates, separate features from the target, and create training and test sets.

In this step, get ready to:
  • Remove duplicate rows and separate features from the target variable.
  • Define which features are categorical and which are numeric.
  • Split the data into training and test sets.
Remove duplicates and separate features from the target

Duplicate rows would let the model memorize repeated examples instead of learning general patterns. Removing them gives you a clean dataset to work with.

  • Create a new cell in your notebook. Drop duplicates and reset the index by running this code:
# Cell 8: Remove duplicates
df_clean = df.drop_duplicates().reset_index(drop=True)
print(f"Shape after removing duplicates: {df_clean.shape}")

What does this code do?

The drop_duplicates() method removes every row that is an exact copy of another row.

The reset_index(drop=True) call renumbers the rows from 0 so there are no gaps in the index after rows are removed.

  • Run the cell. You should see the shape drop from 1025 rows down to approximately 302 rows.

Now you need to separate the input features from the target variable. The model learns from the features and tries to predict the target.

  • In a new cell, separate features and target by running this code:
# Cell 9: Separate features and target
X = df_clean.drop('target', axis=1)
y = df_clean['target']
print(f"Features shape: {X.shape}")
print(f"Target distribution:\n{y.value_counts()}")

What does this code do?

The variable X holds all 13 input features (everything except the target column). The variable y holds just the target column, which is what the model will learn to predict.

The value_counts() output shows how many patients have heart disease (1) versus no disease (0). This tells you whether the classes are balanced.

  • Run the cell. You should see that X has 13 columns, and y shows counts for both 0 and 1.

Seeing a KeyError?

Make sure you ran Cell 8 first so df_clean exists. If you accidentally typed df instead of df_clean, the code will still run but you will have duplicates in your data.

Help me debug a KeyError when separating features and target in my heart disease notebook.

Define numeric and categorical feature groups

Machine learning models process numbers and categories differently. Numeric features (like age or cholesterol) need scaling. Categorical features (like chest pain type) need encoding into numbers the model can read.

  • In a new cell, define which columns are categorical and which are numeric by running this code:
# Cell 10: Define feature groups
categorical_features = ['sex', 'cp', 'fbs', 'restecg', 'exang', 'slope', 'ca', 'thal']
numeric_features = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak']

print(f"Categorical features ({len(categorical_features)}): {categorical_features}")
print(f"Numeric features ({len(numeric_features)}): {numeric_features}")

Why split features into two groups?

Categorical features like cp (chest pain type) use numbers to represent categories (0, 1, 2, 3), not quantities. A model treating these as continuous numbers would assume 3 is "more" than 1, which is meaningless for categories.

Numeric features like age and chol represent real quantities. They need a different transformation (scaling) to bring them onto a similar range so one large-valued feature does not dominate.

  • Run the cell. You should see 8 categorical features and 5 numeric features printed.

Feature count does not add up to 13?

Double-check that every feature name is spelled exactly as it appears in the dataset. You can run list(X.columns) to see all column names.

Help me check why my feature groups do not add up to 13.

Split into training and test sets

A model needs to be tested on data it has never seen during training. Splitting the data now ensures your evaluation later is honest and not inflated by memorization.

  • In a new cell, create the train/test split by running this code:
# Cell 11: Train/test split
from sklearn.model_selection import 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")

What does this code do?

  • train_test_split from scikit-learn randomly divides the data into two groups.
  • test_size=0.2 puts 20% of rows aside for testing and keeps 80% for training.
  • random_state=42 locks the random shuffle so you get the same split every time you run the cell.
  • stratify=y ensures the training and test sets have the same ratio of disease vs. no-disease patients as the full dataset.
  • Run the cell. You should see approximately 241 training samples and 61 test samples.

Seeing a NameError for X or y?

This means Cell 9 did not run successfully. Go back and run Cell 8 and Cell 9 in order before running Cell 11.

Help me fix a NameError when running train_test_split.

✔️ Awesome, I've got everything!

Great. Double check you have run all cells from Cell 8 through Cell 11 in order.

ⓧ I'd like to double check the full code

Here is the complete code for this step (Cells 8 through 11). Compare it against your notebook:

# Cell 8: Remove duplicates
df_clean = df.drop_duplicates().reset_index(drop=True)
print(f"Shape after removing duplicates: {df_clean.shape}")

# Cell 9: Separate features and target
X = df_clean.drop('target', axis=1)
y = df_clean['target']
print(f"Features shape: {X.shape}")
print(f"Target distribution:\n{y.value_counts()}")

# Cell 10: Define feature groups
categorical_features = ['sex', 'cp', 'fbs', 'restecg', 'exang', 'slope', 'ca', 'thal']
numeric_features = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak']

print(f"Categorical features ({len(categorical_features)}): {categorical_features}")
print(f"Numeric features ({len(numeric_features)}): {numeric_features}")

# Cell 11: Train/test split
from sklearn.model_selection import 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")

Your data is clean, organized, and split into training and test sets. Next up, you will build a preprocessing pipeline and train your first machine learning models.

Build and Train the ML Pipeline

Your data is clean, split, and ready to learn from. Now comes the exciting part: teaching a machine to predict heart disease.

In this step, you will build a scikit-learn Pipeline that handles preprocessing and model training in one seamless object. You will train two different classifiers and compare their accuracy to find the best performer.

In this step, get ready to:
  • Create preprocessing pipelines with ColumnTransformer for mixed feature types.
  • Train a Logistic Regression model as a baseline.
  • Train a Random Forest model and compare both models' accuracy.
Create preprocessing pipelines with ColumnTransformer

Your dataset has two types of features: numeric ones like age and cholesterol that need scaling, and categorical ones like chest pain type that need encoding. A ColumnTransformer lets you apply different transformations to each group in a single step.

  • Create a new code cell in your notebook and build the preprocessing pipeline by pasting this code:
# Cell 12: Build preprocessing pipeline
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer

numeric_transformer = Pipeline(steps=[
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline(steps=[
    ('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ]
)

What does this code do?

This cell defines how each feature type gets prepared for the model.

  • numeric_transformer applies StandardScaler to center numeric features around zero with unit variance. This prevents features with large ranges (like cholesterol) from dominating the model.
  • categorical_transformer applies OneHotEncoder to convert categorical values into binary columns. The handle_unknown='ignore' flag ensures the model won't crash if it encounters a new category at prediction time.
  • preprocessor combines both transformers using ColumnTransformer, routing each feature to its correct transformer automatically.

Why use a Pipeline instead of manual preprocessing?

Manual preprocessing (calling .fit_transform() separately) is error-prone. It is easy to accidentally fit your scaler on the test set, which leaks future information into training. A Pipeline guarantees that preprocessing is always fit on training data only.

Pipelines also simplify deployment. When you export the model later, the preprocessing logic travels with it. No separate scripts needed.

  • Run the cell by pressing Shift+Enter.
  • The cell completes without output. This is expected because you are defining the transformers, not running them yet.

Seeing an ImportError?

  • Make sure the import lines are spelled exactly as shown: sklearn.pipeline, sklearn.preprocessing, and sklearn.compose.
  • Confirm you are running this in Google Colab, which has scikit-learn pre-installed.

Still stuck? Help me fix my scikit-learn import error in Google Colab

Train a Logistic Regression model

Logistic Regression is the simplest classifier for binary problems. It serves as your baseline: any fancier model should beat this score to justify its complexity.

  • Create a new code cell and train your first model by pasting this code:
# Cell 13: Train Logistic Regression
from sklearn.linear_model import LogisticRegression

lr_pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', LogisticRegression(max_iter=1000, random_state=42))
])

lr_pipeline.fit(X_train, y_train)
lr_score = lr_pipeline.score(X_test, y_test)
print(f"Logistic Regression Accuracy: {lr_score:.4f}")

What does this code do?

  • lr_pipeline chains your preprocessor with a LogisticRegression classifier into a single object. When you call .fit(), it preprocesses the training data and trains the model in one call.
  • max_iter=1000 gives the optimizer enough iterations to converge on this dataset.
  • .score() returns the accuracy on the test set: the proportion of patients correctly classified.
  • Run the cell. You should see an accuracy score printed, typically around 0.82 to 0.87.

Seeing a NameError for X_train or preprocessor?

  • Make sure you ran all previous cells in order. Google Colab cells must be executed sequentially for variables to persist.
  • Click Runtime in the top menu, then Run all to re-execute every cell from the top.

Need more help? Help me fix a NameError in my Colab notebook

Train a Random Forest model and compare

A Random Forest is an ensemble method that trains many decision trees and averages their predictions. It typically outperforms Logistic Regression on datasets with non-linear relationships.

  • Create a new code cell and train the Random Forest model by pasting this code:
# Cell 14: Train Random Forest
from sklearn.ensemble import RandomForestClassifier

rf_pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

rf_pipeline.fit(X_train, y_train)
rf_score = rf_pipeline.score(X_test, y_test)
print(f"Random Forest Accuracy: {rf_score:.4f}")
print(f"\nModel Comparison:")
print(f"  Logistic Regression: {lr_score:.4f}")
print(f"  Random Forest:       {rf_score:.4f}")

What does this code do?

  • rf_pipeline uses the same preprocessor but swaps in a RandomForestClassifier with 100 decision trees (n_estimators=100).
  • The comparison printout shows both models' accuracy side by side. The higher number wins.
  • random_state=42 ensures reproducibility. Anyone running this notebook gets the same results.
  • Run the cell. You should see both accuracy scores printed with a comparison showing which model performed better.

Random Forest score seems too low?

  • Double-check that you removed duplicates in Cell 8. Training on duplicated data can inflate or distort results.
  • Verify that your categorical_features and numeric_features lists match the ones from Cell 10 exactly. A missing feature means the model is missing information.

Need help? Help me understand why my Random Forest accuracy is low

✔️ Awesome, I've got everything!

Great. Double check you have both accuracy scores printed and the model comparison visible in your notebook output.

ⓧ I'd like to double check the full code

Here is the complete code for all cells up to this point:

# Cell 1: Setup Kaggle API
from google.colab import files
files.upload()  # Upload kaggle.json

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

# Cell 2: Download dataset
!kaggle datasets download -d johnsmith88/heart-disease-dataset --unzip -p /content/data

import os
print(os.listdir('/content/data'))

# Cell 3: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv('/content/data/heart.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names: {list(df.columns)}")
df.head()

# Cell 4: Data info and statistics
df.info()
df.describe()

# Cell 5: Target distribution
sns.countplot(x='target', data=df)
plt.title('Heart Disease Distribution (1=Disease, 0=No Disease)')
plt.show()

# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Feature Correlation Heatmap')
plt.tight_layout()
plt.show()

# Cell 7: Check for duplicates
duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {duplicates}")
print(f"Unique rows: {df.shape[0] - duplicates}")

# Cell 8: Remove duplicates
df_clean = df.drop_duplicates().reset_index(drop=True)
print(f"Shape after removing duplicates: {df_clean.shape}")

# Cell 9: Separate features and target
X = df_clean.drop('target', axis=1)
y = df_clean['target']
print(f"Features shape: {X.shape}")
print(f"Target distribution:\n{y.value_counts()}")

# Cell 10: Define feature groups
categorical_features = ['sex', 'cp', 'fbs', 'restecg', 'exang', 'slope', 'ca', 'thal']
numeric_features = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak']

print(f"Categorical features ({len(categorical_features)}): {categorical_features}")
print(f"Numeric features ({len(numeric_features)}): {numeric_features}")

# Cell 11: Train/test split
from sklearn.model_selection import 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")

# Cell 12: Build preprocessing pipeline
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer

numeric_transformer = Pipeline(steps=[
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline(steps=[
    ('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ]
)

# Cell 13: Train Logistic Regression
from sklearn.linear_model import LogisticRegression

lr_pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', LogisticRegression(max_iter=1000, random_state=42))
])

lr_pipeline.fit(X_train, y_train)
lr_score = lr_pipeline.score(X_test, y_test)
print(f"Logistic Regression Accuracy: {lr_score:.4f}")

# Cell 14: Train Random Forest
from sklearn.ensemble import RandomForestClassifier

rf_pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

rf_pipeline.fit(X_train, y_train)
rf_score = rf_pipeline.score(X_test, y_test)
print(f"Random Forest Accuracy: {rf_score:.4f}")
print(f"\nModel Comparison:")
print(f"  Logistic Regression: {lr_score:.4f}")
print(f"  Random Forest:       {rf_score:.4f}")

You now have two trained models with concrete accuracy scores. Next up, you will evaluate the best performer with a confusion matrix, ROC curve, and classification report, then export it as a deployable file.

Evaluate the Best Model and Export

You now have two trained models with accuracy scores you can compare. Training models is only half the story, though.

In this step, you will evaluate the winning model with professional metrics that go beyond raw accuracy. You will also export the trained pipeline as a file that future projects can load and use for predictions.

In this step, get ready to:
  • Generate a classification report with precision, recall, and F1-score.
  • Visualize the confusion matrix and ROC curve.
  • Export the best model as a .joblib file for deployment.
Generate detailed evaluation metrics

Accuracy tells you how often the model is right overall. But in medical prediction, you need to know more. Did the model miss patients who actually have heart disease? Did it falsely alarm healthy patients?

  • Create a new code cell in your notebook and add the following code to select the best model and generate a full classification report:
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, roc_curve

# Select whichever model scored higher on the test set
best_pipeline = rf_pipeline if rf_score > lr_score else lr_pipeline
best_name = "Random Forest" if rf_score > lr_score else "Logistic Regression"
print(f"Best model: {best_name}\n")

# Generate predictions and print detailed metrics
y_pred = best_pipeline.predict(X_test)
print(classification_report(y_test, y_pred, target_names=['No Disease', 'Heart Disease']))

What does this code do?

The first two lines automatically pick the model with the higher test accuracy. This means your evaluation always targets the winner.

The classification_report function prints precision (how many positive predictions were correct), recall (how many actual positives the model caught), and F1-score (the balance between the two) for each class.

  • Run the cell. You should see a table with precision, recall, and F1-score columns for both "No Disease" and "Heart Disease" classes.

Seeing a NameError?

Make sure you ran all previous cells in order. The variables rf_pipeline, lr_pipeline, rf_score, and lr_score must exist from Step 4.

help me fix a NameError in my model evaluation cell

Visualize the confusion matrix and ROC curve

Numbers in a table are precise, but visualizations reveal patterns at a glance. A confusion matrix shows exactly where the model confuses one class for another.

  • Create a new cell and plot the confusion matrix by adding this code:
# Build and display the confusion matrix as a heatmap
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['No Disease', 'Heart Disease'],
            yticklabels=['No Disease', 'Heart Disease'])
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title(f'Confusion Matrix - {best_name}')
plt.show()

How do I read a confusion matrix?

The top-left and bottom-right cells show correct predictions. The top-right cell shows healthy patients the model incorrectly flagged as having disease (false positives). The bottom-left cell shows disease patients the model missed (false negatives).

In medical contexts, false negatives are especially dangerous because a missed diagnosis delays treatment.

  • Run the cell. You should see a blue heatmap with numbers in each of the four quadrants.

Heatmap not displaying?

Make sure y_pred exists from the previous cell. If you restarted your runtime, rerun all cells from the top.

help me debug my confusion matrix plot

Next, the ROC curve plots how well your model separates the two classes across all possible decision thresholds. The AUC score summarizes this into a single number between 0 and 1.

  • Create a new cell and plot the ROC curve by adding this code:
# Calculate prediction probabilities and plot ROC curve
y_prob = best_pipeline.predict_proba(X_test)[:, 1]
fpr, tpr, _ = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)

plt.plot(fpr, tpr, label=f'{best_name} (AUC = {auc_score:.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')
plt.legend()
plt.show()

What does the ROC curve tell me?

The dashed diagonal line represents random guessing (AUC = 0.5). The further your model's curve bows toward the top-left corner, the better it separates the classes. An AUC of 1.0 means perfect separation.

For this dataset, an AUC above 0.85 indicates strong predictive power.

  • Run the cell. You should see a curved line above the dashed diagonal, with an AUC score displayed in the legend.

ROC curve looks flat or sits on the diagonal?

Check that best_pipeline was trained correctly in a previous cell. If the model was never fitted, predict_proba will produce meaningless values.

help me understand why my ROC curve looks wrong

Export the model with joblib

Your trained pipeline lives in Colab's memory right now. If the runtime disconnects, it disappears. Exporting it as a .joblib file lets you reload the exact same model later without retraining.

  • Create a new cell and save the trained pipeline plus a feature reference file by adding this code:
from joblib import dump
import json

# Save the trained pipeline to disk
dump(best_pipeline, '/content/heart_disease_model.joblib')
print("Model saved as heart_disease_model.joblib")

# Also save the feature names for the API
feature_info = {
    'numeric_features': numeric_features,
    'categorical_features': categorical_features,
    'all_features': list(X.columns)
}
with open('/content/feature_info.json', 'w') as f:
    json.dump(feature_info, f)
print("Feature info saved as feature_info.json")

Why save feature_info.json separately?

The .joblib file contains the trained model weights and preprocessing steps. But when you build an API in a future project, that API needs to know what features to expect from incoming requests.

The feature_info.json file records exactly which columns are numeric and which are categorical so your API can validate input data before passing it to the model.

  • Run the cell. You should see two confirmation messages printed.

Seeing an ImportError for joblib?

The joblib library is pre-installed in Google Colab. If you see an error, check that you typed from joblib import dump exactly as shown.

help me fix an ImportError for joblib in Colab

  • Create one more cell and download the exported files to your local machine by adding this code:
# Download the model and feature info to your computer
from google.colab import files
files.download('/content/heart_disease_model.joblib')
files.download('/content/feature_info.json')

What does files.download do?

This triggers your browser's file download dialog. Each file will appear in your computer's default Downloads folder. Keep these files safe. You will load them in future projects to serve predictions through a web API.

  • Run the cell. Your browser should prompt you to download heart_disease_model.joblib and feature_info.json.
  • Confirm both files appear in your Downloads folder.

Download not starting?

Some browsers block automatic downloads. Check for a blocked-download notification in your browser's address bar and allow it. You may also need to allow pop-ups for colab.research.google.com.

help me download files from Google Colab

✔️ Awesome, I've got everything!

Great. Confirm that both heart_disease_model.joblib and feature_info.json are saved in your Downloads folder.

ⓧ I'd like to double check the full code

Here is the complete code for all cells you added in this step:

# Cell 15: Evaluation metrics
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, roc_curve

best_pipeline = rf_pipeline if rf_score > lr_score else lr_pipeline
best_name = "Random Forest" if rf_score > lr_score else "Logistic Regression"
print(f"Best model: {best_name}\n")

y_pred = best_pipeline.predict(X_test)
print(classification_report(y_test, y_pred, target_names=['No Disease', 'Heart Disease']))

# Cell 16: Confusion matrix
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['No Disease', 'Heart Disease'],
            yticklabels=['No Disease', 'Heart Disease'])
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title(f'Confusion Matrix - {best_name}')
plt.show()

# Cell 17: ROC curve
y_prob = best_pipeline.predict_proba(X_test)[:, 1]
fpr, tpr, _ = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)

plt.plot(fpr, tpr, label=f'{best_name} (AUC = {auc_score:.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')
plt.legend()
plt.show()

# Cell 18: Export model
from joblib import dump
import json

dump(best_pipeline, '/content/heart_disease_model.joblib')
print("Model saved as heart_disease_model.joblib")

feature_info = {
    'numeric_features': numeric_features,
    'categorical_features': categorical_features,
    'all_features': list(X.columns)
}
with open('/content/feature_info.json', 'w') as f:
    json.dump(feature_info, f)
print("Feature info saved as feature_info.json")

# Cell 19: Download files
from google.colab import files
files.download('/content/heart_disease_model.joblib')
files.download('/content/feature_info.json')

Secret mission

Hyperparameter Tuning with GridSearchCV

Your model works, but is it the best it can be? In this bonus challenge, you will use GridSearchCV to systematically search for optimal hyperparameters. This is the technique that separates junior data scientists from senior ones in interviews.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab and on your local machine, so there are no ongoing costs. Decide whether to keep your resources available, pause them, or delete them entirely.

Resources you used:

  • Google Colab notebook (stored in Google Drive).
  • Kaggle account and API token (kaggle.json).
  • Local files: heart_disease_model.joblib, feature_info.json, and optionally heart_disease_model_tuned.joblib.

Keep everything running

No action needed. Choose this if you plan to continue with Projects 2 and 3 in this series, which use the exported model files.

  • Your Google Colab notebook stays in Google Drive and costs nothing to store.
  • Your Kaggle account remains active for downloading future datasets.
  • Your local .joblib and .json files are ready for the next project.
  • The Colab runtime disconnects automatically after inactivity. Files in /content/ are deleted when the runtime resets, but your notebook code is safe in Google Drive.

Pause - I'll come back to this later

Free up your Colab runtime while keeping everything you need to resume later.

  • In Google Colab, click Runtime in the top menu, then click Disconnect and delete runtime. This frees the cloud VM but keeps your notebook code in Google Drive.
  • Keep your local heart_disease_model.joblib and feature_info.json files. You will need them for Projects 2 and 3.
  • Keep your kaggle.json file so you do not have to regenerate a token next time.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the Google Colab notebook: open Google Drive, find the notebook in the Colab Notebooks folder, right-click it, and select Move to trash.
  • Delete your local project files. On macOS/Linux, open your terminal and run:
  • On Windows, open PowerShell and run:
rm ~/Downloads/heart_disease_model.joblib
rm ~/Downloads/feature_info.json
rm ~/Downloads/heart_disease_model_tuned.joblib
rm ~/Downloads/kaggle.json
Remove-Item ~\Downloads\heart_disease_model.joblib
Remove-Item ~\Downloads\feature_info.json
Remove-Item ~\Downloads\heart_disease_model_tuned.joblib
Remove-Item ~\Downloads\kaggle.json
  • Delete your Kaggle account: go to https://www.kaggle.com/account/delete and follow the prompts to confirm deletion.
  • Revoke your Kaggle API token: if you chose not to delete your account, go to your Kaggle Settings page, scroll to the API section, and click Expire Token to invalidate the old token.

Nice Work!

Nice Work!

You just built a complete machine learning pipeline that predicts heart disease from real patient data. From raw CSV to a deployment-ready model artifact, you executed every stage of the ML workflow that data science teams use in production.

You learned how to:

  • Build a professional scikit-learn Pipeline with ColumnTransformer to preprocess mixed numeric and categorical features without data leakage.
  • Train and compare Logistic Regression and Random Forest classifiers, evaluating them with a confusion matrix, classification report, and ROC-AUC curve.
  • Export a trained model as a .joblib file ready for deployment in a web application.
  • Secret Mission: Tuned hyperparameters with GridSearchCV to squeeze optimal performance from your model.

Ready to quiz yourself?