Skip to content

fix: Use Fisher-Yates for shuffledIndices - #221

Open
SamuelSchlesinger wants to merge 3 commits into
DataHaskell:mainfrom
SamuelSchlesinger:fix/shuffle-fisher-yates
Open

fix: Use Fisher-Yates for shuffledIndices#221
SamuelSchlesinger wants to merge 3 commits into
DataHaskell:mainfrom
SamuelSchlesinger:fix/shuffle-fisher-yates

Conversation

@SamuelSchlesinger

Copy link
Copy Markdown

The shuffle from #172 isn't Fisher-Yates: it picks a random number of steps up front and never lets an element stay in place, so the permutation is biased, and on a one-row frame randomR (1, 0) leads to an out-of-bounds swap (index out of bounds (1,1) for about half of seeds). Replace it with the standard backwards Fisher-Yates.

Seeded outputs change as a result. Tests: the single-row case, and a fixed-point-rate check across seeds.

shuffledIndices seeds its loop with a random bound, randomR (1, k - 1),
so indices above that bound are never touched and roughly half the
vector keeps its original order. The inner step then draws from
randomR (1, maxInd) rather than (0, maxInd), which is Sattolo's
algorithm and never leaves the head element in place.

Over 400 seeds of a ten-element shuffle, positions 0 and 1 never keep
their index and positions 4 through 9 keep it far too often. Shuffling a
single index passes a reversed range to randomR, whose result is not
specified; here it swaps past the end of a one-element vector.
The loop started from a random bound instead of the last index, leaving
everything above it in place, and each step drew from
randomR (1, maxInd) so the head element could never stay put. Walk from
the last index down to 1, swapping with a draw from randomR (0, i). The
loop is empty for k <= 1, which also removes the out-of-bounds swap on a
one-row frame.
@daikonradish

Copy link
Copy Markdown
Contributor

keen eye. can you do a test for randomness please? https://cnut1648.github.io/files/posts/Test_for_rand.pdf

@SamuelSchlesinger

SamuelSchlesinger commented Aug 24, 2026

Copy link
Copy Markdown
Author

Done. Replaced the fixed-point check with two χ² tests at α = 0.001, following the frequency test in that note (Knuth 3.3.2):

  • full permutation distribution: n = 5 over all 120 outcomes, 12000 draws (df 119). This one also catches correlated positions, which a per-position test can't see.
  • position × item table: n = 10, 5000 draws (df 81).

Seeds are fixed so both are deterministic. Fisher-Yates scores 114.7 and 83.4 against bounds of 172.4 and 126.1; the previous implementation scores 137088 and 114988 on the same procedure.

Replace the fixed-point rate check with two chi-squared tests at
alpha = 0.001: one over the full permutation distribution (n = 5,
12000 draws, df 119), which also catches correlated positions, and
Knuth's frequency test over the position-by-item table (n = 10, 5000
draws, df 81). Seeds are fixed, so both are deterministic.

Fisher-Yates scores 114.7 and 83.4 against bounds of 172.4 and 126.1;
the previous shuffle scores 137088 and 114988 on the same procedure.
@SamuelSchlesinger
SamuelSchlesinger force-pushed the fix/shuffle-fisher-yates branch from c6fe9f9 to e2259a8 Compare August 24, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants