🎯 What You'll Learn

  • Why preprocessing is essential, and which algorithms are sensitive to feature scale
  • Encode categorical variables using LabelEncoder, OrdinalEncoder, and OneHotEncoder — and when to use each
  • Scale numerical features with StandardScaler, MinMaxScaler, and RobustScaler
  • Apply the golden rule: fit on training data only, then transform both train and test
  • Build robust preprocessing pipelines with sklearn.Pipeline and ColumnTransformer
⚙️
Setup

All tools in this lesson are part of scikit-learn: pip install scikit-learn pandas numpy. Import conventions used throughout: import pandas as pd, import numpy as np, from sklearn.preprocessing import ....

1 Why Preprocessing Matters

Raw data rarely comes in a form that ML algorithms can directly consume. Text categories, missing values, wildly different numerical scales — these all need to be addressed before training. But beyond convenience, preprocessing is mathematically necessary for many algorithm families:

(The algorithm names below are a preview of Lessons 11–28 — you don't need to know them yet. What matters is that every algorithm falls into one of these families, and each family has different preprocessing needs.)

  • Distance-based algorithms (e.g., KNN — Lesson 16): these compute distances between data points. If one feature ranges 0–100 and another ranges 0–0.001, the first feature dominates every distance calculation — the second is effectively ignored. Scaling is essential.
  • Gradient-based algorithms (e.g., linear and logistic regression — Lessons 11, 15): gradients are proportional to feature magnitudes. Features with large scales produce large gradients, causing instability and slow convergence. Scaling smooths the loss landscape.
  • Tree-based algorithms (Lessons 22–24): these split on individual feature thresholds and are completely scale-invariant. Scaling doesn't help or hurt them.
  • Algorithms that assume numeric input: virtually all sklearn estimators require numeric arrays. Categorical text columns must be encoded before training.
🔑
The Core Rule

All preprocessing transformers must be fit on the training set only and then applied to both train and test sets. Fitting on the full dataset contaminates training with test statistics — a form of data leakage that inflates evaluation metrics. Using sklearn Pipelines enforces this rule automatically.

2 Categorical Encoding

Machine learning models work with numbers. Categorical features — "city", "product_type", "education_level" — must be converted to numeric representations. The method you choose depends on the nature of the category.

Label Encoding (for target variable or binary categories)

Maps each category to an integer. Appropriate for the target variable in classification, and for truly binary categories (yes/no). Never use for nominal features with more than 2 categories in tree-free models — it imposes a false ordinal relationship.

In [1]:
from sklearn.preprocessing import LabelEncoder
import pandas as pd

le = LabelEncoder()
y = ['cat', 'dog', 'cat', 'fish', 'dog']
y_encoded = le.fit_transform(y)
print(y_encoded)         # [0 1 0 2 1]
print(le.classes_)       # ['cat' 'dog' 'fish']

# Inverse: recover original labels from predictions
print(le.inverse_transform([0, 2]))  # ['cat' 'fish']

Ordinal Encoding (for ordered categories)

When a categorical variable has a meaningful order (Small < Medium < Large, Bronze < Silver < Gold), OrdinalEncoder lets you specify that order explicitly.

In [2]:
from sklearn.preprocessing import OrdinalEncoder

oe = OrdinalEncoder(categories=[['Low', 'Medium', 'High']])
X = [['Low'], ['High'], ['Medium'], ['Low'], ['High']]
X_encoded = oe.fit_transform(X)
print(X_encoded.ravel())   # [0. 2. 1. 0. 2.]
# Low→0, Medium→1, High→2  — order is preserved

One-Hot Encoding (for nominal categories)

For categories without natural ordering (city names, product types, job titles), one-hot encoding creates a new binary column for each category. A row gets a 1 in the column for its category and 0 in all others. This avoids the false ordinal relationship.

In [3]:
from sklearn.preprocessing import OneHotEncoder
import numpy as np

# Nominal feature: no natural order between cities
cities = [['London'], ['Paris'], ['Tokyo'], ['London'], ['Berlin']]
ohe = OneHotEncoder(sparse_output=False)  # dense matrix for readability
encoded = ohe.fit_transform(cities)

print(ohe.categories_)          # [array(['Berlin', 'London', 'Paris', 'Tokyo'])]
print(encoded)
# [[0. 1. 0. 0.]   # London
#  [0. 0. 1. 0.]   # Paris
#  [0. 0. 0. 1.]   # Tokyo
#  [0. 1. 0. 0.]   # London
#  [1. 0. 0. 0.]]  # Berlin

# Pandas equivalent for quick exploration
df = pd.DataFrame({'city': ['London', 'Paris', 'Tokyo', 'London']})
print(pd.get_dummies(df, drop_first=True))
#    city_Paris  city_Tokyo  city_London  (Berlin dropped as reference)
# Use drop_first=True to avoid the dummy variable trap
Before: 1 categorical column After: one-hot encoded (4 binary columns) city Row 0 London Row 1 Paris Row 2 Tokyo Row 3 London Row 4 Berlin OneHotEncoder city_Berlin city_London city_Paris city_Tokyo 0 London = 11 0 0 0 0 Paris = 11 0 0 0 0 Tokyo = 11 0 London = 11 0 0 Berlin = 11 0 0 0 Each row gets exactly one 1 — in the column matching its original category, 0 elsewhere. With drop='first' (or drop_first=True), the city_Berlin column is removed as the reference category.

One-hot encoding the cities column from the code above: 1 nominal column with 4 categories becomes 4 binary columns. Hover a cell to see which row/category it represents.

⚠️
The Dummy Variable Trap

If you have k categories and create k binary columns, the last column is perfectly predictable from the others — they always sum to 1. This creates multicollinearity, which destabilises linear models. The fix: drop one category column (use drop='first' in OneHotEncoder or drop_first=True in pd.get_dummies). Tree-based models are immune — you can skip drop='first' for them.

Handling Unseen Categories

In [4]:
# OneHotEncoder by default raises an error on unseen categories in test set.
# Use handle_unknown='ignore' to replace unseen categories with all zeros.
ohe = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
ohe.fit(X_train_cats)
# Test data might have a city the model never saw in training:
X_test_transformed = ohe.transform(X_test_cats)   # no error — unseen → all zeros

3 Feature Scaling

Feature scaling transforms numerical features so they occupy comparable ranges. The three most important scalers in scikit-learn each have different assumptions and use cases:

StandardScaler — z-score normalization

Centers each feature to mean=0 and standard deviation=1. The resulting values are called z-scores. This is the default choice for most gradient-based and distance-based algorithms.

In [5]:
from sklearn.preprocessing import StandardScaler
import numpy as np

X = np.array([[1000, 2],
              [2000, 3],
              [3000, 5],
              [4000, 4]])

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print("Means:  ", scaler.mean_)         # [2500.    3.5]
print("Std:    ", scaler.scale_)        # [1118.03  1.12]
print("Scaled:\n", X_scaled)
# [[-1.342 -1.342]
#  [-0.447 -0.447]
#  [ 0.447  1.342]
#  [ 1.342  0.447]]
# Each column now has mean≈0 and std=1

MinMaxScaler — range normalization

Scales features to a fixed range, default [0, 1]. Preserves zero values and is useful when you need bounded outputs (e.g., feeding into a sigmoid layer or image pixel normalization).

In [6]:
from sklearn.preprocessing import MinMaxScaler

mms = MinMaxScaler(feature_range=(0, 1))
X_mms = mms.fit_transform(X)
print(X_mms)
# [[0.    0.   ]
#  [0.333 0.333]
#  [0.667 1.   ]
#  [1.    0.667]]
# Values now in [0, 1] for both columns

# MinMaxScaler is sensitive to outliers — one extreme value compresses everything else
# e.g., if one house sold for $50M in a dataset of typical homes, all others get squished near 0

RobustScaler — median and IQR normalization

Uses the median and interquartile range (IQR) instead of mean and standard deviation. Since median and IQR are insensitive to outliers, this scaler is the right choice when your data contains significant outliers you want to preserve (not remove).

In [7]:
from sklearn.preprocessing import RobustScaler

X_with_outliers = np.array([[1, 2], [2, 3], [3, 4], [4, 5], [100, 6]])
rs = RobustScaler()
X_robust = rs.fit_transform(X_with_outliers)

print("Median: ", rs.center_)    # [3. 4.]
print("IQR:    ", rs.scale_)     # [2. 2.]
print(X_robust)
# [[-1.   -1.  ]
#  [-0.5  -0.5 ]
#  [ 0.    0.  ]
#  [ 0.5   0.5 ]
#  [48.5   1.  ]]  ← outlier (100) still stands out but doesn't crush other values
Scaler Formula Output range When to use
StandardScaler(x − μ) / σUnbounded, mean=0 std=1⭐ Default for linear models, SVMs, KNN, PCA
MinMaxScaler(x − min) / (max − min)[0, 1] (or custom)Image data, neural network input layers
RobustScaler(x − median) / IQRUnbounded, robust to outliersData with significant outliers you want to keep

Scaling changes the axis a feature lives on, but it never changes the shape of its distribution. Below is a simulated sqft feature (modeled on the house-size column used throughout this lesson) shown raw, then after each scaler — notice the histogram outline stays identical, only the x-axis numbers move.

Before — raw sqft values

After — StandardScaler output

4 The Critical Rule: Fit on Train Only

This is the most commonly violated rule in data preprocessing, and it produces the most insidious bugs — because the code runs fine, evaluations look great, and the problem only surfaces in production.

The Wrong Way (Data Leakage)

In [8]:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np

X = np.random.randn(1000, 5)
y = np.random.randint(0, 2, 1000)

# ❌ WRONG — fit on the FULL dataset before splitting
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)           # uses test statistics!
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)
# The test set's mean and std leaked into the scaler
# Evaluation metrics are optimistically biased

The Right Way

In [9]:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np

X = np.random.randn(1000, 5)
y = np.random.randint(0, 2, 1000)

# ✓ CORRECT — split FIRST, then fit scaler on training data only
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)   # learns mean/std FROM TRAIN ONLY
X_test_scaled  = scaler.transform(X_test)         # applies train statistics to test

# The distinction between fit_transform and transform:
# fit_transform(X_train) = fit(X_train) → transform(X_train)  [computes AND applies]
# transform(X_test)      = apply training stats to test data   [applies only]
print("Train scaler mean:", scaler.mean_)   # computed from training set only
⚠️
Why This Actually Matters

If you scale using the full dataset, the scaler learns statistics (mean, std) that include test data. During evaluation, the model "saw" test statistics during preprocessing — it's like giving away answers before the exam. The model appears to generalize better than it truly does. When deployed on genuinely new data, it performs worse than expected.

5 Train/Test Split Best Practices

In [10]:
from sklearn.model_selection import train_test_split
import pandas as pd

df = pd.read_csv('house_prices.csv')
X = df.drop('price', axis=1)
y = df['price']

# Basic split
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,      # 80% train, 20% test
    random_state=42,    # reproducibility — always set this
)
print(X_train.shape, X_test.shape)  # (1168, 79) (292, 79)

