NumPy Random Generator: Reproducible Practical Examples

Use np.random.default_rng() to create a local random Generator for sampling and simulation. Learn how seeds, advancing state and child streams affect reproducibility, then practise with a synthetic demand model.

Seeded outputs below illustrate one environment. NumPy Generator does not guarantee identical output across versions; replay checks compare runs inside the current environment.

Create a Generator with default_rng

Create a local Generator and call its methods. It owns its random state rather than using the legacy global state. A seed makes a run repeatable under the same environment and sequence of calls. Omitting it requests fresh operating-system entropy.

import numpy as np
from numpy.random import Generator, PCG64, Philox, SeedSequence
from copy import deepcopy
import json

rng = np.random.default_rng(42)
print("BitGenerator:", type(rng.bit_generator).__name__)
print("Integers:", rng.integers(0, 10, size=5))
print("Normal:", np.round(rng.normal(0, 1, size=3), 4))

Expected output

BitGenerator: PCG64
Integers: [0 7 6 4 4]
Normal: [ 0.9406 -1.951  -1.3022]

Same Seed and Same Calls Replay a Run

Compare two freshly created generators using the same seed. Reproducibility also depends on the BitGenerator, NumPy environment and sequence of methods and arguments. Generator does not promise identical output across all future versions.

a = np.random.default_rng(42)
b = np.random.default_rng(42)
x = a.integers(0, 100, size=6)
y = b.integers(0, 100, size=6)
print(x)
print("Replay matches:", np.array_equal(x, y))
np.testing.assert_array_equal(x, y)

Expected output

[ 8 77 65 43 43 85]
Replay matches: True

Drawing Advances State; Recreating Restarts It

Recreating a seeded generator inside a loop repeats its first draw. Construct it once when you want a continuing stream. Additional random calls advance state and can change later results.

restarted = [np.random.default_rng(42).integers(0, 100) for _ in range(4)]
rng = np.random.default_rng(42)
continued = [rng.integers(0, 100) for _ in range(4)]
print("Repeated restart:", restarted)
print("Continuing stream:", continued)
assert len(set(restarted)) == 1

Expected output

Repeated restart: [np.int64(8), np.int64(8), np.int64(8), np.int64(8)]
Continuing stream: [np.int64(8), np.int64(77), np.int64(65), np.int64(43)]

Integer Bounds, endpoint and size

The lower bound is included and the upper bound is excluded by default. For a six-sided die, use integers(1, 7) or explicitly include six with endpoint=True. A tuple size creates a multidimensional array. See legacy randint() and shape.

rng = np.random.default_rng(10)
rolls = rng.integers(1, 7, size=(2, 3), dtype=np.int32)
inclusive = rng.integers(1, 6, size=5, endpoint=True)
print("Rolls:")
print(rolls)
print("Inclusive upper bound:", inclusive)
assert rolls.shape == (2, 3)
assert ((rolls >= 1) & (rolls <= 6)).all()

Expected output

Rolls:
[[5 6 2]
 [2 5 5]]
Inclusive upper bound: [4 1 5 4 1]

Uniform Floats and Normal Samples

random generates floats from zero inclusive to one exclusive. normal uses loc as the mean and scale as the standard deviation; a small sample need not have exactly those statistics. Other methods include lognormal, gamma, beta and binomial.

rng = np.random.default_rng(101)
uniform = rng.random((2, 3))
normal = rng.normal(loc=10, scale=2, size=(2, 3))
print("Uniform:")
print(np.round(uniform, 4))
print("Normal:")
print(np.round(normal, 4))
print("Poisson counts:", rng.poisson(lam=3.5, size=4))
assert ((uniform >= 0) & (uniform < 1)).all()

Expected output

Uniform:
[[0.9435 0.3594 0.7848]
 [0.5913 0.2943 0.9227]]
Normal:
[[13.4208 12.1216 11.4153]
 [11.3755  8.2729 11.928 ]]
Poisson counts: [1 5 2 7]

Weighted choice and Sampling without Replacement

