Validation Strategies and Data Leakage
Learn task-matched validation with grouped, stratified, and temporal splits, out-of-fold predictions, and preprocessing within the training boundary.
Machine Learning from ScratchAll chapters Validation Strategies and Data LeakageFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Validation Strategies and Data Leakage
A model can predict another observation from a familiar customer much more accurately than an observation from a customer it has never seen. A random row split can mix those two tasks when each customer contributes several records. The resulting score may look useful while answering a different question from the one the model will face at deployment.
Validation starts by defining what the model will know when it makes a prediction. We then construct held-out data that reproduce that situation as closely as the available sample permits. New-customer predictions require holding out whole customers; future predictions require respecting time and label availability. Learned preprocessing belongs inside the training boundary of each evaluation.
Building on Statistical Modelling, Standardization, and Normalization, we examine the evaluation procedure around a fitted model. A 1-nearest-neighbor (1-NN) regressor keeps the examples simple: it predicts with the target of the closest training observation. Its behavior also shows why the numerical effect of a poor split depends on the data and learner.
The chapter develops five parts of an evaluation procedure:
- Training fits parameters, validation guides choices, and final testing assesses the frozen choice.
- Preprocessing is part of the model and must be fitted inside the same training fold as the estimator.
- The split should match the task: preserve useful class proportions, hold out new entities, and respect prediction-time information.
- Out-of-fold predictions provide held-out predictions for covered rows; retain the fold assignments to record which observations each fitted pipeline excluded.
- Reproducibility requires a specified data snapshot, procedure, and fold assignment; randomized procedures also need recorded randomness settings.
Advantages
A deployment-relevant split makes model comparisons more useful. Holding out whole customers asks whether the fitted procedure transfers to new customers, while a chronological split asks how it behaves on later observations under the chosen information boundary. These designs address particular sources of dependence and information reuse; they do not by themselves guarantee independent groups, a representative sample, or unchanged deployment conditions.
Comparing a convenient row split with a task-relevant split can reveal a problem during development. A large discrepancy warrants checking entity overlap, available features, and time order. A small discrepancy is not proof of a sound evaluation: a learner may ignore the unavailable information, or other differences between the partitions may offset its numerical effect.
Designing the split also clarifies the feature contract. A spending summary based entirely on outcomes known before prediction can be useful history; a summary that includes the held-out outcome cannot be used for that prediction. Removing unavailable information makes feature computation consistent with that availability boundary, although its measured score may become worse.
Disadvantages
Holding out complete entities can leave few independent units for fitting and evaluation. For example, five equal group folds over 100 customers train on 80 customers at a time. Cross-validation then assesses fits trained on fewer customers than a final fit using all 100. If additional training data improve that learning procedure, this can make CV pessimistic for the final fit; neither improvement nor the bias direction is universal.
Grouped splits require identifiers that represent the entity boundary of the task. A customer should retain the same identifier across relevant tables and visits. Inconsistent identifiers or joins can hide overlap, while assigning one identifier to different customers can create false overlap. Temporal evaluation also needs event times, feature availability times, and, when relevant, the time at which each target becomes known.
With few or heterogeneous groups, results can depend strongly on which groups are held out. Increasing the fold count changes the training sizes and evaluation partitions, but does not create new customers. Report the limited evidence, compare models on common partitions, and use uncertainty methods whose sampling assumptions fit the groups and time structure. Resampling a handful of observed groups cannot supply diversity that was never collected.
Formula
The formulas separate the population quantity we want from the sample quantity we can compute. We first define risk and held-out loss, then describe which observations may contribute to fitting. The grouped and temporal constraints express different deployment tasks; cross-validation repeats the corresponding fitting and evaluation procedure.
Let the collected observations be
Here is the feature vector, is the regression target, and identifies the customer or other group. The examples use the group column to construct splits, rather than as an input to 1-NN. We use squared loss, ; classification uses a loss suited to its target.
Training, validation, and final test roles
Let be the joint distribution of a fresh deployment observation , and let be a fixed predictor. Assume the loss has a finite expectation. Generalization risk is its expected loss:
We generally do not know this population expectation. Validation estimates it from a finite sample. The fitted model and all learned transformations must be evaluated under the information restrictions of the intended task, rather than on rows that contributed to their fitting or selection.
Let , , and be disjoint sets of row indices. The corresponding data subsets are denoted by , , and . Fit on the training subset and let be nonempty. Its empirical test risk is the average loss on the held-out indices:
Two details in that expression carry all the weight:
- We divided by , not by : the estimator is an average over the specific rows in the test set, not over the whole data.
- The model is fixed before those rows are scored; nothing about them influenced the fit.
Sufficient conditions for this additive mean loss to be conditionally unbiased for , given the fitted model, are:
- each with is drawn from the same distribution as the future data, and
- each test observation is statistically independent of the fitted model .
Under these conditions, conditioning on the fitted model leaves each test loss with expectation , so their mean has that expectation too. Dependence among test rows can still affect its variance. Correlated training and test observations can make the model and test rows statistically dependent even without direct test-data reuse. Distribution shift and adaptive use of test information can invalidate this argument. The result concerns the mean loss; applying a nonlinear operation such as a square root need not preserve unbiasedness.
In practice we divide into three disjoint sets with sizes , , and such that
- Training fits model parameters under a given hyperparameter setting.
- Validation compares candidate hyperparameters and chooses the best.
- Test assesses the chosen, frozen procedure on data that did not guide that choice.
The third role matters because validation outcomes guide model selection. Selecting the candidate with the best noisy validation result and reporting that same result can introduce optimism. Separate test data assess the choice without participating in it; another option is an outer resampling scheme that includes the entire selection procedure inside each outer training split.
A practical protocol is to inspect the final test result after choices are fixed. Recomputing the same frozen result does not itself change its statistical role. Changing the model, features, threshold, or evaluation protocol in response to that result uses it for validation; the revised procedure then needs fresh uninvolved evaluation data.
Allocations such as or are examples, not prescriptions. The needed sizes depend on the learning problem, event counts, group structure, and required precision. Cross-validation rotates validation folds within the development data after the final test portion has been reserved.
Preprocessing and target leakage
Preprocessing is part of the fitted procedure. Let be a scaler, imputer, encoder, or feature selector; fit its parameters on training data and fit the predictor on the transformed training rows. If the transform is instead learned from all collected rows, the resulting composite predictor is
The transform has used held-out information. A scaler or unsupervised imputer uses held-out feature values; a supervised selector or target encoder can also use held-out labels. This violates the standard inductive evaluation boundary even when the fitted predictor itself only receives training rows.
For example, a target encoder replaces category by its mean target. If is the category of row and occurs in the collected data, the full-data mean is
This mean includes the labels of held-out rows in category . It therefore cannot be used to encode those rows in an inductive test. A training-only mean is
Apply this fitted mapping to held-out categories. If a category has no training observations, the denominator is zero, so an explicit fallback is needed; the implementation uses the overall training-target mean. Training rows encoded using their own labels can still overfit even when the outer held-out labels are excluded. Inner cross-fitting can construct training encodings from other training folds; scikit-learn documents this behavior for TargetEncoder.fit_transform, whereas fit followed by transform does not invoke that cross-fitting scheme.
Target leakage occurs when a feature supplies the outcome, or information unavailable when that prediction would have been made. A discharge field recorded after the prediction origin or a refund recorded only after the predicted return are examples when those events have not yet occurred. A historical purchase count can be valid. Audit the computation and availability of each feature at the prediction origin; neither a grouped split nor a modest score proves that this audit has passed.
Stratified, grouped, and temporal splits
For an i.i.d. prediction task, a random row split can be appropriate. Exchangeability means the joint distribution is unchanged by permuting row order; it does not mean the rows are independent. Repeated entities and time order require checking whether the available information and dependence match the intended task, rather than treating every row as a fresh independent case.
Stratified splits aim to preserve class proportions across folds, up to integer and allocation constraints. If is the fraction of class among indices in fold , the aim is . This reduces variation in class counts, but does not create independence or preserve all sources of uncertainty. A rare class with fewer observations than folds cannot appear in every fold. When groups or time impose other constraints, exact class balance may be infeasible or inappropriate.
Grouped splits hold out whole entities for a new-entity task. To see why this can matter, consider with mutually independent, zero-mean residuals that are independent of the random group effects, all with finite second moments. Two distinct rows in the same group then have covariance , which is positive when that effect varies. A learner that recognizes the group may use its training history. For an unseen-group test, impose
Every group is assigned wholly to one side. This removes direct entity overlap for the chosen grouping level. Five hundred rows from five customers still provide only five sampled customer histories; their statistical information also depends on within-customer loss dependence and group sizes, so it is not generally equivalent to exactly five independent rows. Group disjointness does not ensure that customers are otherwise independent or representative of future customers.
Temporal splits are appropriate when the task predicts later observations from information available earlier. For a simple chronological hold-out, let be the event time and choose cutoff :
Training precedes the held-out event period in this construction, but event times alone do not establish label or feature availability. Delayed labels and overlapping outcome windows may require additional exclusions. A known future calendar value can be available at prediction time; an unobserved future outcome cannot. Future observations from existing patients call for chronology with eligible patient history. Future observations from previously unseen patients require both temporal and group hold-outs.
Out-of-fold predictions and reproducible evaluation
For the following cross-validation notation, use only development data after the final test set has been reserved, and let now denote the development sample size. Choose an integer with and partition the development indices into nonempty disjoint folds . Fit on all development rows outside , including fitting every learned transform there. The unweighted mean of the fold mean losses is
Here is the complete pipeline fitted without fold . For a complete K-fold partition, each development observation appears in exactly one held-out fold. Its out-of-fold prediction is
The function identifies the fold holding out row . An OOF prediction excludes that row from fitting its prediction pipeline, provided preprocessing and any selection are also restricted correctly. OOF values support error analysis, stacking, calibration, and threshold selection. If they guide a choice, they serve as validation data; the reused selected score is not an independent final test.
GroupKFold forms a complete partition of the groups, so every row can receive one group-held-out prediction. TimeSeriesSplit instead trains on earlier indices and tests on later blocks. With its default unbounded training window, successive training sets expand. Its initial training block receives no held-out prediction, and max_train_size can limit the training history. A manual temporal prediction array should leave unscored rows explicit; cross_val_predict requires every input row to appear in a test fold exactly once and therefore does not accept ordinary TimeSeriesSplit output.
Record the data snapshot, stable row identities, split configuration or exact fold assignments, and the fitted procedure. For a randomized splitter or estimator, also record the relevant randomness settings and dependency versions. A fixed seed does not protect against a changed row order or changed data. Save and apply indices to the same ordered dataset, or map them through stable row identifiers.
Mathematical properties
A few properties of the estimators above matter for how we interpret them.
CV fits use fewer development observations than a final fit on all development data. For equal row folds, each fit uses rows; grouped folds can differ in size. The CV result therefore describes those training sizes and held-out tasks. It can be pessimistic when the learning procedure improves with additional data, but its bias and variance do not have a universal direction or monotonic relationship with .
The variation of a held-out mean depends on the individual loss variances and their covariances. Under independent losses with a common finite variance , its variance is for scored rows. The general variance formula includes pairwise covariances; dependent losses can still be uncorrelated. CV fits share data, so treating the fold scores as independent replicates and dividing their standard deviation by does not give a general confidence interval.
For a chosen pair of evaluations, define a signed observed loss difference:
A positive difference means the evaluation labeled leaky reports a smaller loss in this pair; a negative value means it reports a larger loss. Its sign and magnitude are not guaranteed. If the two evaluations also change customers, training sizes, or fitted models, the gap does not isolate the causal effect of one information leak. The examples below diagnose the information boundary directly.
Loss units matter. Squared-error risk has squared-target units, whereas RMSE has target units. A gap between two RMSEs is therefore a separate descriptive quantity, not the same number as the squared-loss difference above. Neither gap is generally equal to a customer-effect variance.
Visualizing Data Leakage
We first inspect row and group allocations, then examine a simulation where the numerical effect of customer overlap is small and nonmonotone. Together they separate a structural check from a score-based diagnostic.
The allocation diagrams contain six customers with four observations each. The row allocation alternates training and held-out observations within each customer; it is a deterministic illustration of overlap, rather than a random draw. The group allocation holds out customers E and F together.
PythonIn [3]Expand
rng_part = np.random.default_rng(11)
n_cust = 6
rows_per = 4
cust_value = rng_part.uniform(20, 80, n_cust)
cust_labels = [f"Customer {chr(65 + k)}" for k in range(n_cust)]
row_cust = np.repeat(np.arange(n_cust), rows_per)
row_val = cust_value[row_cust] + rng_part.normal(0, 2.5, n_cust * rows_per)
row_pos = np.arange(n_cust * rows_per)
row_is_train_random = (row_pos % rows_per) % 2 == 0
row_is_train_group = row_cust < 4
customer_tick_positions = [row_pos[row_cust == k].mean() for k in range(n_cust)]
allocation_y_limits = (row_val.min() - 5, row_val.max() + 5)
allocation_panels = [
(row_is_train_random, "Row allocation"),
(row_is_train_group, "Group allocation"),
]VisualizationsOut [4]Expand


