NumPy 基础:从数组到统计计算的基石
说实话,在接触批量质控之前,我处理实验数据用的还是纯 Python 列表。那时候有个项目,要对 10 万条质控数据做归一化,跑一次要等两分钟。后来换成 NumPy,几秒钟搞定。嗯,从那以后,我再也离不开它了。
NumPy 是 Python 做科学计算的根基。没有它,后面的统计、建模、可视化全都转不动。这一章,我们就把它拆开揉碎了讲。你想想看,数组是怎么创建的?索引怎么玩?矩阵运算和随机数生成又是怎么一回事?全部过一遍,为后续的批处理做好铺垫。
3.1 数组创建:不止一种姿势
NumPy 的核心数据结构叫 ndarray,也就是 n 维数组。你可以把它理解成“升级版”的 Python 列表——速度快、占内存少、而且支持向量化操作。我个人习惯在一开始就用 np.array() 来创建小型测试数组,好理解,也好调试。
import numpy as np
# 从列表创建一维数组
arr1 = np.array([1.5, 2.3, 4.7, 8.1])
print(arr1)
# 输出: [1.5 2.3 4.7 8.1]
# 从嵌套列表创建二维数组
arr2 = np.array([[1, 2], [3, 4], [5, 6]])
print(arr2.shape) # 输出: (3, 2)
print(arr2.ndim) # 输出: 2
print(arr2.dtype) # 输出: int64
# 指定数据类型
arr3 = np.array([1, 2, 3], dtype=np.float32)
print(arr3.dtype) # 输出: float32
但实际项目中,更常用的是专门用来生成序列和数据范围的函数。比如 arange,类似 Python 的 range,但返回的是数组。
# arange: 起始、终止(不含)、步长
arr4 = np.arange(0, 10, 2)
print(arr4) # 输出: [0 2 4 6 8]
# linspace: 在指定区间内生成等间隔的数
arr5 = np.linspace(0, 1, 5)
print(arr5) # 输出: [0. 0.25 0.5 0.75 1. ]
# zeros / ones / eye:常用初始化数组
zeros = np.zeros((3, 4))
print(zeros.shape) # (3, 4)
ones = np.ones((2, 3))
print(ones) # 全是1.0
identity = np.eye(4)
print(identity) # 单位矩阵
3.2 索引与切片:灵活访问数据
数组的索引方式和 Python 列表很像,但扩展到了多维。你想想看,在质控业务中,我们经常需要从数据矩阵中抽取特定行(样品)或特定列(检测指标)。
arr = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
# 取第1行第2列(从0开始)
print(arr[1, 2]) # 输出: 60
# 取第1行的所有列
print(arr[1, :]) # 输出: [40 50 60]
# 取所有行的第2列
print(arr[:, 2]) # 输出: [30 60 90]
# 取子矩阵:第0到1行,第1到2列
print(arr[0:2, 1:3])
# 输出:
# [[20 30]
# [50 60]]
嗯,这里要注意切片操作产生的是原数组的视图,不是副本。修改视图会改变原数组,这在项目中很容易踩坑。
# 切片返回的是视图
view = arr[0:2, 0:2]
view[0, 0] = 999
print(arr[0, 0]) # 输出: 999(原数组被改了!)
# 显式复制避免副作用
copy = arr[0:2, 0:2].copy()
copy[0, 0] = 0
print(arr[0, 0]) # 输出: 999(原数组不受影响)
花式索引和布尔索引也是高频操作。尤其在质控中,我们要筛选超出上下限的异常值,布尔索引是最直接的方法。
# 花式索引:用整数列表/数组取指定行
arr = np.arange(12).reshape(4, 3)
rows = [0, 2, 3]
print(arr[rows, :])
# 输出:
# [[ 0 1 2]
# [ 6 7 8]
# [ 9 10 11]]
# 布尔索引:筛选满足条件的元素
arr = np.array([5, 12, 3, 18, 7, 9])
mask = arr > 10
print(mask) # 输出: [False True False True False False]
print(arr[mask]) # 输出: [12 18]
3.3 矩阵运算:从逐元素到矩阵乘法
在批次质控中,统计计算大部分是矩阵和向量操作。比如计算每个样品的 Z-score,或者做协方差矩阵。NumPy 的广播机制让这些操作变得极其简洁。
# 逐元素运算
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a + b) # 输出: [5 7 9]
print(a * b) # 输出: [4 10 18]
print(a ** 2) # 输出: [1 4 9]
# 二维数组逐元素运算
A = np.array([[1, 2], [3, 4]])
B = np.array([[10, 20], [30, 40]])
print(A + B)
# 输出:
# [[11 22]
# [33 44]]
矩阵乘法用的是 dot 函数或 @ 运算符,这和逐元素乘(*)完全是两码事。我个人习惯用 @ 运算符,代码更干净。
# 矩阵乘法
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 三种写法等价
C1 = np.dot(A, B)
C2 = A.dot(B)
C3 = A @ B
print(C3)
# 输出:
# [[19 22]
# [43 50]]
# 向量点积
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
print(v1 @ v2) # 输出: 32(1*4 + 2*5 + 3*6)
广播机制是 NumPy 最强大的特性之一。说白了,就是形状不同的数组也能进行运算,只要维度兼容。
# 广播:一维向量加到二维数组的每一行
data = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
mean = np.mean(data, axis=0) # 每列均值: [4. 5. 6.]
centered = data - mean # 广播自动对齐
print(centered)
# 输出:
# [[-3. -3. -3.]
# [ 0. 0. 0.]
# [ 3. 3. 3.]]
| 函数 | 用途 | 示例输出 (arr = [3, 1, 4, 1, 5, 9]) |
|---|---|---|
| np.sum(arr) | 求和 | 23 |
| np.mean(arr) | 均值 | 3.833... |
| np.std(arr) | 标准差 | 2.716... |
| np.var(arr) | 方差 | 7.472... |
| np.min(arr) | 最小值 | 1 |
| np.max(arr) | 最大值 | 9 |
| np.argmin(arr) | 最小值索引 | 1 或 3 |
| np.argmax(arr) | 最大值索引 | 5 |
这些统计函数默认对全部元素操作。加上 axis 参数就能按行或按列计算了,这在处理批次数据时非常关键。
# axis 参数的使用
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
row_means = np.mean(matrix, axis=1) # 每行均值
col_means = np.mean(matrix, axis=0) # 每列均值
print(row_means) # 输出: [2. 5. 8.]
print(col_means) # 输出: [4. 5. 6.]
3.4 随机数生成:为模拟和采样赋能
随机数在质控中哪里用得到?举个最常见的场景——从大批量数据中随机抽样做验证。或者在做性能测试时,模拟生成一批随机数据来压测处理流程。NumPy 的 np.random 模块就是干这个的。
# 基本随机数生成
# 均匀分布 [0, 1)
rand_vals = np.random.rand(5)
print(rand_vals) # 比如: [0.123 0.456 0.789 0.234 0.567]
# 标准正态分布 N(0,1)
randn_vals = np.random.randn(3, 4)
print(randn_vals.shape) # (3, 4)
# 指定范围均匀分布的整数
int_vals = np.random.randint(0, 10, size=(3, 3))
print(int_vals)
# 输出示例:
# [[7 2 9]
# [4 1 6]
# [8 0 3]]
关于随机数,你一定要记住一件事——种子。为什么?因为机器生成的“随机数”其实是伪随机的。设置种子后,每次运行生成的序列都一样,这对调试和结果复现至关重要。
# 设置种子保证可复现
np.random.seed(42)
a = np.random.rand(3)
print(a) # 每次运行都输出: [0.37454012 0.95071431 0.73199394]
# 没设种子的话,每次结果都不一样
b = np.random.rand(3)
print(b) # 第二次运行会不同
高级一点的随机函数还有 np.random.normal、np.random.uniform、np.random.choice 等,后面写模拟程序时会频繁用到。
# 从正态分布采样
mu, sigma = 50, 5
samples = np.random.normal(loc=mu, scale=sigma, size=1000)
print(np.mean(samples)) # 接近50
print(np.std(samples)) # 接近5
# 从给定数组中随机选择
colors = ['red', 'blue', 'green', 'yellow']
selected = np.random.choice(colors, size=5, p=[0.5, 0.3, 0.1, 0.1])
print(selected) # 按概率加权随机
好了,NumPy 的基础就到这了。数组创建、索引切片、矩阵运算、随机数生成——这四个点你多练练,后面的统计计算章节我们会反复回来用它们。说实话,NumPy 这东西你越用越顺手,很多操作写出来自己都觉得优雅。