Week 02 · Page 3 of 4 · Practical Work (Travaux Pratiques)
The goal of this practical is not to write clever code. It is to see, with your own numbers, the gap between an optimistic validation score and an honest one.
Setup
You will need Python 3 with the following packages: numpy, pandas, and scikit-learn. If you do not have these installed, run:
pip install numpy pandas scikit-learnSave the dataset below as week2_data.csv in the same folder as your script. This is a synthetic dataset constructed for this exercise: 12 parent compounds, each appearing at 2 strain levels (24 rows total). Each parent has its own small composition-independent shift in formation energy — a stand-in for chemistry the single feature does not fully capture — while the two strain rows of a given parent are nearly identical to each other.
parent_id,strain_pct,feature_dchi,formation_energy
1,0,0.501,-0.1946
1,2,0.511,-0.1751
2,0,0.631,-0.3494
2,2,0.652,-0.3607
3,0,0.78,-0.3187
3,2,0.793,-0.3247
4,0,0.906,-0.2659
4,2,0.923,-0.2572
5,0,1.042,-0.6153
5,2,1.062,-0.5879
6,0,1.182,-0.7048
6,2,1.206,-0.7004
7,0,1.319,-0.5236
7,2,1.332,-0.4961
8,0,1.458,-0.7059
8,2,1.474,-0.7046
9,0,1.584,-0.9822
9,2,1.609,-0.9584
10,0,1.729,-0.9226
10,2,1.749,-0.9028
11,0,1.86,-1.1316
11,2,1.889,-1.1224
12,0,1.996,-1.223
12,2,2.022,-1.2041We deliberately use a 1-nearest-neighbor model for this exercise rather than linear regression. A 1-nearest-neighbor model predicts a new point's label by copying the label of the single closest training point — which makes it the easiest possible model to "cheat" with if a near-duplicate happens to leak into the training set. Create a file week2_practical.py:
import pandas as pd
import numpy as np
from sklearn.neighbors import KNeighborsRegressor
from sklearn.model_selection import KFold, GroupKFold, cross_val_score
data = pd.read_csv("week2_data.csv")
X = data[["feature_dchi"]].values
y = data["formation_energy"].values
groups = data["parent_id"].values
model = KNeighborsRegressor(n_neighbors=1)
# --- Naive random 4-fold split (ignores parent_id grouping) ---
naive_cv = KFold(n_splits=4, shuffle=True, random_state=0)
naive_scores = cross_val_score(model, X, y, cv=naive_cv, scoring="neg_mean_absolute_error")
print("Naive random split, fold MAEs:", -naive_scores)
print("Naive random split, mean MAE:", -naive_scores.mean())Run this script (python week2_practical.py) and record the mean MAE it prints. You should see a small number — roughly 0.04 eV/atom.
Add the following to the same script, below what you already wrote:
# --- Group-aware 4-fold split (respects parent_id grouping) ---
group_cv = GroupKFold(n_splits=4)
group_scores = cross_val_score(model, X, y, cv=group_cv, groups=groups, scoring="neg_mean_absolute_error")
print("Group-aware split, fold MAEs:", -group_scores)
print("Group-aware split, mean MAE:", -group_scores.mean())Run the script again and record this second mean MAE. You should see a substantially larger number than in Step 1 — roughly 0.16 eV/atom, around four times worse.
You have just reproduced, with real code and real numbers, the leakage scenario from Directed Work Problem 3. With the naive split, a strain variant of a parent compound frequently ends up in the test fold while its near-identical sibling sits in the training fold; the 1-nearest-neighbor model then "predicts" the test point by copying its sibling's almost-identical label, producing an artificially low error. The group-aware split removes this shortcut entirely — every parent compound is fully on one side of the split or the other — and the reported error rises sharply to reflect genuine generalization to unseen parent compounds.
Now repeat both evaluations using from sklearn.linear_model import LinearRegression in place of KNeighborsRegressor(n_neighbors=1), keeping everything else the same. You should find the naive-vs-grouped gap is much smaller for linear regression. Write one sentence explaining why a model's susceptibility to this kind of leakage depends on the model, not only on the data.
The size of a leakage effect depends on how easily a model can "memorize" near-duplicates. Highly flexible, memorization-prone models (nearest-neighbor methods, deep networks, large ensembles) are far more exposed to grouped data leakage than simple models like linear regression — which is one more reason the bias/variance framing from the Lesson and the validation strategy you choose are not independent decisions.
Optional extension
If you want to go further: increase n_neighbors from 1 to 3 and rerun both splits. Does the gap between the naive and group-aware error shrink, grow, or stay the same? Try to predict the answer before running the code, based on what averaging over more neighbors should do to a model's ability to "copy" a single leaked near-duplicate.
0 Comments