Validation Strategies and Data Leakage

Michael BrenndoerferOctober 25, 202647 min read

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
Part 4 of 15Model Evaluation and Selection
Chapter 31Validation Strategies and Data Leakage
Reading the full chapter · 10 sections · 47 min10 sections · 47 min

All 10 sections · 47 min read

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

Notes & comments

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

D={(xi,yi,gi)}i=1n,\mathcal{D} = \{(x_i, y_i, g_i)\}_{i=1}^{n},

Here xi∈Rdx_i \in \mathbb{R}^d is the feature vector, yi∈Ry_i \in \mathbb{R} is the regression target, and gig_i 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, L(y^,y)=(y^−y)2L(\hat y,y)=(\hat y-y)^2; classification uses a loss suited to its target.

Training, validation, and final test roles

Let P\mathcal P be the joint distribution of a fresh deployment observation (x,y)(x,y), and let ff be a fixed predictor. Assume the loss L(f(x),y)L(f(x),y) has a finite expectation. Generalization risk is its expected loss:

R(f)=E(x,y)∼P[L(f(x),y)].\mathcal{R}(f) = \mathbb{E}_{(x, y) \sim \mathcal{P}}\left[ L(f(x), y) \right].

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 ItrainI_\text{train}, IvalI_\text{val}, and ItestI_\text{test} be disjoint sets of row indices. The corresponding data subsets are denoted by Dtrain\mathcal D_\text{train}, Dval\mathcal D_\text{val}, and Dtest\mathcal D_\text{test}. Fit f^\hat f on the training subset and let ItestI_\text{test} be nonempty. Its empirical test risk is the average loss on the held-out indices:

R^test(f^)=1∣Itest∣∑i∈ItestL(f^(xi),yi).\hat{\mathcal{R}}_\text{test}(\hat f) = \frac{1}{|I_\text{test}|} \sum_{i \in I_\text{test}} L(\hat f(x_i), y_i).

Two details in that expression carry all the weight:

  • We divided by ∣Itest∣|I_\text{test}|, not by nn: 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 R(f^)\mathcal R(\hat f), given the fitted model, are:

  • each (xi,yi)(x_i, y_i) with i∈Itesti \in I_\text{test} is drawn from the same distribution P\mathcal{P} as the future data, and
  • each test observation is statistically independent of the fitted model f^\hat f.

Under these conditions, conditioning on the fitted model leaves each test loss with expectation R(f^)\mathcal R(\hat f), 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 D\mathcal{D} into three disjoint sets with sizes ntrainn_\text{train}, nvaln_\text{val}, and ntestn_\text{test} such that

ntrain+nval+ntest=n.n_\text{train} + n_\text{val} + n_\text{test} = n.
  • 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 60/20/2060/20/20 or 70/15/1570/15/15 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 ϕ\phi 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

f^leaked(x)=f^θ(ϕ^D(x)).\hat{f}_\text{leaked}(x) = \hat{f}_\theta\big(\hat{\phi}_\mathcal{D}(x)\big).

The transform ϕ^D\hat\phi_\mathcal D 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 cc by its mean target. If cic_i is the category of row ii and cc occurs in the collected data, the full-data mean is

TED(c)=1∣{i:ci=c}∣∑i:ci=cyi,\text{TE}_\mathcal{D}(c) = \frac{1}{|\{i : c_i = c\}|} \sum_{i : c_i = c} y_i,

This mean includes the labels of held-out rows in category cc. It therefore cannot be used to encode those rows in an inductive test. A training-only mean is

TEtrain(c)=1∣{i∈Itrain:ci=c}∣∑i∈Itrain:ci=cyi,\text{TE}_\text{train}(c) = \frac{1}{|\{i \in I_\text{train} : c_i = c\}|} \sum_{i \in I_\text{train} : c_i = c} y_i,

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 pc(Fk)p_c(F_k) is the fraction of class cc among indices in fold FkF_k, the aim is pc(Fk)≈pc(D)p_c(F_k)\approx p_c(\mathcal D). 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 yi=θgi+εiy_i=\theta_{g_i}+\varepsilon_i 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 Var⁡(θgi)\operatorname{Var}(\theta_{g_i}), which is positive when that effect varies. A learner that recognizes the group may use its training history. For an unseen-group test, impose

