3.3 随机性、抽样与可复现性
随机性支撑着模拟、实验、训练集/测试集划分和抽样。在软件中,NumPy 通常生成伪随机(pseudorandom)值:确定性算法把内部状态扩展成一串在统计上表现得像随机数据的序列。它的价值在于,受控序列可以回放。
可复现随机过程并不等于“永远使用种子 42”。完整记录应包括生成器、初始状态,以及消耗该状态的操作。
创建显式生成器
现代 NumPy 代码应使用 np.random.default_rng 创建生成器(Generator):
import numpy as np
rng = np.random.default_rng(2026)整数 2026 是随机种子(seed),用于初始化生成器状态。用相同种子创建第二个生成器,并按相同顺序调用相同操作,就能重现序列:
rng_a = np.random.default_rng(2026)
rng_b = np.random.default_rng(2026)
first = rng_a.integers(0, 10, size=5)
replay = rng_b.integers(0, 10, size=5)
assert np.array_equal(first, replay)多抽取一次,会改变之后的每个位置:
rng_c = np.random.default_rng(2026)
rng_c.integers(0, 10) # 消耗一个值
shifted = rng_c.integers(0, 10, size=5)种子相同,调用序列却不同。因此,重构代码、新增一次诊断抽样,甚至改变数组形状,都可能改变后续结果。
把生成器作为参数传入函数,可以让这个依赖关系显式可见:
def simulate_waits(rng, size):
return rng.normal(loc=38, scale=7, size=size)
rng = np.random.default_rng(2026)
waits = simulate_waits(rng, size=1_000)这种做法避免了对旧式模块级随机状态的隐藏依赖,也方便测试:测试代码可以传入使用已知种子初始化的生成器。
让概率分布匹配问题
生成器提供多种概率分布,其参数表达不同假设:
rng = np.random.default_rng(2026)
uniform_0_to_1 = rng.random(5)
dice = rng.integers(1, 7, size=20) # 不包含 high=7
waits = rng.normal(loc=38, scale=7, size=1_000)random 从半开区间 均匀抽取;integers(1, 7) 生成 1 到 6 的整数;normal 中的 loc 表示均值,scale 表示标准差。
选择一个方便的分布,并不会让它自动成为有效模型。正态分布允许负值并且左右对称,因此可能不适合强烈右偏的等待时间。应先探索真实数据,明确建模假设,并进行敏感性检查。
若模拟需要多条独立随机流,不要随意使用 100、101、102 这样的相邻种子并假定它们彼此独立。SeedSequence 可以生成受到统一管理、可复现的子状态:
root = np.random.SeedSequence(2026)
children = root.spawn(3)
generators = [np.random.default_rng(child) for child in children]保存根种子,以及每条子随机流对应的任务。即使并行任务完成顺序不同,每个任务仍能保有自己的可复现随机流。
放回抽样与不放回抽样
Generator.choice 可以从总体中抽取对象:
order_ids = np.array(["A104", "A105", "A106", "A107", "A108"])
sample = rng.choice(order_ids, size=3, replace=False)
bootstrap = rng.choice(order_ids, size=8, replace=True)不放回抽样(without replacement)中,每个对象最多出现一次,样本量不能超过总体大小。放回抽样(with replacement)会在每次抽取后把对象放回,因此允许重复,也允许样本量大于总体。Bootstrap 方法会有意使用放回抽样;要求订单唯一的审计样本通常不会。
可以通过 p 提供概率,但概率必须非负且总和为 1:
weights = np.array([0.10, 0.15, 0.25, 0.20, 0.30])
weighted = rng.choice(order_ids, size=3, replace=False, p=weights)权重会改变抽样设计,进而改变估计结果的解释方式。应将权重与样本一起保存;若要推断总体,还要采用适当的调查加权或重要性加权方法。
permutation 返回打乱后的副本或位置置换,而 shuffle 会原地修改数组:
shuffled_copy = rng.permutation(order_ids)
working = order_ids.copy()
rng.shuffle(working)若后续代码仍需要原始顺序,应优先保留副本。只有在所有权明确时,原地修改才安全。
可复现不等于有代表性
假设总体包含 8 条 North 区域记录和 4 条 South 区域记录。一次可复现的简单随机抽样可能完全没有 South 记录。种子能让其他分析者回放这个结果,却无法修复覆盖偏差,也不能保证群体平衡。
当重要群体规模较小时,可以通过分层抽样(stratified sampling)在每个群体内部抽样:
zones = np.array(["North"] * 8 + ["South"] * 4)
north = np.flatnonzero(zones == "North")
south = np.flatnonzero(zones == "South")
selected = np.concatenate([
rng.choice(north, size=3, replace=False),
rng.choice(south, size=3, replace=False),
])这个设计让两个群体的样本量相等,却不与总体占比相等。直接计算六条样本的无权重均值,会让 South 相对于原总体被过度代表。因此,抽样设计必须与后续分析一起规划。
一个可审计的随机流程应记录:
- 当精确回放很重要时,记录 NumPy 版本和生成器类型。
- 根种子或保存下来的生成器状态。
- 总体定义、排除规则与排序方式。
- 样本量、放回规则、抽取概率与分层方式。
- 消耗随机状态的代码版本和操作序列。
不要使用普通伪随机生成器创建密码、令牌或加密密钥。这些任务需要 Python secrets 模块一类面向安全的工具。
本章最后一节将深入数组语法的底层:弄清哪些操作共享存储空间,以及一条管道会创建多少个全尺寸缓冲区,可以帮助你同时写出正确且高效的代码。