Predict Heart Disease with ML

Build a complete ML pipeline to classify heart disease risk from patient data.

Introduction

30 Second Summary

Heart disease is the leading cause of death worldwide. Doctors look for patterns in patient data to assess risk, but what if a computer could learn those patterns automatically?

In this project, you will build a machine learning pipeline in Python that predicts whether a patient has heart disease based on their medical measurements. You will load a real dataset, explore and clean it, train three different prediction models, and evaluate which one performs best.

What You'll Build

A complete machine learning pipeline in a Jupyter Notebook that trains and compares three classification algorithms on the UCI Cleveland Heart Disease dataset (303 patients, 13 features).

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

  • A Jupyter Notebook containing a full ML pipeline from data loading to model evaluation.
  • Three trained classification models (Logistic Regression, Random Forest, KNN) compared using proper evaluation metrics.
  • Data visualizations showing feature distributions, correlations, and model performance.
  • Secret Mission: Add cross-validation for more robust evaluation and tune your best model's hyperparameters.

Are there any prerequisites?

You only need basic programming knowledge (loops, functions, conditionals). No prior Python data science or machine learning experience is required.

Set Up Your Python ML Environment

Before you can train any machine learning models, you need a properly configured Python environment with the right libraries.

In this step, you will verify your Python version, create an isolated project folder with a virtual environment, and install all the data science packages you need.

In this step, get ready to:
  • Verify that Python 3.11 or newer is installed.
  • Create a project folder and virtual environment, then install ML packages.
  • Launch Jupyter Notebook and confirm all imports work.
Verify your Python version

The ML libraries you will install in this project require Python 3.11 or newer.

Why Python 3.11+?

The current versions of pandas and NumPy both require Python 3.11 as their minimum version.

If you run an older version, package installation will fail with confusing compatibility errors.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.11 or higher

You are good to go. Move on to the next substep.

ⓧ I see an older version

Your system has a Python version older than 3.11 installed. Some operating systems ship with an older Python by default, so the python3 command may resolve to the older version even after you install a newer one.

  • Download and install the latest Python from python.org/downloads.
  • After installing, close and reopen your terminal, then run the version check again.
  • If python3 --version still shows the old version on macOS, try python3.11 --version or python3.12 --version to confirm the newer version is available. Use that versioned command for the rest of the project.

ⓧ Command not found

Python is not installed on your system yet.

  • Download the latest Python from python.org/downloads.
  • Run the installer. On Windows, make sure to check the box that says Add python.exe to PATH before clicking Install.
  • Close and reopen your terminal after installation, then run the version check again.
Create a project folder and install packages

A virtual environment isolates your project's packages from the rest of your system. This way, the libraries you install here will not conflict with other Python projects on your machine.

  • Move to your Desktop by running this command:

macOS

cd ~/Desktop

Windows

cd ~/Desktop
  • Create your project folder and move into it by running these commands:

macOS

mkdir heart-disease-ml
cd heart-disease-ml

Windows

mkdir heart-disease-ml
cd heart-disease-ml
  • Confirm the folder was created by running:
ls

You should see an empty output since the folder is brand new.

  • Create a virtual environment by running this command:

macOS

python3 -m venv venv

Windows

python -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\Activate.ps1

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

PowerShell blocking the activation script?

Windows PowerShell may block script execution by default. Run this command first to allow it, then try the activation command again:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

help me activate my Python virtual environment on Windows

  • Install all required packages by running this command:
pip install notebook pandas numpy matplotlib scikit-learn

What are these packages?

  • notebook is Jupyter Notebook, the interactive coding environment where you will write and run your ML code cell by cell.
  • pandas loads, cleans, and manipulates tabular data (like spreadsheets).
  • numpy provides fast math operations on arrays of numbers.
  • matplotlib creates charts and visualizations.
  • scikit-learn is the machine learning library containing the classification algorithms you will train.

The installation may take a minute or two. When it finishes, you should see a Successfully installed message listing all the packages.

Installation errors?

If you see errors about incompatible versions, double check that your virtual environment is active (you should see (venv) in your prompt) and that your Python version is 3.11 or newer.

help me fix pip install errors for pandas numpy matplotlib scikit-learn

Launch Jupyter Notebook and verify imports

Jupyter Notebook lets you write and run Python code in small cells, which is perfect for building an ML pipeline step by step.

  • Launch Jupyter Notebook by running this command:
jupyter notebook

Your default web browser should open automatically with the Jupyter interface. You will see a file browser showing your empty heart-disease-ml folder.

Browser didn't open?

Look in your terminal for a URL that starts with http://localhost:8888. Copy and paste that full URL into your browser manually.

help me open Jupyter Notebook in my browser

  • In the Jupyter interface, click **New** in the top right corner.
  • Select Python 3 from the dropdown menu.
  • A new notebook opens in a new browser tab.
  • Click the title at the top (it will say Untitled) and rename it to heart_disease_prediction.

Now you will paste your first cell of code to verify that all packages are importable.

  • Click into the first empty cell and paste the following code:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print("All imports successful!")

What does this code do?

This cell imports every library you will use throughout the project. You are importing them all upfront so you can catch any installation issues now instead of mid-project.

  • pandas, numpy, and matplotlib handle data loading, math, and charts.
  • The sklearn imports bring in the three ML models (Logistic Regression, Random Forest, KNN), data splitting, feature scaling, and evaluation tools.
  • Run the cell by pressing Shift+Enter.

You should see All imports successful! printed below the cell.

Seeing a ModuleNotFoundError?

This means the Jupyter Notebook is using a different Python than the one where you installed the packages. Go back to your terminal, stop Jupyter with Ctrl+C, make sure (venv) appears in your prompt, and rerun jupyter notebook.

help me fix ModuleNotFoundError in Jupyter Notebook

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook.

ⓧ I'd like to double check the full code

Here is the complete code for Cell 1 of your notebook:

# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print("All imports successful!")

Your ML environment is ready to go. Next up, you will load a real heart disease dataset and explore what the data looks like.

Load and Explore the Dataset

Your Python ML environment is ready to go. Now it's time to meet the data you'll be training models on.

The UCI Cleveland Heart Disease dataset contains 303 patient records with 13 medical measurements. In this step, you'll load it into pandas, visualize patterns in the data, and clean up missing values so it's ready for model training.

In this step, get ready to:
  • Load the Heart Disease dataset from the UCI archive and inspect its structure.
  • Visualize feature distributions and correlations with matplotlib.
  • Clean missing values to prepare the data for model training.
Load the dataset and inspect its structure

The dataset lives in the UCI Machine Learning Repository as a raw CSV file with no column headers. You need to assign column names manually and tell pandas how to interpret missing values.

  • Create a new cell in your notebook (click the + button in the toolbar) and paste the following code to load the dataset:
# Cell 2: Load the dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
column_names = [
    "age", "sex", "cp", "trestbps", "chol", "fbs", "restecg",
    "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"
]
df = pd.read_csv(url, names=column_names, na_values="?")

print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
df.head()

What does this code do?

The url variable points to the raw dataset file hosted by the UCI archive. The column_names list provides human-readable headers since the file has none.

The na_values="?" parameter tells pandas to treat any ? character as a missing value (NaN) instead of a literal string. This saves you a manual find-and-replace step later.

  • Run the cell by pressing Shift+Enter.
  • You should see Dataset shape: (303, 14) followed by a table showing the first 5 rows of patient data.

Seeing a connection error?

The dataset loads from a live URL, so you need an active internet connection. If your network blocks the UCI archive, check your firewall or VPN settings.

If you see a URLError or timeout, try running the cell again after a few seconds.

Help me debug my dataset loading error.

  • Create another new cell and paste this code to explore the dataset's types and statistics:
# Cell 3: Basic exploration
print(df.info())
print(f"\nStatistical summary:")
df.describe()

What does this code do?

The .info() method shows each column's name, data type, and how many non-null values it has. This instantly reveals which columns have missing data.

The .describe() method shows statistical summaries (mean, standard deviation, min, max) for every numeric column. These help you spot unusual ranges or outliers.

  • Run the cell. You should see a full column listing with data types, followed by a table of statistics for all 14 columns.

The raw target column contains values from 0 to 4, where 0 means no disease and 1 through 4 represent increasing severity. Standard practice is to simplify this into a binary prediction: disease present or not.

  • Create a new cell and paste this code to binarize the target and check class balance:
# Cell 4: Check target distribution
# Convert target to binary: 0 = no disease, 1 = disease present
df["target"] = df["target"].apply(lambda x: 1 if x > 0 else 0)
print("Target distribution:")
print(df["target"].value_counts())
print(f"\nBalance: {df['target'].value_counts(normalize=True)}")

What does this code do?

The lambda x: 1 if x > 0 else 0 converts every target value greater than 0 into 1. This turns the 5-class problem into a binary classification: does the patient have heart disease (1) or not (0)?

The .value_counts(normalize=True) shows the percentage split between the two classes. A roughly even split (around 45/55) means the dataset is balanced, so accuracy will be a reasonable metric.

  • Run the cell. You should see the count and percentage for each class (0 and 1).

Seeing a KeyError for target?

This means Cell 2 didn't assign column names correctly. Go back to Cell 2, confirm the column_names list has exactly 14 entries, and re-run it before running Cell 4 again.

Help me fix my target column error.

Visualize feature distributions and correlations

Visualizations reveal patterns that raw numbers hide. Histograms show how each feature is distributed, and a correlation heatmap shows which features are related to the target.

  • Create a new cell and paste this code to generate histograms for all features:
# Cell 5: Visualize feature distributions
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
axes = axes.flatten()
for i, col in enumerate(df.columns):
    axes[i].hist(df[col].dropna(), bins=20, edgecolor="black", alpha=0.7)
    axes[i].set_title(col)
axes[-1].set_visible(False)
plt.tight_layout()
plt.savefig("feature_distributions.png", dpi=100, bbox_inches="tight")
plt.show()

What does this code do?

This creates a 3x5 grid of subplots. Each subplot shows a histogram for one column, revealing how values are spread across patients.

The axes[-1].set_visible(False) hides the 15th subplot since you only have 14 columns. The plt.savefig() call saves the figure as a PNG file in your project folder.

  • Run the cell. You should see a grid of 14 histograms showing distributions for each feature (age, cholesterol, max heart rate, etc.).
  • Confirm that feature_distributions.png was saved in your heart-disease-ml folder.

Histograms not appearing?

If the cell runs but shows no plot, make sure you ran Cell 1 first (it imports matplotlib.pyplot as plt). Jupyter needs the import before any plotting calls work.

Help me get my matplotlib plots to display in Jupyter.

  • Create a new cell and paste this code to generate a correlation heatmap:
# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
correlation = df.corr(numeric_only=True)
im = plt.imshow(correlation, cmap="coolwarm", aspect="auto")
plt.colorbar(im)
plt.xticks(range(len(correlation.columns)), correlation.columns, rotation=45, ha="right")
plt.yticks(range(len(correlation.columns)), correlation.columns)
plt.title("Feature Correlation Heatmap")
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()

What is a correlation heatmap?

A correlation heatmap shows how strongly each pair of features relates to each other. Values close to 1 or -1 mean a strong relationship. Values close to 0 mean almost no relationship.

Look at the target row to see which features are most predictive of heart disease. Features with higher absolute correlation to the target will likely be more useful to your models.

  • Run the cell. You should see a color-coded grid with feature names on both axes. Red cells indicate positive correlation and blue cells indicate negative correlation.
  • Confirm that correlation_heatmap.png was saved in your heart-disease-ml folder.
Handle missing values

The dataset uses ? characters for missing values in the ca and thal columns. You already told pandas to treat these as NaN when loading. Now you need to remove the affected rows.

  • Create a new cell and paste this code to check and remove missing values:
# Cell 7: Handle missing values
print("Missing values before cleaning:")
print(df.isnull().sum())
df = df.dropna()
print(f"\nDataset shape after dropping missing values: {df.shape}")
print(f"Missing values after cleaning: {df.isnull().sum().sum()}")

What does this code do?

First, .isnull().sum() counts missing values per column so you can see exactly where gaps exist.

Then .dropna() removes any row containing at least one missing value. Only about 6 rows are affected out of 303, so you lose minimal data. The final print confirms zero missing values remain.

  • Run the cell. You should see that ca and thal have a few missing values before cleaning, and the final shape is approximately (297, 14).

Still seeing missing values after dropna?

Make sure you are assigning the result back to df. Writing df.dropna() without the df = at the start creates a new DataFrame but doesn't overwrite the original.

Help me understand why dropna isn't working.

✔️ Awesome, I've got everything!

Great. Make sure all your cells are saved. Jupyter auto-saves periodically, but you can press Cmd+S (macOS) or Ctrl+S (Windows) to save now.

ⓧ I'd like to double check the full code

Here is the complete notebook code from Cell 1 through Cell 7. Compare yours against this:

# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print("All imports successful!")

# Cell 2: Load the dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
column_names = [
    "age", "sex", "cp", "trestbps", "chol", "fbs", "restecg",
    "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"
]
df = pd.read_csv(url, names=column_names, na_values="?")

print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Basic exploration
print(df.info())
print(f"\nStatistical summary:")
df.describe()

# Cell 4: Check target distribution
# Convert target to binary: 0 = no disease, 1 = disease present
df["target"] = df["target"].apply(lambda x: 1 if x > 0 else 0)
print("Target distribution:")
print(df["target"].value_counts())
print(f"\nBalance: {df['target'].value_counts(normalize=True)}")

# Cell 5: Visualize feature distributions
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
axes = axes.flatten()
for i, col in enumerate(df.columns):
    axes[i].hist(df[col].dropna(), bins=20, edgecolor="black", alpha=0.7)
    axes[i].set_title(col)
axes[-1].set_visible(False)
plt.tight_layout()
plt.savefig("feature_distributions.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
correlation = df.corr(numeric_only=True)
im = plt.imshow(correlation, cmap="coolwarm", aspect="auto")
plt.colorbar(im)
plt.xticks(range(len(correlation.columns)), correlation.columns, rotation=45, ha="right")
plt.yticks(range(len(correlation.columns)), correlation.columns)
plt.title("Feature Correlation Heatmap")
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 7: Handle missing values
print("Missing values before cleaning:")
print(df.isnull().sum())
df = df.dropna()
print(f"\nDataset shape after dropping missing values: {df.shape}")
print(f"Missing values after cleaning: {df.isnull().sum().sum()}")

Your data is loaded, explored, and cleaned. Next up, you'll split it into training and test sets, then train your first machine learning model.

Preprocess Data and Train Your First Model

Your dataset is clean, explored, and free of missing values. Now comes the core of any ML project: preparing your data for a model and building your first predictor.

Before a model can learn patterns, you need to split the data into two groups. One group teaches the model. The other tests whether it actually learned something useful or just memorized the answers.

In this step, get ready to:
  • Split the dataset into training and testing sets.
  • Train a Logistic Regression classifier as your baseline model.
  • Evaluate the model using metrics that matter for medical predictions.
Separate features from target and split the data

A model needs to know what it's looking at (the features) and what it's trying to predict (the target). You also need to hold back some data the model never sees during training, so you can test it fairly afterwards.

  • Create a new cell in your notebook and add the following code to separate features from the target and split the data:
# Cell 8: Prepare features and target, split data
X = df.drop("target", axis=1)
y = df["target"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

What does this code do?

This cell does two things:

  • X gets all 13 feature columns (age, cholesterol, etc.) while y gets just the target column (0 = no disease, 1 = disease).
  • train_test_split randomly shuffles the data and puts 80% into training and 20% into testing. The random_state=42 ensures you get the same split every time you run the cell, making results reproducible.
  • Run the cell by pressing Shift+Enter.
  • You should see output showing approximately 237 training samples and 60 test samples.

Why 80/20?

An 80/20 split is a common starting point in ML. You want enough training data for the model to learn patterns, but enough test data to get a reliable performance estimate. With ~297 rows, 80% gives ~237 rows to learn from and ~60 to test on.

Seeing a NameError?

If you see an error mentioning df or train_test_split not being defined, make sure you ran all previous cells (Cells 1 through 7) in order. The imports and data loading must execute before this cell.

Help me fix a NameError in my Jupyter Notebook

Train a Logistic Regression model

Logistic Regression is the standard starting point for binary classification problems. It draws a decision boundary between the two classes (disease vs. no disease) and gives you a simple baseline to measure other models against.

  • Create a new cell and add the following code to train your first model and evaluate it:
# Cell 9: Train Logistic Regression (baseline)
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_predictions = lr_model.predict(X_test)

print("=== Logistic Regression ===")
print(f"Accuracy: {accuracy_score(y_test, lr_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, lr_predictions, target_names=["No Disease", "Disease"]))

What does this code do?

  • LogisticRegression(max_iter=1000) creates the model. The default max_iter is 100, which often is not enough for this dataset to converge. Setting it to 1000 gives the solver enough iterations to find the best decision boundary.
  • .fit(X_train, y_train) trains the model on the training data. .predict(X_test) then asks the trained model to classify each patient in the test set.
  • classification_report prints precision, recall, and F1 score for each class. These metrics give a much fuller picture than accuracy alone.
  • Run the cell by pressing Shift+Enter.
  • You should see an accuracy score followed by a full classification report table with precision, recall, and F1 columns for both "No Disease" and "Disease" classes.

Seeing a ConvergenceWarning?

If you see a warning about the solver not converging, double-check that your code has max_iter=1000 (not the default 100). This dataset needs more iterations for the solver to find the optimal solution.

Help me fix a ConvergenceWarning in my Logistic Regression model

Understand your evaluation metrics

The classification report you just printed contains four key metrics. In a medical context like heart disease prediction, these numbers mean very different things.

What do these metrics mean for heart disease?

  • Accuracy is the percentage of all predictions the model got right. It sounds useful, but if 90% of patients were healthy, a model that always says "no disease" would get 90% accuracy while missing every sick patient.
  • Precision answers: "Of the patients the model flagged as having disease, how many actually have it?" Low precision means many false alarms.
  • Recall answers: "Of the patients who actually have disease, how many did the model catch?" Low recall means the model misses sick patients.
  • F1 Score is the balance between precision and recall. In medical settings, recall is often more important because missing a disease is worse than a false alarm.

What is a confusion matrix?

A confusion matrix is a 2x2 grid that shows exactly where the model gets confused. It breaks predictions into four categories:

  • True Negatives: correctly predicted no disease.
  • True Positives: correctly predicted disease.
  • False Positives: predicted disease when there was none (false alarm).
  • False Negatives: predicted no disease when there was (missed case).

You will visualize this as a heatmap in the next step when comparing all your models.

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook with all cells through Cell 9.

ⓧ I'd like to double check the full code

Here is the complete notebook through Cell 9. Compare your cells against this:

# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print("All imports successful!")

# Cell 2: Load the dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
column_names = [
    "age", "sex", "cp", "trestbps", "chol", "fbs", "restecg",
    "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"
]
df = pd.read_csv(url, names=column_names, na_values="?")

print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Basic exploration
print(df.info())
print(f"\nStatistical summary:")
df.describe()

# Cell 4: Check target distribution
df["target"] = df["target"].apply(lambda x: 1 if x > 0 else 0)
print("Target distribution:")
print(df["target"].value_counts())
print(f"\nBalance: {df['target'].value_counts(normalize=True)}")

# Cell 5: Visualize feature distributions
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
axes = axes.flatten()
for i, col in enumerate(df.columns):
    axes[i].hist(df[col].dropna(), bins=20, edgecolor="black", alpha=0.7)
    axes[i].set_title(col)
axes[-1].set_visible(False)
plt.tight_layout()
plt.savefig("feature_distributions.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
correlation = df.corr(numeric_only=True)
im = plt.imshow(correlation, cmap="coolwarm", aspect="auto")
plt.colorbar(im)
plt.xticks(range(len(correlation.columns)), correlation.columns, rotation=45, ha="right")
plt.yticks(range(len(correlation.columns)), correlation.columns)
plt.title("Feature Correlation Heatmap")
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 7: Handle missing values
print("Missing values before cleaning:")
print(df.isnull().sum())
df = df.dropna()
print(f"\nDataset shape after dropping missing values: {df.shape}")
print(f"Missing values after cleaning: {df.isnull().sum().sum()}")

# Cell 8: Prepare features and target, split data
X = df.drop("target", axis=1)
y = df["target"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

# Cell 9: Train Logistic Regression (baseline)
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_predictions = lr_model.predict(X_test)

print("=== Logistic Regression ===")
print(f"Accuracy: {accuracy_score(y_test, lr_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, lr_predictions, target_names=["No Disease", "Disease"]))

You have a working baseline model with proper evaluation metrics. Next up, you will train two more classifiers and compare them side-by-side to find which algorithm performs best on this dataset.

Train Additional Models and Compare

You now have a baseline Logistic Regression model trained and evaluated with a full classification report.

One model alone cannot tell you much. In this step, you will train two additional classifiers and compare all three side-by-side to find which algorithm best predicts heart disease on this dataset.

In this step, get ready to:
  • Train a Random Forest classifier on the same training data.
  • Train a K-Nearest Neighbors classifier with scaled features.
  • Compare all three models and visualize the results.
Train a Random Forest classifier

A Random Forest builds many decision trees on random subsets of the data and averages their predictions. This reduces overfitting compared to a single decision tree.

  • Train a Random Forest model by adding this code in a new cell (Cell 10):
# Cell 10: Train Random Forest
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)

print("=== Random Forest ===")
print(f"Accuracy: {accuracy_score(y_test, rf_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, rf_predictions, target_names=["No Disease", "Disease"]))

What does this code do?

  • `RandomForestClassifier(n_estimators=100)` creates a forest of 100 decision trees. Each tree trains on a random sample of the data and considers a random subset of features at each split.
  • `random_state=42` ensures you get the same results each time you run the cell, making your experiment reproducible.
  • The classification report shows precision, recall, and F1 for both classes so you can compare against Logistic Regression.
  • Run the cell by pressing **Shift+Enter**.
  • You should see an accuracy score and a classification report table printed below the cell.

Seeing a NameError?

If you see an error about RandomForestClassifier not being defined, make sure you ran Cell 1 (the imports cell) earlier in this session. You can re-run it by clicking into that cell and pressing **Shift+Enter**.

help me fix a NameError for RandomForestClassifier in my Jupyter Notebook

Train a K-Nearest Neighbors classifier

K-Nearest Neighbors (KNN) predicts a patient's class by looking at the 5 closest patients in the training set and taking a majority vote. Because KNN uses distance to measure "closeness," features with larger numeric ranges (like cholesterol in the hundreds) would dominate features with smaller ranges (like fasting blood sugar, 0 or 1).

Scaling all features to the same range fixes this problem.

  • Train a KNN model with scaled features by adding this code in a new cell (Cell 11):
# Cell 11: Train KNN (with scaling)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

knn_model = KNeighborsClassifier(n_neighbors=5)
knn_model.fit(X_train_scaled, y_train)
knn_predictions = knn_model.predict(X_test_scaled)

print("=== K-Nearest Neighbors ===")
print(f"Accuracy: {accuracy_score(y_test, knn_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, knn_predictions, target_names=["No Disease", "Disease"]))

Why scale features for KNN but not the other models?

`StandardScaler` transforms each feature to have a mean of 0 and a standard deviation of 1. This puts all features on a level playing field for distance calculations.

Logistic Regression and Random Forest do not strictly require scaling. Random Forest splits features independently (no distance math), so scale does not affect it. We only scale for KNN because it is the distance-based algorithm here.

Notice that we use `fit_transform` on training data (learns the mean/std and transforms) but only `transform` on test data (uses the same mean/std from training). This prevents data leakage from the test set.

  • Run the cell by pressing **Shift+Enter**.
  • You should see the KNN accuracy and classification report printed below the cell.

Seeing very low KNN accuracy?

If your KNN accuracy is noticeably lower than expected, double-check that you are fitting KNN on X_train_scaled (not X_train). Using unscaled data for a distance-based algorithm produces poor results.

help me debug low KNN accuracy in my heart disease prediction notebook

Compare all three models side-by-side

Now that all three models are trained, you can put their results in one place and see which algorithm performs best on this heart disease dataset.

  • Create a comparison summary by adding this code in a new cell (Cell 12):
# Cell 12: Compare models
results = pd.DataFrame({
    "Model": ["Logistic Regression", "Random Forest", "KNN"],
    "Accuracy": [
        accuracy_score(y_test, lr_predictions),
        accuracy_score(y_test, rf_predictions),
        accuracy_score(y_test, knn_predictions),
    ],
})
print("=== Model Comparison ===")
print(results.to_string(index=False))

What does this code do?

This creates a pandas DataFrame with one row per model and their accuracy scores. The `to_string(index=False)` prints a clean table without row numbers.

  • Run the cell. You should see a table listing all three models with their accuracy scores.
  • Visualize the comparison with a bar chart by adding this code in a new cell (Cell 13):
# Cell 13: Visualize comparison
models = ["Logistic Regression", "Random Forest", "KNN"]
accuracies = [
    accuracy_score(y_test, lr_predictions),
    accuracy_score(y_test, rf_predictions),
    accuracy_score(y_test, knn_predictions),
]

plt.figure(figsize=(8, 5))
bars = plt.bar(models, accuracies, color=["steelblue", "forestgreen", "coral"], edgecolor="black")
plt.ylabel("Accuracy")
plt.title("Model Comparison: Heart Disease Prediction")
plt.ylim(0.5, 1.0)
for bar, acc in zip(bars, accuracies):
    plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01,
             f"{acc:.3f}", ha="center", fontsize=12)
plt.tight_layout()
plt.savefig("model_comparison.png", dpi=100, bbox_inches="tight")
plt.show()

What does this code do?

  • `plt.bar()` creates a bar chart with a different color for each model.
  • `plt.ylim(0.5, 1.0)` starts the y-axis at 0.5 so small accuracy differences are visually obvious.
  • The `for` loop adds numeric labels on top of each bar so you can read exact values.
  • `plt.savefig()` saves the chart as `model_comparison.png` in your project folder.
  • Run the cell. You should see a bar chart with three colored bars showing the accuracy of each model, with numeric labels on top.

Bar chart not displaying?

If you see a blank output or text like <Figure size ...> instead of a chart, make sure `plt.show()` is the last line in the cell. Also confirm that you ran the imports cell (Cell 1) earlier which imports matplotlib.pyplot.

help me fix my matplotlib bar chart not showing in Jupyter Notebook

  • Plot the confusion matrix for your best-performing model by adding this code in a new cell (Cell 14):
# Cell 14: Confusion matrix for best model
best_predictions = lr_predictions  # Replace with actual best model's predictions
cm = confusion_matrix(y_test, best_predictions)

fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(cm, cmap="Blues")
ax.set_xticks([0, 1])
ax.set_yticks([0, 1])
ax.set_xticklabels(["No Disease", "Disease"])
ax.set_yticklabels(["No Disease", "Disease"])
ax.set_xlabel("Predicted")
ax.set_ylabel("Actual")
ax.set_title("Confusion Matrix")

for i in range(2):
    for j in range(2):
        ax.text(j, i, str(cm[i, j]), ha="center", va="center",
                color="white" if cm[i, j] > cm.max() / 2 else "black", fontsize=16)

plt.colorbar(im)
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=100, bbox_inches="tight")
plt.show()

What is a confusion matrix?

A confusion matrix shows the four possible outcomes for a binary classifier. The rows are the actual labels and the columns are what the model predicted.

  • Top-left: true negatives (correctly predicted no disease).
  • Top-right: false positives (predicted disease, but patient is healthy).
  • Bottom-left: false negatives (predicted no disease, but patient is sick). This is the most dangerous error in a medical context.
  • Bottom-right: true positives (correctly predicted disease).

The comment `# Replace with actual best model's predictions` reminds you to swap lr_predictions for whichever model performed best in your comparison. If Logistic Regression had the highest accuracy, leave it as-is.

  • Run the cell. You should see a 2x2 colored grid with numbers in each cell showing how many predictions fell into each category.
  • Check your comparison chart and confusion matrix. Note which model had the highest accuracy.

Confusion matrix looks wrong?

If all values are in one row or column, confirm that best_predictions is set to a valid predictions variable (lr_predictions, rf_predictions, or knn_predictions). Also verify that y_test still exists from your earlier cell run.

help me fix my confusion matrix output in my heart disease prediction notebook

  • Add one final cell below Cell 14. Change the cell type to **Markdown** (click the dropdown at the top of the cell that says "Code" and select **Markdown**).
  • Write 2-3 sentences explaining which model performed best, what its accuracy was, and why you think it outperformed the others.

What to write in your markdown cell

Look at your bar chart and classification reports. Mention the winning model's accuracy, and note whether it also had the best recall for the Disease class. In medical prediction, catching sick patients (recall) matters more than overall accuracy.

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook with all cells from 1 through 14 plus your markdown summary cell.

ⓧ I'd like to double check the full code

Here is the full notebook code through Cell 14. Compare it against your cells:

# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print("All imports successful!")

# Cell 2: Load the dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
column_names = [
    "age", "sex", "cp", "trestbps", "chol", "fbs", "restecg",
    "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"
]
df = pd.read_csv(url, names=column_names, na_values="?")

print(f"Dataset shape: {df.shape}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Basic exploration
print(df.info())
print(f"\nStatistical summary:")
df.describe()

# Cell 4: Check target distribution
df["target"] = df["target"].apply(lambda x: 1 if x > 0 else 0)
print("Target distribution:")
print(df["target"].value_counts())
print(f"\nBalance: {df['target'].value_counts(normalize=True)}")

# Cell 5: Visualize feature distributions
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
axes = axes.flatten()
for i, col in enumerate(df.columns):
    axes[i].hist(df[col].dropna(), bins=20, edgecolor="black", alpha=0.7)
    axes[i].set_title(col)
axes[-1].set_visible(False)
plt.tight_layout()
plt.savefig("feature_distributions.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 6: Correlation heatmap
plt.figure(figsize=(12, 8))
correlation = df.corr(numeric_only=True)
im = plt.imshow(correlation, cmap="coolwarm", aspect="auto")
plt.colorbar(im)
plt.xticks(range(len(correlation.columns)), correlation.columns, rotation=45, ha="right")
plt.yticks(range(len(correlation.columns)), correlation.columns)
plt.title("Feature Correlation Heatmap")
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 7: Handle missing values
print("Missing values before cleaning:")
print(df.isnull().sum())
df = df.dropna()
print(f"\nDataset shape after dropping missing values: {df.shape}")
print(f"Missing values after cleaning: {df.isnull().sum().sum()}")

# Cell 8: Prepare features and target, split data
X = df.drop("target", axis=1)
y = df["target"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

# Cell 9: Train Logistic Regression (baseline)
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_predictions = lr_model.predict(X_test)

print("=== Logistic Regression ===")
print(f"Accuracy: {accuracy_score(y_test, lr_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, lr_predictions, target_names=["No Disease", "Disease"]))

# Cell 10: Train Random Forest
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)

print("=== Random Forest ===")
print(f"Accuracy: {accuracy_score(y_test, rf_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, rf_predictions, target_names=["No Disease", "Disease"]))

# Cell 11: Train KNN (with scaling)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

knn_model = KNeighborsClassifier(n_neighbors=5)
knn_model.fit(X_train_scaled, y_train)
knn_predictions = knn_model.predict(X_test_scaled)

print("=== K-Nearest Neighbors ===")
print(f"Accuracy: {accuracy_score(y_test, knn_predictions):.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, knn_predictions, target_names=["No Disease", "Disease"]))

# Cell 12: Compare models
results = pd.DataFrame({
    "Model": ["Logistic Regression", "Random Forest", "KNN"],
    "Accuracy": [
        accuracy_score(y_test, lr_predictions),
        accuracy_score(y_test, rf_predictions),
        accuracy_score(y_test, knn_predictions),
    ],
})
print("=== Model Comparison ===")
print(results.to_string(index=False))

# Cell 13: Visualize comparison
models = ["Logistic Regression", "Random Forest", "KNN"]
accuracies = [
    accuracy_score(y_test, lr_predictions),
    accuracy_score(y_test, rf_predictions),
    accuracy_score(y_test, knn_predictions),
]

plt.figure(figsize=(8, 5))
bars = plt.bar(models, accuracies, color=["steelblue", "forestgreen", "coral"], edgecolor="black")
plt.ylabel("Accuracy")
plt.title("Model Comparison: Heart Disease Prediction")
plt.ylim(0.5, 1.0)
for bar, acc in zip(bars, accuracies):
    plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01,
             f"{acc:.3f}", ha="center", fontsize=12)
plt.tight_layout()
plt.savefig("model_comparison.png", dpi=100, bbox_inches="tight")
plt.show()

# Cell 14: Confusion matrix for best model
best_predictions = lr_predictions  # Replace with actual best model's predictions
cm = confusion_matrix(y_test, best_predictions)

fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(cm, cmap="Blues")
ax.set_xticks([0, 1])
ax.set_yticks([0, 1])
ax.set_xticklabels(["No Disease", "Disease"])
ax.set_yticklabels(["No Disease", "Disease"])
ax.set_xlabel("Predicted")
ax.set_ylabel("Actual")
ax.set_title("Confusion Matrix")

for i in range(2):
    for j in range(2):
        ax.text(j, i, str(cm[i, j]), ha="center", va="center",
                color="white" if cm[i, j] > cm.max() / 2 else "black", fontsize=16)

plt.colorbar(im)
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=100, bbox_inches="tight")
plt.show()

Secret mission

Cross-Validation and Hyperparameter Tuning

You compared three models on a single train/test split. But what if that split happened to be unusually easy or hard? In this mission, you will use cross-validation to get a more robust performance estimate and GridSearchCV to systematically find the best hyperparameters for your top model.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Jupyter Notebook server (running process in your terminal).
  • The heart-disease-ml project folder on your Desktop (contains your virtual environment, notebook, and saved PNG visualizations).

Keep everything running

No action needed. Choose this if you are still actively building or want to continue experimenting with your models.

  • Your Jupyter Notebook server stays running in the terminal. You can return to it in your browser at any time.
  • Your heart-disease-ml folder stays on your Desktop with all your work intact.

Pause - I'll come back to this later

Shut down the Jupyter server to free up your terminal. Your files stay on disk so you can resume later.

  • Switch to the terminal where Jupyter is running and press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate

When you want to resume, navigate back to your project folder, reactivate the virtual environment, and relaunch Jupyter.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your notebook, visualizations, and the virtual environment with all installed packages.

  • Switch to the terminal where Jupyter is running and press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate
  • Delete the entire project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf heart-disease-ml
  • On Windows, run:
cd ~\Desktop
rmdir /s /q heart-disease-ml
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see heart-disease-ml in the output.

Nice Work!

Nice Work!

You just built a complete machine learning pipeline that predicts heart disease from real patient data. From raw CSV to trained classifiers to evaluation metrics, you now own the end-to-end workflow that forms the foundation of every ML project.

You've learned how to:

  • Built a complete ML pipeline from data loading and exploration through model evaluation, using pandas, NumPy, and matplotlib in a Jupyter Notebook.
  • Trained and compared three classification algorithms (Logistic Regression, Random Forest, and KNN) on the UCI Cleveland Heart Disease dataset.
  • Evaluated models using precision, recall, F1 score, and a confusion matrix to understand model performance beyond simple accuracy.
  • Secret Mission: Used cross-validation for robust evaluation and GridSearchCV for hyperparameter tuning to squeeze the best performance from your model.

Ready to quiz yourself?