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