specsr.data.splits

Group-aware train/test splitting for the augmented spectra datasets.

Background

The augmented .npz datasets contain 21 rows per physical galaxy: one original followed by 20 stochastic realizations (Gaussian redshift offset plus 10%-amplitude flux noise). Every row carries the same sky position as its parent, and the id column is only a running row index – it does not encode provenance.

The original get_or_make_split drew a flat rng.permutation(N) over all rows, so ~16-17 augmented siblings of each test galaxy landed in the training set. Because the augmentations are near-duplicates, this leaked the test set into training and inflated every reported held-out metric.

This module splits on the parent galaxy instead, so all 21 rows of a galaxy fall on the same side of the split.

Grouping key

Parents are recovered from (ra, dec, field), which augmentation leaves untouched. This is preferred over the positional formula

parent(i) = i if i < G else (i - G) // n_aug

because it additionally merges genuine duplicate observations of the same object. The DR3 file has 1,187 rows spanning only 1,163 distinct sky positions (23 objects appear more than once); those duplicates would straddle a positional split. The DR4 file is clean (2,507 galaxies, uniform 21x), and there the two schemes agree exactly.

Module Attributes

SPLIT_SCHEME_3WAY

Bumped when the three-way scheme changes, so old caches are never reused.

Functions

assert_clean_3way(train_idx, val_idx, ...)

Fail loudly if any guarantee of the three-way split is violated.

assert_no_group_leakage(train_idx, test_idx, ...)

Fail loudly if any galaxy has rows on both sides of the split.

get_or_make_split(dataset_path, N[, ...])

Group-aware replacement for the original flat-permutation splitter.

get_or_make_split_3way(dataset_path[, ...])

Cached three-way split for a built product.

get_training_split(dataset_path[, ...])

Return (train_idx, val_idx, split_path) for a training run.

hash_file(path)

make_group_split(groups[, train_frac, seed])

Split galaxies (not rows) into train/test, then expand back to rows.

make_group_split_3way(groups, is_original[, ...])

Split galaxies three ways, evaluating on real spectra only.

parent_group_ids([dataset_path, ra, dec, field])

Return an integer parent-galaxy label per row.

specsr.data.splits.hash_file(path)[source]
specsr.data.splits.parent_group_ids(dataset_path=None, ra=None, dec=None, field=None)[source]

Return an integer parent-galaxy label per row.

Pass either dataset_path or the ra/dec/field arrays. Rows sharing a sky position (and field) get the same label.

specsr.data.splits.make_group_split(groups, train_frac=0.8, seed=42)[source]

Split galaxies (not rows) into train/test, then expand back to rows.

train_frac is applied to the number of galaxies. Because every galaxy contributes the same number of rows in these datasets, the resulting row fractions match closely; they are not forced to match exactly.

specsr.data.splits.assert_no_group_leakage(train_idx, test_idx, groups)[source]

Fail loudly if any galaxy has rows on both sides of the split.

specsr.data.splits.get_or_make_split(dataset_path, N, train_frac=0.8, seed=42, split_dir='splits')[source]

Group-aware replacement for the original flat-permutation splitter.

Signature matches the previous helper so call sites need no changes, but cache files use a distinct groupsplit_ prefix: old leaky split_*.npz files are ignored rather than reused.

specsr.data.splits.SPLIT_SCHEME_3WAY = 'groupsplit3-v1'

Bumped when the three-way scheme changes, so old caches are never reused.

specsr.data.splits.make_group_split_3way(groups, is_original, train_frac=0.8, val_frac=0.1, seed=42, allow_empty_test=False)[source]

Split galaxies three ways, evaluating on real spectra only.

Returns (train_idx, val_idx, test_idx) as row indices.

Two separate guarantees, for two separate problems:

Galaxies never straddle a boundary. All 21 rows of a galaxy (1 original + 20 augmentations) land in the same partition. A split drawn over rows put ~16 near-duplicate siblings of every held-out galaxy into training.

Validation and test contain only originals. Augmentation is a training-time technique: a synthetic realization is not an observation. Evaluating on augmented rows would report performance on perturbations rather than on real spectra, and would compute statistics over 21x correlated rows, understating uncertainties — the effective sample size is the number of galaxies, not the number of rows.

Why validation and test are separate. Training saves a checkpoint whenever the monitored metric improves, so over N epochs the saved model is the best of N draws on whatever set is monitored. Reporting that same set conflates genuine convergence with a favourable fluctuation, and the two cannot be separated using the set the selection was made on. Validation makes decisions; test is measured once.

Parameters:
specsr.data.splits.assert_clean_3way(train_idx, val_idx, test_idx, groups, is_original)[source]

Fail loudly if any guarantee of the three-way split is violated.

specsr.data.splits.get_or_make_split_3way(dataset_path, train_frac=0.8, val_frac=0.1, seed=42, split_dir='splits', allow_empty_test=False)[source]

Cached three-way split for a built product.

Requires the product to carry parent_id and is_original; products built before those existed are rejected rather than guessed at.

Parameters:
specsr.data.splits.get_training_split(dataset_path, train_frac=0.8, val_frac=0.2, seed=42, split_dir='splits', allow_empty_test=True)[source]

Return (train_idx, val_idx, split_path) for a training run.

Deliberately does not return the test indices. Training monitors the validation set for checkpoint selection and the learning-rate schedule; the test set must not be loaded during training at all, so that the number eventually reported is not the best of many draws on the set being reported.

Retrieve the test indices separately, once, at evaluation time:

_, _, test_idx, _ = get_or_make_split_3way(path)