Build an ML Classification Pipeline

Train and evaluate ML models to classify tumors as malignant or benign.

Introduction

30 Second Summary

Every time a doctor examines a tissue sample and classifies a tumor as "malignant" or "benign," they are performing classification. Machine learning can learn to do the same thing from raw measurements alone.

In this project, you will build a complete classification pipeline that predicts breast cancer diagnosis from real cell nucleus measurements. You will load data, preprocess it safely, train two different models, and evaluate them with industry-standard metrics.

What You'll Build

A Python-based ML pipeline using scikit-learn that trains and compares two classifiers on the Breast Cancer Wisconsin dataset (569 samples, 30 features).

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

  • A trained Logistic Regression and Random Forest model that predict whether a tumor is malignant or benign from cell measurements.
  • A reusable scikit-learn Pipeline that chains preprocessing and model training into a single, leakage-free workflow.
  • A quantitative model comparison using cross-validation scores, classification reports, and a saved confusion matrix visualization.
  • Secret Mission: Tune your Random Forest with RandomizedSearchCV to squeeze out better performance.

Are there any prerequisites?

You need Python 3.11+ and VS Code installed on your machine. Basic familiarity with Python variables and data types is helpful, but no prior machine learning experience is required.

Set Up Your ML Environment

Before you can train a model, you need a workspace where all your code lives. Your ml-pipeline folder already has a virtual environment with your data science packages installed.

In this step, you will load that folder into VS Code and create the Python script that will hold your entire classification pipeline.

In this step, get ready to:
  • Create classify.py with all the library imports your pipeline needs.
  • Verify the imports run without errors.
Open your project folder in VS Code

VS Code needs your project folder loaded so it can display your files in the sidebar and run commands from the right directory.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the ml-pipeline folder, and click Open.

You should see ml-pipeline appear in the Explorer sidebar on the left.

Trust the authors prompt

VS Code may ask if you trust the authors of this folder. Click Yes, I trust the authors to continue.

Create the project file

Your entire pipeline will live in a single Python file. This keeps the workflow linear and easy to follow from top to bottom.

  • In the Explorer sidebar, click the New File... button (the file icon with a plus sign at the top of the sidebar).
  • Type classify.py and press Enter.

The file opens in the editor. Now you will add all the imports your pipeline needs.

  • Paste the following import block into classify.py:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay

What do these imports do?

  • numpy, pandas, and matplotlib handle numeric arrays, tabular data, and plotting.
  • load_breast_cancer gives you a built-in dataset to train on.
  • train_test_split and cross_val_score split your data and measure performance across multiple folds.
  • StandardScaler and Pipeline let you chain preprocessing and training into one leakage-free workflow.
  • LogisticRegression and RandomForestClassifier are the two models you will compare.
  • classification_report, confusion_matrix, and ConfusionMatrixDisplay produce evaluation metrics and a visual confusion matrix.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

Red squiggly lines under the imports?

If VS Code highlights your imports with warnings, it may not be using your virtual environment's Python interpreter. Click the Python version in the bottom status bar, then select the interpreter inside your ml-pipeline virtual environment folder.

help me fix Python interpreter warnings in VS Code

✔️ Awesome, I've got everything!

Great. Make sure you have saved classify.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete classify.py at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay
Run the script to verify your imports

Running the script now confirms every package is installed and importable. If something is wrong, you will catch it here before writing real logic.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Make sure your virtual environment is active. You should see (ml-pipeline) or similar at the start of your terminal prompt.
  • Run the script by entering this command:
python classify.py

You should see no output at all. A blank return to the prompt means every import succeeded.

Seeing a ModuleNotFoundError?

This means a package is not installed in the active environment. Confirm your virtual environment is active (check for the (ml-pipeline) prefix in your prompt), then re-run the install command:

pip install scikit-learn pandas matplotlib

help me fix ModuleNotFoundError for sklearn

Your environment is ready. Next up, you will load a real dataset and explore what 569 tumor samples look like as raw numbers.

Load and Explore the Dataset

Your environment is ready and all imports run without errors. Now it's time to bring in real data.

Before training any model, you need to understand what you're working with. In this step, you'll load a real medical dataset and explore its structure so you know exactly what the model will learn from.

In this step, get ready to:
  • Load the Breast Cancer Wisconsin dataset from scikit-learn.
  • Explore the data to understand feature scales, class distribution, and structure.
  • Identify key characteristics that will inform your model choices.
Load the Breast Cancer Wisconsin dataset

