Build a Wine Classification ML Pipeline

Build a scikit-learn pipeline that classifies wine varieties with 97%+ accuracy.

Introduction

30 Second Summary

Every bottle of wine carries a chemical fingerprint. A machine learning model can learn to read these patterns and identify the grape variety from measurements alone.

In this project, you will build a Python script that classifies wines into three varieties based on 13 chemical measurements. You will see firsthand how adding preprocessing to your model transforms inconsistent results into a reliable 97%+ accuracy.

What You'll Build

Run your script in the terminal and watch it print accuracy scores proving your pipeline can reliably sort 178 wines into the correct variety from nothing but their chemical data.

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

  • A script you can run that classifies wine into three varieties from 13 chemical measurements, printing 97%+ accuracy to your terminal.
  • A visible before-and-after comparison showing how feature scaling inside a scikit-learn Pipeline transforms inconsistent model performance into rock-solid predictions.
  • Cross-validation results across 5 folds you can point to as proof your model generalizes to new data, not just memorizes the training set.
  • Secret Mission: pit three classifiers against each other in a model showdown and crown the champion based on real accuracy numbers.

Are there any prerequisites?

This project assumes basic Python knowledge (variables, functions, and loops). No prior machine learning experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Project

To build your wine classification pipeline, you need a dedicated workspace and two key libraries: pandas for loading and exploring data, and scikit-learn for training machine learning models.

In this step, you'll create your project folder, install both libraries, and set up the Python file you'll work in for the rest of the project.

In this step, get ready to:
  • Create a project folder on your Desktop and open it in VS Code.
  • Install pandas and scikit-learn via pip.
  • Verify the libraries are ready to use and create your project file.
Create a project folder and open it in VS Code

You need a dedicated folder for your project files. Creating it on your Desktop makes it easy to find later.

  • In VS Code, open a terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir wine-ml-pipeline
  • Now open this folder as your VS Code workspace.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the wine-ml-pipeline folder, and click Open.

VS Code reloads with wine-ml-pipeline as your workspace. You should see the folder name in the Explorer sidebar on the left.

  • Open a fresh terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Confirm your terminal is in the right folder by running:
pwd

You should see a path ending in wine-ml-pipeline.

Don't see wine-ml-pipeline in the path?

Make sure you selected the wine-ml-pipeline folder itself in the Open Folder dialog, not your Desktop. Close the terminal and try File > Open Folder again.

help me fix my VS Code workspace path

Install the required libraries

Your project needs pandas to load and explore datasets, and scikit-learn to build and evaluate ML models. One pip command installs both.

  • Install pandas and scikit-learn by running this command:
pip install scikit-learn pandas

What does this command do?

This installs scikit-learn 1.9.0 and pandas 3.0.5 (the latest stable versions) along with their dependencies like NumPy and SciPy.

You'll see a stream of download and install messages in your terminal. The process takes about 30 seconds depending on your internet speed.

Wait until you see the Successfully installed message at the end of the output.

Seeing a permissions error?

If you see a permissions error, try running pip install --user scikit-learn pandas instead. This installs the packages in your user directory without needing admin access.

If you see pip: command not found, try pip3 instead of pip.

help me fix my pip install error

Verify the installation and create your project file

Before writing any code, confirm that both libraries import successfully and show the expected version numbers.

What do you think will happen when you import these libraries and print their versions? If the install worked, you should see clean version numbers with no errors.

  • Verify your installation by running this command:
python -c "import sklearn; import pandas; print(sklearn.__version__); print(pandas.__version__)"

You should see two version numbers printed, one per line:

1.9.0
3.0.5

Seeing a ModuleNotFoundError?

If Python cannot find the modules, your pip and python commands might point to different Python installations. Try running python3 instead of python, or reinstall with python -m pip install scikit-learn pandas to ensure they match.

help me fix my Python import error

  • Now create your project file. In the Explorer sidebar on the left, hover over the WINE-ML-PIPELINE header.
  • Click the New File icon (a page with a + symbol).
  • Type pipeline.py and press Enter.

You should see pipeline.py appear in the Explorer sidebar and open as an empty file in the editor.

Why one file for the whole project?

Keeping everything in a single pipeline.py file makes it easy to run your entire ML pipeline with one command. For a project this size, a single script is clearer than splitting across multiple files.

Your workspace is ready. Next up, you'll load a real dataset into pipeline.py and explore what wine data actually looks like.

Load and Explore the Wine Data

Your environment is set up with pandas and scikit-learn installed. Now it's time to meet the data you'll be classifying.

Before building any model, you need to see what the data looks like. Raw numbers mean nothing until you understand the shape, feature names, and scale differences between columns. In this step, you'll load the Wine dataset and explore its structure to get a feel for what your pipeline will work with.

In this step, get ready to:
  • Load the Wine dataset as a pandas DataFrame and separate features from labels.
  • Print shape, feature names, and class distribution to understand the data.
  • Explore summary statistics and notice how wildly different the feature scales are.
Load the Wine dataset and separate features from labels

The Wine dataset contains 178 wine samples, each described by 13 chemical measurements. Your job is to classify each sample into one of 3 wine varieties based on those measurements.

  • Open pipeline.py in VS Code and add the following code to load the dataset and print basic information:
import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score, classification_report


# ==== Step 2: Load and Explore the Wine Data ====
print("=" * 60)
print("STEP 2: Loading and Exploring the Wine Dataset")
print("=" * 60)

wine = load_wine(as_frame=True)
X = wine.data
y = wine.target

print(f"\nDataset shape: {X.shape}")
print(f"Number of samples: {X.shape[0]}")
print(f"Number of features: {X.shape[1]}")
print(f"\nFeature names:\n{list(X.columns)}")
print(f"\nTarget classes: {list(wine.target_names)}")

What does this code do?

The imports bring in everything you'll need for the full project, not just this step. You're importing tools for data loading, model training, preprocessing, and evaluation all at once so you won't need to add imports later.

  • load_wine(as_frame=True) loads scikit-learn's built-in Wine dataset and returns it as a pandas DataFrame instead of a raw NumPy array.
  • X = wine.data grabs the 13 chemical measurements (the features your model will learn from).
  • y = wine.target grabs the wine variety label for each sample (the thing your model will predict).

Why separate X and y?

In machine learning, you always separate your data into features (X) and the target label (y). The model learns patterns in X that predict y. Here, X holds 178 rows of 13 chemical measurements, and y holds 178 wine variety labels.

  • Save pipeline.py.
  • Run the script in your terminal:
python pipeline.py

You should see the dataset shape printed as (178, 13), a list of 13 feature names like alcohol, malic_acid, ash, and the three target classes.

Seeing a ModuleNotFoundError?

If you see an error about sklearn or pandas not being found, make sure you installed both libraries in Step 1. Re-run pip install scikit-learn pandas and try again.

help me fix a ModuleNotFoundError when running my pipeline.py script

Explore the data statistics

You know the data has 178 samples and 13 features. Now you need to see how those features are distributed and notice the scale differences between them.

  • Add the following code to the bottom of pipeline.py to print class distribution and summary statistics:
print(f"\nClass distribution:")
print(y.value_counts().sort_index())
print(f"\nFirst 5 rows:")
print(X.head())
print(f"\nFeature statistics (notice the different scales!):")
print(X.describe().loc[["mean", "min", "max"]])

What does this code do?

  • y.value_counts().sort_index() shows how many wines belong to each of the 3 classes. An uneven distribution could bias your model.
  • X.head() prints the first 5 rows so you can see actual data values.
  • X.describe().loc[["mean", "min", "max"]] pulls out summary statistics. This is where you'll spot the scale problem that motivates the entire pipeline.
  • Save pipeline.py.

Before you run this, what do you think the feature ranges will look like? Will all 13 chemical measurements be on similar scales, or wildly different?

  • Run the script again:
python pipeline.py

Look at the feature statistics table at the bottom of the output. Notice how proline ranges from 278 to 1680, while magnesium ranges from 70 to 162. These wildly different scales will cause problems for your model in the next step.

Why do different scales matter?

Algorithms like Logistic Regression treat features mathematically. When one feature has values in the thousands and another has values under 10, the model assumes the big-number feature is more important. That's not true here. You'll fix this in Step 4 with StandardScaler.

Output looks garbled or cut off?

If columns are wrapping oddly, that's just your terminal width. Try widening your terminal window. The data is still correct as long as you see all 13 feature names and the statistics table.

help me read the output of my wine dataset exploration script

✔️ Awesome, I've got everything!

Great. Double check you've saved pipeline.py and you can see all 13 feature names, the class distribution, and the statistics table in your terminal output.

ⓧ I'd like to double check the full code

Here's the complete pipeline.py at this point:

import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score, classification_report


# ==== Step 2: Load and Explore the Wine Data ====
print("=" * 60)
print("STEP 2: Loading and Exploring the Wine Dataset")
print("=" * 60)

wine = load_wine(as_frame=True)
X = wine.data
y = wine.target

print(f"\nDataset shape: {X.shape}")
print(f"Number of samples: {X.shape[0]}")
print(f"Number of features: {X.shape[1]}")
print(f"\nFeature names:\n{list(X.columns)}")
print(f"\nTarget classes: {list(wine.target_names)}")
print(f"\nClass distribution:")
print(y.value_counts().sort_index())
print(f"\nFirst 5 rows:")
print(X.head())
print(f"\nFeature statistics (notice the different scales!):")
print(X.describe().loc[["mean", "min", "max"]])

You now know what the Wine dataset looks like: 178 samples, 13 features with wildly different scales, and 3 target classes. Next up, you'll train a model directly on this raw data to see how those scale differences cause real problems.

Train a Model on Raw Data

In Step 2, you explored the Wine dataset and noticed something important: the features have wildly different scales. Now it's time to see what happens when you throw that raw data at a model without any preprocessing.

Logistic Regression treats all features equally by their numerical magnitude. When one feature ranges from 278 to 1680 and another ranges from 0.7 to 5.1, the model gives the large-valued feature outsized influence. You're about to see this problem firsthand.

In this step, get ready to:
  • Split the dataset into training and test sets.
  • Train a Logistic Regression model on unscaled features.
  • Run cross-validation to reveal the model's inconsistency.
Split into train and test sets

Before you can train a model, you need to hold back some data for testing. If you test on the same data you trained on, the model just memorizes the answers and you learn nothing about how it handles new data.

  • In pipeline.py, add the following code at the bottom of the file, below the Step 2 code:
# ==== Step 3: Train a Model on Raw Data ====
print("\n" + "=" * 60)
print("STEP 3: Training on Raw (Unscaled) Data")
print("=" * 60)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, 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?

  • train_test_split randomly shuffles the data and splits it into two groups: 80% for training, 20% for testing.
  • random_state=42 locks the randomness so you get the same split every time you run the script. This makes your results reproducible.
  • The print statements confirm the split: 142 training samples and 36 test samples.
  • Save pipeline.py.
  • Run your script to confirm the split by running:
python pipeline.py
  • At the bottom of the output (after all the Step 2 data), you should see the new Step 3 header and the train/test split confirmation: 142 samples for training, 36 for testing.

Seeing a NameError for X or y?

  • Make sure your Step 3 code is below the Step 2 code. The variables X and y are created in Step 2, so Step 3 must come after.
  • Check that you haven't accidentally deleted the X = wine.data or y = wine.target lines from Step 2.

Why is my train_test_split throwing a NameError?

Train a Logistic Regression model directly on raw features

Now you'll fit a Logistic Regression model on the raw, unscaled training data. This model will try to learn the relationship between the 13 chemical measurements and the wine variety, treating every feature value at face value.

  • Add this code below the train/test split in pipeline.py:
raw_model = LogisticRegression(max_iter=10000, random_state=42)
raw_model.fit(X_train, y_train)
raw_predictions = raw_model.predict(X_test)
raw_accuracy = accuracy_score(y_test, raw_predictions)
print(f"\nRaw model accuracy on test set: {raw_accuracy:.4f}")

What does this code do?

  • LogisticRegression(max_iter=10000, random_state=42) creates a classifier. max_iter=10000 gives the model plenty of iterations to converge.
  • .fit(X_train, y_train) trains the model on the 142 training samples.
  • .predict(X_test) generates predictions for the 36 held-out test samples, and accuracy_score compares those predictions against the real labels.
  • Save pipeline.py and run the script:
python pipeline.py
  • At the bottom of the output, you should see an accuracy score of approximately 0.9444 to 0.9722. That looks pretty good for a first attempt.

A 95-97% accuracy on a single test split might make you think the model is working well. But a single number from a single split can be misleading. What if you just got lucky with which samples ended up in the test set?

Run cross-validation on the raw model to expose instability

Cross-validation solves this problem by splitting the data into 5 different folds, training on 4 and testing on 1 each time. You get 5 accuracy scores instead of just one. If the model is truly reliable, all 5 scores should be consistently high.

  • Add this code at the bottom of pipeline.py:
raw_cv_scores = cross_val_score(
    LogisticRegression(max_iter=10000, random_state=42), X, y, cv=5
)
print(f"\nRaw model cross-validation scores: {raw_cv_scores}")
print(f"Mean: {raw_cv_scores.mean():.4f} (+/- {raw_cv_scores.std():.4f})")
print("^ Notice the variance between folds - the model is inconsistent!")

What does this code do?

  • cross_val_score splits the full dataset (all 178 samples) into 5 folds and trains a fresh model on each combination.
  • cv=5 means 5-fold cross-validation: each fold gets a turn as the test set.
  • The mean and standard deviation tell you both the average performance and how much it varies. High variance means the model is unstable.
  • Save pipeline.py.

Before you run this, think: do you expect the model to score consistently high across all 5 folds, or will some folds perform noticeably worse than others?

  • Run the script:
python pipeline.py

Look at the 5 cross-validation scores at the bottom of the output. Some folds score high, but others drop significantly. The standard deviation shows this inconsistency numerically. The model is fragile because it's working with features on completely different scales.

This is the problem. A model that works well on one split but poorly on another cannot be trusted in production. The fix? Preprocessing the features so they're all on the same scale before the model sees them.

Scores all look the same or very high?

  • Make sure you're passing a fresh LogisticRegression(max_iter=10000, random_state=42) to cross_val_score, not raw_model (which was already fitted).
  • Confirm you're passing the full dataset X, y (not X_train, y_train). Cross-validation needs the complete data to do its own splitting.

Why are my cross-validation scores not showing variance?

✔️ 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

Your pipeline.py should now have these sections in order. Scroll through your file and confirm each one is present:

  • The import block at the top (numpy, pandas, sklearn imports).
  • Step 2 section: loads the Wine dataset, prints shape, feature names, class distribution, first 5 rows, and statistics.
  • Step 3 section header: prints the step title.
  • Train/test split: train_test_split(X, y, test_size=0.2, random_state=42) with shape prints.
  • Raw model training: LogisticRegression(max_iter=10000, random_state=42) fitted and evaluated on test set.
  • Cross-validation: cross_val_score with cv=5 printing all 5 scores, mean, and standard deviation.

You've proven the model is unreliable on raw data. Next up, you'll build a proper ML pipeline that preprocesses the features before training. That single change will make the scores jump higher and become consistent across all folds.

Build a Pipeline with Preprocessing

In the previous step, you saw cross-validation scores bounce around between folds. That inconsistency means the model is unreliable on data it hasn't seen before.

The root cause is that features have wildly different scales, and Logistic Regression treats large-valued features as more important. A scikit-learn Pipeline fixes this by chaining preprocessing with the classifier, guaranteeing scaling is fitted only on training data in each fold.

In this step, get ready to:
  • Create a scikit-learn Pipeline with StandardScaler and Logistic Regression.
  • Run cross-validation to compare pipeline scores against the raw model.
  • Evaluate the final pipeline on the held-out test set.
Create a scikit-learn Pipeline

The make_pipeline function chains processing steps together. Data flows through each step in order: first StandardScaler normalizes the features, then LogisticRegression classifies them.

  • In pipeline.py, scroll to the bottom of your file (after the raw model cross-validation output).
  • Add the following code to create the pipeline and print its structure:
# ==== Step 4: Build a Proper Pipeline ====
print("\n" + "=" * 60)
print("STEP 4: Building a Pipeline with Preprocessing")
print("=" * 60)

pipeline = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=10000, random_state=42)
)
print(f"\nPipeline structure:\n{pipeline}")

What does this code do?

make_pipeline creates a Pipeline object that runs two steps in sequence. First, StandardScaler() subtracts the mean and divides by the standard deviation for each feature, making all columns comparable in magnitude.

Then the scaled data passes to LogisticRegression for classification. Because both steps are chained into one object, you never accidentally scale test data with test statistics.

  • Save pipeline.py.
  • Run the script to confirm the pipeline is created:
python pipeline.py

At the bottom of the output, you should see the pipeline structure showing two steps: standardscaler and logisticregression.

Don't see the pipeline structure?

  • Make sure you saved pipeline.py before running the command.
  • Check that your new code is placed after the last print statement from Step 3, not inside it.

Help me debug my pipeline creation code

Run cross-validation on the pipeline

Now for the real test. You'll run the same 5-fold cross-validation on the pipeline and compare the scores to the raw model from Step 3.

  • In pipeline.py, add this code below the pipeline structure print:
pipeline_cv_scores = cross_val_score(pipeline, X, y, cv=5)
print(f"\nPipeline cross-validation scores: {pipeline_cv_scores}")
print(f"Mean: {pipeline_cv_scores.mean():.4f} (+/- {pipeline_cv_scores.std():.4f})")
print(f"\nImprovement over raw model:")
print(f"  Raw mean:      {raw_cv_scores.mean():.4f} (+/- {raw_cv_scores.std():.4f})")
print(f"  Pipeline mean: {pipeline_cv_scores.mean():.4f} (+/- {pipeline_cv_scores.std():.4f})")

What does this code do?

cross_val_score splits the data into 5 folds, trains the pipeline on 4 folds, and tests on the remaining fold. It repeats this 5 times so every sample gets used for testing exactly once.

Because the Pipeline is the estimator, the scaler is fitted only on each training fold. This prevents data leakage (test data statistics leaking into training). The comparison lines print both scores side by side so you can see the improvement.

  • Save pipeline.py.

Before you run the script, think about this: do you expect the pipeline's cross-validation scores to be higher or lower than the raw model's? And do you expect more or less variance between folds?

  • Run the script to see the comparison:
python pipeline.py

The pipeline cross-validation scores should all be 97%+ with much less variance between folds. Compare that to the raw model scores above, which were lower and bounced around more.

Why is the difference so dramatic?

StandardScaler puts all 13 features on the same scale. Without it, features like "proline" (range 278-1680) dominated the model while features like "color_intensity" (range 1.28-13.0) were essentially ignored.

Scaling lets Logistic Regression weigh each feature fairly, producing stable and accurate predictions across every fold.

Scores not improving?

  • Make sure you're passing pipeline (the Pipeline object) to cross_val_score, not raw_model.
  • Confirm the make_pipeline call includes both StandardScaler() and LogisticRegression(max_iter=10000, random_state=42) with parentheses on each.

Help me debug my pipeline cross-validation scores

Train the final pipeline and evaluate on the test set

Cross-validation proved the pipeline generalizes well. Now you'll fit it on the full training set and get a final accuracy score plus a per-class breakdown on the held-out test data.

  • In pipeline.py, add this code below the improvement comparison:
pipeline.fit(X_train, y_train)
pipeline_predictions = pipeline.predict(X_test)
pipeline_accuracy = accuracy_score(y_test, pipeline_predictions)
print(f"\nPipeline accuracy on test set: {pipeline_accuracy:.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, pipeline_predictions, target_names=wine.target_names))

What does this code do?

pipeline.fit(X_train, y_train) trains the full pipeline on the training set. Internally, it fits the scaler on X_train, transforms it, then trains Logistic Regression on the scaled features.

classification_report shows precision, recall, and F1-score for each wine class. This tells you not just overall accuracy, but how well the model identifies each variety.

  • Save pipeline.py.

Before you run this, what accuracy do you expect on the test set? Will it match the cross-validation mean, or could it differ?

  • Run the script to see your final evaluation:
python pipeline.py

You should see a pipeline accuracy of 97%+ on the test set. The classification report shows high precision, recall, and F1-score across all three wine classes.

Seeing lower accuracy than expected?

  • Confirm you're calling pipeline.fit(X_train, y_train) and not pipeline.fit(X, y). Training on the full dataset and testing on a subset of it would give misleading results.
  • Check that pipeline.predict(X_test) uses X_test (not X_train).

Help me debug my pipeline evaluation results

✔️ Awesome, I've got everything!

Great. Double check you've saved pipeline.py and that your terminal output shows the pipeline structure, cross-validation scores, and classification report.

ⓧ I'd like to double check the full code

Your pipeline.py file is now longer than 30 lines. Scroll through your file and confirm these sections are present, in this order:

  • Imports at the top (numpy, pandas, sklearn modules).
  • Step 2: Load and explore the Wine dataset (load_wine, print shape, features, statistics).
  • Step 3: Train on raw data (train_test_split, raw LogisticRegression, cross_val_score on raw model).
  • Step 4: Build a Proper Pipeline (make_pipeline, pipeline cross-validation, comparison, pipeline.fit, classification_report).

If any section is missing, scroll up to the relevant chunk in this step and add it.

Your pipeline is classifying wine varieties at 97%+ accuracy with consistent scores across all folds. Next up, you'll look inside the pipeline to understand exactly how StandardScaler transforms the data and why doing it inside a Pipeline prevents data leakage.

Understand What the Pipeline Does

In the last step, you built a Pipeline that dramatically improved your model's consistency. The cross-validation scores jumped up and the variance dropped.

Knowing how to call make_pipeline is not enough. You need to see what actually happens inside: how StandardScaler transforms the data, and why doing this inside a Pipeline prevents data leakage compared to scaling the full dataset upfront.

In this step, get ready to:
  • Inspect the fitted scaler inside the pipeline to see what it learned from the training data.
  • Compare a raw sample to its scaled version to visualize the transformation.
  • Understand how Pipeline prevents data leakage automatically.
Inspect the fitted scaler inside the pipeline

Your fitted pipeline has a StandardScaler inside it that learned specific statistics from the training data. You can reach in and see exactly what values it stored.

  • In pipeline.py, scroll to the bottom of the file (after the Classification Report print statement).
  • Add the following code to extract the scaler and print what it learned:
# ==== Step 5: Understand What the Pipeline Does ====
print("\n" + "=" * 60)
print("STEP 5: Inside the Pipeline - How Scaling Works")
print("=" * 60)

# Access the scaler step inside the pipeline by its auto-generated name
scaler = pipeline.named_steps["standardscaler"]
print("\nLearned feature means (from training data only):")
# Build a table showing what the scaler learned from X_train
feature_stats = pd.DataFrame({
    "Feature": X.columns,
    "Mean": scaler.mean_,
    "Std Dev": scaler.scale_
})
print(feature_stats.to_string(index=False))

What does this code do?

The named_steps attribute lets you access individual components inside a Pipeline by name. When you use make_pipeline, it auto-names each step as the lowercase version of the class name.

The scaler.mean_ and scaler.scale_ attributes store the mean and standard deviation the scaler computed from the training data only. These are the values it uses to transform new data.

  • Save pipeline.py.
  • Run your script to see the scaler's learned statistics:
python pipeline.py

Scroll to the bottom of the output. You should see a table with 13 rows showing each feature's mean and standard deviation.

What should I see?

The table shows wildly different means and standard deviations across features. For example, "proline" has a mean around 700 while "nonflavanoid_phenols" has a mean around 0.36. This confirms why unscaled features confuse the model.

Not seeing the Step 5 output?

Make sure you added the new code after the existing Classification Report section, not inside it. The new code should be at the very bottom of the file.

Check that you saved the file before running. Help me debug my pipeline.py output.

Show before vs. after scaling

Numbers like "mean" and "standard deviation" are abstract. Seeing the actual transformation on a real data point makes the concept click.

  • Add the following code below the print(feature_stats.to_string(index=False)) line:
print("\n\nBefore vs After Scaling (first sample):")
# Grab the first training sample in its original form
sample_raw = X_train.iloc[[0]]
# Transform it using the scaler that was fitted on training data
sample_scaled = scaler.transform(sample_raw)
# Build a side-by-side comparison table
comparison = pd.DataFrame({
    "Feature": X.columns,
    "Raw Value": sample_raw.values[0],
    "Scaled Value": sample_scaled[0]
})
print(comparison.to_string(index=False))
print("\n^ After scaling, all features have comparable magnitudes (near 0 mean, ~1 std)")

What does this code do?

It takes one real wine sample and shows it before and after StandardScaler processes it. The scaler subtracts the training mean and divides by the training standard deviation for each feature.

After scaling, every feature ends up with values near 0 (centered) with a spread of about 1. This makes all 13 features equally important to the model, regardless of their original measurement units.

  • Save pipeline.py and run the script again:
python pipeline.py

Scroll to the new output. You should see two columns side by side: raw values (like 746.0 for proline) next to scaled values (small numbers near 0).

Seeing a KeyError or AttributeError?

If you see a KeyError on standardscaler, make sure the string is all lowercase with no spaces. This is the auto-generated name from make_pipeline.

If you see an AttributeError on scaler, make sure the scaler extraction line appears after pipeline.fit(X_train, y_train) in the Step 4 section. The pipeline must be fitted before you can inspect its components. Help me fix my scaler access code.

Demonstrate why Pipeline prevents data leakage

The Pipeline did more than just make your code cleaner. It solved a subtle but critical problem: data leakage.

What is data leakage?

Data leakage happens when information from outside the training set sneaks into the model's training process. If you scale the entire dataset first and then split into train/test, the scaler uses test data statistics during training. Your accuracy looks great in development but crashes on truly unseen data.

A Pipeline prevents this by fitting the scaler only on training data inside each cross-validation fold. Test data is always transformed using statistics it never contributed to.

  • Add the following code at the bottom of pipeline.py to print a summary of how Pipeline prevents leakage:
print("\n\nWhy Pipeline prevents data leakage:")
print("- Without Pipeline: you scale ALL data first, then split.")
print("  The scaler 'sees' test data statistics during training = LEAKAGE!")
print("- With Pipeline: scaling is fitted ONLY on training data in each CV fold.")
print("  Test data is transformed using training statistics = NO LEAKAGE!")

print("\n" + "=" * 60)
print("COMPLETE: Your ML pipeline is working!")
print(f"Final accuracy: {pipeline_accuracy:.4f}")
print("=" * 60)

What does this code do?

This prints a summary of the two approaches side by side. Without a Pipeline, you risk leaking test statistics into training. With a Pipeline, each cross-validation fold fits the scaler fresh on its own training portion.

The final lines print a completion banner with your pipeline's accuracy score, confirming the entire workflow ran successfully from raw data to predictions.

  • Save pipeline.py.

Before you run the script one final time, what do you expect the "Final accuracy" value to be? Think about what accuracy you saw from the pipeline in Step 4.

  • Run the script to see the complete output:
python pipeline.py

Scroll to the Step 5 section of the output. You should see the before/after comparison showing all features now have scaled values near 0, the data leakage explanation, and a final accuracy matching what you saw in Step 4.

Seeing unexpected output or errors?

If the final accuracy line shows a NameError for pipeline_accuracy, make sure your Step 5 code is below the Step 4 section where that variable is defined.

If any of the Step 5 output is missing, check that your code is added sequentially at the bottom of the file, not accidentally placed inside an earlier section. Help me debug my final pipeline.py script.

✔️ Awesome, I've got everything!

Great. Double check you have saved pipeline.py and your output shows all five steps running end-to-end.

ⓧ I'd like to double check the full code

Your pipeline.py should contain these sections in order. Scroll through your file and confirm each is present:

  • Import block at the top (numpy, pandas, sklearn imports).
  • Step 2: Load and Explore the Wine Data (load_wine, print shape/features/stats).
  • Step 3: Train a Model on Raw Data (train_test_split, raw LogisticRegression, cross_val_score on raw model).
  • Step 4: Build a Proper Pipeline (make_pipeline with StandardScaler + LogisticRegression, pipeline cross_val_score, fit/predict/classification_report).
  • Step 5: Inside the Pipeline (scaler extraction via named_steps, feature_stats DataFrame, sample_raw/sample_scaled comparison, data leakage explanation, final accuracy banner).

Secret mission

Model Showdown

You've built one pipeline with Logistic Regression. But real ML engineers always compare multiple models before declaring a winner. Can you pit three classifiers against each other and find the champion?

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 wine-ml-pipeline project folder on your Desktop.
  • Python packages installed via pip: pandas and scikit-learn.

Keep everything running

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

  • Your wine-ml-pipeline folder stays on your Desktop.
  • The pandas and scikit-learn packages remain installed and available for future Python projects.

Pause - I'll come back to this later

Since this project has no running processes or servers, there is nothing to pause. Your files and packages will be waiting for you when you return.

  • Your project folder and installed packages remain in place with no cost.
  • To pick up where you left off, open the wine-ml-pipeline folder in VS Code and run python pipeline.py.

Delete - I don't want to use this again

Remove all project files and uninstall the packages you added during this project.

  • Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/wine-ml-pipeline

On Windows, run this instead:

rmdir /s /q "$HOME\Desktop\wine-ml-pipeline"
  • Uninstall the Python packages by running:
pip uninstall scikit-learn pandas -y
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see wine-ml-pipeline in the listing.

Nice Work!

Nice Work!

You just built a complete machine learning pipeline that classifies wine varieties from chemical measurements with 97%+ accuracy.

You've learned how to:

  • Build a scikit-learn Pipeline that chains preprocessing and classification together, preventing data leakage by fitting the scaler only on training data in each cross-validation fold.
  • See firsthand why feature scaling matters by comparing raw model performance (inconsistent fold scores) against a pipeline with StandardScaler (consistent 97%+ accuracy).
  • Use cross-validation to prove your model generalizes across unseen data, not just memorizes a single lucky train/test split.
  • Secret Mission: Compare multiple classifiers (Logistic Regression, Random Forest, SVM) and pick the champion based on cross-validation scores.

Ready to quiz yourself?