Gtrain∩Gtest=∅,Gtrain={gi:i∈Itrain}.\mathcal{G}_\text{train} \cap \mathcal{G}_\text{test} = \emptyset, \qquad \mathcal{G}_\text{train} = \{g_i : i \in I_\text{train}\}.

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 tit_i be the event time and choose cutoff tsplitt_\text{split}:

ti≤tsplit,∀ i∈Itrain,tj>tsplit,∀ j∈Itest.t_i \leq t_\text{split}, \quad \forall\, i \in I_\text{train}, \qquad t_j > t_\text{split}, \quad \forall\, j \in I_\text{test}.

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 nn now denote the development sample size. Choose an integer KK with 2≤K≤n2 \le K \le n and partition the development indices into KK nonempty disjoint folds F1,…,FKF_1,\ldots,F_K. Fit f^(−k)\hat f^{(-k)} on all development rows outside FkF_k, including fitting every learned transform there. The unweighted mean of the fold mean losses is

R^KCV=1K∑k=1K1∣Fk∣∑i∈FkL(f^(−k)(xi),yi),\hat{\mathcal{R}}^\text{CV}_K = \frac{1}{K} \sum_{k=1}^K \frac{1}{|F_k|} \sum_{i \in F_k} L\big(\hat{f}^{(-k)}(x_i), y_i\big),

Here f^(−k)\hat f^{(-k)} is the complete pipeline fitted without fold kk. For a complete K-fold partition, each development observation appears in exactly one held-out fold. Its out-of-fold prediction is

y^ioof=f^(−k(i))(xi),i=1,…,n,\hat{y}^\text{oof}_i = \hat{f}^{(-k(i))}(x_i), \qquad i = 1, \dots, n,

The function k(i)k(i) identifies the fold holding out row ii. 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 (1−1/K)n(1-1/K)n 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 KK.

The variation of a held-out mean depends on the individual loss variances and their covariances. Under independent losses with a common finite variance σL2\sigma_L^2, its variance is σL2/m\sigma_L^2/m for mm scored rows. The general variance formula includes pairwise covariances; dependent losses can still be uncorrelated. CV fits share data, so treating the KK fold scores as independent replicates and dividing their standard deviation by K\sqrt K does not give a general confidence interval.

For a chosen pair of evaluations, define a signed observed loss difference:

Δleak  =  R^honest−R^leaked.\Delta_\text{leak} \;=\; \hat{\mathcal{R}}^\text{honest} - \hat{\mathcal{R}}^\text{leaked}.

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]
CollapseExpand
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]
CollapseExpand
Six customers A through F each have four observations, alternating filled training and open held-out markers within each customer.
Deterministic row allocation: alternating observations from every customer are assigned to training and hold-out. Filled markers are training rows and open markers are held-out rows. This diagram shows entity overlap; it does not fit a predictor or establish a score.
The four observations of customers A through D are filled training markers; all observations of E and F are open held-out markers.
Whole-customer allocation: A through D are training customers and E and F are held out together. Feature ranges can still overlap across customers; the identifier boundary, rather than feature separation, defines this split.

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]
CollapseExpand
RMSE falls from about 3.47 at zero overlap to 3.16 at 40 percent overlap, then rises to about 3.67 at full overlap.
Test RMSE of a 1-NN pipeline as the fraction of test customers whose other rows also appear in training increases from 0 (group split) to 1 (row split). The curve is not monotone here: the scalar feature predicts customer baselines across groups, and the simulation also changes the selected customers and test-set size, so the endpoint gap is not an isolated causal effect of leakage.

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: (x,y)=(10,5)(x, y) = (10, 5) and (11,6)(11, 6)
  • Customer B: (x,y)=(50,25)(x, y) = (50, 25) and (51,26)(51, 26)
  • Customer C: (x,y)=(20,10)(x, y) = (20, 10) and (21,11)(21, 11)
  • Customer D: (x,y)=(80,40)(x, y) = (80, 40) and (81,41)(81, 41)

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.

Out[8]:
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
Worked example dataset with four customers and two observations each.
customerrowxy
A1105
A2116
B15025
B25126
C12010
C22111
D18040
D28141

We use a 1-NN regressor on a single scalar feature xx. The prediction for a test observation is the target of the training observation with the smallest ∣xtrain−xtest∣|x_\text{train} - x_\text{test}|. 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: {(10,5),(50,25),(20,10),(80,40)}\{(10, 5), (50, 25), (20, 10), (80, 40)\}, one row per customer.

Test set: {(11,6),(51,26),(21,11),(81,41)}\{(11, 6), (51, 26), (21, 11), (81, 41)\}, the sibling row of each customer.

For each test row, we find the nearest training xx and read off its yy.

  • Test row (11,6)(11, 6): distances ∣11−10∣=1|11-10| = 1, ∣11−50∣=39|11-50| = 39, ∣11−20∣=9|11-20| = 9, ∣11−80∣=69|11-80| = 69. Nearest is x=10x=10 with y=5y=5. Prediction =5= 5. Squared error =(5−6)2=1= (5-6)^2 = 1.
  • Test row (51,26)(51, 26): distances ∣51−10∣=41|51-10| = 41, ∣51−50∣=1|51-50| = 1, ∣51−20∣=31|51-20| = 31, ∣51−80∣=29|51-80| = 29. Nearest is x=50x=50 with y=25y=25. Prediction =25= 25. Squared error =(25−26)2=1= (25-26)^2 = 1.
  • Test row (21,11)(21, 11): distances ∣21−10∣=11|21-10| = 11, ∣21−50∣=29|21-50| = 29, ∣21−20∣=1|21-20| = 1, ∣21−80∣=59|21-80| = 59. Nearest is x=20x=20 with y=10y=10. Prediction =10= 10. Squared error =(10−11)2=1= (10-11)^2 = 1.
  • Test row (81,41)(81, 41): distances ∣81−10∣=71|81-10| = 71, ∣81−50∣=31|81-50| = 31, ∣81−20∣=61|81-20| = 61, ∣81−80∣=1|81-80| = 1. Nearest is x=80x=80 with y=40y=40. Prediction =40= 40. Squared error =(40−41)2=1= (40-41)^2 = 1.

Mean squared error =14(1+1+1+1)=1= \frac{1}{4}(1 + 1 + 1 + 1) = 1. RMSE =1=1.000= \sqrt{1} = 1.000.

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: {(10,5),(11,6),(50,25),(51,26)}\{(10, 5), (11, 6), (50, 25), (51, 26)\}.

Test set: {(20,10),(21,11),(80,40),(81,41)}\{(20, 10), (21, 11), (80, 40), (81, 41)\}.

  • Test row (20,10)(20, 10): distances ∣20−10∣=10|20-10| = 10, ∣20−11∣=9|20-11| = 9, ∣20−50∣=30|20-50| = 30, ∣20−51∣=31|20-51| = 31. Nearest is x=11x=11 with y=6y=6. Prediction =6= 6. Squared error =(6−10)2=16= (6-10)^2 = 16.
  • Test row (21,11)(21, 11): distances ∣21−10∣=11|21-10| = 11, ∣21−11∣=10|21-11| = 10, ∣21−50∣=29|21-50| = 29, ∣21−51∣=30|21-51| = 30. Nearest is x=11x=11 with y=6y=6. Prediction =6= 6. Squared error =(6−11)2=25= (6-11)^2 = 25.
  • Test row (80,40)(80, 40): distances ∣80−10∣=70|80-10| = 70, ∣80−11∣=69|80-11| = 69, ∣80−50∣=30|80-50| = 30, ∣80−51∣=29|80-51| = 29. Nearest is x=51x=51 with y=26y=26. Prediction =26= 26. Squared error =(26−40)2=196= (26-40)^2 = 196.
  • Test row (81,41)(81, 41): distances ∣81−10∣=71|81-10| = 71, ∣81−11∣=70|81-11| = 70, ∣81−50∣=31|81-50| = 31, ∣81−51∣=30|81-51| = 30. Nearest is x=51x=51 with y=26y=26. Prediction =26= 26. Squared error =(26−41)2=225= (26-41)^2 = 225.

