Build a Leak-Free Preprocessing Pipeline
Build a scikit-learn Pipeline that cleans messy retail data without leaking.
Introduction
30 Second Summary
Real-world data is messy. Missing values, typos in category names, and numbers stored as text hide in every dataset you will encounter on the job.
In this project, you will build a reusable scikit-learn Pipeline that cleans a messy 25,000-row retail dataset and produces model-ready predictions without data leakage. Along the way, you will experience firsthand why preprocessing matters by watching a model crash on dirty data before you fix it.
What You'll Build
You hand your Pipeline a row of messy, never-before-seen retail data and it returns a profit prediction instantly, handling every cleaning and transformation step behind the scenes.
By the end of this project, you'll have:
- A single Pipeline object you can feed raw, dirty data into and get predictions out the other end, with no manual cleaning steps in between.
- Cross-validated accuracy scores proving your pipeline outperforms a naive baseline and prevents data leakage by fitting only on training data.
- Hands-on experience diagnosing five categories of real-world data quality issues, from type mismatches to duplicate rows.
- Secret Mission: Tune hyperparameters across your full pipeline with GridSearchCV and export it for reuse with joblib.
Are there any prerequisites?
This project assumes intermediate Python knowledge and basic familiarity with pandas DataFrames. You do not need prior machine learning experience.
Before We Start
Building a machine learning model is only as good as the data you feed it. In the real world, data arrives messy. Columns have wrong types, values are missing, and categories are spelled inconsistently.
You need the right libraries installed and the dataset downloaded before you can start exploring the data. In this step, you will install everything, download a deliberately messy retail sales dataset, and confirm it loads correctly.
In this step, get ready to:
- Install scikit-learn, pandas, and matplotlib.
- Download the Superstore Sales dataset from Kaggle.
- Verify your setup by importing libraries and loading the data.
Install required packages
Your project needs three core libraries. pandas handles tabular data, scikit-learn provides machine learning tools, and matplotlib creates visualizations.
- Install all three packages by running this command in a new code cell in your Jupyter notebook:
pip install scikit-learn pandas matplotlib
What does this command do?
Running pip install inside a Jupyter code cell installs packages directly into the Python environment your notebook uses. You get all three libraries in a single command.
- Confirm the installation succeeded by running this version check in a new code cell:
import sklearn
print(sklearn.__version__)
You should see 1.9.0 printed as the output.
Seeing an error during install?
- Make sure you are running the cell as code, not as markdown.
- If you see a permission error, try adding --user to the end of the install command.
- help me troubleshoot my pip install error in Jupyter
Download the dataset
You will use the "Superstore Sales: The Data Quality Challenge" dataset from Kaggle. It contains 25,000 rows of retail sales data with intentional data quality problems baked in.
- Open the dataset page: Superstore Sales: The Data Quality Challenge on Kaggle.
- Click the Download button near the top of the page.
- Unzip the downloaded file.
- Place both Superstore_Orders_q1.csv and Superstore_Orders_q2.csv in the same folder where your Jupyter notebook is saved.
Why two CSV files?
The dataset splits transactions across two quarterly files. This mimics how real companies store data in separate exports. You will combine them in the next cell.
Not sure where your notebook is saved?
- In Jupyter, look at the browser tab or the top of the page for the notebook filename and path.
- Place the CSV files in the same directory shown in that path.
- help me figure out where to put my CSV files so Jupyter can find them
Import libraries and load the data
Now you will import all the libraries you need for this project and load the dataset to confirm everything connects.
- Create a new code cell and paste the following imports:
# Cell 1: Setup and imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.dummy import DummyClassifier
What does this code do?
- pandas and numpy handle data manipulation and numeric operations.
- matplotlib.pyplot creates charts for visualizing data quality issues.
- The sklearn imports bring in the building blocks for your preprocessing pipeline: splitting data, imputing missing values, scaling numbers, encoding categories, and training a classifier.
- Run the cell.
You should see no output and no errors. A clean run means all imports resolved correctly.
- In a new code cell, load both CSVs and print a quick summary by pasting this code:
# Cell 2: Load and combine data
df_q1 = pd.read_csv("Superstore_Orders_q1.csv")
df_q2 = pd.read_csv("Superstore_Orders_q2.csv")
df = pd.concat([df_q1, df_q2], ignore_index=True)
print(f"Shape: {df.shape}")
print(f"\nData types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nCategory unique values: {df['Category'].unique()}")
What does this code do?
- pd.read_csv() loads each quarterly CSV into a DataFrame.
- pd.concat() stacks both DataFrames vertically into one combined dataset.
- The print statements give you a snapshot of the dataset shape, column types, missing value counts, and the unique Category values.
Before you run this cell, what do you expect the shape to look like for a 25,000-row dataset with 21 columns?
- Run the cell.
You should see the shape printed as approximately (25000, 21). You will also see the Category column showing inconsistent values like "Tech", "technologies", and "Furni".
Getting a FileNotFoundError?
- Double check that Superstore_Orders_q1.csv and Superstore_Orders_q2.csv are in the same directory as your notebook file.
- The filenames are case-sensitive. Make sure the capitalization matches exactly.
- help me fix a FileNotFoundError when loading CSVs in Jupyter
✔️ Awesome, I've got everything!
Great. Make sure both cells are saved in your notebook before moving on.
ⓧ I'd like to double check the full code
Here is the complete code you should have across your two cells so far:
# Cell 1: Setup and imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.dummy import DummyClassifier
# Cell 2: Load and combine data
df_q1 = pd.read_csv("Superstore_Orders_q1.csv")
df_q2 = pd.read_csv("Superstore_Orders_q2.csv")
df = pd.concat([df_q1, df_q2], ignore_index=True)
print(f"Shape: {df.shape}")
print(f"\nData types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nCategory unique values: {df['Category'].unique()}")
Your environment is ready and the data is loaded. Next up, you will profile this dirty dataset to understand exactly what is broken before you try to fix anything.
Profile the Dirty Data
Your libraries are installed and the dataset is downloaded. Before you clean anything, you need to understand exactly what is broken.
Data profiling is the diagnostic step that reveals the specific issues you will fix. Without it, you would guess at preprocessing strategies instead of making informed decisions about imputation, encoding, and type conversion.
In this step, get ready to:
- Run a data quality audit on the combined dataset.
- Identify five categories of data quality problems.
- Visualize the missing value pattern with a bar chart.
Run the data quality audit
Cell 2 in your notebook already combines both CSV files and prints diagnostic information. The output from this cell is your data quality audit.
- In your notebook, scroll to Cell 2 and check the output that printed when you ran it.
- Look at four things: the overall shape (total rows and columns), the data types of each column, the count of missing values per column, and the unique values in the Category column.
Before you read on, what do you notice about the Profit column's dtype? What about the Category values?
What should I see?
The shape shows approximately 25,000 rows and 21 columns. Under data types, the Profit column shows as object instead of float64. This means pandas read it as a string, not a number.
The missing values output shows non-zero counts for Profit, Discount, and Region. The Category unique values include inconsistent entries like Tech, technologies, Furni, and OfficeSupply alongside proper names like Technology and Furniture.
Identify the five categories of data problems
The audit output reveals five distinct categories of data quality issues. Each one would cause a model to crash or produce wrong results if left unfixed.
- Profit column stored as string (object dtype) because the values contain commas as thousand separators.
- Order Date and Ship Date stored as text strings instead of proper datetime objects.
- Category field has typos and variations. Multiple spellings map to the same category.
- Missing values in Profit, Discount, and Region columns (Region also contains -- placeholders that pandas does not recognize as NaN).
- 200+ duplicate rows that inflate the dataset and skew statistics.
Why does the Profit column show as object?
When a CSV contains numbers formatted with commas (like 1,234.56), pandas cannot parse them as floats. It falls back to storing the entire column as strings. This is the most common cause of unexpected object dtypes in numeric-looking columns.
Visualize the missing data pattern
Numbers in a table are hard to compare at a glance. A bar chart makes it immediately obvious which columns have the worst missing data problems and how they compare to each other.
- Create a new cell (Cell 3) in your notebook and add the following code to visualize missing values as percentages:
# Cell 3: Visualize missing data
missing_pct = df.isnull().sum() / len(df) * 100
missing_pct = missing_pct[missing_pct > 0].sort_values(ascending=False)
plt.figure(figsize=(10, 5))
missing_pct.plot(kind="bar")
plt.title("Percentage of Missing Values by Column")
plt.ylabel("% Missing")
plt.tight_layout()
plt.show()
What does this code do?
- df.isnull().sum() / len(df) * 100 converts raw missing counts into percentages of total rows, making columns with different scales comparable.
- The filter missing_pct[missing_pct > 0] keeps only columns that actually have missing data, so the chart is not cluttered with zero bars.
- sort_values(ascending=False) orders them from worst to least, so the biggest problems jump out first.
What do you think the chart will show? Which columns do you expect to have the highest percentage of missing values?
- Run Cell 3.
You see a bar chart titled "Percentage of Missing Values by Column" with bars for the columns that have missing data. Profit, Discount, and Region each show their respective percentages.
Not seeing the chart?
- Make sure you ran Cell 2 before Cell 3. Cell 3 depends on the df variable created in Cell 2.
- Check that plt.show() is the last line. In some Jupyter configurations, the chart only renders if show() is called explicitly.
- If you see a NameError: name 'df' is not defined, restart your kernel and run all cells from the top in order.
Help me debug my missing data visualization.
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook with all three cells.
ⓧ I'd like to double check the full code
Here is the complete code for your notebook so far. Scroll through and confirm each cell matches:
# Cell 1: Setup and imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.dummy import DummyClassifier
# Cell 2: Load and combine data
df_q1 = pd.read_csv("Superstore_Orders_q1.csv")
df_q2 = pd.read_csv("Superstore_Orders_q2.csv")
df = pd.concat([df_q1, df_q2], ignore_index=True)
print(f"Shape: {df.shape}")
print(f"\nData types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nCategory unique values: {df['Category'].unique()}")
# Cell 3: Visualize missing data
missing_pct = df.isnull().sum() / len(df) * 100
missing_pct = missing_pct[missing_pct > 0].sort_values(ascending=False)
plt.figure(figsize=(10, 5))
missing_pct.plot(kind="bar")
plt.title("Percentage of Missing Values by Column")
plt.ylabel("% Missing")
plt.tight_layout()
plt.show()
You now have a complete picture of what is broken in this dataset. Next up, you will try feeding this dirty data directly into a model to see exactly why these problems are not just cosmetic.
Attempt a Naive Model
You've profiled the data and identified five categories of quality problems. But do those problems actually prevent you from building a model? Maybe scikit-learn can handle a few messy values on its own.
In this step, you'll find out the hard way. You'll try to feed the raw, dirty data directly into a machine learning model with zero preprocessing. The result will teach you more about why preprocessing matters than any explanation ever could.
In this step, get ready to:
- Define a prediction task (classifying transactions as profitable or not).
- Attempt to fit a model on the raw data without any cleaning.
- Observe and document why the model crashes.
Define the prediction task
Every machine learning model needs a clear target to predict. Your goal is a simple binary classification: given a transaction, is it profitable or not?
- Target variable: is_profitable (1 if Profit > 0, else 0).
- Features: Sales, Quantity, Discount, Category, and Region.
Sounds straightforward. To create the target, you just need to check whether each row's Profit value is greater than zero. Let's try it.
Attempt to create the target from raw data
Remember from your profiling that the Profit column is stored as strings (object dtype) because of comma formatting. You're about to try a numeric comparison on those strings.
- Create a new cell (Cell 4) in your notebook and add the following code:
# Cell 4: Naive model attempt (this will fail)
target = (df["Profit"] > 0).astype(int) # This crashes - Profit is a string!
Before you run this cell, what do you think will happen? The Profit column contains strings like "1,245.67". Can Python compare a string to the number 0 using >?
- Run the cell now.
Observe and document the crash
Your cell throws a TypeError. Python cannot compare a string to an integer with the > operator. The model never even gets a chance to fit.
Why does this crash?
The Profit column has dtype object because values contain commas (e.g. "1,245.67"). Python does not know how to evaluate whether a string is greater than an integer.
This is only the first wall. Even if you fixed the target, the model would crash again because Category and Region are text columns that scikit-learn's RandomForestClassifier cannot accept. And even after that, the missing values (NaN) in multiple columns would cause yet another failure.
Three layers of problems, each one enough to crash the model on its own. This is exactly why preprocessing is not optional.
This crash is intentional. You now have firsthand proof that dirty data breaks models. The next step is to clean the structural issues so you can eventually build a working pipeline.
- Update your Cell 4 to comment out the failing line so your notebook stays runnable. Replace the cell contents with this:
# Cell 4: Naive model attempt (this will fail)
# Uncomment to see the error:
# from sklearn.ensemble import RandomForestClassifier
# target = (df["Profit"] > 0).astype(int) # This crashes - Profit is a string!
What does this commented cell do?
The commented code serves as documentation. Anyone reading your notebook later can see exactly what you tried, why it failed, and uncomment it to reproduce the error themselves. This is a standard practice in exploratory data science notebooks.
- Save your notebook.
- Confirm Cell 4 runs without errors now that the code is commented out.
Still seeing an error after commenting?
Make sure every line of code in Cell 4 starts with #. If any line is left uncommented, the cell will still try to execute it.
help me fix errors in my Jupyter Notebook cell
✔️ Awesome, I've got everything!
Great. Double check you've saved your notebook with Cell 4 commented out.
ⓧ I'd like to double check the full code
Your notebook should have four cells in this order. Scroll up and confirm each is present:
- Cell 1: Setup and imports (pandas, numpy, matplotlib, sklearn imports).
- Cell 2: Load and combine data (reads both CSVs, concatenates, prints shape and dtypes).
- Cell 3: Visualize missing data (bar chart of missing value percentages).
- Cell 4: Naive model attempt (all code lines commented out with #).
You've just proven that this dataset is too broken for any model to handle as-is. Three separate issues block you: string-typed numbers, unencoded categoricals, and missing values. In the next step, you'll roll up your sleeves and fix the structural problems with pandas so the data is at least loadable by scikit-learn.
Clean the Data with Pandas
Your naive model crashed because the raw data has fundamental type issues that scikit-learn cannot handle.
In this step, you will fix the problems that prevent the data from even being loaded into a model. You will parse the Profit column from string to numeric, standardize messy category labels, remove duplicates, and create your prediction target.
In this step, get ready to:
- Parse the Profit column and date columns into their correct data types.
- Standardize the Category column and remove duplicate rows.
- Create the target variable for your classification task.
Fix the Profit and date columns
The Profit column is stored as a string because the values contain commas (e.g. "1,234.56"). You need to strip those commas and convert the column to a numeric type before you can do any math on it.
- Create a new cell in your notebook below Cell 4 and add the following code to fix Profit and parse the date columns:
# Fix Profit column - remove commas and convert to float
df["Profit"] = df["Profit"].astype(str).str.replace(",", "", regex=False)
df["Profit"] = pd.to_numeric(df["Profit"], errors="coerce")
# Parse date columns
df["Order Date"] = pd.to_datetime(df["Order Date"], format="mixed", dayfirst=False)
df["Ship Date"] = pd.to_datetime(df["Ship Date"], format="mixed", dayfirst=False)
What does this code do?
The Profit fix happens in two stages. First, .str.replace(",", "") strips out the comma characters that were preventing numeric conversion. Then pd.to_numeric(..., errors="coerce") converts the cleaned strings to floats, turning any remaining unparseable values into NaN instead of crashing.
The date columns use format="mixed" because the dataset contains inconsistent date formats. This tells pandas to try multiple parsers automatically.
- Run the cell.
- In a new cell below, verify the types changed by running:
print(df[["Profit", "Order Date", "Ship Date"]].dtypes)
- Run the cell. You should see float64 for Profit and datetime64[ns] for both date columns.
Still seeing object type for Profit?
Make sure you ran Cell 2 (the cell that loads and combines the DataFrames) before running your cleaning cell. If you restarted your kernel, you need to re-run all cells from the top.
help me debug why my Profit column is still showing as object type after running pd.to_numeric
Standardize the Category column
Your profiling revealed that the Category column has typos and abbreviations like "Tech", "technologies", "Furni", and "OfficeSupply". You need to map all these variations to three canonical values.
- In a new cell, add this mapping dictionary and apply it to the Category column:
# Standardize Category column
category_mapping = {
"Tech": "Technology",
"technologies": "Technology",
"Technology": "Technology",
"Furni": "Furniture",
"Furniture": "Furniture",
"OfficeSupply": "Office Supplies",
"Office Supplies": "Office Supplies",
}
df["Category"] = df["Category"].map(category_mapping)
What does this code do?
The category_mapping dictionary maps every known variant to its canonical form. Calling .map() replaces each value in the column with its mapped result.
Any value not in the dictionary becomes NaN. This is intentional. It flags unexpected categories rather than silently passing them through.
- Run the cell, then verify the standardization worked by running this in a new cell:
print(df["Category"].unique())
- Run the cell. You should see exactly three values: Technology, Furniture, and Office Supplies (plus possibly nan for any unmapped entries).
Seeing more than 3 categories?
Double-check that every key in your category_mapping dictionary matches the exact spelling and casing shown in your profiling output from Step 2. Python string matching is case-sensitive.
help me figure out why my category mapping didn't standardize all values
Remove duplicates and fix the Region column
The Region column contains "--" strings and empty values that need to become NaN so the imputer can handle them later in the pipeline. You also have 200+ exact duplicate rows to remove.
- In a new cell, add this code to fix Region and drop duplicates:
# Fix Region - replace blank and "--" with NaN
df["Region"] = df["Region"].replace({"--": np.nan, "": np.nan})
# Remove duplicates
rows_before = len(df)
df = df.drop_duplicates()
print(f"Removed {rows_before - len(df)} duplicate rows")
Why replace with NaN instead of dropping?
Region still has useful rows where other columns are complete. By replacing the placeholder strings with NaN, you preserve those rows and let the SimpleImputer fill in the most frequent region later.
Dropping the rows entirely would mean losing data unnecessarily. The pipeline you build in the next step handles NaN values automatically.
- Run the cell. You should see output like Removed 200 duplicate rows (the exact number may vary slightly).
Seeing 0 duplicates removed?
Make sure you are calling drop_duplicates() on the full DataFrame df, not on a filtered subset. Also confirm you have run Cell 2 (the cell that concatenates both CSV files) earlier in this session.
help me figure out why drop_duplicates is removing 0 rows from my DataFrame
Create the target variable
Your classification task is predicting whether a transaction is profitable. You need a binary target column: 1 if Profit is greater than 0, and 0 otherwise.
- In a new cell, create the target variable and finalize the DataFrame:
# Create binary target: is the transaction profitable?
df["is_profitable"] = (df["Profit"] > 0).astype(int)
# Drop rows where Profit was NaN (target must be known)
df = df.dropna(subset=["Profit"])
print(f"Final shape: {df.shape}")
print(f"Target distribution:\n{df['is_profitable'].value_counts(normalize=True)}")
Why drop rows where Profit is NaN?
The target variable is_profitable is derived from Profit. If Profit is NaN, you cannot know whether the transaction was profitable or not.
For features (like Region), missing values can be imputed. But a missing target is fundamentally unknowable. Those rows must be excluded from training.
- Run the cell. You should see the final shape printed (the row count will be lower than the original due to dropped NaN rows and duplicates) along with the target distribution showing the proportion of profitable vs. non-profitable transactions.
Before you run the final verification, what do you expect df.dtypes to show for the Profit column now?
- Confirm everything is clean by running this in a new cell:
print(df.dtypes)
print(f"\nCategory unique values: {df['Category'].unique()}")
- Run the cell. Profit shows as float64, Category shows exactly 3 unique standardized values (Technology, Furniture, Office Supplies), and your row count is lower than the original 25,000 due to duplicate and NaN removal.
Profit still showing as object?
This usually means the cleaning cell did not run successfully, or your kernel restarted and you forgot to re-run cells in order. In Jupyter, click **Kernel** in the menu bar, then **Restart & Run All** to execute every cell from the top.
help me debug why my DataFrame dtypes are not reflecting my cleaning steps
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook with all cells run in order.
ⓧ I'd like to double check the full code
Your notebook should now contain the following cells in order. Scroll up and confirm each one is present.
- Cell 1: Setup and imports (pandas, numpy, matplotlib, sklearn).
- Cell 2: Load and combine data (read both CSVs, concat, print shape and dtypes).
- Cell 3: Visualize missing data (bar chart of missing percentages).
- Cell 4: Naive model attempt (commented-out code that crashes).
- Cell 5: Clean structural issues (fix Profit, parse dates, standardize Category, fix Region, remove duplicates).
- Cell 6: Create target and final cleaning (is_profitable column, drop NaN Profit rows, print shape and distribution).
If you combined the cleaning code into one cell or split it across several, that is fine. The important thing is that all transformations from Cell 5 and Cell 6 above are present and have been run.
Your data is now structurally sound. Next up, you will build a scikit-learn Pipeline that handles imputation, encoding, and scaling without leaking test data into training.
Build the Leak-Free Pipeline
Your data is clean, but there is one more trap waiting. If you impute missing values or scale features using the full dataset before splitting, the model "sees" test information during training. This is called data leakage, and it produces overly optimistic scores that collapse on real-world data.
The scikit-learn Pipeline with ColumnTransformer solves this by guaranteeing that all transformations are fitted only on training data. In this step, you will build that pipeline and prove it outperforms a naive baseline.
In this step, get ready to:
- Define numeric and categorical sub-pipelines that handle each feature type separately.
- Assemble the full Pipeline, split the data, and fit on training data only.
- Cross-validate the pipeline and compare its accuracy against a baseline.
Define your features and build sub-pipelines
Your dataset has two kinds of features. Numeric columns like Sales and Quantity need imputation and scaling. Categorical columns like Category and Region need imputation and one-hot encoding.
- Create a new cell in your notebook and define the feature columns and sub-pipelines by pasting this code:
# Cell 7: Define features and build pipeline
feature_cols = ["Sales", "Quantity", "Discount", "Category", "Region", "Segment", "Ship Mode"]
X = df[feature_cols]
y = df["is_profitable"]
numeric_features = ["Sales", "Quantity", "Discount"]
categorical_features = ["Category", "Region", "Segment", "Ship Mode"]
# Numeric sub-pipeline: impute then scale
numeric_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
# Categorical sub-pipeline: impute then encode
categorical_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
What does this code do?
- feature_cols lists the 7 columns the model will use as inputs. X holds the features and y holds the target.
- numeric_transformer fills missing numeric values with the median, then scales them to have zero mean and unit variance.
- categorical_transformer fills missing categories with the most frequent value, then converts each category into binary columns. The handle_unknown="ignore" parameter prevents errors if new categories appear at prediction time.
- Run the cell.
- The cell completes without output or errors, confirming that all column names in feature_cols exist in your cleaned DataFrame.
Seeing a KeyError?
A KeyError means one of the column names does not match your DataFrame. Check that your Cell 5 cleaning code ran successfully and that you did not rename any columns.
Help me debug a KeyError in my feature column definitions.
Assemble the full pipeline and train it
Now you combine both sub-pipelines into a single ColumnTransformer that routes each column to the correct transformer. Then you chain that with a classifier to create the full Pipeline.
Critically, you split the data BEFORE calling fit. The Pipeline ensures that imputation and scaling only learn from the training rows.
- Create a new cell and build the full pipeline, split your data, and train it by pasting this code:
# Combine with ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
("num", numeric_transformer, numeric_features),
("cat", categorical_transformer, categorical_features),
])
# Full pipeline: preprocessing + model
pipeline = Pipeline(steps=[
("preprocessor", preprocessor),
("classifier", RandomForestClassifier(n_estimators=100, random_state=42)),
])
# Cell 8: Train-test split and evaluate
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
pipeline.fit(X_train, y_train)
test_score = pipeline.score(X_test, y_test)
print(f"Test accuracy: {test_score:.4f}")
What does this code do?
- preprocessor is the ColumnTransformer. It sends the 3 numeric columns through numeric_transformer and the 4 categorical columns through categorical_transformer.
- pipeline chains preprocessing with a RandomForestClassifier. When you call pipeline.fit(X_train, y_train), every transformer inside learns ONLY from training data.
- train_test_split holds out 20% of rows for testing. The stratify=y argument keeps the profitable/unprofitable ratio equal across both sets.
Why split BEFORE fitting?
If you call fit on the full dataset, the imputer learns the median from ALL rows (including test). When you later score on the test set, the model has already "seen" that data through the imputed values. This inflates accuracy and hides real-world performance. The Pipeline guarantees fit only touches training rows.
- Run the cell.
- You should see a line like Test accuracy: 0.7XXX printed (the exact value depends on the data split, but expect something in the 70-80% range).
Seeing an error instead of accuracy?
- If you see a NameError for numeric_transformer or categorical_transformer, make sure you ran the previous cell (Cell 7) first.
- If you see a ValueError about column shapes, confirm your Cell 6 cleaning code ran and df has the expected columns.
Help me fix my pipeline training error.
Cross-validate and compare to a baseline
A single train/test split can be misleading. The accuracy depends on which rows landed in which set. Cross-validation runs the full pipeline 5 times, each time holding out a different 20% slice, giving you a more reliable estimate.
You will also compare against a DummyClassifier that always predicts the most common class. If your pipeline cannot beat this baseline, the features are not helping.
- Before you run the next cell, think: what accuracy would a model get if it just predicted the majority class every time? Look at the target distribution you printed earlier to estimate.
- Create a new cell and run cross-validation with a baseline comparison by pasting this code:
# Cell 9: Cross-validation
cv_scores = cross_val_score(pipeline, X, y, cv=5, scoring="accuracy")
print(f"Cross-validated accuracy: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")
# Baseline comparison
baseline = DummyClassifier(strategy="most_frequent")
baseline_scores = cross_val_score(baseline, X, y, cv=5, scoring="accuracy")
print(f"Baseline accuracy: {baseline_scores.mean():.4f}")
print(f"Pipeline improvement over baseline: {(cv_scores.mean() - baseline_scores.mean()) * 100:.1f} percentage points")
What does this code do?
- cross_val_score splits the data into 5 folds, trains and evaluates the full pipeline on each fold, and returns 5 accuracy scores. The mean gives you a robust estimate.
- DummyClassifier(strategy="most_frequent") always predicts the majority class. It sets a floor: your pipeline must beat this to prove it learned something useful from the features.
- The improvement line calculates how many percentage points your pipeline exceeds the naive baseline.
- Run the cell.
- You should see three lines printed: the cross-validated accuracy (expect ~70-80%), the baseline accuracy (the majority class proportion), and the improvement in percentage points.
- Confirm the pipeline improvement over baseline is a positive number. This proves your preprocessing and model are learning real patterns, not just memorizing the majority class.
Cross-validation taking a very long time?
cross_val_score fits the pipeline 5 times from scratch. On a ~25,000-row dataset with a Random Forest, this may take 15-30 seconds. If it runs for more than a minute, check that your DataFrame does not contain many more rows than expected (run len(df) to verify).
Help me understand why cross-validation is slow.
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook with all cells executed.
ⓧ I'd like to double check the full code
Your notebook should contain these cells in order. Scroll up and confirm each one is present:
- Cell 1: Setup and imports (pandas, numpy, matplotlib, sklearn classes).
- Cell 2: Load and combine both CSVs into df.
- Cell 3: Visualize missing data with a bar chart.
- Cell 4: Naive model attempt (commented out).
- Cell 5: Clean structural issues (Profit parsing, dates, Category mapping, Region fix, deduplication).
- Cell 6: Create target variable and drop NaN Profit rows.
- Cell 7: Define features, build numeric and categorical sub-pipelines.
- Cell 8: ColumnTransformer + full Pipeline + train_test_split + fit + score (prints test accuracy).
- Cell 9: Cross-validation scores + DummyClassifier baseline comparison (prints 3 lines).
You now have a complete, leak-free preprocessing pipeline that demonstrably outperforms a naive baseline. Next up, you will take this pipeline further in the Secret Mission by tuning its hyperparameters and exporting it for reuse.
Secret mission
Tune and Export Your Pipeline
Your pipeline works, but can it be better? Take your preprocessing pipeline from functional to interview-ready by tuning hyperparameters across the entire pipeline and saving it for reuse on any new data.
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 as a portfolio piece or delete them entirely.
Resources you used:
- Dataset CSV files (Superstore_Orders_q1.csv and Superstore_Orders_q2.csv) in your working directory.
- Saved pipeline file (pipeline.joblib) on your local disk.
- Jupyter notebook file(s) containing your preprocessing pipeline code.
- Python packages installed via pip (scikit-learn, pandas, matplotlib).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with the pipeline or want to keep the notebook as a portfolio piece.
- Your dataset files, notebook, and saved pipeline remain in your working directory.
- The scikit-learn, pandas, and matplotlib packages remain installed in your Python environment.
Pause - I'll come back to this later
Shut down Jupyter to free up memory. Your files stay on disk for when you return.
- In your Jupyter notebook, click Kernel then Shut Down All Kernels to free memory.
- Close the Jupyter browser tab.
- In the terminal where Jupyter is running, press Ctrl+C to stop the Jupyter server.
Delete - I don't want to use this again
Remove all project files and uninstall the packages you added.
- Shut down the Jupyter kernel and server (see Pause tab above).
- Delete the dataset files and saved pipeline by running this in your terminal:
rm Superstore_Orders_q1.csv Superstore_Orders_q2.csv pipeline.joblib
- Delete your Jupyter notebook file(s) from the same directory.
- Uninstall the Python packages by running:
pip uninstall scikit-learn pandas matplotlib -y
- Confirm the files are removed by running:
ls
You should no longer see Superstore_Orders_q1.csv, Superstore_Orders_q2.csv, or pipeline.joblib in the output.
Nice Work!
Nice Work!
You just built a production-ready preprocessing pipeline that takes messy, real-world retail data and transforms it into clean, model-ready features. You experienced the crash, diagnosed the problems, and solved them with a leak-free approach that any interviewer would respect.
You've learned how to:
- Profile a 25,000-row dataset to identify data quality issues like type mismatches, inconsistent categories, missing values, and duplicates.
- Build a leak-free scikit-learn Pipeline with ColumnTransformer that handles numeric and categorical features separately, fitting only on training data.
- Prove your pipeline adds real value by comparing cross-validated accuracy scores against a DummyClassifier baseline.
- Secret Mission: Tuned hyperparameters across the full pipeline with GridSearchCV and exported a reusable pipeline with joblib.
Ready to quiz yourself?