NumPy · Module 9 of 13 · 5 concepts · about 6 min

Randomness & Sampling

Drawing numbers deterministically, and sampling a dataset without replacement or leakage.

At work: Fair, repeatable randomness for A/B tests, simulations, train/test splits and sampling large datasets.

Random numbers with a Generator

What it is

rng = np.random.default_rng(seed) creates a generator; rng.standard_normal, rng.integers, rng.permutation, rng.choice draw from it. The legacy np.random.seed / np.random.rand interface uses one hidden global state.

Why it matters

A generator you pass around makes a result reproducible and a test deterministic without any function secretly depending on global state that some import may already have advanced.

In the real world: Simulating thousands of possible demand scenarios to decide how much stock a warehouse should hold.

rng = np.random.default_rng(0)
X = rng.standard_normal((1000, 8))
idx = rng.permutation(len(X))[:100]      # a random subsample, reproducibly
rng.integers(0, 10, size=5)              # high is exclusive, unlike randint

Seeding globally with np.random.seed(0) and believing the run is reproducible. Anything else in the process that draws from the global generator changes your numbers, and library code does draw from it.

Reproducibility: seeding and spawning generators

What it is

The same seed always produces the same sequence of draws from a Generator, however many times it is called. rng.spawn(k) produces k independent child generators from one parent, for workers or repeated experiments that must not share a stream.

Why it matters

A pipeline built from independent generators is reproducible piece by piece: reseed the whole thing and every part reproduces exactly, including in parallel, without one part's draws shifting another's.

In the real world: Fixing the seed so an A/B test assigns the same users to the same group every time the analysis is rerun.

rng = np.random.default_rng(0)
workers = rng.spawn(4)                # 4 independent generators
[w.integers(0, 100) for w in workers]  # different numbers, all reproducible

np.random.default_rng(0).standard_normal(3) == np.random.default_rng(0).standard_normal(3)
# array([ True,  True,  True]) — same seed, same sequence, every time

Creating a fresh default_rng(0) inside a loop, once per iteration. Every iteration then draws the exact same numbers — a bug that does not look wrong until someone checks whether two iterations actually differ.

Practise it: Reproducible Train and Test Indices

Common distributions: uniform, normal, integers, choice

What it is

rng.random(size) draws uniform [0, 1); rng.uniform(low, high) a wider uniform range; rng.normal(loc, scale) a Gaussian with scale as the standard deviation; rng.integers(low, high) whole numbers, high exclusive.

Why it matters

Synthetic data for a test, a baseline to compare a real model against, and Monte Carlo estimates are all built from one of these four calls — knowing which one avoids reaching for if/for to build a distribution by hand.

In the real world: Generating realistic fake transactions to test a fraud pipeline without using real customer data.

rng = np.random.default_rng(0)
rng.random(4)                 # array([0.64, 0.27, 0.27, 0.81]) — [0, 1)
rng.normal(loc=0, scale=1, size=4)
rng.integers(low=1, high=7, size=4)   # a die roll: 1..6

Passing a variance instead of a standard deviation to rng.normal(loc, scale). scale is the standard deviation; a variance passed by mistake produces a distribution narrower — or wider — than intended, silently.

Sampling and shuffling without replacement

What it is

rng.permutation(n) returns a random reordering of 0..n-1 as a new array; rng.shuffle(arr) reorders arr in place and returns None; rng.choice(arr, size, replace=False) samples distinct elements directly.

Why it matters

A random subsample, a shuffled minibatch order, and a bootstrap resample are three different things that look alike — knowing which function does which stops the wrong one from silently changing an array you meant to leave alone.

In the real world: Picking 1,000 random customers for a survey, with nobody picked twice.

Visual example

Sampling

A permutation is a new order for old positions — every value keeps its identity, only its slot changes.

x
value
10
20
30
40
50
x[rng.permutation(len(x))]
x[order] — order = [2, 0, 4, 1, 3]
value
30
10
50
20
40

Why this matters

The array is never sampled cell by cell. One permutation of positions reorders (or subsets, with a slice) everything aligned to it in one pass — features, labels and ids together.

Common misconception

Wrong idea: shuffling an array changes what values it holds.

Correct idea: it changes only where each value sits. Every value from the original array is still there exactly once.

rng = np.random.default_rng(0)
x = np.array([10, 20, 30, 40, 50])
order = rng.permutation(len(x))     # array([2, 0, 4, 1, 3]) — new positions
x[order]                            # the array, reordered — x itself is untouched
rng.shuffle(x)                      # reorders x in place; returns None

Writing x = rng.shuffle(x). shuffle mutates in place and returns None, so this silently replaces the whole array with nothing.

Practise it: Reproducible Train and Test Indices

Train/test split via a random permutation

What it is

A reproducible split is one seeded permutation, cut in two: idx = rng.permutation(n); train_idx, test_idx = idx[:k], idx[k:], then index every aligned array — features and labels — with the same two slices.

Why it matters

The split has to happen before any statistic is computed from the data. A mean, a standard deviation or a vocabulary built from the full array has already seen the rows that are meant to be held out.

In the real world: Holding back a random 20% of past loans to test a credit-risk model on data it has never seen.

rng = np.random.default_rng(0)
n = len(X)
idx = rng.permutation(n)
cut = int(0.8 * n)
train_idx, test_idx = idx[:cut], idx[cut:]
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]

Standardising the full array with (X - X.mean(0)) / X.std(0) and splitting afterwards. The permutation makes the split reproducible; it does not undo a mean that already included the test rows — fit the statistic on X_train alone.

Practise it: Reproducible Train and Test Indices, Standardise Features That Have Holes In Them