Mean squared error =14(16+25+196+225)=4624=115.5= \frac{1}{4}(16 + 25 + 196 + 225) = \frac{462}{4} = 115.5. RMSE =115.5≈10.747= \sqrt{115.5} \approx 10.747.

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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
Each of five folds has six shared customers for random row CV and zero for grouped CV, with all zero values labeled explicitly.
All six customers occur on both sides of every random fold in this draw, while every grouped fold has zero shared customers. This checks the entity boundary for an unseen-customer task. It does not establish which training observation a nearest-neighbor predictor selects.
PythonIn [17]
CollapseExpand
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]
CollapseExpand
Three bars show RMSE about 2.30 for random K-fold, 9.98 for Group K-fold, and 23.06 for the grouped hold-out.
Random row CV, grouped CV, and a grouped hold-out on six synthetic customers with 60 observations each. Their scores describe different held-out partitions. Only six groups are available, and the large disagreement between grouped CV and the grouped hold-out shows why one split cannot establish a precise deployment risk.

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]
CollapseExpand
Predicted versus actual target for the row hold-out, with many points close to the identity line.
Row hold-out predictions for the fixed synthetic draw. Many observations lie near the identity line. Sharing customers makes siblings available, but does not guarantee that every nearest neighbor is a sibling.
Predicted versus actual target for the customer hold-out, including a low-target cluster with predictions above the identity line.
Whole-customer hold-out predictions on its two selected unseen customers. Predictions use training customers, and this partition has larger errors. This sample-specific comparison does not establish a universal error ordering.

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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
Category-mean changes above and below a zero reference line, plotted against each training-only category mean.
The vertical coordinate is the full-data category mean minus its training-only mean. Zero means unchanged; departures from zero reveal the contribution of held-out targets. The horizontal coordinate retains the training mean, so adding the vertical difference recovers the full-data mean. The shifts are small in this draw but the information boundary is still violated.

Finally, we demonstrate the splitter API for stratified and temporal data. The output shows how each splitter partitions the row indices.

ConsoleOut [28]
CollapseExpand
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]
CollapseExpand
Three rows show blue training slots 0–8, 0–15, and 0–22, followed by orange held-out slots 9–15, 16–22, and 23–29.
Default expanding-window folds on 30 illustrative ordered observations. Every unit-wide slot represents one observation. Training ends before each held-out block begins; later folds can train on observations used to evaluate earlier fits. The first nine observations are never held out in this three-split construction.

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]
CollapseExpand
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]
CollapseExpand
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 nkn_k be the number of held-out rows and rkr_k the RMSE in fold kk. Squaring rkr_k recovers its mean squared error, so pooled RMSE for those same predictions is

RMSE⁡pooled=∑k=1Knkrk2∑k=1Knk.\operatorname{RMSE}_{\text{pooled}} =\sqrt{\frac{\sum_{k=1}^{K} n_k r_k^2}{\sum_{k=1}^{K} n_k}}.

This weights individual squared errors equally, then takes one square root. It generally differs from the arithmetic mean of rkr_k, even with equal fold sizes. Pooled R2R^2 also differs in general from mean fold R2R^2, 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 yy. 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 nn. 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

Question 1 of 80 of 8 completed
In the three-way evaluation protocol described in the chapter, what is the role of the validation set?
Join the discussionAsk a question or share what you learned.

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!

A different angle
All writing
Machine Learning

Hyperparameter Search and Nested Cross-Validation

Master hyperparameter search and nested cross-validation. Compare grid and random search, diagnose selection bias, and evaluate tuning procedures honestly.

51 min read
Machine Learning

Classification Metrics and Decision Thresholds

Learn how confusion matrices, precision, recall, ROC AUC, and average precision guide threshold selection under class imbalance and decision costs in practice.

68 min read
World Models Handbook · Vol. 4

Frontier Questions and Research Roadmaps

Frontier questions and research roadmaps for world models: generality contracts, causal structure, persistent memory, and decision-relevant evaluation.

98 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026validationstrategies, author = {Michael Brenndoerfer}, title = {Validation Strategies and Data Leakage}, year = {2026}, url = {https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2026). Validation Strategies and Data Leakage. Retrieved from https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch
MLAAcademic
Michael Brenndoerfer. "Validation Strategies and Data Leakage." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Validation Strategies and Data Leakage." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Validation Strategies and Data Leakage'. Available at: https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Validation Strategies and Data Leakage. https://mbrenndoerfer.com/writing/validation-data-leakage-from-scratch
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy