Welcome to Week 12

  • Quick overview of today’s plan:

    • Why the simple train/test split isn’t enough
    • How cross-validation solves the “peeking problem”
    • Finding optimal model settings systematically
    • Turning raw data into powerful features

Important

This week is all about trying to optimize model performance!

Discussion: Homework & Questions

Questions from Week 11?

  • Random Forests and ensemble methods?
  • Feature importance interpretation?
  • When to use trees vs. other models?
  • Anything confusing in the quiz or class lab?
  • Time to ask!

Activity

Converse with your neighbor and identify…

  • 1 new thing you learned last week that was clear and well explained
  • 1 thing we covered last week that is still confusing

The Problem: We’ve Been Peeking

The Test Set Contamination Issue

Remember the golden rule from Module 8?

“Don’t touch the test set until you’ve selected your final model”

But then we did this:

  • Tried different max_depth values → evaluated on test set
  • Compared models → chose based on test set performance
  • Tuned hyperparameters → peeked at test set each time
  • Added/removed features → checked test set results

The consequence

Test scores become optimistically biased and untrustworthy.

Think-Pair-Share

Scenario: You’re a data scientist at a retail company. You build a customer churn prediction model. During development, you try 10 different model configurations, evaluating each on your test set. You pick the best one (test accuracy: 87%) and present it to management.

Discuss with your neighbor:

  • Why might the 87% test accuracy be misleading?
  • What could happen when you deploy this model to production?
  • How would you explain this problem to a non-technical manager?

Then we’ll take a few responses…

Solution: Cross-Validation

Wrong Way vs. Right Way

❌ What we’ve been doing:

flowchart TD
    A[Full Dataset] --> B[Train 80%]
    A --> C[Test 20%]
    B --> D[Train Model 1]
    D --> E[Evaluate on Test]
    C --> E
    E --> F{Good?}
    F -->|No| G[Try Model 2]
    G --> H[Evaluate on Test]
    C --> H
    H --> I{Good?}
    I -->|No| J[Try Model 3]
    J --> K[Evaluate on Test]
    C --> K
    K -->|Yes| L[Report Score]

    style C fill:#ff6b6b
    style E fill:#ff6b6b
    style H fill:#ff6b6b
    style K fill:#ff6b6b

Problem: Multiple peeks contaminate test set!

✓ What we should do:

flowchart TD
    A[Full Dataset] --> B[Train 80%]
    A --> C[Test 20%<br/>LOCKED]
    B --> D[5-Fold CV<br/>on Training]
    D --> E[Try Models]
    D --> F[Tune Params]
    D --> G[Engineer Features]
    E & F & G --> H[Select Best]
    H --> I[Retrain on<br/>Full Training]
    I --> J[Test ONCE]
    C --> J

    style C fill:#51cf66
    style D fill:#51cf66
    style J fill:#ffd43b

Solution: Use CV, keep test pristine!

How K-Fold Cross-Validation Works

The idea:

  • Split training data into K equal parts (folds)
  • Rotate which fold is used for validation
  • Average results across all K iterations

Benefits:

  • Test set stays completely untouched
  • Every training point gets validated once
  • More reliable performance estimates
  • Can make unlimited decisions without peeking

Cross-Validation in Scikit-Learn

from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split

# Step 1: Split data (test set locked away)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Step 2: Use CV to evaluate on TRAINING SET ONLY
dt = DecisionTreeClassifier(max_depth=5)
cv_scores = cross_val_score(dt, X_train, y_train, cv=5, scoring='accuracy')

print(f"CV Scores: {cv_scores}")
print(f"Mean CV Accuracy: {cv_scores.mean():.3f} (+/- {cv_scores.std():.3f})")

Output:

CV Scores: [0.847, 0.853, 0.862, 0.841, 0.858]
Mean CV Accuracy: 0.852 (+/- 0.008)

Notice: We never touched X_test or y_test!

Cross-Validation: Key Takeaway

The Golden Rule of Cross-Validation

Split your data FIRST, then use cross-validation on the training set for ALL modeling decisions.

  • ✅ Compare different models
  • ✅ Tune hyperparameters
  • ✅ Select features
  • ✅ Evaluate preprocessing choices

Your test set should be touched ONCE at the very end.

Why this matters:

  • Test set contamination is subtle and easy to do accidentally
  • Every peek at the test set makes your performance estimates less trustworthy
  • Cross-validation gives you unlimited practice exams while keeping your final exam pristine
  • This is the difference between amateur and professional data science

