One change, one measurement
What it is
An experiment is a written hypothesis, exactly one deliberate change from a named baseline, and a metric decided before the run. Everything else — split, seed, data version, evaluation code — is held fixed.
Why it matters
Without it, a project becomes one long undifferentiated attempt whose current state nobody can explain. With it, every result is attributable: you can say which of nine changes produced the gain, and you can undo the eight that did not.
In the real world: Changing both the features and the model in one go leaves nobody sure which one helped.
Visual example
runs.jsonlA run log makes a project legible: what changed, against what, and by how much.
Which differences survive the spread
exp-010: +0.026 against a fold sd of 0.017-0.019 — plausibly real
exp-011: +0.007 against a fold sd of 0.021 — a coin flip dressed as progress
Boosting may still be worth keeping for other reasons, but not on this evidence
Why this matters
The log does not just record history; it prices each change against the noise floor, which is the only way to know whether the last two weeks produced anything.
Common misconception
Wrong idea: the run with the best mean score is the best model.
Correct idea: the best model is the one whose advantage exceeds the uncertainty of the comparison. A 0.007 improvement measured with a 0.021 spread is not a result.
import json, hashlib, pathlib, datetime
def log_run(name, hypothesis, params, metrics, data_version, seed):
record = {
'name': name,
'hypothesis': hypothesis,
'params': params,
'metrics': metrics,
'data_version': data_version,
'seed': seed,
'created_at': datetime.datetime.now(datetime.timezone.utc).isoformat(),
}
record['id'] = hashlib.sha1(json.dumps(record, sort_keys=True).encode()).hexdigest()[:10]
pathlib.Path('runs.jsonl').open('a').write(json.dumps(record) + '\n')
return record['id']
log_run(
name='exp-012-group-split',
hypothesis='the random split leaked users; '
'a group split will lower AUC to the honest value',
params={'cv': 'StratifiedGroupKFold(5)', 'model': 'hgb', 'lr': 0.05},
metrics={'cv_auc_mean': 0.791, 'cv_auc_std': 0.014},
data_version='snap-2026-09-01', seed=0,
)The tempting shortcut is to bundle a data fix with a model change because both look like improvements. If the pair nets out to zero you will discard both, including the one that worked; if it gains, you will keep the one that does not. Land them as two runs even when it costs an extra hour.