# For CLASSIFICATION — use stratify to preserve class proportions
df_clf = pd.read_csv('churn.csv')
X_clf = df_clf.drop('churned', axis=1)
y_clf = df_clf['churned']

X_train, X_test, y_train, y_test = train_test_split(
    X_clf, y_clf,
    test_size=0.2,
    random_state=42,
    stratify=y_clf,     # ensures train/test have same churn rate
)

# Verify stratification worked
print("Train churn rate:", y_train.mean())   # e.g., 0.142
print("Test churn rate: ", y_test.mean())    # e.g., 0.142  ← same!
💡
Stratification is critical for imbalanced datasets

If your dataset is 95% class 0 and 5% class 1, a random split might put all class-1 examples in the training set by chance, leaving you with an empty class in the test set. stratify=y guarantees both splits maintain the original 95/5 ratio. Always use it for classification.

6 sklearn Pipelines: The Right Way to Chain Steps

The manual approach to preprocessing — split, fit scaler, transform train, transform test — is error-prone and verbose. The elegant solution is a Pipeline: a single object that chains multiple steps and applies the fit-on-train rule automatically.

Raw input DataFrame: numeric + categorical columns, possibly with missing values Raw Data X_train / X_test SimpleImputer fills missing values — fit on X_train only SimpleImputer fill missing values step 1 ColumnTransformer: StandardScaler on numeric cols, OneHotEncoder on categorical cols ColumnTransformer scale numeric + one-hot categorical All preprocessing fit_transform()'d on train, transform()'d on test — same statistics reused Preprocessed numeric matrix Final estimator: e.g. LogisticRegression, Ridge, RandomForestClassifier Model .fit() / .predict() final step pipeline.fit(X_train, y_train) calls fit_transform() on every step except the last, then .fit() on the model

