🧠 Section 1 · Foundations 🟡 Intermediate MODULE 04

Train-Test Split & Cross-Validation

⏱️ 22 min read
📖 Model Evaluation Setup
🧩 4 Quiz Questions
🏗️ 1 Challenge · 3 tasks
Your progress in Section 180%
🎯 What you'll learn: You've already used train_test_split() as a black box in earlier lessons — this lesson opens it up. You'll see why a single random split can give a misleadingly good or bad performance estimate, how k-fold cross-validation solves that by testing on every row exactly once, and how stratified splitting keeps class proportions intact for classification problems.

train_test_split() in Detail

train_test_split, from sklearn.model_selection, randomly shuffles the rows of X and y together and divides them into a training portion and a held-out test portion.

basic_split.py
PYTHON
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.25,      # 25% of rows held out for testing
    random_state=42     # fixes the shuffle so results are reproducible
)

print("Train rows:", len(X_train), "| Test rows:", len(X_test))
📐
test_size
The fraction (or exact count) of rows set aside as the test set. Common values are 0.2 or 0.25 — enough rows to evaluate reliably, without starving the model of training data.
🎲
random_state
Seeds the random shuffle. Fixing it to any integer means anyone re-running the exact code gets the exact same train/test rows — essential for reproducible results and fair comparisons between models.

Why a Single Split Can Be Noisy

A single train_test_split() call picks one particular random sample of rows for testing. On a small or unevenly-distributed dataset, that one sample might happen to be unusually easy or unusually hard — purely by chance. Change random_state from 42 to 7, and the reported test accuracy can shift meaningfully, even though nothing about the model or the data actually changed.

⚠️
One number, one lucky (or unlucky) sample
If you evaluate a model with a single split and report "87% accuracy," that number is really an estimate with some uncertainty attached — not a fixed fact about the model. Two different splits of the same data can legitimately produce noticeably different scores, especially with fewer rows or class imbalance.

The fix isn't to abandon the train/test split — it's to average performance over MULTIPLE different splits, so a single unlucky sample can't dominate the result. That's exactly what cross-validation does.

K-Fold Cross-Validation

K-fold cross-validation splits the data into k equally-sized "folds." It then runs k separate rounds: in each round, one fold is held out as the test set and the model trains on the remaining k − 1 folds. Every row gets used as test data exactly once, across the k rounds — so the final average score isn't at the mercy of one lucky or unlucky split.

Fold 1Fold 2Fold 3Fold 4Fold 5
Round 1TESTtraintraintraintrain
Round 2trainTESTtraintraintrain
Round 3traintrainTESTtraintrain
Round 4traintraintrainTESTtrain
Round 5traintraintraintrainTEST

5-fold cross-validation: each fold is the test set exactly once, across 5 training rounds.

cross_val_score.py
PYTHON
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(random_state=42)

# cv=5 runs 5-fold cross-validation and returns one score per fold
scores = cross_val_score(model, X, y, cv=5)

print("Fold scores:", scores)
print("Mean accuracy:", scores.mean())
print("Std deviation:", scores.std())
Read the spread, not just the mean
The standard deviation across fold scores tells you how stable the model's performance is. A mean of 0.85 with folds tightly clustered around it is a much more trustworthy result than the same 0.85 mean produced by folds ranging from 0.65 to 0.99.

Note that cross_val_score handles the splitting internally — you pass the FULL X and y, not a pre-split train/test pair. A common workflow is to use cross-validation on the training set for model comparison and tuning, then do one final check on a completely separate test set you held out from the start.

Stratified Splitting for Classification

A plain random split can, by chance, put too many or too few examples of a rare class into the test set — especially with imbalanced classification data (say, 90% "no churn" and 10% "churn"). Stratified splitting forces every split to preserve the original class proportions.

stratified_split.py
PYTHON
from sklearn.model_selection import train_test_split

# stratify=y keeps the class balance of y the same in both splits
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

The same idea extends to cross-validation with StratifiedKFold, which builds folds that each preserve the overall class ratio — the recommended default for classification cross-validation.

stratified_kfold.py
PYTHON
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(random_state=42)

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skf)

print("Stratified fold scores:", scores)
print("Mean accuracy:", scores.mean())
📝
Regression doesn't use stratify=y the same way
stratify works on discrete class labels, so it's a classification-only tool. For regression, plain KFold (no stratification) is the standard choice, since the target is continuous rather than a set of categories.

Lesson Summary

train_test_split(test_size=..., random_state=...) creates one reproducible train/test division.
A single split can be noisy — the reported score depends partly on which rows happened to land in the test set.
K-fold cross-validation (cross_val_score) tests on every row exactly once across k rounds, giving a more reliable estimate.
stratify=y and StratifiedKFold preserve class proportions for classification problems.
🧩 Knowledge Check — Lesson 4
4 questions on splitting and cross-validation.
1. What does test_size=0.2 mean in train_test_split?
2. Why can a single train/test split give a misleading performance estimate?
3. In 5-fold cross-validation, how many times does the model get trained?
4. What does stratify=y in train_test_split guarantee for a classification problem?
💪
Try It Yourself — Lesson 4
Compare splitting strategies · Intermediate Level

Get hands-on with all three tools from this lesson.

Task 1: Compare two random_state values 🎲

Run train_test_split(X, y, test_size=0.2, random_state=1) and then again with random_state=99. Train the same model on each split and compare the two test accuracies. Are they identical? Write a sentence on why or why not.
Task 2: Run 5-fold cross-validation 🔁

Using cross_val_score(model, X, y, cv=5) from Section 3, print the 5 fold scores, the mean, and the standard deviation. Compare the mean to the single-split score from Task 1.
Task 3: Verify stratification worked ✅

Split an imbalanced classification dataset both with and without stratify=y. Using pandas.Series(y_test).value_counts(normalize=True) on each result, confirm the stratified version's class proportions match the full dataset's more closely.
💡 Show hints if you're stuck
  • Task 1: The two accuracies will usually differ at least slightly — different rows end up in the test set each time, even though the model and data are unchanged.
  • Task 2: The cross-validation mean is generally the more trustworthy number since it isn't dependent on one particular split.
  • Task 3: Without stratification, a rare class's proportion in the test set can swing noticeably from run to run — with stratify=y, it should stay very close to the original ratio every time.
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 4 Complete!

You now understand why single splits can mislead, how cross-validation fixes that, and how stratification preserves class balance. Next: the single most important failure mode in ML — overfitting vs underfitting.

Module 04 of 24 Section 1 — What is Machine Learning?