The diagrams show which customers occur on each side of the split. Their feature ranges can overlap even when the customer identifiers do not. No predictor is fitted in these diagrams, so they do not establish a numerical prediction error. The hand-worked example below supplies an explicit predictor and targets.
The next simulation varies how many held-out customers also have training observations. It uses a scalar feature correlated with the customer baseline, so observations from other customers can still be informative. The function also redraws the held-out customers and changes the number of held-out rows at different overlap settings; this is a descriptive simulation, not a controlled estimate of an overlap effect.
VisualizationOut [6]Expand

The computed curve is not monotone: RMSE ranges from about 3.16 to 3.73 in this run and finishes above its zero-overlap value. Here the scalar feature is itself a noisy measurement of the customer baseline, so nearby observations from other customers can also predict the target. In addition, the function changes the selected customers and the number of test rows as overlap changes. This is a counterexample to treating overlap as a universal numerical penalty: the curve does not isolate a causal effect, and a small score gap does not establish that an evaluation matches deployment. For an unseen-customer task, group disjointness remains a design requirement regardless of this gap.
Example
We now replay the same logic on a dataset small enough that every number can be verified by hand, which separates the mechanism from any library behavior. We use four customers, each contributing two rows. This is a smaller version of the setup in the first figure.
The four customers and their observations are:
- Customer A: and
- Customer B: and
- Customer C: and
- Customer D: and
For each customer, the second observation is one unit higher in feature space and one unit higher in the target, so a model that assigns each row its same-customer sibling will predict it almost exactly.
| customer | row | x | y | |
|---|---|---|---|---|
| 0 | A | 1 | 10 | 5 |
| 1 | A | 2 | 11 | 6 |
| 2 | B | 1 | 50 | 25 |
| 3 | B | 2 | 51 | 26 |
| 4 | C | 1 | 20 | 10 |
| 5 | C | 2 | 21 | 11 |
| 6 | D | 1 | 80 | 40 |
| 7 | D | 2 | 81 | 41 |
| customer | row | x | y |
|---|---|---|---|
| A | 1 | 10 | 5 |
| A | 2 | 11 | 6 |
| B | 1 | 50 | 25 |
| B | 2 | 51 | 26 |
| C | 1 | 20 | 10 |
| C | 2 | 21 | 11 |
| D | 1 | 80 | 40 |
| D | 2 | 81 | 41 |
We use a 1-NN regressor on a single scalar feature . The prediction for a test observation is the target of the training observation with the smallest . For our data this rule is easy to evaluate by hand.
The row split
The leaking row split assigns the first row of every customer to training and the second to test. This deterministic allocation guarantees a training sibling for every test row. It illustrates overlap that a random row split can produce; a random split can also place both of a customer's rows in the test set.
Training set: , one row per customer.
Test set: , the sibling row of each customer.
For each test row, we find the nearest training and read off its .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
Mean squared error . RMSE .
The group split
Now we assign whole customers to one side, enforcing the group-disjointness condition. We place customers A and B in training and customers C and D in test. All four test rows belong to unseen customers.
Training set: .
Test set: .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
- Test row : distances , , , . Nearest is with . Prediction . Squared error .
Mean squared error . RMSE .
Reading the two estimates side by side
The row-split RMSE is 1.000 and the group-split RMSE is about 10.747, a difference of about 9.747 target units. The row split tests siblings of training observations; the group split tests different customers on a different set of target values. This illustrates how task and partition choices can change the error. It does not estimate a universal leakage penalty or imply that every learner would make the same errors.
PythonIn [9]Expand
def one_nn_predict(train_df, x_test_values):
preds = []
for x_val in x_test_values:
distances = np.abs(train_df["x"].values - x_val)
nearest = train_df.iloc[np.argmin(distances)]
preds.append(nearest["y"])
return np.array(preds)
row_train = example_df[example_df["row"] == 1].reset_index(drop=True)
row_test = example_df[example_df["row"] == 2].reset_index(drop=True)
row_preds = one_nn_predict(row_train, row_test["x"].values)
row_rmse = np.sqrt(np.mean((row_preds - row_test["y"].values) ** 2))
group_train = example_df[example_df["customer"].isin(["A", "B"])].reset_index(
drop=True
)
group_test = example_df[example_df["customer"].isin(["C", "D"])].reset_index(
drop=True
)
group_preds = one_nn_predict(group_train, group_test["x"].values)
group_rmse = np.sqrt(np.mean((group_preds - group_test["y"].values) ** 2))ConsoleOut [10]Expand
Row split RMSE: 1.000 Group split RMSE: 10.747 Score difference: 9.747
In this tiny row allocation, every nearest training observation is the held-out row's same-customer sibling. In the grouped allocation, C and D have no training siblings, and the stated 1-NN rule selects rows from A or B. These are properties of this constructed dataset, not guarantees for all repeated-customer data.
Implementation in scikit-learn
We next use six synthetic customers with 60 observations each. This deliberately small example makes the limited group count visible and remains inexpensive to execute. The hand-written 1-NN rule above supplies the primitive prediction mechanism; scikit-learn supplies the splitters and the fold-fitted preprocessing below.
We will cover five things in sequence:
- generate data with repeated customers and a customer-level target component,
- compute cross-validated RMSE with a leaking row split and with a group split,
- compute an honest held-out RMSE on unseen customers as a reference,
- inspect preprocessing information flow even when the observed score gap is zero or has the opposite sign,
- demonstrate the API for stratified and time-series splitters.
PythonIn [11]Expand
rng_imp = np.random.default_rng(1234)
n_customers = 6
rows_per_cust = 60
cust_baseline = rng_imp.normal(50, 20, n_customers)
cust_ids = np.repeat(np.arange(n_customers), rows_per_cust)
X = (
np.repeat(cust_baseline, rows_per_cust)
+ rng_imp.normal(0, 2.0, n_customers * rows_per_cust)
).reshape(-1, 1)
y = np.repeat(cust_baseline, rows_per_cust) + rng_imp.normal(
0, 1.5, n_customers * rows_per_cust
)ConsoleOut [12]Expand
Observations: 360 Customers: 6 Baseline y range: [17.9, 108.3] Mean y: 60.57, std y: 26.85
Each customer has a latent baseline. The scalar feature is that baseline plus Gaussian noise with standard deviation 2.0, and the target is the same baseline plus independent noise with standard deviation 1.5. A nearby sibling may therefore be informative, but rows from other customers with similar features can also be useful. The seed fixes this particular synthetic draw; it does not make it representative of every deployment.
PythonIn [13]Expand
def cv_rmse(splitter, X, y, groups=None):
scores = []
for tr, te in splitter.split(X, y, groups):
pipeline = make_pipeline(
StandardScaler(), KNeighborsRegressor(n_neighbors=1)
)
pipeline.fit(X[tr], y[tr])
preds = pipeline.predict(X[te])
scores.append(np.sqrt(np.mean((preds - y[te]) ** 2)))
return np.array(scores)
random_kf = cv_rmse(KFold(n_splits=5, shuffle=True, random_state=42), X, y)
group_kf = cv_rmse(GroupKFold(n_splits=5), X, y, groups=cust_ids)
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr_gss, te_gss = next(gss.split(X, y, groups=cust_ids))
pipeline = make_pipeline(StandardScaler(), KNeighborsRegressor(n_neighbors=1))
pipeline.fit(X[tr_gss], y[tr_gss])
holdout_rmse = np.sqrt(np.mean((pipeline.predict(X[te_gss]) - y[te_gss]) ** 2))ConsoleOut [14]Expand
Random K-fold RMSE: 2.297 ± 0.244 (fold SD) Group K-fold RMSE: 9.979 ± 13.923 (fold SD) Group held-out RMSE: 23.060
For these six customers, the executed mean fold RMSE is about 2.30 for random K-fold and 9.98 for GroupKFold; the separate grouped hold-out has RMSE about 23.06. They use the same predictor but ask different questions about existing versus unseen customers. With only six groups and five folds, the grouped test folds contain either one or two customers, so both the fold scores and the hold-out are highly sensitive to which customers are held out. Their disagreement is a reason to report the actual partition and limited evidence, rather than to claim a precise deployment estimate. The printed ± values are descriptive population standard deviations of the five fold scores, not standard errors or confidence intervals.
The fold-level counts make entity overlap visible independently of the score. Every random fold in this draw shares all six customers with its training rows, whereas every grouped fold shares none. Overlap does not by itself establish that 1-NN selects a sibling; the nearest-neighbor distances determine that.
VisualizationOut [16]Expand

PythonIn [17]Expand
score_labels = ["Random row CV", "Grouped CV", "Grouped hold-out"]
score_values = [random_kf.mean(), group_kf.mean(), holdout_rmse]The bar chart shows these scores side by side.
VisualizationOut [18]Expand

The prediction scatters show the row and group hold-outs separately. The row split places many predictions near the identity line; the grouped hold-out has larger errors for its selected unseen customers. These figures compare particular samples, so their appearance does not establish a universal ordering of model performance.
VisualizationsOut [20]Expand


Now we turn to preprocessing while holding the group folds fixed. Fitting the scaler on all rows violates the training-only preprocessing boundary. In this one-dimensional 1-NN example, however, applying a common positive affine transformation to training and test distances preserves their nearest-neighbor ordering, apart from numerical tie effects. We should therefore expect the two scores to agree here, rather than expect a guaranteed score benefit from leakage.
PythonIn [21]Expand
def cv_rmse_scaling_leak(X, y, groups):
clean_scores, leaky_scores = [], []
for tr, te in GroupKFold(n_splits=5).split(X, y, groups):
scaler_clean = StandardScaler().fit(X[tr])
model = KNeighborsRegressor(n_neighbors=1)
model.fit(scaler_clean.transform(X[tr]), y[tr])
clean_scores.append(
np.sqrt(
np.mean(
(model.predict(scaler_clean.transform(X[te])) - y[te]) ** 2
)
)
)
scaler_leaky = StandardScaler().fit(X)
model_leaky = KNeighborsRegressor(n_neighbors=1)
model_leaky.fit(scaler_leaky.transform(X[tr]), y[tr])
leaky_scores.append(
np.sqrt(
np.mean(
(model_leaky.predict(scaler_leaky.transform(X[te])) - y[te])
** 2
)
)
)
return np.array(clean_scores), np.array(leaky_scores)
clean_scale, leaky_scale = cv_rmse_scaling_leak(X, y, cust_ids)ConsoleOut [22]Expand
Scaler fit per fold: 9.9789 Scaler fit on all data: 9.9789 Difference: 0.0000
The two scores are equal in the executed comparison. That equality follows from this predictor and transformation; it does not make globally fitted preprocessing a valid general evaluation procedure. Leakage must be identified by information flow, because its presence does not guarantee a visible improvement in every score.
PythonIn [23]Expand
rng_te = np.random.default_rng(7)
n_te = 800
cats_te = rng_te.integers(0, 30, n_te)
cat_effect = rng_te.normal(0, 4, 30)
y_te_target = cat_effect[cats_te] + rng_te.normal(0, 0.5, n_te)
tr_idx = rng_te.choice(n_te, size=int(0.8 * n_te), replace=False)
te_idx = np.setdiff1d(np.arange(n_te), tr_idx)
def encode_leaky(cats, y):
mapping = pd.Series(y).groupby(cats).mean()
return pd.Series(cats).map(mapping).values
def encode_clean(cats_tr, y_tr, cats_te):
mapping = pd.Series(y_tr).groupby(cats_tr).mean()
return pd.Series(cats_te).map(mapping).fillna(y_tr.mean()).values
feat_all = encode_leaky(cats_te, y_te_target).reshape(-1, 1)
model_leaky = KNeighborsRegressor(n_neighbors=1)
model_leaky.fit(feat_all[tr_idx], y_te_target[tr_idx])
rmse_target_leak = np.sqrt(
np.mean((model_leaky.predict(feat_all[te_idx]) - y_te_target[te_idx]) ** 2)
)
feat_train_clean = encode_clean(
cats_te[tr_idx], y_te_target[tr_idx], cats_te[tr_idx]
).reshape(-1, 1)
feat_test_clean = encode_clean(
cats_te[tr_idx], y_te_target[tr_idx], cats_te[te_idx]
).reshape(-1, 1)
model_clean = KNeighborsRegressor(n_neighbors=1)
model_clean.fit(feat_train_clean, y_te_target[tr_idx])
rmse_target_clean = np.sqrt(
np.mean((model_clean.predict(feat_test_clean) - y_te_target[te_idx]) ** 2)
)ConsoleOut [24]Expand
Target encoder fit on all data: 0.6937 Target encoder fit on train: 0.6766
The full-data target encoder uses held-out labels in its category means and is therefore leaky even though its executed RMSE (about 0.694) is slightly worse than the training-only version (about 0.677) in this draw. Both exceed the simulated noise standard deviation of 0.5. With 1-NN, repeated category encodings create ties, and predictions can depend on which training observation is selected among tied neighbors. This experiment demonstrates the information-flow violation, not a guaranteed direction or magnitude of score inflation. A finite-sample RMSE below a noise standard deviation would also not, by itself, prove leakage.
The category-level encodings show where the leak enters. A full-data mean includes the target values of rows that later appear in the test split, so it can shift away from the training-only estimate.
VisualizationOut [26]Expand

