Random numbers and sampling
Seed it, or you can’t tell a real change from noise.
default_rngrng.randomrng.integersrng.normalrng.choicerng.permutationseedsWatch it happen
Play it through, or step back and forth yourself.
rng = np.random.default_rng(0)rng.random(3)[0.637 0.270 0.041]rng = np.random.default_rng(0)rng.random(3)[0.637 0.270 0.041]rng = np.random.default_rng(1)rng.random(3)[0.511 0.951 0.144]Modern NumPy wants you to create a generator first: rng = np.random.default_rng(0). The 0 is the seed. The older np.random.rand style still works but shares one hidden global generator, which is exactly the thing that makes results irreproducible.
The idea
Modern NumPy asks you to make a generator first, and then draw from it:
rng = np.random.default_rng(0)
rng.random(3) # [0.637 0.270 0.041]You'll still see np.random.rand and np.random.seed in older tutorials. They work, but they share one hidden global generator, so any library you import can move your numbers underneath you. The generator form keeps randomness local and explicit.
The seed is the point
That 0 is the seed, and it makes the sequence reproducible: the same numbers, every run, on every machine. Without it you can't tell whether your model improved or you just got a luckier shuffle.
Seed once, at the top. Re-seeding inside a loop is a classic mistake — it correlates your draws instead of decorrelating them, and in the worst case gives you the identical "random" value every iteration.
The distributions you'll actually use
rng.random(size) # uniform floats in [0, 1)
rng.integers(low, high, size) # whole numbers, high excluded
rng.normal(loc, scale, size) # the bell curve
rng.standard_normal(size) # normal with mean 0, spread 1rng.integers excludes the high end, matching range. (The old randint did too, but random_integers didn't — one of several reasons the new API exists.)
Sampling from data you already have
rng.choice(x, 3) # three draws, repeats allowed
rng.choice(x, 3, replace=False) # three distinct items
rng.choice(x, 5, p=weights) # weighted; p must sum to 1replace=False is the difference between "roll a die three times" and "deal three cards". Asking for more items than exist without replacement is an error, which is the right behaviour.
Shuffling
rng.shuffle(x) # in place, returns None
rng.permutation(x) # a shuffled copyThe same in-place-versus-copy split as sort. And the same trick as argsort applies when you have parallel arrays — shuffle the indices, then apply them to everything:
order = rng.permutation(len(features))
features, labels = features[order], labels[order]Shuffling features and labels separately would destroy the correspondence — the same class of bug as sorting one column of a table, and it silently produces a model that learns nothing.
On a 2-D array, rng.shuffle only shuffles along axis 0 — it reorders rows and leaves each row intact, which is nearly always what you want for a dataset.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedMake rng = np.random.default_rng(0), then print three uniforms, five integers under 10, and four normals around 50 rounded to one decimal. Then print np.random.default_rng(0).random(3) twice, to prove the seed decides the numbers. Finish by drawing 3 of [10, 20, 30, 40] without replacement.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Create a generator seeded with 42 and return three uniform floats from it.
Using a generator seeded with 0, return five integers in [0, 100).
From v, draw two distinct values using a generator seeded with 7.
Return v shuffled with a generator seeded with 3, as a new array — leaving v itself untouched.
