split_samples

split_samples(samples, *, fractions=(0.7, 0.15, 0.15), group_by=None, seed=0)

Divide samples between the splits, keeping every group whole.

Deterministic: the same samples, fractions, pattern and seed give the same split on any machine. That is not a nicety - a split that moves between runs makes two results incomparable, and the reason for a difference becomes impossible to find.

Groups are shuffled, then handed out one at a time to whichever split is furthest below its target share in samples. Assigning by group count instead would drift badly whenever groups differ in size, which for patients they always do.

Parameters

Name Type Description Default
samples Sequence[Sample] What was discovered. Nothing is read from disk here - this partitions a list, so a specification with a split block gains no side effect on disk. required
fractions Sequence[float] | Mapping[str, float] Two or three, summing to one: train, validation, and optionally test. A mapping names them instead, for a split that is not in that order. (0.7, 0.15, 0.15)
group_by str | None A regular expression read against each sample’s key; everything sharing a group lands in exactly one split. None means every sample is its own group, which is right only when the samples are genuinely independent - slices of one patient are not. None
seed int Which shuffle. The split is otherwise deterministic. 0

Returns

Name Type Description
Split A Split, with train, validation and test tuples and the group each sample
Split was assigned to.

Raises

Name Type Description
SplitError If the fractions are not two or three, if group_by matches no sample key - a pattern that matches nothing is a mistake, not an empty result - or if there are fewer groups than splits to fill.

from pyplatypus.data.paths import Sample import pathlib samples = [ … Sample(key=f”patient{p:02d}_slice{s}“, images=(pathlib.Path(”x.png”),)) … for p in range(1, 5) … for s in range(3) … ] len(samples) 12 split = split_samples(samples, fractions=(0.5, 0.5), group_by=r”^(patient)_“) len(split.train) + len(split.validation) 12

A patient is never on both sides, which is the whole point:

def patients(part): … return {key.split(“_“)[0] for key in (sample.key for sample in part)} patients(split.train) & patients(split.validation) set()

Without group_by, every slice is its own group and the same patient lands in both - which makes validation measure memory rather than generalisation:

loose = split_samples(samples, fractions=(0.5, 0.5)) bool(patients(loose.train) & patients(loose.validation)) True