NumPy 基础:从数组到统计计算的基石

说实话,在接触批量质控之前,我处理实验数据用的还是纯 Python 列表。那时候有个项目,要对 10 万条质控数据做归一化,跑一次要等两分钟。后来换成 NumPy,几秒钟搞定。嗯,从那以后,我再也离不开它了。

NumPy 是 Python 做科学计算的根基。没有它,后面的统计、建模、可视化全都转不动。这一章,我们就把它拆开揉碎了讲。你想想看,数组是怎么创建的?索引怎么玩?矩阵运算和随机数生成又是怎么一回事?全部过一遍,为后续的批处理做好铺垫。

NumPy 基础知识体系 NumPy 基础 数组创建 索引与切片 矩阵运算 随机数生成 np.array() np.arange() np.zeros/ones 花式索引 切片语法 逐元素运算 矩阵乘法 dot rand/randn 设置种子 💡 学习路径建议 先掌握数组创建,再学索引切片 矩阵运算和随机数生成是统计计算的核心工具

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)  # 单位矩阵
个人经验: 我建议你养成一个习惯——创建数组时顺手检查 shape 和 dtype。在批量质控中,数据维度不匹配是常见的 bug 来源。比如我吃过一次亏:某次读取 96 孔板数据,忘记指定 dtype,结果后面计算平均值时报错,查了半天才发现是 int 精度不够,改用 float64 就好了。

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(原数组不受影响)
曾经有一次我... 在处理 384 孔板数据时,用切片取了一组对照孔,然后直接修改了切片。结果发现原始数据全变了,导致整个批次质控结果出错。从那以后,只要需要修改提取出来的数据,我一定会用 .copy()。

花式索引和布尔索引也是高频操作。尤其在质控中,我们要筛选超出上下限的异常值,布尔索引是最直接的方法。

# 花式索引:用整数列表/数组取指定行
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)  # 第二次运行会不同
我在项目中的做法: 每个带有随机操作的质控脚本,我都会在运行日志里记录 seed 值。如果某次结果有问题,复制 seed 就能完全复现现场。我建议你也养成这个习惯,debug 时能省下大量时间。

高级一点的随机函数还有 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 这东西你越用越顺手,很多操作写出来自己都觉得优雅。