Probabilities must be nonnegative and sum to one. replace=False selects distinct entries and cannot request more samples than the available population. Repeated labels in the population can still produce repeated labels.

rng = np.random.default_rng(101)
labels = np.array(['A', 'B', 'C'])
print("Weighted selection:", rng.choice(labels, size=5, p=[0.2, 0.5, 0.3]))
ids = np.arange(100, 110)
sample = rng.choice(ids, size=4, replace=False)
print("Distinct IDs:", sample)
assert np.unique(sample).size == 4

Expected output

Weighted selection: ['C' 'B' 'C' 'B' 'B']
Distinct IDs: [106 108 107 102]

shuffle Changes the Input; permutation Returns a Copy

Use permutation when the original record order must stay available. A 2D permutation along axis=0 moves entire rows together, preserving the relationship between fields.

rng = np.random.default_rng(12)
records = np.array([[101, 80], [102, 60], [103, 90], [104, 70]])
shuffled = rng.permutation(records, axis=0)
print("Original:")
print(records)
print("Permuted rows:")
print(shuffled)
assert set(map(tuple, records)) == set(map(tuple, shuffled))
assert not np.shares_memory(records, shuffled)
items = np.arange(5)
rng.shuffle(items)
print("Shuffled in place:", items)

Expected output

Original:
[[101  80]
 [102  60]
 [103  90]
 [104  70]]
Permuted rows:
[[102  60]
 [103  90]
 [104  70]
 [101  80]]
Shuffled in place: [1 4 3 0 2]

Choose a BitGenerator Explicitly

Generator supplies distribution methods; a BitGenerator supplies raw randomness and state. default_rng currently uses PCG64. Explicit PCG64 or Philox records the intended algorithm, but changing the algorithm changes the stream. For very large parallel workloads, consult NumPy guidance on PCG64DXSM and Philox.

rng_pcg = Generator(PCG64(12345))
rng_philox = Generator(Philox(2025))
print("PCG64:", rng_pcg.integers(10, size=4))
print("Philox:", np.round(rng_philox.uniform(size=3), 4))
assert type(rng_pcg.bit_generator).__name__ == 'PCG64'
assert type(rng_philox.bit_generator).__name__ == 'Philox' 

Expected output

PCG64: [6 2 7 3]
Philox: [0.0697 0.7539 0.4566]

SeedSequence Creates Reproducible Child Streams

Spawn separate child sequences for simulations or workers. They are designed to provide independent streams with very high probability; this is not a mathematical guarantee of no overlap. Reconstruct the root seed and repeat the spawn structure to replay the same assignment.

def make_streams(seed, count):
    children = SeedSequence(seed).spawn(count)
    return [Generator(PCG64(child)) for child in children]

streams = make_streams(2025, 3)
replay = make_streams(2025, 3)
for i, (rng, again) in enumerate(zip(streams, replay), 1):
    values = rng.integers(0, 100, size=3)
    print("Stream", i, values)
    np.testing.assert_array_equal(values, again.integers(0, 100, size=3))

Expected output

Stream 1 [87 99 72]
Stream 2 [53 71 62]
Stream 3 [ 5 72 24]

Assign One Stream per Worker or Simulation

This runnable example simulates four worker jobs without starting processes. Assign child streams to stable job IDs so scheduling changes do not change the assignment. Reusing the same seed independently in every worker repeats the same stream; sharing mutable state also makes results sensitive to call order.

def worker_counts(rng):
    return rng.poisson(3, size=4)

root_seed = 123
streams = make_streams(root_seed, 4)
results = [worker_counts(rng) for rng in streams]
replayed = [worker_counts(rng) for rng in make_streams(root_seed, 4)]
for job, (values, expected) in enumerate(zip(results, replayed)):
    print("Job", job, values)
    np.testing.assert_array_equal(values, expected)

Expected output

Job 0 [2 1 2 2]
Job 1 [2 3 1 1]
Job 2 [2 6 2 2]
Job 3 [4 2 3 4]

Save and Restore an Exact Continuation