Hyperparameter Tuning

The Bias-Variance Tradeoff

Every model makes two types of errors:

High Bias (Underfitting)

  • Model too simple
  • Misses patterns in data
  • Consistent errors
  • Poor on training AND validation

High Variance (Overfitting)

  • Model too complex
  • Memorizes noise
  • Sensitive to specific training data
  • Great on training, poor on validation

The sweet spot: Balance bias and variance for best generalization.

Hyperparameters control this tradeoff: max_depth, n_estimators, K, etc.

Visualizing Bias-Variance with KNN

Caution

More advanced models (i.e. Decision Trees, Random Forests, Gradient Boosted Machines) are more flexible to align with data patterns; however, it is up to us to tune these models to balance the bias-variance tradeoff!

The Manual Approach (What We’ve Been Doing)

In previous chapters, we manually tried different values:

# Try max_depth = 5
dt1 = DecisionTreeClassifier(max_depth=5)
score1 = cross_val_score(dt1, X_train, y_train)
print(f"max_depth=5: {score1.mean()}")

# Try max_depth = 10
dt2 = DecisionTreeClassifier(max_depth=10)
score2 = cross_val_score(dt2, X_train, y_train)
print(f"max_depth=10: {score2.mean()}")

# Try max_depth = 15
dt3 = DecisionTreeClassifier(max_depth=15)
score3 = cross_val_score(dt3, X_train, y_train)
print(f"max_depth=15: {score3.mean()}")

# ... keep trying values manually

Problems with this approach:

  • ⏱️ Time-consuming - Repetitive code for each value
  • 🐛 Error-prone - Easy to make copy-paste mistakes
  • 📊 Limited exploration - Only try a few values
  • 🔄 Not systematic - What about combinations of parameters?
  • 📝 Hard to track - Which combination was best?

A Better Way Exists!

We need an automated, systematic approach to search through hyperparameter combinations efficiently.

Think-Pair-Share: Spotting Overfitting

Scenario: You’ve tuned a gradient boosting model and see these results:

Configuration A:
  Training Accuracy: 92.3%
  CV Accuracy: 88.7%

Configuration B:
  Training Accuracy: 99.8%
  CV Accuracy: 85.1%

Discuss:

  • Which configuration is overfitting more?
  • Which would you deploy to production?
  • What does the gap between training and CV tell you?

Feature Engineering

What is Feature Engineering?

The process of creating, transforming, and selecting features to help ML models learn better.

Raw data:

YearBuilt: 1995
YearRemodel: 2015

Engineered feature:

RelativeAge = 2025 - max(YearBuilt, YearRemodel)
            = 2025 - 2015 = 10 years

An older home (1995) with recent remodel (2015) has a younger relative age than a newer home (2010) without remodeling!

Raw features:

GrLivArea: 2000 sq ft
OverallQual: 8/10

Engineered interaction:

Size_x_Quality: 16,000

Captures combined effect!


Why it matters

Good features often matter more than fancy algorithms!

Four Essential Techniques

1. Encoding Categorical Variables

  • Dummy encoding: One column per category
  • Label encoding: Single numerical column
  • Ordinal: Preserve ordering

2. Scaling Numerical Features

  • StandardScaler: Mean=0, Std=1
  • MinMaxScaler: Range [0, 1]
  • Crucial for distance-based algorithms

3. Creating New Features

  • Polynomial: Square, cube terms
  • Interactions: Feature × Feature
  • Domain-specific: Age, ratios, etc.

4. Handling Missing Data

  • Imputation: Fill with median/mode
  • Missingness indicators
  • Know when to drop vs. impute

Hands-On Demo: Encoding & Scaling

Original Features:

import pandas as pd
from sklearn.preprocessing import LabelEncoder, StandardScaler

# Load Ames data
ames = pd.read_csv('../data/ames_clean.csv')

# Look at original features
features_to_eng = ['Neighborhood', 'GrLivArea', 'YearBuilt', 'TotalBsmtSF']
print(ames[features_to_eng])
     Neighborhood  GrLivArea  YearBuilt  TotalBsmtSF
0         CollgCr       1710       2003          856
1         Veenker       1262       1976         1262
2         CollgCr       1786       2001          920
3         Crawfor       1717       1915          756
4         NoRidge       2198       2000         1145
...           ...        ...        ...          ...
1455      Gilbert       1647       1999          953
1456       NWAmes       2073       1978         1542
1457      Crawfor       2340       1941         1152
1458        NAmes       1078       1950         1078
1459      Edwards       1256       1965         1256

[1460 rows x 4 columns]

Engineered Features:

# 1. Encode Neighborhood: 28 categories → integers
le = LabelEncoder()
ames['Neighborhood_Encoded'] = le.fit_transform(ames['Neighborhood'])

# 2. Scale numerical features: mean=0, std=1
scaler = StandardScaler()
features_to_scale = ['Neighborhood_Encoded', 'GrLivArea', 'YearBuilt', 'TotalBsmtSF']
ames[features_to_scale] = scaler.fit_transform(ames[features_to_scale])

# Look at engineered features
print(ames[features_to_scale])
      Neighborhood_Encoded  GrLivArea  YearBuilt  TotalBsmtSF
0                -1.206215   0.370333   1.050994    -0.459303
1                 1.954302  -0.482512   0.156734     0.466465
2                -1.206215   0.515013   0.984752    -0.313369
3                -1.039872   0.383659  -1.863632    -0.687324
4                 0.457215   1.299326   0.951632     0.199680
...                    ...        ...        ...          ...
1455             -0.707186   0.250402   0.918511    -0.238122
1456              0.290872   1.061367   0.222975     1.104925
1457             -1.039872   1.569647  -1.002492     0.215641
1458             -0.041814  -0.832788  -0.704406     0.046905
1459             -0.873529  -0.493934  -0.207594     0.452784

[1460 rows x 4 columns]

Preventing Data Leakage with Pipelines

The problem: If you fit a scaler on the entire dataset (train + test), test set information “leaks” into training.

The solution: Pipelines ensure transformations fit on training data only.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier

# Create pipeline: scaling → model
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', RandomForestClassifier(n_estimators=100))
])

# Fit on training data (scaler learns from X_train only!)
pipeline.fit(X_train, y_train)

# Predict on test data (scaler uses training stats to transform X_test)
predictions = pipeline.predict(X_test)

Important

Pipelines = reproducible, leak-free workflows!

Putting It All Together

The Complete Professional Workflow

flowchart TD
    A[Full Dataset] --> B[Stage 1:<br/>Train/Test Split]
    B --> C[Training Set 80%]
    B --> D[Test Set 20%<br/>🔒 LOCKED]

    C --> E[Stage 2:<br/>Cross-Validation]
    E --> F[Try different models<br/>Tune hyperparameters<br/>Engineer features]
    F --> G[Stage 3:<br/>Select Best Approach]

    G --> H[Stage 4:<br/>Retrain on Full<br/>Training Set]

    H --> I[Stage 5:<br/>Evaluate on Test Set<br/>ONCE]
    D --> I
    I --> J[Report Final<br/>Performance]

    style D fill:#ff6b6b
    style E fill:#51cf66
    style I fill:#ffd43b
    style J fill:#51cf66

Looking Ahead

Key Takeaways

  • Cross-validation prevents test set contamination and gives reliable performance estimates
  • Hyperparameter tuning finds the sweet spot in the bias-variance tradeoff
  • Feature engineering turns raw data into powerful inputs (often matters more than algorithms!)
  • Pipelines ensure reproducible, leak-free workflows
  • The 5-stage workflow is how professionals build trustworthy models

Connection to Thursday’s Lab

This Week’s Lab Preview

In Thursday’s lab, you’ll get hands-on practice with:

  • Implementing the 5-stage workflow from scratch
  • Using GridSearchCV to tune a Random Forest
  • Building feature engineering pipelines
  • Comparing performance with and without proper CV
  • Seeing what happens when you peek at the test set (spoiler: bad things!)

Come prepared to apply today’s concepts!

Questions & Next Steps

Looking Ahead

Btwn now & Thursday: Complete reading Chapters 28-30

Thursday Lab: Hands-on practice with CV, hyperparameter tuning, and feature engineering

This weekend: Week 12 quiz & homework (based on lab)


Next Week: Move into the world of unsupervised ML

Next next week: Advanced topics & Final project prep!

Any Final Questions?

  • About today’s concepts?
  • About Thursday’s lab?
  • About upcoming assignments?
  • About rest of the term?
See you Thursday for hands-on practice!