Scikit-learn bundles several real-world datasets for practice. The Breast Cancer Wisconsin dataset contains measurements of cell nuclei from tumor biopsies. Each sample is labelled as malignant or benign.

  • In classify.py, add the following code below your import statements to load the dataset and convert it into a pandas DataFrame:
# Step 2: Load and explore the dataset
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df["target"] = data.target

What does this code do?

load_breast_cancer() returns a Bunch object containing the feature data, target labels, feature names, and target class names. It's all bundled in one call with no file downloads needed.

pd.DataFrame(data.data, columns=data.feature_names) converts the raw NumPy array into a labelled table. Adding df["target"] puts the diagnosis labels (0 or 1) into the same table for convenience.

You won't see any output yet. The next chunk adds print statements that reveal what's inside the dataset.

Explore the data

With the data loaded, you need to inspect its shape, features, class balance, and value ranges. This tells you whether the data needs preprocessing and which models might struggle.

  • Add these print statements below the code you just wrote:
print("Dataset shape:", df.shape)
print("\nFeature names:", list(data.feature_names))
print("\nTarget classes:", list(data.target_names))
print("\nClass distribution:")
print(f"  Malignant (0): {(data.target == 0).sum()} samples")
print(f"  Benign (1): {(data.target == 1).sum()} samples")
print("\nFeature statistics:")
print(df.describe().T[["mean", "std", "min", "max"]].head(10))
print("\nFirst 5 rows:")
print(df.head())

What does this code do?

  • df.shape prints the number of rows and columns (569 samples, 31 columns including the target).
  • data.feature_names and data.target_names reveal the 30 measurement names and the two class labels.
  • df.describe().T[["mean", "std", "min", "max"]].head(10) shows summary statistics transposed so each feature is a row. This makes it easy to spot wildly different scales.
  • df.head() prints the first 5 rows so you can see actual values.
  • Save classify.py.
  • Run the script from your terminal by running:
python classify.py
  • You should see output showing Dataset shape: (569, 31), a list of 30 feature names, the target classes ['malignant', 'benign'], class distribution counts, a statistics table, and the first 5 rows of raw data.

Seeing a NameError or ModuleNotFoundError?

  • Make sure your virtual environment is still active. You should see (sklearn-env) at the start of your terminal prompt.
  • Check that the new code appears below all the import statements, not inside or above them.
  • Confirm you saved the file before running it.

Why is my classify.py throwing a NameError when I try to load the dataset?

Understand what you are working with

Take a moment to read the output. The numbers tell a story about what your model will face.

What should I notice in the output?

  • 569 samples with 30 numeric features. Each feature is a physical measurement of a cell nucleus (radius, texture, perimeter, area, smoothness, and more).
  • Two classes: malignant (212 samples) and benign (357 samples). There is a slight class imbalance but nothing extreme.
  • Features have very different scales. For example, mean area ranges from roughly 140 to 2500, while mean smoothness ranges from 0.05 to 0.16. This matters because some models are sensitive to scale differences.

Why does scale matter?

Logistic Regression uses gradient-based optimization. When one feature has values in the thousands and another has values near zero, the gradient steps become uneven. This slows convergence and can hurt accuracy.

You'll fix this in the next step by adding a StandardScaler to your pipeline.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's what your complete classify.py should look like at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay

# Step 2: Load and explore the dataset
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df["target"] = data.target

print("Dataset shape:", df.shape)
print("\nFeature names:", list(data.feature_names))
print("\nTarget classes:", list(data.target_names))
print("\nClass distribution:")
print(f"  Malignant (0): {(data.target == 0).sum()} samples")
print(f"  Benign (1): {(data.target == 1).sum()} samples")
print("\nFeature statistics:")
print(df.describe().T[["mean", "std", "min", "max"]].head(10))
print("\nFirst 5 rows:")
print(df.head())

You now know your dataset inside and out: 569 samples, 30 features with wildly different scales, and a binary target. Next up, you'll split this data and build a proper ML pipeline that handles those scale differences automatically.

Split the Data and Build a Pipeline

In the previous step, you explored the Breast Cancer Wisconsin dataset and saw that it has 30 features with wildly different scales. Now it is time to turn that raw data into something a model can learn from.

Before training any model, you need to separate your data into a training set and a test set. The model learns from the training set, then you measure its performance on the test set (data it has never seen). This prevents you from accidentally memorizing the answers instead of learning the patterns.

