A One-Line Sampler Can Quietly Double the Odds
`Math.floor` and `Math.round` differ by one word. For four choices, one is uniform; the other makes the middle values twice as likely.
Series parts
- Part 1 A One-Line Sampler Can Quietly Double the Odds
- Part 2 When Randomness Should Take Sides
- Part 3 The Raffle Changes After Every Winner
- Part 4 A Fair Sample in 100 Slots, Forever
- Part 5 The Hidden Full-Table Scan in `ORDER BY random()`
- Part 6 How Wrong Can a Random Sample Be?
- Part 7 Small Data Structures That Lie Just Enough
- Part 8 When Luck Is Part of the Proof
- Part 9 I Found a Probability Bug in My Old TSP Solver
- Part 10 Softmax Is Not Confidence
- Part 11 The Useful Fiction of Hidden Causes
- Part 12 A Latent Space Is Not Automatically a Place
- Part 13 Before a Token Is Chosen, Context Has to Move
- Part 14 Who Actually Chooses the Next Token?
- Part 15 One Token, Zero Materialized Logits
- Part 1 A One-Line Sampler Can Quietly Double the Odds
- Part 2 When Randomness Should Take Sides
- Part 3 The Raffle Changes After Every Winner
- Part 4 A Fair Sample in 100 Slots, Forever
- Part 5 The Hidden Full-Table Scan in `ORDER BY random()`
- Part 6 How Wrong Can a Random Sample Be?
- Part 7 Small Data Structures That Lie Just Enough
- Part 8 When Luck Is Part of the Proof
- Part 9 I Found a Probability Bug in My Old TSP Solver
- Part 10 Softmax Is Not Confidence
- Part 11 The Useful Fiction of Hidden Causes
- Part 12 A Latent Space Is Not Automatically a Place
- Part 13 Before a Token Is Chosen, Context Has to Move
- Part 14 Who Actually Chooses the Next Token?
- Part 15 One Token, Zero Materialized Logits
On this page
Change one function name in a one-line sampler and the middle entries become twice as likely as the edges. Nothing crashes. The output still looks random.
That is why I want to start this series with an array index, not a casino or a Monte Carlo simulation. Most software randomness enters through a boring door: several choices are acceptable, none deserves a preference the code can defend, and one of them still has to be picked.
const i = Math.floor(Math.random() * n);
The line looks trivial. Its contract is not: does every valid index receive the same probability?
What “fair” means
Suppose you have a finite set with n elements:
To sample uniformly from means that every element has the same probability of being chosen. Written with standard probability notation1:
Uniform means equal odds. Nothing about the definition promises tidy-looking short runs.
random does not mean “messy-looking,” and fair does not mean “everyone gets picked quickly.”
A uniform sampler can pick the same item twice in a row. It can pick the same item five times in a row. That may feel suspicious, but it is not evidence of unfairness. Fairness is not about how the first few draws feel. It is about the long-run frequencies.
If you draw from a uniform sampler many times, each item should appear about equally often. Not exactly equally. Just with no built-in favoritism.
A sampler is not fair because its output looks disorderly. It is fair because its probabilities are right.
The smallest useful implementation
In TypeScript, a minimal uniform sampler over a non-empty array looks like this:
export type NonEmptyArray<T> = readonly [T, ...T[]];
export function sampleUniform<T>(xs: NonEmptyArray<T>): T {
const i = Math.floor(Math.random() * xs.length);
return xs[i];
}
For simulations, randomized tests, and other ordinary non-security work, this is often enough.
If Math.random() behaves like a uniform draw from , then multiplying by xs.length gives a number in , and Math.floor(...) maps each equal-width interval to one array index.
For n = 4, the intervals look like this:
[0, 1)maps to index 0[1, 2)maps to index 1[2, 3)maps to index 2[3, 4)maps to index 3
Each interval has the same width, so each index gets the same probability. That is the whole trick.
A bug that looks innocent
There is a version of this code that many people write once, trust for a few years, and then quietly regret:
const i = Math.round(Math.random() * (n - 1));
It looks reasonable. It is not.
The problem is that rounding does not carve the interval into equal pieces. Math.round groups numbers by “nearest integer,” and the first and last groups get cut in half by the boundaries of the range.
If n = 4, then Math.random() * (n - 1) is uniform on [0, 3). The mapping is:
[0, 0.5)maps to index 0[0.5, 1.5)maps to index 1[1.5, 2.5)maps to index 2[2.5, 3)maps to index 3
That means indices 0 and 3 get intervals of width 0.5, while indices 1 and 2 get intervals of width 1. Since probability is proportional to interval width, the edge indices each get probability 1/6, while the middle indices each get 1/3. The middle values are twice as likely.
A sampler can look random and still be biased. I like this bug because it is silent, plausible, and measurable. Software does not have to crash to tilt reality.
At small sample sizes, both versions look innocent. Give them a few thousand draws and the disguise falls apart: Math.floor drifts toward equal frequencies, while Math.round keeps feeding the middle indices. One word created a twofold bias.
What this code does not promise
Math.random() is a pseudorandom generator whose algorithm and seed are deliberately hidden by JavaScript. It is unsuitable for security-sensitive choices, and it gives you no audit trail for a public raffle. The code above explains how to map a draw from onto equal-width intervals. It does not prove that the underlying generator is unpredictable, reproducible, or acceptable for high-stakes selection.
Those requirements need a different design: cryptographic randomness when unpredictability matters, an explicit seeded generator when replay matters, and a published procedure when people must be able to audit the draw. “Uniform index” is only one layer of the contract.
What uniform sampling buys you
Uniform sampling is an act of restraint. When the options are genuinely symmetric, the system has no reason to prefer one, so it should not smuggle in a preference through arithmetic.
Real options rarely remain symmetric. A server has more spare capacity, a task is more urgent, a candidate move looks more promising. Once the code has evidence for a preference, equal odds become the dishonest choice.