Finally, we demonstrate the splitter API for stratified and temporal data. The output shows how each splitter partitions the row indices.
ConsoleOut [28]Expand
StratifiedKFold fold sizes and class balance (using a coarse binary label): Fold 1: n_test= 90, balanced positive rate=0.500 Fold 2: n_test= 90, balanced positive rate=0.500 Fold 3: n_test= 90, balanced positive rate=0.500 Fold 4: n_test= 90, balanced positive rate=0.500 TimeSeriesSplit train and test ranges (illustrative ordered indices): Fold 1: train rows 0 to 8, test rows 9 to 15 Fold 2: train rows 0 to 15, test rows 16 to 22 Fold 3: train rows 0 to 22, test rows 23 to 29
The four stratified folds have positive fraction 0.500 in this constructed label example. This row stratification does not protect customer disjointness and is shown only to explain the API. The temporal example uses 30 illustrative, equally spaced time indices; it does not claim that the synthetic customer records form a real chronological dataset. Its default training windows expand before each held-out block.
The printed ranges are easier to interpret as windows. The figure places each fold's training and test intervals on the row index axis, so the past-then-future constraint is visible at a glance.
VisualizationOut [30]Expand

Computing and checking out-of-fold predictions
The earlier scores are development demonstrations. The grouped hold-out is an illustrative reference, not an untouched external-test claim. For the following OOF calculation, use only its training-side customers and keep its held-out indices outside this computation. Three group folds over those four development customers give every development row one held-out prediction.
We implement standardization and 1-NN from primitive array operations, then compare the resulting predictions with a scikit-learn pipeline on exactly the same folds. The primitive predictor requires finite two-dimensional feature arrays and a one-dimensional target; it rejects inconsistent shapes and an empty training set, and raises an error if distance arithmetic overflows. It chooses the first training row on an exact distance tie. That tie rule is explicit because reference implementations can choose differently when equally distant neighbors have different targets.
PythonIn [31]Expand
def nearest_predict(X_train, y_train, X_query):
X_train = np.asarray(X_train, dtype=float)
y_train = np.asarray(y_train, dtype=float)
X_query = np.asarray(X_query, dtype=float)
if X_train.ndim != 2 or X_query.ndim != 2 or y_train.ndim != 1:
raise ValueError("Expected two feature matrices and one target vector")
if len(X_train) == 0 or X_train.shape[1] == 0:
raise ValueError("Training requires observations and features")
if len(y_train) != len(X_train) or X_query.shape[1] != X_train.shape[1]:
raise ValueError("Inconsistent training or query dimensions")
if not all(np.isfinite(a).all() for a in (X_train, y_train, X_query)):
raise ValueError("Inputs must be finite")
if len(X_query) == 0:
return np.empty(0, dtype=float)
with np.errstate(over="raise", invalid="raise"):
differences = X_query[:, None, :] - X_train[None, :, :]
squared_distances = np.sum(differences**2, axis=2)
return y_train[np.argmin(squared_distances, axis=1)]
X_development = X[tr_gss].copy()
y_development = y[tr_gss].copy()
groups_development = cust_ids[tr_gss].copy()
development_folds = list(
GroupKFold(n_splits=3).split(
X_development, y_development, groups_development
)
)
manual_oof = np.full(len(y_development), np.nan)
heldout_counts = np.zeros(len(y_development), dtype=int)
oof_fold_rmse = []
oof_fold_sizes = []
for train_index, heldout_index in development_folds:
assert len(np.intersect1d(train_index, heldout_index)) == 0
assert set(groups_development[train_index]).isdisjoint(
groups_development[heldout_index]
)
# Learn these statistics from this fold's training rows only.
fold_mean = X_development[train_index].mean(axis=0)
fold_sd = X_development[train_index].std(axis=0, ddof=0)
fold_scale = np.where(fold_sd > 0, fold_sd, 1.0)
train_scaled = (X_development[train_index] - fold_mean) / fold_scale
heldout_scaled = (X_development[heldout_index] - fold_mean) / fold_scale
prediction = nearest_predict(
train_scaled, y_development[train_index], heldout_scaled
)
manual_oof[heldout_index] = prediction
heldout_counts[heldout_index] += 1
oof_fold_sizes.append(len(heldout_index))
oof_fold_rmse.append(
np.sqrt(np.mean((prediction - y_development[heldout_index]) ** 2))
)
assert np.all(heldout_counts == 1)
assert np.isfinite(manual_oof).all()
from sklearn.model_selection import cross_val_predict
reference_oof = cross_val_predict(
make_pipeline(
StandardScaler(),
KNeighborsRegressor(n_neighbors=1, algorithm="brute"),
),
X_development,
y_development,
cv=development_folds,
)
assert np.allclose(manual_oof, reference_oof, rtol=0, atol=1e-12)
pooled_oof_rmse = np.sqrt(np.mean((manual_oof - y_development) ** 2))
reference_oof_rmse = np.sqrt(np.mean((reference_oof - y_development) ** 2))
oof_fold_rmse = np.asarray(oof_fold_rmse)
oof_fold_sizes = np.asarray(oof_fold_sizes)
weighted_fold_rmse = np.sqrt(
np.average(oof_fold_rmse**2, weights=oof_fold_sizes)
)
assert np.isclose(pooled_oof_rmse, weighted_fold_rmse)ConsoleOut [32]Expand
Development customers: 4 Development rows: 240 Every row held out once: True Reference predictions agree: True Pooled OOF RMSE: 22.300 Mean fold RMSE: 15.613
The predictions are stored at the original development-row indices, rather than concatenated in fold order. Each fold fits its own mean and population standard deviation; a constant training feature uses scale 1. The numerical assertion checks agreement with the reference on this dataset, whose nearest-neighbor comparisons have no exact ties. It does not assert that all tie choices or near-constant-feature conventions match across implementations.
Pooling and averaging fold scores answer different aggregation questions. Let be the number of held-out rows and the RMSE in fold . Squaring recovers its mean squared error, so pooled RMSE for those same predictions is
This weights individual squared errors equally, then takes one square root. It generally differs from the arithmetic mean of , even with equal fold sizes. Pooled also differs in general from mean fold , because its baseline uses the pooled target mean. State the metric, weighting, and aggregation used. OOF predictions used to select a procedure still require fresh uninvolved evaluation of that selected procedure.
Key Parameters
KFold(n_splits=5, shuffle=True, random_state=42) partitions row indices into five folds. Shuffling can be useful for an i.i.d. task when storage order is incidental. If order represents prediction-time availability, choose a temporal design instead. With shuffle=True, the recorded random_state controls the random fold assignment for the same input order.
GroupKFold(n_splits=5) keeps each customer wholly within one held-out fold. At least five distinct customers are needed. With default shuffle=False, the implementation attempts to balance sample counts; group sizes can prevent exact balance. shuffle=True, available in the installed scikit-learn 1.8 environment, randomizes groups and attempts to balance group counts instead. StratifiedGroupKFold additionally attempts to preserve class proportions, subject to group constraints; it is not a general cure for every group-size imbalance.
GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) creates one group-disjoint hold-out. Its fractional test_size refers to groups, rounded up, rather than directly to rows. Here it holds out two of the six customers. A development hold-out used for choices has a validation role, while a final test must remain uninvolved in those choices.
StratifiedKFold(n_splits=4, shuffle=True, random_state=42) creates four folds with approximate class-proportion balance. In the current example we stratified on a binarized version of . For actual classification, pass the class labels directly.
TimeSeriesSplit(n_splits=3) creates three later test blocks from ordered input indices. The default unbounded training window expands; max_train_size can cap it. gap excludes a specified number of rows from the end of each training window. Choose exclusions from label windows and availability times; a gap does not automatically establish independence or equal a forecast horizon measured in calendar time.
Key Methods
.split(X, y=None, groups=None) yields (train_indices, test_indices) pairs. For grouped splitters, groups is required and must be an array of group identifiers of length . For StratifiedKFold, pass the class label as y.
sklearn.pipeline.make_pipeline(scaler, model) constructs a composite estimator whose .fit(X_train, y_train) fits each step on the training fold only, and whose .predict(X) applies the same transformations to new data. When fitted within each training fold, this keeps the pipeline's learned transforms inside that fold. Features constructed outside the pipeline still need an availability audit.
.get_n_splits(X=None, y=None, groups=None) returns the number of splits. An OOF array instead has one position per input row; a coverage array can check which rows received predictions.
Practical Implications
Choose the evaluation task and protocol before inspecting candidate scores. A new-customer task calls for group hold-outs, a future-observation task calls for prediction-origin restrictions, and an i.i.d. classification comparison may benefit from stratification. Future predictions for previously unseen patients need both group and temporal restrictions. Future predictions for existing patients can use eligible past patient history; they do not automatically require patient-disjoint folds.
Reserve test data early and retain their identities. Fit and select using development data, then evaluate the frozen procedure on the intended held-out task. If test results guide further choices, regard those data as validation and use fresh uninvolved data for the revised procedure. This separation does not remove possible population shift or sampling uncertainty.
The following practices connect the split to feature construction, reporting, and repeated model comparisons.
Best Practices
Record the task, splitter, and data identities as part of the experimental design. Reuse common partitions for paired comparisons of candidate models, and inspect paired score differences rather than comparing only marginal score spreads. Keep the final test out of those decisions. Classification Metrics and Decision Thresholds applies the same separation to threshold choices.
Wrap every transformation in a pipeline so that the transformer is fitted inside the same training fold as the estimator. make_pipeline(StandardScaler(), KNeighborsRegressor()) fits the scaler on the training rows of each fold and applies the learned statistics to the held-out rows at predict time, which removes the leak by construction. Where a pipeline is impractical, fit the transformer with .fit(X_train) and apply it with .transform(X_test) by hand, using the same recorded row assignment for both calls. What the pipeline enforces is fit-predict consistency with respect to the split, not any property of the estimator itself.
Choose the fold count with the available group count, class counts, training sizes, and computation budget in mind. A randomized row splitter needs recorded randomness settings; a chronological splitter needs ordered, availability-consistent input. Choose any temporal exclusions from the task's label and feature windows. Five or ten folds are examples, not a reason to override those constraints.
Data Requirements and Preprocessing
Use stable identifiers at the level that is new at deployment. A session identifier cannot detect two sessions from the same customer; a customer identifier cannot detect shared households or sites when those are the units that must be held out. Temporal ordering should describe when observations and labels become available to fitting and prediction, rather than relying only on record insertion order.
For each feature, document how its value can be computed at the prediction origin. Past target summaries can be eligible history; a future calendar value can be known in advance. A held-out outcome or an unavailable future event cannot be used. Fit learned scalers, imputers, selectors, and encoders within the training boundary. A pipeline helps enforce this boundary for its steps, but cannot repair a target-derived or unavailable feature that was already built outside it.
Missingness and outliers may differ across groups or periods. Fit imputation statistics on training rows and inspect how performance varies on the held-out task. A fold aligned with a real deployment missingness pattern may be informative rather than erroneous. Neither such alignment nor held-out preprocessing implies a universal numerical direction or size of score bias.
Common Pitfalls
A surprisingly good score, a large row-versus-group difference, or a disagreement with a manual hold-out warrants investigation. None supplies a universal numerical threshold for declaring leakage. A finite-sample RMSE can fall below a population noise standard deviation by chance; score differences can also reflect task difficulty, sampling, or distribution shift. Inspect the information flow and reproduce the feature construction.
Repeatedly changing the procedure after consulting a held-out score uses that score for validation. Unstable identifiers, incorrect joins, or transforms fitted before splitting can introduce additional information reuse. Address those mechanisms directly, then obtain an evaluation that did not inform the resulting revision.
With few groups, report the partition and its limitations. A large held-out group receives more weight in a pooled row loss, whereas an unweighted mean of fold scores assigns equal weight to folds. An equal-customer mean loss is another estimand. Choose and state the weighting that matches the task instead of interpreting every difference as a model improvement.
Computational Considerations
CV computation depends on the number of fits, the observations in each fit, and the learning algorithm. Grouped and row K-fold with similar training sizes are not inherently ordered in cost. Temporal training windows can grow over successive splits. Time representative fits for the actual procedure before choosing a resampling budget.
Save the split assignment to reproduce the evaluation. Index storage grows with the number of stored indices and repeats, so it can matter for very large datasets. Sequentially fitting and scoring folds can avoid retaining all fitted estimators at once; an OOF array still stores one prediction per scored row.
Distinguish the cost of repeating the splitter from the cost of fitting models and constructing features. For many procedures the fits dominate, but this is an empirical property of the workload. Parallelizing folds can reduce elapsed time while increasing memory use, so keep the CPU-sized examples here sequential.
Performance and Deployment Considerations
The independent customer count and within-customer dependence both affect the precision of a new-customer evaluation. In the current six-customer example, the grouped fold RMSE standard deviation is about 13.92, compared with about 0.24 for row CV. These are descriptive spreads over different folds and different evaluation questions; neither is automatically a standard error or a confidence interval. Use common partitions for paired model comparisons and inspect the paired score differences. A group bootstrap can be useful under appropriate sampling assumptions, but six observed groups give little information about the population of future customers.
Compare deployment errors with a development estimate for the same prediction task and metric, while accounting for sampling and label delays. A gap can reflect population shift, changed feature availability, implementation errors, or uncertainty. It does not uniquely identify a change in independence or prove that leakage occurred.
Collecting additional independent customers can improve evidence about new-customer performance under suitable sampling. Additional observations from existing customers can improve within-customer estimation, but do not supply previously unobserved customer types. Report both the group count and the number of observations when discussing precision.
Summary
Validation specifies which information the fitted procedure may use and which deployment task the held-out score assesses. A random row split can be appropriate for an i.i.d. task, while an unseen-customer task needs whole-customer hold-outs and future prediction needs prediction-origin restrictions. Repeated entities alone do not make every evaluation invalid.
Expected risk and empirical mean loss coincide in expectation under the stated distribution and fit-independence conditions; nonlinear scores need separate care. Training fits parameters, validation guides choices, and final testing evaluates the frozen choice. Learned preprocessing belongs inside each training fold, and target or future information must be available at the prediction origin. Group disjointness and pipelines address specific boundaries, rather than proving the entire evaluation valid.
We worked through a four-customer, eight-row example with row-split RMSE 1.00 and group-split RMSE about 10.75. The six-customer simulation produces mean random-fold RMSE about 2.30, grouped-fold RMSE about 9.98, and grouped hold-out RMSE about 23.06. The spread between the latter two is a warning about the limited group count and the particular partitions, rather than evidence that two estimates have converged. The overlap and target-encoding experiments also show that a known information leak need not produce a fixed or even favorable score change. Split design and prediction-time feature availability must be checked directly.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about validation strategies and data leakage.
Validation Strategies and Data Leakage
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.


Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!