0008 numpy s legacy generator is replayed - CyrilB1531/lodestar GitHub Wiki
0008 — numpy's legacy generator is replayed
Status: accepted · Date: 2026-09-25
Context
0004 records a portable seed as not
written: reproducing MT19937 bought parity that an explicit input already gave, and a wrong
Mersenne Twister fails quietly. Every shuffled splitter followed it, taking the permutation
scikit-learn drew rather than the random_state that drew it.
train_test_split(stratify=y) ended that equivalence
(#1157). StratifiedShuffleSplit decides
how many rows each class sends to each side with _approximate_mode, which breaks ties between
classes with rng.choice. No permutation a caller hands in carries that choice, so the input that
replaced the seed cannot replay the reference's split.
Two .NET packages reproduce numpy.random.RandomState, read on 2026-09-25:
RandomState(42).permutation(10) is [8 1 5 0 7 2 9 4 3 6] from numpy 2, from NumSharp 0.70.0
(net8.0 and net10.0 only) and from NumpyDotNet 0.9.87.2 (netstandard2.0, BSD, last published
2025-05-14). Neither can be a dependency of Lodestar.Preprocessing: a core package carries none
(0003).
Decision
numpy's legacy generator is written here, internal to the package that draws from it, and the
seed crosses the public API as a number. The part replayed is exactly what check_random_state
reaches: MT19937 seeded by init_genrand, bounded integers by masked rejection, Fisher-Yates from
the top for shuffle and permutation, and choice(replace=False) as permutation(n)[:size].
A seed is a long in [0, 2³² − 1], numpy's range, so no generator type is published and nothing
goes to Lodestar.Abstractions. A second package that draws moves it to src/Shared/.
This amends 0004's row: a portable seed is written here, for the splitters. The inputs that
replaced a seed stay — a permutation, and the Ω of the sparse decomposition — because a caller who
holds one must not need a seed to use it.
Consequences
- The quiet failure
0004feared is made loud. A corpus of numpy's raw 32-bit outputs, permutations and choices over seeds from 0 to 2³² − 1 is replayed before any splitter is: a generator one bit wrong fails there by name, not three calls later as a fold that differs. - The two packages above are named where the seed is documented, so the claim that .NET has no copy of numpy's generator is withdrawn where it was published.
- Parity now extends to the seed only where the reference's draw is replayed call for call. A
reference that draws from
np.random.default_rng, from Python'srandom, or from a generator outside numpy is not covered by this record.