---
title: "Pipelines tutorial"
author: "Dr. Ghita Berrada"
date: 03 March 2025
format: 
  html:
    from: markdown+emoji
    page-layout: full
    toc: true
    toc-depth: 2
    html-math-method: mathjax
self-contained: true
jupyter: python3
engine: jupyter
bibliography:  ../../../../references/references.bib
editor:
  render-on-save: true
  preview: true
---

Until W05, we've used the `scikit-learn` library to perform our data pre-processing steps, our model training and our model predictions step-by-step. What if we could aggregate all these steps?

That's exactly what `scikit-learn` `Pipelines` allows us to do!

**Why would you want to use `scikit-learn` `Pipelines`?**

- Simplicity: Combining preprocessing and model training in one step. Simplifying your code
- Reusability: Improving maintainability and reproducibility. Easily reusing the same pipeline with different datasets. 
- Reduced Error: Avoiding common mistakes like forgetting to apply transformations to test data.

**Do you need additional library installs?**

No! As long as you have `pandas` and `scikit-learn` installed, you're good to go!

## ⚙️ Setup 

### Loading libraries

We start, as usual, by loading all the libraries we'll need in this file
```{python}
from sklearn.datasets import load_iris
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
```

### Loading the Iris dataset

The example dataset we'll use to showcase the `Pipelines` functionalities is the famous `Iris` dataset. You can find this dataset in various sources. We'll load it from `scikit-learn.datasets`.

```{python}
# Load the Iris dataset
data = load_iris()
X, y = data.data, data.target
# Create a DataFrame for better visualization
df = pd.DataFrame(X, columns=data.feature_names)
df['target'] = y
df.sample(5)
```

Note that this is actually a multi-class classification problem as `target` has three values:

```{python}
df.target.unique()
```

Let's get a quick idea of class imbalance:

```{python}
class_distribution = np.bincount(df.target) / len(df.target)
print("Class Imbalance (Proportions):")
print(f"Class 0: {class_distribution[0]:.2f}\nClass 1: {class_distribution[1]:.2f}\nClass 2: {class_distribution[2]:.2f}")
```

There is no class imbalance in this case. All classes are perfectly balanced. 

## Training/test split

Let's split the data in training and test sets

```{python}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
```

## Defining the Pipeline

The dataset is fairly simple but our features are numeric and vary in range so we need to standardize them before training the `scikit-learn`'s `LogisticRegression` model^[which is a multinomial regression model under the hood since we have a multiclass problem].
The test set will also need standardizing.

We create a `Pipeline` that will handle this for us automatically.

```{python}
pipeline = Pipeline([
	('scaler', StandardScaler()),  # Step 1: Standardize features
	('model', LogisticRegression())  # Step 2: Logistic Regression model
])
```

## Training and evaluating the model

Now, it's time to train and evaluate^[Note that accuracy is acceptable here because the classes are balanced.] the model using the pipeline we've created.

```{python}
pipeline.fit(X_train, y_train)	# Model training

y_pred = pipeline.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f"Model Accuracy: {accuracy * 100:.2f}%")
# Output -> Model Accuracy: 100.00% 
```

We didn't need to manually process the training or testing datasets: our pipeline handled it automatically for us. 

This can be particularly useful in production ML workflows, especially when:

- there are multiples features that require different handling
- there are multiple pre-processing steps

It can quickly become difficult to handle the processing at multiple pipeline stages and maintain them over time. 

Thanks to `scikit-learn`'s `Pipelines`, we can aggregate all processing and modeling steps in one place so it is much easier to tweak a portion of the workflow without having to manage it separately for training and evaluation stages.


## To read further about `Pipelines`

To read further about `Pipelines`'s functionalities, you could have a look at:

- this [Medium post](https://medium.com/@sahin.samia/scikit-learn-pipelines-explained-streamline-and-optimize-your-machine-learning-processes-f17b1beb86a4)
- this [tutorial on Python-Bloggers](https://python-bloggers.com/2024/02/creating-scikit-learn-pipelines/)
- or look at the [`Pipeline` documentation](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html#sklearn.pipeline.Pipeline) and examples that come with it.