Your training and test tables can have no repeated record IDs while containing exactly the same people. If your research question concerns predictions for unseen subjects, a row-level deduplication check does not establish the boundary you need.
Here is a complete, deliberately small example. It uses twelve self-written synthetic records and Python's standard library. It does not fit a model, compare predictive performance, or describe a customer result. The useful output is a split audit that another person can recompute.
Start with the prediction target
Suppose four people, A through D, each contribute three observations. A record identifier distinguishes observations; a subject identifier binds the three observations from one person together. These identifiers answer different questions.
For an unseen-person target, training and testing should not share that person. Stable subject-specific features could otherwise carry across the split, so the test would not represent a wholly unseen subject. This is a possible failure mechanism, not a measured effect in our example: there are no features, outcomes, or fitted predictions here.
For future observations from people already known to the model, the evaluation question changes. A subject boundary alone cannot protect time order. You must specify what would have been available when the prediction was made, and construct the evaluation accordingly. Do not choose the boundary after looking for whichever score is highest.
Build the material and preserve the two identifiers
rows = [ {"record_id": f"{person}{visit}", "subject_id": person, "visit": visit} for person in "ABCD" for visit in (1, 2, 3) ] # First two records from every person train; third record tests. row_train = [r for r in rows if r["visit"] < 3] row_test = [r for r in rows if r["visit"] == 3] # One illustrative unseen-person holdout: all of D stays together. group_train = [r for r in rows if r["subject_id"] != "D"] group_test = [r for r in rows if r["subject_id"] == "D"] def audit(train, test): train_records = {r["record_id"] for r in train} test_records = {r["record_id"] for r in test} train_subjects = {r["subject_id"] for r in train} test_subjects = {r["subject_id"] for r in test} return { "train_rows": len(train), "test_rows": len(test), "duplicate_record_ids": sorted(train_records & test_records), "overlapping_subjects": sorted(train_subjects & test_subjects), } print("row split:", audit(row_train, row_test)) print("subject holdout:", audit(group_train, group_test)) Exact output from running this code locally:
row split: {'train_rows': 8, 'test_rows': 4, 'duplicate_record_ids': [], 'overlapping_subjects': ['A', 'B', 'C', 'D']} subject holdout: {'train_rows': 9, 'test_rows': 3, 'duplicate_record_ids': [], 'overlapping_subjects': []} The row split passes the duplicate-record check. It fails the unseen-subject boundary: all four test subjects were present in training. The second split reserves all three records from D and therefore has no shared subject IDs.
Notice that the row counts also changed, from 8/4 to 9/3. You cannot interpret these counts as a controlled accuracy comparison. No accuracy was calculated, and an empty intersection does not show that a model improved.
The local audit assumes the identifiers are correct. If one person appears under two different IDs, a set intersection will not recover that hidden identity. Conversely, unrelated people with an accidentally shared ID could produce a misleading intersection. Check how identifiers were created before treating the audit as conclusive.
One holdout is not an executed cross-validation study
The second split illustrates one held-out subject. It is not a claim that we ran GroupKFold or validated a predictive model. Group cross-validation rotates whole groups between training and testing while preserving the relevant group boundary within each fold.
Choose the grouping unit from the intended use. Repeated observations may be grouped by participant; a different study could require attention to sites, experiments, batches, or other dependencies. A participant-only split does not automatically answer an unseen-site question. Enough groups and suitable outcomes still need to exist for the proposed evaluation.
In this example, four tiny synthetic groups are teaching material. They are not evidence that four groups would be adequate for a real research study.
Keep preprocessing inside the training boundary
Even a correct subject split can be undermined if preprocessing learns from held-out data. Imputation, scaling, and feature selection need to be fitted on the training portion of each fold, with the fitted rules then applied to the held-out portion.
A pipeline can help maintain that separation for transformations it actually contains. It cannot repair features that were already constructed using future information, incorrect identity linkage, or a test set that influenced your research choices. Retain the provenance of upstream transformations as well as the final split.
The scikit-learn common-pitfalls documentation explains why splitting before learning preprocessing rules matters. Its grouped cross-validation documentation explains the distinct role of groups; a plain row-level split is not a substitute for supplying the grouping information.
Give the AI a checkable audit request
An assistant can help organize this work, but its conclusion needs visible supporting artifacts. A useful request is:
Given this authorized synthetic table and its frozen split flags, report training and test row counts, repeated record IDs, and intersecting subject IDs. State the intended prediction target and the grouping unit. Identify which data fitted each preprocessing operation. Do not train a model or infer accuracy. List missing information and keep the split version unchanged.
Provide subject ID, record ID, any relevant time field, split assignment, and the actual prediction target. If a real dataset has restrictions, use its approved controlled path; this public toy example is not permission to paste private participant-level data into a model.
Then rerun the set calculations independently and compare the exact output. Check whether preprocessing provenance is supported, rather than accepting a reassuring sentence. An assistant cannot infer a missing time boundary, identity mapping, or transformation history from row counts alone.
The practical first question is simple: whom is this evaluation meant to represent? After that, inspect the corresponding boundary. A high score and an empty duplicate-row list can both coexist with the wrong evaluation population.
I work on RigorMed. This article is an original synthetic teaching example, not a customer outcome or a claim that a new product workflow was executed.
Sources
- scikit-learn: cross-validation on grouped data and time series, especially sections 3.1.2.4–3.1.2.6; read November 3, 2026.
- scikit-learn: common pitfalls and data leakage, sections 12.1–12.2; read November 3, 2026.