In this step, get ready to:
  • Split your dataset into training and test sets with proper stratification.
  • Build a scikit-learn Pipeline that chains preprocessing and model training together.
  • Generate predictions on unseen test data.
Perform a train/test split

A train/test split divides your dataset into two non-overlapping groups. The model only sees the training portion during learning. The test portion acts as a stand-in for real-world data the model has never encountered.

  • In classify.py, scroll to the bottom of the file (below all the print statements from Step 2).
  • Add the train/test split code by pasting the following:
# Step 3: Split and build pipeline
X = data.data
y = data.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"\nTrain set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

What does this code do?

  • X = data.data grabs all 30 feature columns (the measurements).
  • y = data.target grabs the labels (0 for malignant, 1 for benign).
  • test_size=0.2 holds out 20% of the data for testing (114 samples) and keeps 80% for training (455 samples).
  • stratify=y ensures the malignant/benign ratio stays the same in both sets, so neither set is accidentally skewed.
  • random_state=42 locks the split so you get the same result every time you run the script.
  • Save classify.py.
  • Run the script in your terminal:
python classify.py
  • At the bottom of the output, you should see:
Train set: 455 samples
Test set: 114 samples

Not seeing the train/test sizes?

  • Make sure you pasted the new code below the existing Step 2 code, not inside or above it.
  • Check that you saved the file before running the command.
  • If you see a NameError for data, confirm your Step 2 code that loads the dataset is still at the top of the file.

Why isn't my train/test split printing correctly?

Build and train the Logistic Regression Pipeline

You noticed in the previous step that features have very different scales (some range from 0 to 1, others from 0 to 4000). Logistic Regression is sensitive to these scale differences. You need to standardize all features before feeding them to the model.

A scikit-learn Pipeline chains these two steps (scaling then training) into a single object. This guarantees the scaler only ever learns from training data, never from test data.

  • Add the Pipeline code below your train/test split by pasting the following at the bottom of classify.py:
# Logistic Regression Pipeline
lr_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=10000, random_state=42))
])

lr_pipeline.fit(X_train, y_train)
lr_predictions = lr_pipeline.predict(X_test)

What does this code do?

  • Pipeline([...]) creates a chain of steps that run in order. Each step is a tuple of (name, transformer/estimator).
  • StandardScaler() rescales each feature to have a mean of 0 and a standard deviation of 1.
  • LogisticRegression(max_iter=10000, random_state=42) is your classifier. The max_iter=10000 ensures the model has enough iterations to converge with 30 features.
  • .fit(X_train, y_train) trains the entire pipeline. The scaler learns means/stds from training data only, then the classifier learns patterns.
  • .predict(X_test) pushes test data through the same scaler (using training stats) then asks the classifier for predictions.

Why use a Pipeline instead of separate steps?

Without a Pipeline, you might accidentally fit the scaler on the entire dataset (train + test) before splitting. This is called data leakage. The model secretly "sees" test data during training, which inflates your accuracy scores.

A Pipeline forces the correct order: the scaler is fit only on training data, and test data is transformed using those training statistics. This is the single most important engineering habit in ML.

  • Save classify.py and run it again:
python classify.py
  • The script completes without errors. You still see the same dataset exploration output and train/test sizes. The pipeline trains and predicts silently in the background.

Seeing a ConvergenceWarning?

  • Check that your LogisticRegression includes max_iter=10000. The default of 100 is often too few for 30 features.
  • Make sure StandardScaler() is the first step in your Pipeline. Without scaling, convergence takes much longer.

Why is my Logistic Regression not converging?

✔️ Awesome, I've got everything!

Great. Double check you have saved classify.py and that the script runs without errors.

ⓧ I'd like to double check the full code

Here is the complete classify.py up to this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay

# Step 2: Load and explore the dataset
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df["target"] = data.target

print("Dataset shape:", df.shape)
print("\nFeature names:", list(data.feature_names))
print("\nTarget classes:", list(data.target_names))
print("\nClass distribution:")
print(f"  Malignant (0): {(data.target == 0).sum()} samples")
print(f"  Benign (1): {(data.target == 1).sum()} samples")
print("\nFeature statistics:")
print(df.describe().T[["mean", "std", "min", "max"]].head(10))
print("\nFirst 5 rows:")
print(df.head())

# Step 3: Split and build pipeline
X = data.data
y = data.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"\nTrain set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

# Logistic Regression Pipeline
lr_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=10000, random_state=42))
])

lr_pipeline.fit(X_train, y_train)
lr_predictions = lr_pipeline.predict(X_test)

Your pipeline is trained and predictions are ready. Next up, you will evaluate how well both Logistic Regression and a Random Forest perform using classification reports and cross-validation.

Evaluate and Compare Models

Your Logistic Regression pipeline is trained and has predictions ready. Now comes the most important part of any ML project: figuring out how good those predictions actually are.

A single accuracy number is rarely enough. In this step, you will evaluate your model with industry-standard metrics, build a second classifier for comparison, and use cross-validation to determine which model you can trust more on unseen data.

In this step, get ready to:
  • Evaluate Logistic Regression with a classification report and confusion matrix.
  • Build and evaluate a Random Forest pipeline on the same data.
  • Compare both models using 5-fold cross-validation.
Print the Logistic Regression classification report and confusion matrix

A classification report shows precision, recall, and F1 score for each class. A confusion matrix shows exactly which predictions were correct and which were wrong.

  • In classify.py, add the following code at the bottom of the file, below the lr_predictions line:
# Step 4: Evaluate and compare models
print("\n" + "=" * 50)
print("LOGISTIC REGRESSION RESULTS")
print("=" * 50)
print(classification_report(y_test, lr_predictions, target_names=data.target_names))

lr_cm = confusion_matrix(y_test, lr_predictions)
print("Confusion Matrix:")
print(lr_cm)

What does this code do?

classification_report calculates precision (of the samples predicted as a class, how many actually were), recall (of the actual samples in a class, how many were correctly found), and F1 score (the harmonic mean of precision and recall) for each class.

confusion_matrix produces a 2x2 grid showing true positives, true negatives, false positives, and false negatives. This tells you exactly where your model is making mistakes.

  • Save classify.py.
  • Run the script in your terminal by running:
python classify.py
  • You should see the classification report table with precision, recall, and F1 columns, plus the confusion matrix printed as a 2x2 array of numbers.

Not seeing the classification report?

Make sure you added the code below the existing lr_predictions = lr_pipeline.predict(X_test) line, not inside it or above it.

Check that target_names=data.target_names is spelled exactly as shown. A typo here causes a NameError.

Help me debug my classification report code

Save the confusion matrix as an image

A numeric matrix is useful, but a color-coded visualization makes it much easier to spot patterns at a glance. You will save the confusion matrix as a PNG file using matplotlib.

  • Add this code directly below the print(lr_cm) line you just added:
# Save confusion matrix visualization
fig, ax = plt.subplots(figsize=(6, 5))
disp = ConfusionMatrixDisplay(confusion_matrix=lr_cm, display_labels=data.target_names)
disp.plot(ax=ax, cmap="Blues")
ax.set_title("Logistic Regression - Confusion Matrix")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150)
print("\nConfusion matrix saved to confusion_matrix.png")

What does this code do?

ConfusionMatrixDisplay turns your raw confusion matrix into a labeled heatmap. The display_labels parameter puts human-readable class names ("malignant" and "benign") on the axes instead of 0 and 1.

plt.savefig("confusion_matrix.png", dpi=150) writes the figure to a file in your project folder. You will see this file appear in your file explorer after running the script.

  • Save classify.py and run it again:
python classify.py
  • You should see the message "Confusion matrix saved to confusion_matrix.png" in your terminal output.
  • Confirm the image was created by running:
ls
  • You should see confusion_matrix.png listed alongside classify.py in the output.

Don't see the PNG file?

Make sure your terminal is inside the ml-pipeline folder when you run the script. The file saves relative to where you run the command.

Check that import matplotlib.pyplot as plt is still at the top of your file. Without it, plt is undefined and the script errors out before saving.

Help me figure out why my confusion matrix image isn't saving

Build and train a Random Forest pipeline

Logistic Regression is a linear model. Random Forest is a non-linear ensemble method that builds many decision trees and combines their votes. Comparing the two reveals whether the classification boundary in this data is linear or more complex.

  • Add this code below the print("\nConfusion matrix saved to confusion_matrix.png") line:
# Random Forest Pipeline
rf_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])

rf_pipeline.fit(X_train, y_train)
rf_predictions = rf_pipeline.predict(X_test)

What does this code do?

This follows the exact same Pipeline pattern as your Logistic Regression model. The only difference is the classifier: RandomForestClassifier(n_estimators=100) builds 100 decision trees and takes a majority vote across all of them.

The StandardScaler is technically not required for Random Forests (they are scale-invariant), but keeping it in the Pipeline makes the comparison fair and the pattern consistent.

  • Save classify.py. You won't see a change yet. The next chunk prints the evaluation results.
Evaluate the Random Forest model
  • Add this code directly below the rf_predictions line:
print("\n" + "=" * 50)
print("RANDOM FOREST RESULTS")
print("=" * 50)
print(classification_report(y_test, rf_predictions, target_names=data.target_names))

rf_cm = confusion_matrix(y_test, rf_predictions)
print("Confusion Matrix:")
print(rf_cm)

What does this code do?

This is the same evaluation pattern you used for Logistic Regression. By using identical metrics for both models, you can directly compare their precision, recall, and F1 scores class by class.

  • Save classify.py and run it:
python classify.py
  • Scroll through the output. You should see both the "LOGISTIC REGRESSION RESULTS" and "RANDOM FOREST RESULTS" sections, each with their own classification report and confusion matrix.

Seeing a NameError for rf_pipeline or rf_predictions?

Make sure the Random Forest Pipeline code appears before the evaluation code in your file. Python reads top to bottom, so the pipeline must be defined and trained before you try to print its results.

Help me fix my Random Forest evaluation code

Compare both models with cross-validation

A single train/test split can be misleading. Cross-validation splits the training data into 5 different folds, trains and evaluates on each fold, and averages the results. This gives you a much more reliable estimate of how each model performs on unseen data.

  • Add this code at the bottom of classify.py, below the Random Forest confusion matrix output:
# Cross-validation comparison
print("\n" + "=" * 50)
print("CROSS-VALIDATION COMPARISON (5-fold)")
print("=" * 50)

lr_cv_scores = cross_val_score(lr_pipeline, X_train, y_train, cv=5, scoring="f1")
rf_cv_scores = cross_val_score(rf_pipeline, X_train, y_train, cv=5, scoring="f1")

print(f"\nLogistic Regression F1: {lr_cv_scores.mean():.4f} (+/- {lr_cv_scores.std():.4f})")
print(f"Random Forest F1:      {rf_cv_scores.mean():.4f} (+/- {rf_cv_scores.std():.4f})")

if lr_cv_scores.mean() > rf_cv_scores.mean():
    print("\nLogistic Regression has higher average F1 score.")
else:
    print("\nRandom Forest has higher average F1 score.")

What does this code do?

cross_val_score runs 5-fold cross-validation on the training data only. It never touches the test set, so this is an honest comparison.

scoring="f1" uses the F1 score as the evaluation metric. F1 is the harmonic mean of precision and recall, making it a good choice when classes are slightly imbalanced (like our 212 malignant vs 357 benign split).

The +/- value is the standard deviation across folds. A low standard deviation means the model performs consistently regardless of which data it sees.

Why use cross-validation instead of just the test set?

A single test set gives you one number. Maybe you got lucky (or unlucky) with which samples ended up in the test set. Cross-validation gives you 5 numbers, one for each fold.

The average of those 5 scores is a more trustworthy estimate. The standard deviation tells you how stable the model is. This is how ML engineers decide which model to deploy in production.

  • Save classify.py and run the complete script:
python classify.py
  • Scroll to the bottom of the output. You should see the "CROSS-VALIDATION COMPARISON (5-fold)" section with F1 scores and standard deviations for both models, followed by a statement saying which model has the higher average F1 score.

Script takes a long time to finish?

Cross-validation trains each model 5 times, so the script takes a few seconds longer than before. If it takes more than 30 seconds, check that you have not accidentally nested cross-validation calls inside a loop.

Help me debug my cross-validation code

✔️ Awesome, I've got everything!

Great. Double check you have saved classify.py and that running it prints both classification reports, both confusion matrices, and the cross-validation comparison.

ⓧ I'd like to double check the full code

Here is the complete classify.py file as it should look at the end of this step:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay

# Step 2: Load and explore the dataset
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df["target"] = data.target

print("Dataset shape:", df.shape)
print("\nFeature names:", list(data.feature_names))
print("\nTarget classes:", list(data.target_names))
print("\nClass distribution:")
print(f"  Malignant (0): {(data.target == 0).sum()} samples")
print(f"  Benign (1): {(data.target == 1).sum()} samples")
print("\nFeature statistics:")
print(df.describe().T[["mean", "std", "min", "max"]].head(10))
print("\nFirst 5 rows:")
print(df.head())

# Step 3: Split and build pipeline
X = data.data
y = data.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"\nTrain set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

# Logistic Regression Pipeline
lr_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=10000, random_state=42))
])

lr_pipeline.fit(X_train, y_train)
lr_predictions = lr_pipeline.predict(X_test)

# Step 4: Evaluate and compare models
print("\n" + "=" * 50)
print("LOGISTIC REGRESSION RESULTS")
print("=" * 50)
print(classification_report(y_test, lr_predictions, target_names=data.target_names))

lr_cm = confusion_matrix(y_test, lr_predictions)
print("Confusion Matrix:")
print(lr_cm)

# Save confusion matrix visualization
fig, ax = plt.subplots(figsize=(6, 5))
disp = ConfusionMatrixDisplay(confusion_matrix=lr_cm, display_labels=data.target_names)
disp.plot(ax=ax, cmap="Blues")
ax.set_title("Logistic Regression - Confusion Matrix")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150)
print("\nConfusion matrix saved to confusion_matrix.png")

# Random Forest Pipeline
rf_pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])

rf_pipeline.fit(X_train, y_train)
rf_predictions = rf_pipeline.predict(X_test)

print("\n" + "=" * 50)
print("RANDOM FOREST RESULTS")
print("=" * 50)
print(classification_report(y_test, rf_predictions, target_names=data.target_names))

rf_cm = confusion_matrix(y_test, rf_predictions)
print("Confusion Matrix:")
print(rf_cm)

# Cross-validation comparison
print("\n" + "=" * 50)
print("CROSS-VALIDATION COMPARISON (5-fold)")
print("=" * 50)

lr_cv_scores = cross_val_score(lr_pipeline, X_train, y_train, cv=5, scoring="f1")
rf_cv_scores = cross_val_score(rf_pipeline, X_train, y_train, cv=5, scoring="f1")

print(f"\nLogistic Regression F1: {lr_cv_scores.mean():.4f} (+/- {lr_cv_scores.std():.4f})")
print(f"Random Forest F1:      {rf_cv_scores.mean():.4f} (+/- {rf_cv_scores.std():.4f})")

if lr_cv_scores.mean() > rf_cv_scores.mean():
    print("\nLogistic Regression has higher average F1 score.")
else:
    print("\nRandom Forest has higher average F1 score.")

You now have a complete model evaluation and comparison workflow. You know exactly how well each model performs, where it makes mistakes, and which one generalizes better to unseen data. Next up, the Secret Mission challenges you to squeeze even better performance out of your Random Forest with hyperparameter tuning.

Secret mission

Hyperparameter Tuning with RandomizedSearchCV

Your Random Forest used default hyperparameters. Can you squeeze out better performance by searching for the optimal configuration? Use RandomizedSearchCV to explore the hyperparameter space and find the best combination for this dataset.

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 locally, so there are no ongoing costs.

Resources you used:

  • The ml-pipeline project folder on your Desktop (contains classify.py, confusion_matrix.png, and the virtual environment).
  • Python virtual environment with scikit-learn, pandas, and matplotlib installed.

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with the pipeline further.

  • Your virtual environment stays active and ready to use.
  • You can re-run classify.py anytime to see your results again.
  • The ml-pipeline folder stays on your Desktop.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files stay on disk so you can pick up where you left off.

  • Deactivate the virtual environment by running this in your terminal:
deactivate
  • Close VS Code.
  • When you come back, open the ml-pipeline folder in VS Code and reactivate your virtual environment with source sklearn-env/bin/activate (macOS/Linux) or sklearn-env\Scripts\activate (Windows).

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code, the saved confusion matrix image, and the virtual environment with all installed packages.

  • Deactivate the virtual environment first:
deactivate
  • Navigate to your Desktop and delete the entire project folder.

On macOS/Linux:

cd ~/Desktop
rm -rf ml-pipeline

On Windows:

cd ~\Desktop
rmdir /s /q ml-pipeline
  • Confirm the folder is gone by running:
ls
  • You should no longer see ml-pipeline in the output.

Nice Work!

Nice Work!

Nice work! You just built a complete machine learning classification pipeline that predicts breast cancer diagnosis from real cell nucleus measurements.

You've learned how to:

  • Build a full ML classification pipeline from raw data loading through model evaluation using the Breast Cancer Wisconsin dataset.
  • Prevent data leakage by wrapping preprocessing and model training inside a scikit-learn Pipeline.
  • Train and compare Logistic Regression vs Random Forest classifiers using classification reports, confusion matrices, and 5-fold cross-validation.
  • Secret Mission: Tuned hyperparameters with RandomizedSearchCV to optimize model performance beyond the defaults.

Ready to quiz yourself?