A seed restarts from the beginning. A state snapshot resumes at its saved position when restored into the matching BitGenerator and environment. Take a copy and restore before making the same subsequent calls.

rng = Generator(PCG64(7))
state = deepcopy(rng.bit_generator.state)
a = rng.standard_normal(5)
rng2 = Generator(PCG64())
rng2.bit_generator.state = state
b = rng2.standard_normal(5)
print("Continuation:", np.round(a, 4))
print("Exact match:", np.array_equal(a, b))
np.testing.assert_array_equal(a, b)

Expected output

Continuation: [ 0.0012  0.2987 -0.2741 -0.8906 -0.4547]
Exact match: True

Serialize PCG64 State with JSON

PCG64 state can be encoded directly with json. This in-memory round trip is equivalent to writing the payload to a UTF-8 file and reading it later. Other BitGenerators can contain arrays needing extra serialization handling. Record the algorithm and environment with the snapshot. See file I/O for saving generated arrays.

rng = Generator(PCG64(2026))
record = {'numpy_version': np.__version__, 'bit_generator': 'PCG64',
          'state': deepcopy(rng.bit_generator.state)}
payload = json.dumps(record)
saved = json.loads(payload)
expected = rng.integers(0, 100, size=5)
restored = Generator(PCG64())
restored.bit_generator.state = saved['state']
actual = restored.integers(0, 100, size=5)
print("Saved algorithm:", saved['bit_generator'])
print("Replay:", actual)
np.testing.assert_array_equal(expected, actual)

Expected output

Saved algorithm: PCG64
Replay: [85 17  2 63 36]

Replace Legacy Calls with Generator Methods

The API migration changes where state lives and can change the generated sequence. rng.random((2, 3)) takes a tuple shape, unlike np.random.rand(2, 3). The replacements below illustrate the methods; identical seeds do not make legacy and modern outputs identical. Related pages: rand(), randn() and random_sample().

rng = np.random.default_rng(24)
print("rand replacement:", np.round(rng.random(3), 4))
print("randn replacement:", np.round(rng.standard_normal(3), 4))
print("randint replacement:", rng.integers(1, 7, size=3))
print("choice replacement:", rng.choice(['A', 'B'], size=3))

Expected output

rand replacement: [0.3303 0.4052 0.5747]
randn replacement: [-0.1871 -0.3395 -0.2277]
randint replacement: [4 6 3]
choice replacement: ['A' 'B' 'B']

Pass a Generator into Reusable Functions

Passing rng makes the source of randomness visible. The function consumes that supplied stream and does not reset it internally. Independent callers can create separate generators with the same seed to replay a simulation.

def daily_orders(rng, days, mean_orders):
    return rng.poisson(lam=mean_orders, size=days)

a = daily_orders(np.random.default_rng(80), 7, 12)
b = daily_orders(np.random.default_rng(80), 7, 12)
print("Synthetic orders:", a)
print("Weekly total:", a.sum())
np.testing.assert_array_equal(a, b)

Expected output

Synthetic orders: [15 13 13 15 16 13 13]
Weekly total: 98

Practical Simulation: Weekly Stock-Shortage Risk

This synthetic model assumes independent daily Poisson demand with mean 12. Simulate 20000 weeks of seven days and estimate the chance that weekly demand exceeds 90 units. This demonstrates a workflow, not a forecast based on real sales; validate assumptions before applying it to a business.

rng = np.random.default_rng(2026)
demand = rng.poisson(lam=12, size=(20000, 7))
weekly = demand.sum(axis=1)
shortage = weekly > 90
probability = shortage.mean()
standard_error = np.sqrt(probability * (1 - probability) / weekly.size)
print("Simulation shape:", demand.shape)
print("Mean weekly demand:", round(float(weekly.mean()), 2))
print("Estimated shortage probability:", round(float(probability), 4))
print("Monte Carlo standard error:", round(float(standard_error), 4))
assert weekly.shape == (20000,)
assert 0 <= probability <= 1

Expected output

