
CSCI 1109 — Practical Data Science
By the end of this module, you should be able to:
In practical data science, two naïve failure modes (among others):
Warning
Good ML ≠ high accuracy on some split.
Good ML = beats a strong baseline without cheating.
Baseline n.: A simple, transparent prediction rule/model that sets the minimum performance that any serious model must beat. ~~~ Very simple (and kind of bad) baselines:
Data leakage n.: Any situation where your model gets information during training that would not be available at prediction time in the real world. ~~~ Common leakage types:
has_defaulted_last_month when predicting will_default_this_month?.hours_to_readmission feature when predicting readmit?.Warning
Leakage makes models look unrealistically good in notebooks, and dangerously bad in the real world.
We’ll compare a clean evaluation workflow to a leaky one.

We want an evaluation procedure that:
This motivates \(k\)-fold cross-validation.
Conceptually:
Each run: one fold is validation (red), others are training (grey):
We’ll simulate a simple loan default prediction task:
defaulted vs not_defaulted).import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
rng = np.random.default_rng(1109)
n = 600
# Synthetic "risk score" feature on a 0–100 scale
risk_score = rng.normal(loc=50, scale=15, size=n).clip(0, 100)
# True probability of default depends on risk score
# (higher risk_score → more likely to default)
p_default = 0.1 + 0.007 * risk_score # roughly 10%–80%
p_default = np.clip(p_default, 0.05, 0.95)
defaulted = rng.binomial(1, p_default)
df = pd.DataFrame({
"risk_score": risk_score,
"defaulted": defaulted
})
X = df[["risk_score"]]
y = df["defaulted"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=1109, stratify=y
)
# 1. Baseline: always predict majority class
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
y_dummy = dummy.predict(X_test)
acc_dummy = accuracy_score(y_test, y_dummy)
# 2. k-NN model
knn = KNeighborsClassifier(n_neighbors=15)
knn.fit(X_train, y_train)
y_knn = knn.predict(X_test)
acc_knn = accuracy_score(y_test, y_knn)
acc_dummy, acc_knn(0.5388888888888889, 0.5388888888888889)
Baseline (majority class) accuracy: 0.539
k-NN model accuracy: 0.539

Questions to ask yourself:
Decision context matters
If missing people who are about to default is costly, accuracy alone may be misleading.
We might care more about recall for the “will default” group.
Baselines give you a floor; decision context tells you what is expected.
We’ll deliberately construct a leaky feature:
label_copy = exact copy of the true label.from sklearn.linear_model import LogisticRegression
# Make a copy with a leaky feature
X_leaky = X.copy()
X_leaky["label_copy"] = y # <- pure target leakage
X_tr_leaky, X_te_leaky, y_tr_leaky, y_te_leaky = train_test_split(
X_leaky, y, test_size=0.3, random_state=1109, stratify=y
)
log_reg_leaky = LogisticRegression(max_iter=1000)
log_reg_leaky.fit(X_tr_leaky, y_tr_leaky)
y_pred_leaky = log_reg_leaky.predict(X_te_leaky)
acc_leaky = accuracy_score(y_te_leaky, y_pred_leaky)
acc_leaky1.0
Leaky logistic regression accuracy: 1.000

🤷 What happened?
Note
This is an exaggerated example, but many real leaks are just subtle versions of this pattern.
Even without an obvious “label copy”, we can leak in more subtle ways.
Bad pattern (leaky):
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # fit on ALL data ❌
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=0
)Good pattern (clean):
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit on train ✅
X_test_scaled = scaler.transform(X_test) # transform testEven better: use a Pipeline.
Using 🔗 sklearn.pipeline.Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=1109, stratify=y
)
pipe = Pipeline([
("scale", StandardScaler()),
("clf", LogisticRegression(max_iter=1000))
])
pipe.fit(X_train, y_train) # scaler fits only on train
y_pred = pipe.predict(X_test) # clean transformation on testBenefits:
Warning
Pipelines don’t guarantee no leakage (there is no ‘silver bullet’ in ML), but they remove some of the most common sources.
When reading model performance for a decision:
Tip
Treat every surprisingly good result as an invitation to hunt for leakage before you celebrate.
Ethical (and practical) risks:
🛠️ Simple mitigations (at this course level):
These ideas show up everywhere in real DS/ML practice:
Tip
As you build more advanced models later in the course, remember: robust baselines + leak-free pipelines are part of being a responsible data scientist.
Which of the following is a reasonable classification baseline?
A. A model that memorizes the training set.
B. A model that predicts the majority class for all cases.
C. A model that uses future labels as features.
D. A model that randomizes predictions with 50/50 chance.
What is the most common cause of train/test contamination?
A. Using a random seed in train_test_split.
B. Fitting scalers or encoders on the full dataset before splitting.
C. Training on 80% instead of 70% of the data.
D. Using a GPU instead of a CPU.
You’re predicting churn next month. Which feature is most suspicious for leakage?
A. Number of support tickets last month.
B. Account age in days.
C. Whether the account was closed next month.
D. Total amount billed so far.
Your model’s test accuracy is 0.97, while the majority-class baseline is 0.94. Which is the BEST first step?
A. Declare victory and deploy immediately.
B. Check for leakage and confirm that preprocessing happens inside the split.
C. Ignore the baseline, since accuracy is very high.
D. Add more hyperparameters to increase accuracy even further.
