Skillversitas

Random Numbers

NumPy generates samples from the standard distributions - uniform, normal, integers, and the discrete family - with explicit seeding so every run is reproducible. The modern API is the Generator created by np.random.default_rng: faster, better distributed, and the form all current code uses. The legacy global np.random.seed state still works and appears in older code, but new work starts with default_rng.

Creating a Generator

import numpy as np

rng = np.random.default_rng(42)      # the seed fixes the sequence
print(rng.uniform(size=5))           # uniform on [0, 1)
print(rng.integers(0, 10, size=6))   # integers in [0, 10)
print(rng.normal(0, 1, size=4))      # standard normal

# reproducibility: same seed, same sequence
a = np.random.default_rng(7).normal(size=3)
b = np.random.default_rng(7).normal(size=3)
print(np.array_equal(a, b))          # True

# different seeds, different streams
c = np.random.default_rng(8).normal(size=3)
print(np.array_equal(a, c))          # False

The seed is the entire reproducibility mechanism: fix it, and the stream is fixed for the life of the process. The size argument returns arrays of any shape directly, and every distribution accepts it. The rule for experiments: one named rng object per experiment, seeded explicitly, so any result can be regenerated from the seed alone.

The Distributions You Will Use

rng = np.random.default_rng(0)

print(rng.uniform(-1, 1, size=3))       # uniform on [low, high)
print(rng.normal(5, 2, size=3))         # normal: mean 5, std 2
print(rng.integers(1, 6, size=5))       # the die roll: [1, 6)
print(rng.poisson(3.0, size=4))         # counts with mean 3
print(rng.exponential(1.5, size=4))     # wait times, scale 1.5
print(rng.binomial(10, 0.3, size=4))   # successes in 10 trials, p=0.3
print(rng.choice(["a", "b", "c"], size=3, p=[0.2, 0.3, 0.5]))

The naming convention is consistent: each distribution takes its parameters in the standard order (location and scale for continuous families, n and p for the binomial, lambda for Poisson), and size shapes the output. choice samples from an array of values - with replace=False for sampling without replacement and p for a custom distribution over the elements.

Permutation and Shuffle

rng = np.random.default_rng(1)

print(rng.permutation(10))                      # shuffled 0..9
print(rng.permutation(np.array([2, 1, 3, 4])))  # shuffled copy

a = np.array([1, 2, 3, 4, 5])
rng.shuffle(a)                                  # in-place: a itself is reordered
print(a)

# the train/test split pattern: permute, then cut
idx = rng.permutation(len(a))
train_idx, test_idx = idx[:4], idx[4:]
print(train_idx, test_idx)

permutation returns a shuffled copy; shuffle reorders in place and mutates its argument. The permute-then-cut idiom is the standard way to build a random train/test partition of array indices - it is the index arrays, not the data, that get shuffled.

The Legacy Global API

# old style - still common in existing code
np.random.seed(0)
x = np.random.randn(3)         # standard normal
y = np.random.rand(3)          # uniform on [0, 1)
z = np.random.randint(0, 10, 4)

# the difference: global mutable state. Any library that calls
# seed() mid-run silently resets your stream - the exact reason
# default_rng exists

The legacy API (np.random.seed plus the global functions) uses a single shared state that any code in the process can reset - a source of hard-to-find non-reproducibility in mixed codebases. When reading old code, treat the global seed as the reproducibility source; when writing new code, use default_rng with a named rng object so streams never collide. The two APIs are not interchangeable mid-stream - do not mix them inside one experiment.

Verifying the Samples

rng = np.random.default_rng(0)
x = rng.normal(5, 2, size=100_000)
print(x.mean(), x.std())              # ~5, ~2 - the law of large numbers

# a histogram-free summary of a discrete draw
d = rng.poisson(3.0, size=100_000)
print(d.mean(), d.max())              # ~3, a tail value well above the mean

Mean and standard deviation of a large sample converging to the parameters is the smoke test that the right distribution was called with the right parameters - a one-line check that catches swapped arguments (mean vs scale) before they propagate through a simulation.

Summary

Generate randomness with np.random.default_rng(seed): one named rng per experiment, the seed as the reproducibility mechanism, size for array-shaped output, and the standard distribution methods (uniform, normal, integers, poisson, choice, ...). Use permutation/shuffle for index-level mixing and the permute-then-cut idiom for splits. Understand the legacy global API well enough to read old code, but keep new code on the generator API - its independent streams are what make concurrent and library-heavy code reproducible.