A single Pipeline (with a ColumnTransformer inside it) chains every preprocessing step and the model into one object — so cross_val_score and .fit() always refit preprocessing on the correct training fold only.

📦
We're borrowing models we haven't studied yet — on purpose

A pipeline needs a model at the end, but the models themselves start next lesson. So here we borrow two from scikit-learn (LogisticRegression, RandomForestClassifier) and treat them as black boxes with three buttons: .fit(X, y) learns from data, .predict(X) makes predictions, and .score(X, y) reports accuracy — the fraction of predictions that were correct. What happens inside these boxes is the subject of Lessons 11–23. Today, keep your eyes on everything that happens before the model.

Simple Pipeline: Scaler + Classifier

In [11]:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.datasets import load_breast_cancer

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

# Build the pipeline
pipeline = Pipeline([
    ('scaler', StandardScaler()),             # step 1: scale features
    ('classifier', LogisticRegression(max_iter=1000))  # step 2: fit model
])

# Training: fit() calls scaler.fit_transform(X_train) then classifier.fit(X_train_scaled, y_train)
pipeline.fit(X_train, y_train)

# Prediction: transform() calls scaler.transform(X_test) then classifier.predict(X_test_scaled)
y_pred = pipeline.predict(X_test)
print("Test accuracy:", pipeline.score(X_test, y_test))  # 0.9737

# Cross-validation — leakage-safe because Pipeline fits scaler separately in each fold
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring='accuracy')
print(f"CV accuracy: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
# CV accuracy: 0.9758 ± 0.0085
🔑
New tool: cross_val_score — what those 5 numbers are

That last call did something clever: it split the training data into 5 equal folds, trained the pipeline on 4 folds and scored it on the 5th, then rotated so each fold took one turn as the scorer — 5 trainings, 5 scores, and we report their mean ± spread. This is k-fold cross-validation, and it gives a far more trustworthy estimate than a single split, because no one lucky or unlucky split can mislead you. You'll use it in nearly every lesson from here on, and Lesson 20 refines it for classification problems.

🔑
Why Pipelines Make Cross-Validation Correct

When you use cross_val_score(pipeline, ...), sklearn fits the entire pipeline independently on each training fold. The scaler's statistics are computed only from that fold's training data — not from validation data. Without a Pipeline, if you pre-scale before cross-validation, you've leaked validation statistics into every fold's training process.

7 ColumnTransformer for Mixed Data Types

Real datasets typically have a mix of numerical and categorical columns, each requiring different preprocessing. ColumnTransformer applies different transformers to different subsets of columns and concatenates the results.

In [12]:
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

# Sample dataset: house price classification (affordable vs not)
data = {
    'sqft':          [1200, 2300, 1800, 3200, 950],
    'bedrooms':      [2, 4, 3, 5, 1],
    'neighborhood':  ['Downtown', 'Suburbs', 'Downtown', 'Rural', 'Suburbs'],
    'house_style':   ['Ranch', 'Colonial', 'Ranch', 'Colonial', 'Ranch'],
    'affordable':    [1, 0, 1, 0, 1],
}
df = pd.DataFrame(data)
X = df.drop('affordable', axis=1)
y = df['affordable']

# Identify column types
numeric_cols     = ['sqft', 'bedrooms']
categorical_cols = ['neighborhood', 'house_style']

# Build transformers for each column type
numeric_transformer = Pipeline([
    ('scaler', StandardScaler()),
])

categorical_transformer = Pipeline([
    ('onehot', OneHotEncoder(handle_unknown='ignore', drop='first')),
])

# Combine into a ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer,     numeric_cols),
        ('cat', categorical_transformer, categorical_cols),
    ],
    remainder='drop'    # drop any columns not listed above
)

# Wrap preprocessor + model in a final Pipeline
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier',   LogisticRegression(max_iter=500)),
])

# Train and evaluate
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=42)
full_pipeline.fit(X_train, y_train)
print("Test accuracy:", full_pipeline.score(X_test, y_test))

# Inspect the output shape after preprocessing
preprocessor.fit(X_train)
X_transformed = preprocessor.transform(X_train)
print("Transformed shape:", X_transformed.shape)
# (3, 5):  2 numeric features + (3-1=2) neighborhood cols + (2-1=1) style col = 5 total

Using make_column_transformer (shorthand)

In [13]:
from sklearn.compose import make_column_transformer

# Shorter syntax for simple cases
preprocessor = make_column_transformer(
    (StandardScaler(),                              numeric_cols),
    (OneHotEncoder(handle_unknown='ignore'),        categorical_cols),
    remainder='passthrough'   # keep other columns unchanged
)

8 Handling Missing Values in Preprocessing

Missing values must be handled before most sklearn estimators can run. The cleanest approach is to add imputation as the first step in a Pipeline or ColumnTransformer, so it too is fit on training data only.

In [14]:
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
import numpy as np
import pandas as pd

# Simulate a dataset with missing values
np.random.seed(42)
n = 200
df = pd.DataFrame({
    'age':         np.random.normal(40, 10, n),
    'income':      np.random.normal(60000, 15000, n),
    'credit_score': np.random.normal(700, 50, n),
    'employment':  np.random.choice(['Full-time', 'Part-time', 'Self-employed', None], n),
    'home_owner':  np.random.choice(['Yes', 'No', None], n),
})
# Introduce some missing values
df.loc[df.sample(20).index, 'age']    = np.nan
df.loc[df.sample(15).index, 'income'] = np.nan

numeric_cols     = ['age', 'income', 'credit_score']
categorical_cols = ['employment', 'home_owner']

# Numeric pipeline: impute with mean, then scale
numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='mean')),   # missing → column mean
    ('scaler',  StandardScaler()),
])

# Categorical pipeline: impute with most frequent value, then one-hot encode
categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),  # missing → mode
    ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])

preprocessor = ColumnTransformer([
    ('num', numeric_pipeline,     numeric_cols),
    ('cat', categorical_pipeline, categorical_cols),
])

# Final pipeline with classifier
model_pipeline = Pipeline([
    ('prep',   preprocessor),
    ('model',  RandomForestClassifier(n_estimators=100, random_state=42)),
])

# imputer.fit() learns mean from TRAINING set only → no leakage
y = np.random.randint(0, 2, n)
X = df
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model_pipeline.fit(X_train, y_train)
print("Accuracy:", model_pipeline.score(X_test, y_test))
💡
Imputation Strategy Guide

strategy='mean' — best for roughly symmetric numeric distributions. strategy='median' — better for skewed numeric distributions (e.g., income, house prices). strategy='most_frequent' — for categorical features and binary variables. strategy='constant', fill_value=0 — when absence of a value is itself informative (e.g., "no prior loans"). For complex patterns, consider IterativeImputer (experimental) which uses other features to predict missing values.

🌍

Real-World Spotlight: House Price Prediction Pipeline

The Ames Housing dataset is a classic real-world dataset with 79 features — a mix of numeric and categorical — and significant missing values. Here's a complete preprocessing pipeline:

🏠
Dataset context: Predict house sale prices from features like GrLivArea (gross living area), LotArea, Neighborhood, HouseStyle. The dataset has 1460 rows and includes 19 features with missing values.
In [15]:
import pandas as pd
import numpy as np
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.linear_model import Ridge
from sklearn.metrics import mean_squared_error

# Load Ames Housing (from kaggle or sklearn.datasets)
df = pd.read_csv('ames_housing.csv')
X = df.drop('SalePrice', axis=1)
y = np.log1p(df['SalePrice'])   # log-transform target to reduce skew

# Identify column types
numeric_features     = X.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_features = X.select_dtypes(include=['object']).columns.tolist()

print(f"Numeric features:     {len(numeric_features)}")    # e.g., 36
print(f"Categorical features: {len(categorical_features)}")  # e.g., 43

# Build full preprocessing pipeline
numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler',  StandardScaler()),
])

categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])

preprocessor = ColumnTransformer([
    ('num', numeric_transformer,     numeric_features),
    ('cat', categorical_transformer, categorical_features),
])

full_pipeline = Pipeline([
    ('prep',  preprocessor),
    ('model', Ridge(alpha=10.0)),
])

# Train/test split — no stratify for regression
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

full_pipeline.fit(X_train, y_train)
y_pred = full_pipeline.predict(X_test)

rmse = mean_squared_error(y_test, y_pred, squared=False)
print(f"Test RMSE (log scale): {rmse:.4f}")    # ~0.131
print(f"Test R²:               {full_pipeline.score(X_test, y_test):.4f}")  # ~0.881

# Cross-validation to verify
cv_rmse = cross_val_score(full_pipeline, X_train, y_train,
                           cv=5, scoring='neg_root_mean_squared_error')
print(f"CV RMSE: {(-cv_rmse).mean():.4f} ± {cv_rmse.std():.4f}")

Quick Check

✍️ Practice Exercises

  1. Load any dataset with a mix of numerical and categorical columns. Identify which columns need OrdinalEncoder (there's an ordering) and which need OneHotEncoder (no ordering). Build a ColumnTransformer that applies the appropriate encoder to each.
  2. Write code that intentionally does it wrong (fit scaler on full data before splitting) and correctly (fit on train only). Use cross_val_score to measure accuracy for both approaches. Observe the inflated performance from the wrong approach.
  3. Build a complete Pipeline for the Titanic dataset: impute 'Age' with median, 'Embarked' with most_frequent; one-hot encode 'Sex' and 'Embarked'; StandardScale 'Age', 'Fare', 'Pclass'; train a LogisticRegression. Evaluate with 5-fold cross-validated accuracy.
  4. After fitting a pipeline, extract the fitted preprocessing steps to inspect what the scaler learned: pipeline.named_steps['scaler'].mean_. Do the same for a pipeline with a ColumnTransformer. How do you access the numeric sub-pipeline's scaler?
▶ Show Titanic pipeline solution
In [16]:
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

df = pd.read_csv('titanic.csv')
X = df[['Age', 'Fare', 'Pclass', 'Sex', 'Embarked']]
y = df['Survived']

numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler',  StandardScaler()),
])
categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)),
])
preprocessor = ColumnTransformer([
    ('num', numeric_transformer,     ['Age', 'Fare', 'Pclass']),
    ('cat', categorical_transformer, ['Sex', 'Embarked']),
])
pipeline = Pipeline([
    ('prep',  preprocessor),
    ('model', LogisticRegression(max_iter=200)),
])
scores = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy')
print(f"CV Accuracy: {scores.mean():.4f} ± {scores.std():.4f}")

📚 Primary Source for This Lesson

scikit-learn User Guide: Preprocessing Data
The official, authoritative documentation for all preprocessing tools covered in this lesson. Includes examples for every scaler and encoder, explanations of the mathematical transformations, and guidance on when to use each. Read sections 6.3 (preprocessing), 6.5 (pipelines), and 6.7 (column transformer).

💬 Getting a "could not convert string to float" error? Or confused about which columns belong in the numeric transformer? Paste your DataFrame's df.info() output and your pipeline code — your AI tutor will help you debug it.