top of page

What Is Train-Test Split?

  • 1 hour ago
  • 28 min read
Train-test split with 80/20 data division.

A model can score 99% on the data it was trained on and still fail the moment it meets a real customer, a real transaction, or a real patient. That gap between "looks great in training" and "falls apart in the real world" is exactly the problem a train-test split exists to catch, by holding back a portion of data the model never gets to study before grading its final exam.


TL;DR


  • A train-test split divides a dataset into a training set the model learns from and a test set used only to measure performance on unseen data.

  • The test set must stay untouched during training and tuning, or the resulting score stops reflecting real-world generalization.

  • The right splitting method—random, stratified, grouped, or chronological—depends on how your data is structured, not on habit.

  • Data leakage—letting test information influence training in any way—is one of the most common causes of misleadingly optimistic results.

  • A single split gives one estimate; cross-validation and a protected final test set work together, rather than replacing each other.



A train-test split is a method for dividing a dataset into two parts: a training set, used to fit a machine learning model, and a test set, held back and used only afterward to evaluate how well the model performs on new, unseen data. This separation estimates real-world performance and helps detect overfitting.





The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Table of Contents



What a Train-Test Split Actually Means


In plain English, a train-test split takes one dataset and cuts it into two pieces. One piece—the training set—is shown to the model so it can learn patterns. The other piece—the test set—is locked away and shown to the model only after training is finished, purely to check how it performs.


More formally, given a dataset of (n) observations, a train-test split partitions the observations into a training subset and a test subset that do not overlap. The model's parameters are estimated using only the training subset. The test subset is then used, exactly once ideally, to compute an unbiased estimate of the model's performance on data drawn from the same underlying population but not seen during fitting.


The words practitioners use here matter. "Held-out" data is data deliberately withheld from training. "Unseen" data is data the model has never encountered in any form, including during preprocessing. "Out-of-sample" is a statistics term for data outside the sample used to fit a model—train-test splitting is a practical way of manufacturing out-of-sample data from a single dataset.


A simple, technically accurate analogy: think of the training set as the practice questions a student studies from, and the test set as the exam questions the student has never seen. A student who "aces" a test made entirely of practice questions they already memorized hasn't demonstrated understanding—only recall. The same logic applies to a machine learning model.


Importantly, the two subsets don't always come from randomly slicing one big pile of rows. They can also come from data that is naturally separated—by time period (last year's transactions vs. this year's), by user (one group of patients vs. another), by location (stores in one region vs. another), by device, or by collection process (data gathered before a system change vs. after). Recognizing which kind of separation applies to your problem is often more important than the exact split percentage you choose.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Why Train-Test Splitting Matters


Generalization is the real goal


The purpose of building a predictive model is almost never to perform well on the exact rows it was trained on—it's to perform well on new rows it hasn't seen yet. That ability is called generalization. A model that memorizes training data instead of learning transferable patterns has overfit; a model too simple to capture the real pattern has underfit. Both failure modes look fine if you only ever check performance on training data, which is precisely why training accuracy alone is not sufficient evidence that a model works.


In-sample vs. out-of-sample performance


In-sample performance measures how well a model fits the data it already saw. Out-of-sample performance measures how well it predicts new data. The two numbers can diverge sharply. A decision tree with no depth limit can reach close to 100% training accuracy on almost any dataset by essentially memorizing it, while its out-of-sample accuracy might be far lower. The bias-variance tradeoff explains why: increasing model flexibility reduces bias but increases variance, and variance is exactly the sensitivity to the specific training sample that hurts generalization.


A test score is an estimate, not a guarantee


Even a well-constructed test set only tells you how the model performed on that particular sample of unseen data. It is a statistical estimate of future performance, not a certification. Two things follow from this. First, the test distribution should resemble the distribution the model will actually meet in deployment—a test set of hospital records from one city may not predict performance in a different healthcare system. Second, any estimate based on a finite sample carries sampling error: the same model, evaluated on a different but equally valid test sample, would likely produce a slightly different number.


Why a single split can be noisy


For small datasets, this sampling error can be large. A test set of 30 examples can easily show 80% accuracy on one random split and 70% on another, purely from which specific examples happened to land in the test set—no change to the model at all. This is one reason practitioners lean on cross-validation for small or noisy datasets, a point covered later in this guide.


Table: In-sample vs. out-of-sample performance


Aspect

In-Sample (Training)

Out-of-Sample (Test)

Data seen by model

Yes

No

Reflects real-world use

Not reliably

Yes, approximately

Sensitive to overfitting

Hides it

Reveals it

Typical use

Fitting parameters

Final performance estimate

Risk if misused

Inflated confidence

Biased if reused repeatedly


How a Train-Test Split Works End to End