Simulation shape: (20000, 7)
Mean weekly demand: 83.77
Estimated shortage probability: 0.2296
Monte Carlo standard error: 0.003

Quality, Speed and Reproducibility Checklist

Vectorized size arguments usually avoid Python loop overhead. Record the seed, NumPy version, algorithm, call sequence, shapes and model assumptions for an experiment. Do not assume reshaping calls or changing sample sizes preserves every later draw. NumPy random generators are intended for simulation and sampling; use a security-specific generator for passwords or authentication tokens.

Four Practice Exercises

1. Spawn three child streams from seed 1234 and draw two Poisson counts with mean 2.5 from each. 2. After five normal draws from seed 77, save state and verify the next five after restoring. 3. Replace rand, randn, randint and choice in a toy script with local Generator methods. 4. Simulate a biased six-sided die with probabilities 0.10, 0.15, 0.20, 0.25, 0.20 and 0.10, then compare frequencies. Finite samples will not exactly match their probabilities.

Solution 1: Child Streams

Recreating the root and spawn tree repeats each child assignment.

streams = make_streams(1234, 3)
replay = make_streams(1234, 3)
for i, (rng, again) in enumerate(zip(streams, replay), 1):
    values = rng.poisson(2.5, size=2)
    print(i, values)
    np.testing.assert_array_equal(values, again.poisson(2.5, size=2))

Expected output

1 [4 1]
2 [3 4]
3 [0 2]

Solution 2: Resume after Five Draws

The snapshot is taken after the first five values, before the continuation.

rng = Generator(PCG64(77))
first = rng.normal(size=5)
state = deepcopy(rng.bit_generator.state)
continuation = rng.normal(size=5)
again = Generator(PCG64())
again.bit_generator.state = state
replayed = again.normal(size=5)
print("First five:", np.round(first, 4))
print("Next five:", np.round(continuation, 4))
print("Exact replay:", np.array_equal(continuation, replayed))
np.testing.assert_array_equal(continuation, replayed)

Expected output

First five: [ 0.4278 -0.5708  2.6545 -1.6085  0.6617]
Next five: [-0.1434 -0.3545  1.0664 -1.8179 -0.9847]
Exact replay: True

Solution 3: A Local Generator Toy Script

Use one Generator instance to produce all four outputs in a defined order.

rng = np.random.default_rng(33)
print("Uniform:", np.round(rng.random((2, 3)), 3))
print("Normal:", np.round(rng.standard_normal(3), 3))
print("Integers:", rng.integers(0, 10, size=3))
print("Labels:", rng.choice(['A', 'B', 'C'], size=3))

Expected output

Uniform: [[0.444 0.568 0.908]
 [0.254 0.589 0.359]]
Normal: [-0.098  0.62   1.837]
Integers: [6 5 4]
Labels: ['A' 'B' 'A']

Solution 4: Biased Die and Empirical Frequencies

Count outcomes with bincount(). The differences measure sampling variation; one sample does not establish generator quality.

rng = np.random.default_rng(9)
p = np.array([0.10, 0.15, 0.20, 0.25, 0.20, 0.10])
rolls = rng.choice(np.arange(1, 7), size=10000, p=p)
counts = np.bincount(rolls, minlength=7)[1:]
frequencies = counts / rolls.size
print("Expected:", p)
print("Observed:", np.round(frequencies, 3))
print("Differences:", np.round(frequencies - p, 3))
assert counts.sum() == rolls.size
np.testing.assert_allclose(frequencies.sum(), 1)

Expected output

Expected: [0.1  0.15 0.2  0.25 0.2  0.1 ]
Observed: [0.098 0.153 0.196 0.257 0.2   0.096]
Differences: [-0.002  0.003 -0.004  0.007  0.    -0.004]

Practice in Google Colab

Open in Google Colab View on GitHub
All sample data is embedded. Run the examples and practise sampling, state replay and simulation. Save a copy in Drive to keep your changes.

Continue with NumPy tutorials, sum(), axis reduction and file I/O. References: Generator documentation, parallel random generation and compatibility policy.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer