| name | dataset-splitting |
| description | Create reproducible train/validation/test splits with stratification, leakage prevention, and distribution validation. Covers random, stratified, grouped, and time-series split strategies. |
| tags | ["data-splitting","train-test-split","cross-validation","stratification","leakage-prevention","dataset-curation"] |
Dataset Splitting
Overview
Train/validation/test splits are the single most important guardrail against overfitting and data leakage. A bad split invalidates everything downstream. Split once, lock the split, and never let test data influence any decision.
When to Use
Use this skill when:
- Preparing data for supervised learning.
- Designing evaluation protocols for model comparison.
- Setting up cross-validation folds.
- Ensuring no data leakage between splits.
Do not use for:
- Unsupervised learning evaluation — different rules apply.
- Time-series forecasting with backtesting — use
darts or prophet skills.
- Data cleaning — use
dataset-cleaning before splitting.
Split Strategies
1. Standard Random Split (IID)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y,
random_state=42
)
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.15, stratify=y, random_state=42
)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.1765, stratify=y_temp, random_state=42
)
2. Stratified Split (Class Imbalance)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=df[['target', 'gender', 'region']].apply(tuple, axis=1),
random_state=42
)
3. Group-Level Split (No Cross-Contamination)
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=df['patient_id']))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
4. Time-Series Split (No Future Leakage)
df = df.sort_values('timestamp')
split_idx = int(len(df) * 0.8)
train = df.iloc[:split_idx]
test = df.iloc[split_idx:]
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
Cross-Validation Setup
from sklearn.model_selection import StratifiedKFold, RepeatedStratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42)
Leakage Prevention Checklist
Before locking the split, verify:
- No ID leakage: same entity does not appear in train and test.
- No temporal leakage: all train timestamps precede all test timestamps.
- No target leakage: no feature derived from test data (including imputation, scaling, encoding).
- No group leakage: groups (patients, users, experiments) are fully in one split.
- Stratification preserved: target distribution similar across splits.
for split_name, split_df in [('train', train_df), ('val', val_df), ('test', test_df)]:
print(f"{split_name}: {split_df['target'].value_counts(normalize=True).to_dict()}")
train_groups = set(train_df['group_id'])
test_groups = set(test_df['group_id'])
assert len(train_groups & test_groups) == 0, "Group leakage detected!"
Locking the Split
Once created, save split assignments immutably:
df['split'] = 'train'
df.loc[val_idx, 'split'] = 'val'
df.loc[test_idx, 'split'] = 'test'
df.to_parquet('dataset_v1.0.0_with_splits.parquet', index=False)
np.savez('split_indices_v1.0.0.npz',
train=train_idx, val=val_idx, test=test_idx)
Quality Gate
A split is valid when:
- Every row belongs to exactly one split.
- No group, entity, or timestamp leakage exists.
- Target distribution is consistent across splits (within tolerance).
- Split artifacts (indices, assignments) are saved and versioned.
- All preprocessing decisions are made using only the training set.