A reliable splitting workflow follows a consistent order. Skipping steps, or doing them out of order, is where most evaluation mistakes originate.


  1. Define the prediction task and target population. What exactly is being predicted, for whom, and under what conditions will the model be used?

  2. Inspect the dataset. Check for repeated entities, time ordering, duplicate or near-duplicate rows, label quality, and class imbalance before choosing anything.

  3. Select an appropriate splitting strategy—random, stratified, grouped, or chronological—based on what step 2 revealed.

  4. Split the data first, before fitting any learned preprocessing steps such as scalers or encoders.

  5. Fit preprocessing only on the training data. Apply the already-fitted transformation to validation and test data; never re-fit on them.

  6. Train the model using the transformed training data.

  7. Use a validation set or cross-validation for model comparisons and hyperparameter decisions—not the test set.

  8. Evaluate the finalized process on the untouched test set, ideally once.

  9. Report the metric, the splitting strategy, the random seed (if applicable), and known limitations.

  10. Monitor performance after deployment, since real-world data can drift away from the distribution the test set represented.


Steps 4 through 6 can be combined safely using scikit-learn's Pipeline, which chains preprocessing and the estimator together so that fit() is always called on training data alone and transform()/predict() is applied consistently to everything else. This single design choice eliminates a large share of leakage mistakes almost automatically, covered in more detail later.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Choosing a Train-Test Split Ratio


There is no split ratio that is correct for every dataset. Practitioners commonly see 50/50, 60/40, 70/30, 75/25, 80/20, and 90/10, but each one is a trade-off between two competing needs: enough training data for the model to learn well, and enough test data for the performance estimate to be trustworthy.


Table: Common split ratios and typical rationale


Ratio (train/test)

When it's often seen

Trade-off

50/50

Very large datasets, or when validation needs equal statistical power

Wastes training data if the dataset isn't huge

60/40

Exploratory work, small models

More robust test estimate, less training signal

70/30

General-purpose default for small-to-medium data

Balanced, commonly cited as a reasonable starting point

80/20

Very common default in tutorials and libraries

Good balance for medium datasets; can be unstable if data is small

90/10

Large datasets, deep learning

10% of a huge dataset is still statistically ample


The percentage alone can be misleading without considering the absolute count. Ten percent of one million rows is 100,000 test examples—more than enough for a precise estimate. Twenty percent of 100 rows is only 20 test examples, which is often too few to trust, especially if there are multiple classes.


Consider three quick arithmetic examples using (n_{\text{test}} \approx p \times n) and (n_{\text{train}} = n - n_{\text{test}}):


  • (n = 500), (p = 0.2): roughly 100 test rows, 400 training rows.

  • (n = 50{,}000), (p = 0.2): roughly 10,000 test rows, 40,000 training rows.

  • (n = 120), (p = 0.3): roughly 36 test rows, 84 training rows—likely too small for a stable estimate if there are several classes.


The choice should reflect: total sample size, model complexity, the number of classes, how rare the minority class is, how much statistical precision the evaluation needs, computational cost of retraining, whether records are naturally grouped, how much time coverage the data spans, the conditions the model will face in deployment, and whether cross-validation will also be used alongside the holdout split. When a dataset is small, favor a larger test set proportionally, or rely more on cross-validation than on a single fixed ratio. When a dataset is large, a smaller test percentage often still yields a large and precise test set while preserving more data for training.


Training, Validation, and Test Sets


These three terms get used loosely, but they serve distinct roles.


  • Training set: the data the model directly learns from—weights, coefficients, or split rules are fit here.

  • Validation set: a separate slice used during development to compare models, tune hyperparameters, and catch overfitting before touching the test set.

  • Test set: reserved for one final, unbiased estimate after every modeling decision has been made.


The validation set exists because model development involves dozens or hundreds of small decisions—which algorithm, which hyperparameters, which features. If every one of those decisions were checked against the test set, the test set would gradually become another validation set, and the resulting "final" score would be optimistic rather than honest. This phenomenon is sometimes called the test set "wearing out" through repeated human decision-making.


Two-way splitting (train/test only) is common for simple projects or when cross-validation substitutes for a dedicated validation set. Three-way splitting (train/validation/test) is common for projects involving hyperparameter search or model comparison. Cross-validation, covered in a later section, can reduce or eliminate the need for a separate fixed validation set by reusing the training data more efficiently across folds.


Table: Purpose and allowed uses


Set

Purpose

Allowed uses

Prohibited uses

Training

Fit model parameters

Model fitting, feature learning

Final evaluation

Validation

Guide development decisions

Hyperparameter tuning, model comparison, early stopping

Reporting as the final score

Test

Estimate real-world performance

One (or very few) final evaluations

Any tuning or repeated peeking


The ideal workflow settles the modeling approach using training and validation data (or cross-validation), and only then evaluates once on the test set, treating that number as the final answer for this iteration of the project.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Scikit-Learn's train_test_split() Explained


Scikit-learn's train_test_split is the standard tool for producing a random holdout split in Python. Its current documented signature is:


sklearn.model_selection.train_test_split(
    *arrays,
    test_size=None,
    train_size=None,
    random_state=None,
    shuffle=True,
    stratify=None
)

A minimal, correct example:


