A test score is only as honest as the split behind it. Three failures inflate it: the same thing on both
sides (duplicate rows, near-duplicates, the same entity), the future in the past (training rows later
than test rows when the model predicts forward), and an easy test set (test data that differs from
production). Shift is not inflation but matters just as much: a test set that looks unlike production
measures the wrong thing. scripts/split_audit.py runs nine checks on two CSVs; scripts/near_duplicates.py
finds the duplicates that exact matching misses, including edited text copies.
Trigger Conditions
Activate this skill when the user:
- Asks "is my train/test split OK?" or "why is my test score so high?"
- Sees a model collapse in production after a great test score
- Has repeated entities, dumps from several systems, scraped or user-generated text (likely duplicates)
- Wants to compare a training set with a production sample (drift before deployment)
- Builds a holdout set and wants to be sure it is clean
CV design (folds rather than one split): cross-validation-strategy-designer; feature-level leakage: target-leakage-detector.
Workflow
1. DESCRIBE -> how was the split made? what will production look like (new entities, later time)?
2. AUDIT -> python scripts/split_audit.py train.csv test.csv --target y --id-col id [--time-col ts --expect forward]
3. NEAR-DUPS -> python scripts/near_duplicates.py train.csv test.csv --drop id [--text-col body]
4. FIX -> dedupe / group split / time split; rebuild the test set
5. SHIFT -> python scripts/split_audit.py train.csv prod_sample.csv --drop id (is test like production?)
6. RECHECK -> re-run both scripts; re-evaluate the model on the cleaned splitChecks
| Code | Check | Why it matters | Fix |
|---|---|---|---|
| SA001 | identical feature rows in both sets | the model has memorised test rows | dedupe before splitting, or group duplicates |
| SA002 | ID / entity on both sides | entity memorisation (customer, patient) | grouped split by entity |
| SA003 | test rows not after the last training time (forward expectation) | future information in training | time-based split, with a gap for label windows |
| SA004 | feature drift (PSI > 0.25; KS) | test unlike training | investigate; maybe the split is right and the world changed |
| SA005 | one feature separates the sets (AUC > 0.7) | split encoded in a feature (time, source system) | remove/rebuild the feature or accept and document a time split |
| SA006 | target distribution shift | metrics not comparable; prior shift | stratify, or recalibrate |
| SA007 | unseen categories in test | encoders must handle unknowns | handle_unknown, monitor rate |
| SA008 | missing-share difference > 10 points | pipeline / source differences | fix extraction |
| SA009 | sizes, ratio, forward/backward | context | - |
On the churn fixture the audit finds the three planted problems (12 duplicated customers, 10.7% of test):
SA001 [high] (rows): 12 test row(s) (10.7%) have an identical feature row in train
SA002 [high] customer_id: 12 customer_id value(s) in both sets, covering 12 test rows (10.7%)
SA003 [high] signup_date: 12 test row(s) (10.7%) are not after the last training time 2024-02-02Near-Duplicates
Exact matching misses: floats re-exported with noise (3 vs 3.0000000001), case and whitespace changes, and
edited copies of text (tickets re-opened, reviews posted twice, product titles with a suffix). near_duplicates.py
rounds numerics to significant digits, normalises text, and uses MinHash with LSH banding on character shingles
to find text pairs above a Jaccard threshold. On the tickets fixture it finds all 20 planted copies (15
upper-cased bodies, 5 float-noise rows). Methods, thresholds and image/audio equivalents:
duplicates-and-entity-leakage.md.
Building a Correct Split
| Production | Split |
|---|---|
| i.i.d. rows | stratified random (train_test_split(stratify=y)) after deduplication |
| New entities | GroupShuffleSplit by entity |
| Next period | train on dates < cutoff, test on dates >= cutoff (+ gap for label windows) |
| New regions / segments | hold out whole segments (out-of-distribution test) |
| Several of the above | time split first, then drop test entities from training if new entities matter |
from sklearn.model_selection import GroupShuffleSplit
df = df.drop_duplicates(subset=feature_cols) # before splitting
tr_idx, te_idx = next(GroupShuffleSplit(test_size=0.2, random_state=0).split(df, groups=df.customer_id))Size guidance, holdout policy and multi-way splits: split-design.md.
Drift and Adversarial Validation
PSI and KS describe per-feature shift; adversarial validation answers the joint question: train a classifier to tell train rows from test (or production) rows.
| Adversarial AUC | Meaning |
|---|---|
| ~0.5 | indistinguishable: the test set represents training conditions |
| 0.6-0.7 | mild shift; check the top features |
| > 0.8 | strong shift or an encoded split (time, source); investigate before trusting test scores |
Code, PSI/KS/Wasserstein details and what to do about shift: drift-and-adversarial-validation.md.
Output Format
When auditing a split, deliver:
- Split description - how it was made and what production looks like.
- Findings -
split_audit.pyandnear_duplicates.pyoutput, high findings first, with the affected share. - Impact - re-evaluated test score after fixing (when possible), or the expected direction of bias.
- Fixes - dedupe, grouped or time split, rebuilt holdout; code.
- Shift - drift and adversarial AUC between test and production samples.
- Holdout policy - how the cleaned test set will be protected and used once.
Scripts
python scripts/split_audit.py train.csv test.csv --target churned --id-col customer_id
python scripts/split_audit.py train.csv test.csv --target churned --time-col signup_date --expect forward
python scripts/split_audit.py train.csv prod_sample.csv --drop customer_id --psi-max 0.2 --jsonSA001-SA009: duplicate feature rows across sets, ID/group overlap, forward-time violations, PSI (train deciles or level shares) with KS for numerics (ISO dates compared as day numbers), univariate train-vs-test AUC, target shift (PSI or binary rate outside 3 SE), unseen levels, missing-share gaps and set sizes. Exit codes: 0 no high findings, 1 high findings, 2 usage error.
python scripts/near_duplicates.py train.csv test.csv --drop id --decimals 3
python scripts/near_duplicates.py train.csv test.csv --text-col body --threshold 0.8 --shingle 5
python scripts/near_duplicates.py data.csv --text-col title --threshold 0.9 # within one fileFuzzy row keys (numerics rounded to --decimals significant digits, normalised text) and MinHash/LSH text
similarity (--perms, --shingle, --threshold); counts of affected test rows and example pairs with line
numbers. Exit codes: 0 none found, 1 near-duplicates found, 2 usage error.
Additional Resources
| File | Content |
|---|---|
| split-design.md | Random, stratified, grouped, time and OOD splits; sizes; train/validation/test roles; holdout protection |
| duplicates-and-entity-leakage.md | Exact and near-duplicate detection (hashing, MinHash, embeddings, perceptual hashes), entity resolution, building group ids |
| drift-and-adversarial-validation.md | PSI, KS, chi-square, Wasserstein, JS divergence; adversarial validation code; handling covariate, prior and concept shift |
anomaly-detection-model-builder
Build and evaluate anomaly detection for tabular, sensor and transaction data with few or no labels: define the anomaly type (point, contextual, collective), choose detectors (robust z-score/MAD and IQR rules, Mahalanobis with robust covariance, k-NN distance, LOF, Isolation Forest, one-class SVM, PCA reconstruction / autoencoders, forecasting residuals for time series), combine them by rank, explain each alert by its deviating features, set thresholds from an alert budget or the score distribution, and evaluate with precision@k, PR-AUC and recall on known incidents, or detector agreement when no labels exist. Includes pure-Python detectors (robust z, Mahalanobis, kNN, Isolation Forest, ensemble) and an evaluation script.
catboost-model-builder
Build CatBoost models that use its strengths correctly: raw categorical columns as cat_features (ordered target statistics, no manual encoding), text_features, Pool construction with NaN-safe categories, ordered boosting and has_time for temporal data, symmetric (oblivious) trees, overfitting detector with eval_set and use_best_model, losses for classification, regression (Quantile, Poisson, Tweedie) and ranking (YetiRank), class weights, monotone constraints, feature importance (PredictionValuesChange, LossFunctionChange) and SHAP values, GPU training and model export. Plans cat/text/numeric columns from a CSV and generates a runnable training script, and checks parameter sets for the conflicts CatBoost rejects (iterations vs n_estimators, Bayesian bootstrap with subsample, multiple class-weight options, depth > 16, use_best_model without eval_set).
categorical-encoding-selector
Choose and implement the right encoding for every categorical column given the model family and the data: one-hot (with infrequent-level grouping and handle_unknown), ordinal with an explicit order, cross-fitted target encoding (scikit-learn TargetEncoder, CatBoost ordered statistics, category_encoders inside CV), frequency/count encoding, feature hashing, native categorical support in LightGBM, HistGradientBoosting, XGBoost and CatBoost, prefix levels for codes (zip3, NAICS-2), and embeddings. Measures cardinality, rare-level share, unseen-level rate on a time-ordered holdout and target signal per column, emits the ColumnTransformer, and quantifies target-encoding leakage (in-sample vs out-of-fold vs holdout AUC).
classification-model-selector
Choose the right classification algorithm for tabular, text or mixed data and prove the choice with a fair bake-off: logistic regression (L2, L1, elastic net), linear SVM, naive Bayes, k-NN, RBF SVM, decision trees, random forest, extra trees, HistGradientBoosting, LightGBM, XGBoost, CatBoost, explainable boosting machines (EBM/GAM), points scorecards and MLPs, matched to data size, width, categorical and high-cardinality share, missingness, class imbalance, text, and constraints such as interpretability, monotonic constraints, calibrated probabilities, latency budgets and tiny deployable models. Ranks candidates with starting hyperparameters and cautions, then generates a bake-off script running every candidate on identical CV folds with per-fold metrics, fit time and prediction latency.
clustering-algorithm-selector
Choose the right clustering algorithm and check that clustering makes sense at all: cluster tendency (Hopkins statistic with a correlated-features caveat, nearest-neighbour isolation), then k-means / MiniBatchKMeans, Gaussian mixtures with BIC, agglomerative (Ward, average, complete, single) with dendrograms, DBSCAN, HDBSCAN, OPTICS, spectral clustering, BIRCH, mean shift, k-prototypes and k-modes for mixed or categorical data, and Gower distance, matched to data size, expected shapes, noise, varying density, known k, soft memberships, hierarchy and streaming. Covers preprocessing (scaling, encoding, dimensionality, distance metrics), validation (internal indices, bootstrap stability, external labels, business profiling) and assigning new points.
collaborative-filtering-implementer
Implement collaborative filtering recommenders that beat popularity: item-based kNN with cosine, Jaccard or asymmetric similarity, shrinkage, neighbourhood size, positive-only or mean-centred rating signals and 'because you liked X' explanations; implicit ALS with confidence weighting, BPR pairwise ranking and the closed-form EASE model in plain NumPy, tuned on a temporal validation split by recall@k and NDCG@k and refit on all data; plus user-kNN, explicit matrix factorisation, the implicit/LightFM libraries, hyperparameter ranges, cold-start fallbacks, scaling with sparse matrices and approximate nearest neighbours. Produces top-k lists in the format of the recommendation evaluator.