from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42
)

Here is what each part does:


  • X, y: the feature matrix and target array (or any number of same-length arrays) to split. Every array passed in must have the same number of samples, since the function slices them together to keep rows aligned.

  • test_size=0.2: reserves 20% of the data for the test set. It can be a float between 0 and 1 (a proportion) or an integer (an absolute count of samples).

  • random_state=42: fixes the internal random number generator so the same split is produced every time the code runs. The number 42 has no statistical meaning—it's a conventional placeholder seed, not a "correct" value. Any fixed integer works identically well for reproducibility.

  • Return order: the function always returns the split arrays in the same relative order they were passed, doubled—X_train, X_test, y_train, y_test for two input arrays, following the pattern train, test for each input in sequence.

  • shuffle=True (default): rows are randomly shuffled before splitting. This is appropriate for independent, identically distributed data but inappropriate for ordered time-series data, where shuffling would leak future information into the training set.

  • stratify=None (default): when set to an array such as stratify=y, the split preserves the class proportions of y in both the train and test subsets, which is valuable for classification problems with class imbalance.


Regarding defaults: if neither test_size nor train_size is specified, test_size defaults to 0.25 according to current scikit-learn documentation—but this default only applies when both parameters are left unset; specifying either one overrides it. It is a good habit to set test_size explicitly rather than relying on this default, since being explicit makes the split ratio visible to anyone reading the code later.


Two clarifications worth internalizing. First, reproducibility and validity are different things. Setting random_state=42 guarantees you get the same split every time, but it does not make that split statistically sound—a reproducible bad split (say, one that happens to separate all of one class into the test set) is still a bad split. Second, scikit-learn's current documentation states that if shuffle=False, then stratify must be None—the two options are incompatible, because stratification requires randomly reassigning rows by class, which is meaningless if the data must stay in its original order (as with time series).


A Complete Classification Example


This example uses scikit-learn's built-in breast cancer dataset, a real, commonly used benchmark for binary classification, and follows a leakage-safe pattern using Pipeline.


from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, roc_auc_score

data = load_breast_cancer()
X, y = data.data, data.target

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

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(max_iter=1000))
])

pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
y_proba = pipeline.predict_proba(X_test)[:, 1]

print(classification_report(y_test, y_pred))
print("ROC AUC:", roc_auc_score(y_test, y_proba))

Walking through the logic: stratify=y keeps the ratio of malignant to benign cases roughly equal in both the training and test subsets, which matters because the classes are not perfectly balanced. The Pipeline bundles StandardScaler and LogisticRegression together, so calling pipeline.fit(X_train, y_train) fits the scaler's mean and standard deviation using only training rows, then fits the model on the scaled training data—the test set never influences the scaler's parameters. classification_report prints precision, recall, and F1-score per class, while roc_auc_score reports a single ranking-quality metric.


Accuracy alone can be misleading here: if 90% of cases were benign, a model that always predicts "benign" would score 90% accuracy while catching zero malignant cases—a dangerous result in a medical context. Precision, recall, and ROC AUC surface that failure where raw accuracy would hide it. The exact numeric scores from this code will vary slightly depending on the scikit-learn version and hardware, so they are intentionally omitted here rather than presented as guaranteed output.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

A Complete Regression Example


Regression predicts a continuous number rather than a category, so stratifying on the raw target the way you would for classification does not apply in the usual sense.


from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np

data = fetch_california_housing()
X, y = data.data, data.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42
)

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", Ridge(alpha=1.0))
])

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

mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)

print(f"MAE: {mae:.3f}, RMSE: {rmse:.3f}, R²: {r2:.3f}")

This example does not use stratify, because the target y here is a continuous house-value figure, not a set of discrete categories. MAE (mean absolute error) reports the average absolute size of the prediction error in the same units as the target—here, hundreds of thousands of dollars. RMSE (root mean squared error) penalizes large errors more heavily than MAE because errors are squared before averaging.  describes the proportion of variance in the target explained by the model, on a scale where 1.0 is a perfect fit and 0 means the model performs no better than always predicting the average. All three numbers should be interpreted in the context of the target variable—an MAE of 0.5 means very different things for house prices measured in hundreds of thousands versus a target measured in single digits.


Stratified Splitting


Stratification means splitting the data so that each subset preserves, as closely as possible, the same proportion of each class found in the full dataset. It matters most for classification problems where classes are imbalanced, because a plain random split can, by chance, place disproportionately few examples of a rare class into the test set—or even none at all.


Consider a fraud-detection dataset with 980 legitimate transactions and 20 fraud cases (98%/2%). A random 80/20 split without stratification could easily place only 2 or 3 fraud cases in the 200-row test set, making any fraud-detection metric extremely noisy. With stratify=y, both the training and test sets would keep approximately the same 98%/2% ratio.


from sklearn.model_selection import train_test_split
from collections import Counter

# y has classes: 0 (legitimate) and 1 (fraud), imbalanced ~98/2
print("Before split:", Counter(y))

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

print("Train distribution:", Counter(y_train))
print("Test distribution:", Counter(y_test))

Stratification does not solve severe imbalance by itself—it only ensures the imbalance is represented proportionally rather than distorted by random chance. If a class has only 5 total examples, even a stratified split may leave too few in the test set to draw a reliable conclusion, and the split may fail outright if a class has fewer members than the number of splits requested. Multilabel problems, where each row can belong to multiple classes simultaneously, need specialized multilabel-stratification approaches, since simple single-label stratification does not account for co-occurring label combinations. It's also worth distinguishing a stratified holdout split, which stratifies one single train/test division, from stratified cross-validation (StratifiedKFold), which stratifies each of several repeated folds.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Group-Aware Splitting


Rows in a dataset are not always independent of each other. Multiple images from the same patient, multiple transactions from the same customer, several sensor readings from the same machine, or many documents from the same author are all examples of grouped data. If a random split places some rows from a patient in the training set and other rows from that same patient in the test set, the model can effectively "recognize" the patient rather than learn a pattern that generalizes to new patients—producing an evaluation score that looks great but doesn't hold up on genuinely new individuals.


The fix is to split by group rather than by row, ensuring every row belonging to a given entity ends up entirely in one subset or the other. Scikit-learn provides GroupShuffleSplit for a single group-aware holdout split and GroupKFold for group-aware cross-validation; StratifiedGroupKFold combines grouping with class stratification when both constraints apply.


from sklearn.model_selection import GroupShuffleSplit

# groups: an array where each entry is, e.g., the patient ID for that row
splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(splitter.split(X, y, groups=groups))

X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]

Note that the requested test_size here refers to a proportion of groups, not necessarily a proportion of rows—if some groups contain many more rows than others, the resulting row-level split percentage may differ somewhat from the requested value.


Time-Series and Chronological Splitting


When observations are ordered in time and the goal is to predict the future from the past, random shuffling is usually the wrong approach. Shuffling would let information from later time points leak into the training set, letting the model implicitly "see the future" during training—a mistake known as look-ahead bias or temporal leakage. The realistic evaluation setup instead trains on the past and tests on the future, exactly mirroring how the model will be used once deployed.


Scikit-learn's TimeSeriesSplit implements this by producing successive train/test folds where the training set only ever contains observations earlier than the test fold.


from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)

for fold, (train_idx, test_idx) in enumerate(tscv.split(X)):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    # fit and evaluate the model per fold here

Two evaluation styles are common: an expanding window, where each successive training fold keeps growing to include all prior data, and a rolling window, where the training fold has a fixed size and slides forward in time, discarding the oldest observations. Some workflows also insert a gap or "embargo" period between training and test folds to avoid subtle leakage when features are computed using rolling calculations that span the boundary. Practitioners should also check whether the training data spans enough seasons or cycles to capture seasonality, and remain aware that a model's accuracy can degrade over time due to distribution drift as real-world conditions shift.


Not every dataset with a timestamp automatically needs a chronological split—the deciding factor is the actual prediction scenario. A dataset with timestamps used only to describe when a static event occurred, with no notion of predicting forward in time, might not require temporal ordering at all. The right question is always: "In deployment, will the model only ever see information from before the moment of prediction?" If yes, the evaluation split should mirror that.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Data Leakage


Data leakage happens whenever information that would not be available at genuine prediction time ends up influencing the model or its evaluation, producing performance numbers that look better than what the model will achieve in the real world. It is not the same thing as ordinary overfitting—overfitting comes from a model that is too flexible for the amount of clean data available; leakage comes from the evaluation setup itself being compromised, regardless of how the model was built.


Leakage takes several distinct forms:


  • Target leakage: a feature that is itself derived from the outcome, or only knowable after the outcome occurs.

  • Train-test contamination: any overlap of information between the training and test partitions.

  • Preprocessing leakage: fitting a scaler, encoder, or imputer on the full dataset before splitting.

  • Duplicate or near-duplicate leakage: identical or nearly identical rows appearing in both splits.

  • Group leakage: records from the same entity appearing in both training and test sets, discussed above.

  • Temporal leakage: future information reaching the training set.

  • Feature-selection leakage: choosing which features to keep based on statistics computed from the full dataset, including the test portion.

  • Imputation leakage: filling missing values using statistics (like the overall mean) computed across the whole dataset.

  • Scaling leakage: fitting a scaler's parameters on all the data rather than training data alone.

  • Encoding leakage: especially target encoding, where a categorical value is replaced by a statistic of the target—if computed using test rows, that's leakage.

  • Oversampling or synthetic-sampling leakage: generating synthetic minority-class examples before splitting, so synthetic copies derived from test rows end up in training.

  • Test-set tuning leakage: repeatedly adjusting the model based on test performance.


The correct order to prevent nearly all of these:


  1. Split the data.

  2. Fit any learned transformation—scaler, imputer, encoder, feature selector—using only the training data.

  3. Apply those already-fitted transformations to the validation and test data (transform, don't refit).

  4. Train the model on the transformed training data.

  5. Evaluate on the untouched held-out data.


Scikit-learn's Pipeline enforces this order structurally: calling pipeline.fit(X_train, y_train) fits every step using only the data passed in, and calling pipeline.predict(X_test) or pipeline.transform(X_test) reuses the parameters already learned from training, never refitting them on the test data.


Bad example (leakage):


from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # fit on the WHOLE dataset, before splitting

X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42
)

Corrected example:


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)   # fit only on training data
X_test_scaled = scaler.transform(X_test)          # transform, don't refit

To be clear about what is not leakage: deterministic operations that use no dataset-level learned statistic—such as converting a date string into a day-of-week integer, or applying a fixed unit conversion—are not leakage, because they don't borrow any information from the dataset as a whole.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Preprocessing and Resampling


Every preprocessing step that learns something from data—an imputer's fill values, a scaler's mean and standard deviation, an encoder's category mapping, a feature selector's chosen columns, a dimensionality-reduction transform, or a target encoder's per-category statistics—should be fit exclusively on the training set and only ever applied ("transformed") on validation and test data.


Outlier handling deserves the same caution when the outlier threshold is learned from the data (for example, "anything beyond 3 standard deviations of the training mean"), since fitting that threshold on the full dataset would again mix in test information.


Oversampling and undersampling, including synthetic techniques like SMOTE, present a subtler risk. If class rebalancing happens before the split, synthetic examples generated from what later becomes test data can end up in the training set—effectively letting the model train on echoes of the test set. Resampling should normally be applied only inside the training fold (or within each cross-validation fold), never on the complete dataset prior to splitting.


Train-Test Split vs. Cross-Validation


A single holdout split is simple, fast to compute, and easy to explain, but it produces only one estimate, which can be noisy for small datasets and depends somewhat on which rows happened to land in the test set. Cross-validation addresses this by repeating the split multiple times and averaging the results.


Table: Holdout split vs. cross-validation approaches


Method

What it does

Best suited for

Single holdout split

One train/test division

Large datasets, quick iteration

K-fold cross-validation

K rotating splits, averaged

Small-to-medium IID datasets

Stratified K-fold

K-fold preserving class ratios

Imbalanced classification

Grouped cross-validation

K-fold respecting entity groups

Repeated-entity data (patients, users)

Time-series cross-validation

Sequential expanding/rolling folds

Ordered, temporal data

Repeated cross-validation

K-fold repeated with different splits

Small datasets needing stability

Nested cross-validation

Outer loop for evaluation, inner loop for tuning

Reducing model-selection bias


Cross-validation reduces the noise of a single lucky or unlucky split, but it does not automatically justify using the final test set for tuning—the same discipline of "tune on validation/cross-validation folds, evaluate once on the untouched test set" still applies. A final holdout test set and cross-validation are not mutually exclusive: many workflows use cross-validation on the training portion for model selection and hyperparameter tuning, then evaluate the finalized pipeline once on a separate test set that was never part of any fold. Nested cross-validation goes a step further, wrapping an inner loop for hyperparameter selection inside an outer loop for performance estimation, which is valuable when even ordinary cross-validation-based model selection risks optimistic bias. Repeated cross-validation—running K-fold multiple times with different random splits—can give a more stable picture of performance variability, which matters most for smaller datasets.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Evaluating Results After the Split


The right metric depends entirely on the task. For classification, common choices include accuracy, precision, recall, F1-score, ROC AUC, and PR AUC, each emphasizing a different aspect of performance—accuracy alone can be dangerously uninformative for imbalanced classes, as shown earlier. For regression, MAE, RMSE, and R² are standard, each expressing error differently, so pick the one that matches how the errors will actually be used or costed in practice.


Class distribution and real decision costs matter: in fraud detection, missing a fraud case (a false negative) is often far more costly than a false alarm, which should influence which metric and threshold get prioritized. A single point-estimate score can also be incomplete without some sense of its uncertainty—bootstrapping the test set, or examining variability across cross-validation folds, gives a rough sense of how much the reported number might shift with a different sample.


A large gap between training and test performance is a signal worth investigating—commonly overfitting, but also possibly a distribution mismatch between the two subsets or a genuine problem with the features. Conversely, a high test score does not, by itself, prove the absence of leakage or a mismatch with the deployment distribution; it should be treated as encouraging evidence, not final proof. And the test set should not be consulted repeatedly to select the "best" model among many candidates, since doing so gradually reintroduces the same bias a validation set was meant to absorb.


Common Mistakes


Mistake

Why it's harmful

Better approach

Evaluating on training data

Hides overfitting entirely

Always evaluate on held-out data

Preprocessing before splitting

Leaks test statistics into training

Fit preprocessing only on training data

Tuning hyperparameters on the test set

Test score becomes optimistic

Use a validation set or cross-validation for tuning

Randomly shuffling time-series data

Leaks future information

Use chronological or TimeSeriesSplit

Splitting rows when entities should be grouped

Model "recognizes" entities rather than generalizing

Use GroupShuffleSplit / GroupKFold

Ignoring class imbalance

Test set may contain almost no minority-class examples

Use stratify=y

Assuming 80/20 is always best

Ignores dataset size and structure

Choose ratio based on data volume and problem

Forgetting duplicates or near-duplicates

Inflates apparent test performance

Deduplicate before splitting

Placing augmented variants of one example in both sets

Effectively duplicates training data into the test set

Split by original example ID before augmenting

Reusing the same test set across many development rounds

Test set "wears out," score becomes biased

Reserve validation for iteration; test set sparingly

Reporting a score with no split details

Impossible to judge reliability

State ratio, method, and sample sizes

Treating random_state=42 as proof of correctness

Reproducible ≠ valid

Justify the splitting strategy itself

Trusting one lucky split as definitive

Ignores sampling variability

Use cross-validation or repeated splits

Building a test set that doesn't represent deployment

Score won't predict real performance

Match test distribution to deployment conditions

Mixing future information into historical features

Produces temporal leakage

Compute features using only past-available data


Choosing the Right Strategy


The decision essentially comes down to answering three questions in order: Are observations independent of each other? Does the data have a meaningful time order relative to the prediction task? Are classes balanced enough for a plain random split to be reliable?


A simplified decision guide:


  • Ordinary independent tabular data, balanced classes or regression target → random split (train_test_split with shuffle=True).

  • Imbalanced classification → stratified split (stratify=y) or StratifiedShuffleSplit / StratifiedKFold.

  • Repeated records per entity (patients, users, machines, authors) → GroupShuffleSplit / GroupKFold, combined with stratification if also imbalanced.

  • Time-ordered observations, forecasting-style task → chronological holdout or TimeSeriesSplit.

  • Geographic or site-based generalization needs → treat site/region as the group for a group-aware split.

  • Very small datasets → favor cross-validation, repeated cross-validation, or nested cross-validation over a single fixed split.

  • Very large datasets → a single holdout split is usually fine and computationally efficient; consider protected slice-level test sets for important subgroups.

  • Streaming or continuously changing data → chronological splitting plus ongoing monitoring for drift after deployment.

  • Competitions with a predefined holdout → follow the competition's split exactly; don't recreate your own from the training portion in a way that mismatches it.

  • Production systems subject to drift → periodic re-evaluation on freshly collected data, not just the original static test set.


The single unifying principle behind every branch of this guide is to match the evaluation split to the real deployment question: what will the model actually be asked to predict, for whom, and using what information, once it's live?


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Small Datasets


Small datasets create specific evaluation challenges. Too few test observations make any single metric unstable; rare classes can vanish from a subset entirely; overall metrics can swing noticeably between two otherwise-reasonable splits; and reserving a large enough test set to be trustworthy can leave too little data left over for training.


Reasonable responses include using stratification where classes are involved, replacing a single split with K-fold or repeated K-fold cross-validation, applying group-aware cross-validation when entities repeat, and using nested cross-validation when both hyperparameter tuning and final evaluation need to happen on limited data. Reporting a measure of uncertainty alongside the point estimate—rather than a single confident-sounding number—is especially important here. When possible, collecting more data is the most direct fix; when not possible, it's better to be transparent about small test counts than to present a fragile estimate as settled fact. No single technique, including leave-one-out cross-validation, should be treated as automatically best for every small-data situation—the right choice still depends on computational budget and how the data is structured.


Large Datasets


Large datasets flip several of the small-data concerns. A smaller test percentage can still produce a large, statistically precise test set—90/10 on a million-row dataset still leaves 100,000 test rows. This also keeps computation manageable, since retraining or repeated cross-validation on very large datasets can be expensive.


It's still worth checking that the test set represents rare subgroups and covers the relevant temporal or geographic range, rather than assuming a large random sample automatically covers everything proportionally. Deduplication remains important even at scale, since duplicate rows can inflate apparent performance regardless of dataset size. Many mature workflows also maintain a durable, protected holdout set that is rarely touched, alongside separate stress-test or slice-evaluation sets built specifically to check performance on known-important subgroups, rather than relying on one aggregate number to represent every use case.


Reproducibility and Randomness


Random splitting produces different subsets each time it runs unless the randomness is controlled. A random seed, passed as random_state, fixes the internal random number generator so the same call produces the same split every time, which is valuable for debugging and for letting others reproduce your exact result.


Reporting the seed used improves reproducibility, but testing only a single seed can hide how much a result depends on which particular rows landed in the test set. Evaluating a model across several different, reasonable random seeds—and looking at the spread of resulting scores—can reveal whether a result is stable or was partly a matter of luck. It is invalid, however, to search across seeds and report only whichever one produced the best-looking test score; that is a form of test-set tuning by another name. Reproducible experiments and robust conclusions are related but distinct: a result can be perfectly reproducible and still be a fragile, unlucky (or lucky) draw.


Production Considerations


An offline train-test split is only useful to the extent it approximates how the model will actually encounter data after deployment. Several forces can erode that approximation over time: distribution shift in the input features, concept drift where the relationship between features and target itself changes, label definitions that get redefined by the business, and delayed labels that aren't available until long after a prediction is made (common in credit risk or long-cycle sales).


Sound practice includes monitoring performance after deployment rather than assuming the original test score holds indefinitely, setting a schedule for re-evaluation, maintaining a protected benchmark set that isn't touched by day-to-day experimentation, backtesting changes before rollout where feasible, and planning for periodic retraining. If the original test set has influenced many rounds of decisions over a project's lifetime, it's worth treating it as compromised and collecting a fresh evaluation sample rather than continuing to lean on the same one indefinitely.


Best-Practice Checklist


Before training a model, confirm:


  • The prediction unit is clearly defined (a row, a patient, a session, a time window).

  • The deployment scenario is understood (what data will be available at prediction time).

  • Independence assumptions have been checked, not assumed.

  • Time ordering has been identified, if present.

  • Groups or repeated entities have been identified.

  • Duplicates and near-duplicates have been checked and removed.

  • Class balance has been examined.

  • The split proportions are justified by dataset size and structure.

  • Preprocessing is fit only on training data.

  • A validation strategy (holdout validation set or cross-validation) is in place for tuning.

  • The evaluation metric matches the task and its real costs.

  • The random seed is recorded.

  • The test set is protected from repeated use.

  • The whole pipeline is reproducible.

  • The split method, metric, and limitations are reported alongside the score.

  • A plan exists for monitoring performance after deployment.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

FAQ


What is a train-test split in simple terms?


It's dividing your dataset into two parts: one the model learns from (training set), and one it's tested on afterward (test set) to see how well it handles data it has never seen.


Why is train-test splitting necessary?


Without it, you can only measure how well a model fits data it already memorized, which tells you almost nothing about whether it will work on new, real-world cases. Splitting creates a fair, unseen test.


What is the best train-test split ratio?


There isn't one universal best ratio. Common choices are 70/30, 80/20, or 90/10, and the right one depends on your dataset size, class balance, and whether you're also using cross-validation.


Is 80/20 always the best split?


No. 80/20 is a popular default, but it can leave too little data for training on small datasets, or waste precision on very large ones. Base the ratio on your specific data, not habit.


What does random_state=42 mean?


It fixes the random number generator so the split is identical every time the code runs. The number 42 has no special statistical meaning—any fixed integer works the same way for reproducibility.


Should you shuffle data before splitting?


Usually yes, for independent tabular data, since scikit-learn's shuffle=True default helps avoid accidental ordering bias. For time-series data, shuffling is usually wrong, because it lets future information leak into training.


When should you use stratify=y?


Use it for classification problems, especially with imbalanced classes, so the training and test sets keep roughly the same class proportions as the full dataset.


Can you stratify regression data?


Not in the standard sense scikit-learn's stratify parameter is built for, since it expects discrete class labels. Continuous regression targets aren't stratified the same way classification labels are.


What is the difference between validation and test data?


Validation data is used repeatedly during development to tune hyperparameters and compare models. Test data is reserved for one final, unbiased performance estimate after development is finished.


Can the test set be used more than once?


Ideally it's used once. Repeated use for tuning or model selection gradually biases the score, since decisions start being optimized against that specific test sample.


Should preprocessing happen before or after splitting?


After splitting. Fit any learned preprocessing step—scalers, imputers, encoders—only on the training data, then apply that same fitted transformation to the test data.


How do you split time-series data?


Typically by chronological order, training on earlier data and testing on later data, using tools like scikit-learn's TimeSeriesSplit rather than a random shuffle.


How do you split records belonging to the same person or group?


Use a group-aware splitter, such as GroupShuffleSplit or GroupKFold, which keeps every record from one entity entirely in either the training set or the test set, never both.


Is cross-validation better than a train-test split?


They serve different purposes. Cross-validation gives a more stable performance estimate, especially for smaller datasets, but a protected final test set is still valuable for an honest, one-time check.


What should you do with a very small dataset?


Favor cross-validation, repeated cross-validation, or nested cross-validation over a single fixed split, and be cautious about drawing firm conclusions from a handful of test examples.


How can you detect data leakage?


Watch for suspiciously high scores, check whether any preprocessing was fit before splitting, confirm entities and duplicates aren't crossing between train and test, and verify that time-ordered data wasn't shuffled.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Key Takeaways


  • A train-test split exists to estimate how a model will perform on data it hasn't seen, not to measure how well it memorized training data.

  • The test set must stay untouched until a final evaluation, or its score stops being trustworthy.

  • The right splitting method—random, stratified, grouped, or chronological—should match the actual structure of your data.

  • Leakage, in any of its many forms, is one of the most common reasons reported performance doesn't hold up in production.

  • Validation sets and cross-validation exist to guide development decisions without contaminating the final test evaluation.

  • Fixing a random seed makes a split reproducible, but reproducibility alone doesn't make a split statistically sound.

  • A good test set should represent the conditions the model will actually face after deployment.

  • No single split ratio, splitting method, or cross-validation scheme is universally correct for every dataset.


Actionable Next Steps


  1. Identify the real prediction unit for your problem—row, patient, session, or time window.

  2. Check the dataset for groups, timestamps, duplicates, and class imbalance before choosing a split method.

  3. Choose a splitting strategy that matches what you found—random, stratified, grouped, or chronological.

  4. Reserve a protected test set and avoid touching it during development.

  5. Build preprocessing and modeling steps into a single Pipeline to prevent leakage.

  6. Use a validation set or cross-validation for all tuning and model-comparison decisions.

  7. Evaluate on the test set only once the modeling workflow is settled.

  8. Record the split method, evaluation metric, and random seed used.

  9. Check performance across important subgroups, not just the overall average.

  10. Plan how you'll monitor performance and refresh the evaluation data after deployment.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Glossary


  • Cross-validation: A technique that splits data into multiple folds, training and evaluating repeatedly across different subsets to produce a more stable performance estimate.

  • Data leakage: Information from outside the legitimate training scope influencing a model or its evaluation, producing inflated results.

  • Distribution shift: A change in the statistical properties of input data over time, which can reduce a deployed model's accuracy.

  • Feature: An individual measurable input variable used by a model to make predictions.

  • Generalization: A model's ability to perform accurately on new, unseen data beyond its training examples.

  • Grouped split: A split that keeps all records belonging to the same entity in one subset, preventing leakage across related rows.

  • Holdout set: Data withheld from training and used for evaluation, sometimes used interchangeably with test set or validation set depending on context.

  • Hyperparameter: A configuration value chosen before training, rather than learned from data, such as tree depth or learning rate.

  • IID (independent and identically distributed): An assumption that observations don't influence each other and come from the same underlying distribution.

  • Label: The correct answer attached to a training example in supervised learning, also called the target.

  • Model selection: The process of choosing among candidate algorithms or configurations based on validation performance.

  • Out-of-sample data: Data that falls outside the sample used to fit a model.

  • Overfitting: When a model fits training data too closely, including its noise, and performs worse on new data as a result.

  • Pipeline: A scikit-learn object that chains preprocessing steps and an estimator so fitting only happens on training data.

  • Random seed: A fixed value that controls a random number generator, making a random process reproducible.

  • Stratification: A splitting method that preserves the proportion of each class across subsets.

  • Target: The variable a supervised model is trained to predict.

  • Temporal leakage: Future information reaching a model during training when the task requires predicting the future from the past.

  • Test set: A labeled subset withheld from training and used, ideally once, to produce a final, unbiased performance estimate.

  • Training set: The labeled data a model directly learns from.

  • Underfitting: When a model is too simple to capture the real patterns in data, performing poorly on both training and test data.

  • Validation set: A subset used during development to tune hyperparameters and compare models, separate from the final test set.


The Model Failure Fieldbook: Learn AI/ML Through 50 Things That Break
$39.00$19.00
See What’s Inside

Sources & References


  1. Scikit-learn developers. "train_test_split — scikit-learn 1.9 documentation." Scikit-learn. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html

  2. Scikit-learn developers. "StratifiedShuffleSplit — scikit-learn 1.9 documentation." Scikit-learn. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedShuffleSplit.html

  3. Scikit-learn developers. "Cross-validation: evaluating estimator performance." Scikit-learn User Guide. https://scikit-learn.org/stable/modules/cross_validation.html

  4. Scikit-learn developers. "3.1. Cross-validation iterators with stratification based on class labels." Scikit-learn User Guide. https://scikit-learn.org/stable/modules/cross_validation.html#stratification

  5. Scikit-learn developers. "sklearn.model_selection.GroupShuffleSplit." Scikit-learn documentation. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupShuffleSplit.html

  6. Scikit-learn developers. "sklearn.model_selection.GroupKFold." Scikit-learn documentation. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupKFold.html

  7. Scikit-learn developers. "sklearn.model_selection.TimeSeriesSplit." Scikit-learn documentation. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html

  8. Scikit-learn developers. "sklearn.pipeline.Pipeline." Scikit-learn documentation. https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html

  9. Scikit-learn developers. "Common pitfalls and recommended practices." Scikit-learn User Guide. https://scikit-learn.org/stable/common_pitfalls.html

  10. Google for Developers. "Machine Learning Crash Course: Training, validation, and test sets." Google. https://developers.google.com/machine-learning/crash-course

  11. Schema.org. "BlogPosting." Schema.org. https://schema.org/BlogPosting

  12. Schema.org. "FAQPage." Schema.org. https://schema.org/FAQPage

  13. Google Search Central. "Article structured data." Google for Developers. https://developers.google.com/search/docs/appearance/structured-data/article




bottom of page