在上一篇中,我们掌握了 NumPy 数组的索引与切片技巧,能够精准提取需要的数据。而 NumPy 真正的性能核心,在于向量化运算和广播机制------ 这两大特性彻底摆脱了 Python 循环的低效,让大规模数值计算速度提升百倍。本文将带你从原理到实践,吃透这两个核心概念。
一、向量化运算:摆脱循环,高效计算
向量化运算的本质是 直接对整个数组进行操作,而非单个元素循环处理。NumPy 的向量化运算由 C 语言底层实现,避开了 Python 解释器的开销,这是它比原生列表快的关键原因。
1. 算术向量化运算
NumPy 支持直接对数组进行加减乘除等算术运算,规则是 元素级运算(即两个数组对应位置的元素分别运算)。
import numpy as np
# 创建两个一维数组
arr1 = np.array([1, 2, 3, 4])
arr2 = np.array([5, 6, 7, 8])
# 1. 标量与数组运算(广播的基础)
print(arr1 + 2) # [3 4 5 6] → 所有元素加2
print(arr1 * 3) # [3 6 9 12] → 所有元素乘3
print(arr1 **2) # [ 1 4 9 16] → 所有元素平方
# 2. 数组与数组运算(元素级)
print(arr1 + arr2) # [ 6 8 10 12]
print(arr1 - arr2) # [-4 -4 -4 -4]
print(arr1 * arr2) # [ 5 12 21 32] → 注意:这是元素积,不是矩阵乘法
print(arr1 / arr2) # [0.2 0.33333333 0.42857143 0.5 ]
对比 Python 循环:计算 1000 万个数的平方,向量化运算的优势会极其明显。
import time
# 生成1000万数据
n = 10_000_000
arr = np.arange(n)
lst = list(range(n))
# NumPy向量化运算
start = time.time()
arr_square = arr** 2
print(f"NumPy耗时:{time.time() - start:.4f} 秒") # 约0.01秒
# Python列表循环
start = time.time()
lst_square = [x **2 for x in lst]
print(f"列表循环耗时:{time.time() - start:.4f} 秒") # 约0.3秒(慢30倍)
2. 数学函数向量化
NumPy 提供了丰富的向量化数学函数,这些函数直接作用于数组的每个元素,无需循环。
python
运行
arr = np.array([0, np.pi/2, np.pi])
# 三角函数
print(np.sin(arr)) # [0.0000000e+00 1.0000000e+00 1.2246468e-16]
print(np.cos(arr)) # [ 1.000000e+00 6.123234e-17 -1.000000e+00]
# 指数与对数
arr_exp = np.exp(arr) # e的x次方
arr_log = np.log(arr + 1) # 自然对数
# 绝对值、开方
print(np.abs(np.array([-1, -2, 3]))) # [1 2 3]
print(np.sqrt(arr1)) # [1. 1.41421356 1.73205081 2. ]
3. 统计函数向量化
统计函数是数据分析的高频需求,NumPy 的统计函数支持指定计算轴,灵活实现行 / 列统计。核心参数 axis 的含义:
-
axis=0:沿列方向计算(对每列的所有行求统计值)
-
axis=1:沿行方向计算(对每行的所有列求统计值)
-
不指定 axis:对整个数组的所有元素求统计值
创建二维数组(3行4列)
arr2d = np.array([[1,2,3,4],
[5,6,7,8],
[9,10,11,12]])1. 求和
print(np.sum(arr2d)) # 78 → 所有元素和
print(np.sum(arr2d, axis=0)) # [15 18 21 24] → 每列求和
print(np.sum(arr2d, axis=1)) # [10 26 42] → 每行求和2. 均值、最大值、最小值
print(np.mean(arr2d)) # 6.5 → 所有元素均值
print(np.max(arr2d)) # 12 → 所有元素最大值
print(np.min(arr2d, axis=1)) # [1 5 9] → 每行最小值3. 中位数、标准差、方差
print(np.median(arr2d)) # 6.5 → 中位数
print(np.std(arr2d)) # 3.452052529534663 → 标准差
print(np.var(arr2d)) # 11.916666666666666 → 方差4. 最值索引
print(np.argmax(arr2d)) # 11 → 最大值的扁平化索引
print(np.argmax(arr2d, axis=0)) # [2 2 2 2] → 每列最大值的行索引
二、广播机制:不同形状数组的运算规则
在向量化运算中,我们发现标量可以直接和任意形状的数组运算,比如 arr + 2。这背后的原理就是 NumPy 的广播机制------ 当两个数组形状不同时,NumPy 会自动扩展数组的形状,使其匹配后再进行元素级运算。
1. 广播的三大核心规则
NumPy 官方定义的广播规则,必须严格遵守:
-
维度对齐:比较两个数组的维度,从最右边的维度开始匹配;
-
缺失维度补 1:如果某个数组的维度数更少,则在其左侧补 1,直到维度数相同;
-
可扩展维度:对于对应维度,只有两种情况可以广播:
- 维度大小相等;
- 其中一个维度大小为1。
若不满足,则抛出 ValueError: operands could not be broadcast together 错误。
2. 广播的典型场景
场景 1:标量与数组广播(最常用)
标量可以看作是形状为 () 的数组,根据规则,会自动扩展为与目标数组相同的形状。
arr = np.array([[1,2], [3,4]]) # shape: (2,2)
scalar = 2 # 扩展后 shape: (2,2)
print(arr + scalar)
# [[3 4]
# [5 6]]
# 扩展过程示意:
# scalar → [[2,2], [2,2]] → 与arr元素级相加
场景 2:一维数组与二维数组广播
当一维数组的列数与二维数组的列数相等时,一维数组会沿行方向扩展。
arr2d = np.array([[1,2,3], [4,5,6]]) # shape: (2,3)
arr1d = np.array([10,20,30]) # shape: (3,) → 补1后 shape: (1,3) → 扩展为 (2,3)
print(arr2d + arr1d)
# [[11 22 33]
# [14 25 36]]
# 扩展过程示意:
# arr1d → [[10,20,30], [10,20,30]] → 与arr2d元素级相加
场景 3:维度大小为 1 的数组广播
当两个数组的维度数相同,但某一维度大小为 1 时,会沿该维度扩展。
arr_a = np.array([[1], [2], [3]]) # shape: (3,1)
arr_b = np.array([[10,20]]) # shape: (1,2)
# 扩展过程:
# arr_a → shape (3,1) → 扩展为 (3,2) → [[1,1], [2,2], [3,3]]
# arr_b → shape (1,2) → 扩展为 (3,2) → [[10,20], [10,20], [10,20]]
print(arr_a + arr_b)
# [[11 21]
# [12 22]
# [13 23]]
场景 4:不兼容的广播(报错示例)
arr1 = np.array([[1,2], [3,4]]) # shape: (2,2)
arr2 = np.array([[1,2,3], [4,5,6]]) # shape: (2,3)
# 尝试相加:最右维度 2 vs 3 → 不满足规则 → 报错
# print(arr1 + arr2)
# ValueError: operands could not be broadcast together with shapes (2,2) (2,3)
3. 广播的实战应用:数据标准化
数据标准化是机器学习预处理的必备步骤,公式为:
其中 μ 是均值,σ 是标准差。利用广播可以一行代码实现。
# 生成3行4列的模拟数据(3个样本,4个特征)
data = np.random.randint(0, 100, size=(3, 4))
print("原始数据:")
print(data)
# 计算每列的均值和标准差
mu = np.mean(data, axis=0) # shape: (4,)
sigma = np.std(data, axis=0) # shape: (4,)
# 标准化:广播自动匹配维度
data_norm = (data - mu) / sigma
print("\n标准化后数据:")
print(data_norm)
三、矩阵运算:线性代数的核心
在数值计算中,矩阵乘法是高频需求,注意区分「元素级乘法」和「矩阵乘法」:
- 元素级乘法:arr1 * arr2 或 np.multiply(arr1, arr2)
- 矩阵乘法:arr1 @ arr2 或 np.dot(arr1, arr2) 或 np.matmul(arr1, arr2)
1. 矩阵乘法的规则
两个矩阵 A(形状 m x n )和 B(形状 n x p )相乘,结果矩阵 C的形状为 m x p ,且满足:
C_{i, j}=\\sum_{k=1}\^n A_{i, k} \\times B_{k, j}
**核心要求**:第一个矩阵的**列数**必须等于第二个矩阵的**行数**。
# 创建两个可相乘的矩阵
A = np.array([[1,2], [3,4], [5,6]]) # shape: (3,2)
B = np.array([[7,8,9], [10,11,12]]) # shape: (2,3)
# 矩阵乘法的三种写法
print(A @ B) # 推荐写法,Python 3.5+ 支持
# print(np.dot(A, B))
# print(np.matmul(A, B))
# 结果:
# [[ 27 30 33]
# [ 61 68 75]
# [ 95 106 117]]
2. 线性代数常用函数
NumPy 的 np.linalg 模块提供了丰富的线性代数工具:
# 创建一个2x2矩阵
arr = np.array([[1,2], [3,4]])
# 1. 矩阵的逆(要求矩阵可逆)
arr_inv = np.linalg.inv(arr)
print(arr_inv)
# [[-2. 1. ]
# [ 1.5 -0.5]]
# 2. 矩阵的行列式
det = np.linalg.det(arr)
print(det) # -2.0000000000000004
# 3. 特征值与特征向量
eigenvalues, eigenvectors = np.linalg.eig(arr)
print("特征值:", eigenvalues)
print("特征向量:", eigenvectors)
# 4. 求解线性方程组 Ax = b
A = np.array([[1,1], [1,2]])
b = np.array([2,3])
x = np.linalg.solve(A, b)
print(x) # [1. 1.] → 方程组的解
四、向量化与广播的性能优化技巧
- 优先使用 NumPy 内置函数:内置函数由 C 实现,比手动写 Python 循环快得多;
- 避免频繁创建临时数组:比如 (arr + 1) * 2 会生成两个临时数组,可合并为 arr * 2 + 2;
- 合理指定数据类型:用 dtype=np.float32 替代 float64,减少内存占用,提升运算速度;
- 利用广播减少循环:遇到「按行 / 列处理数据」的需求,优先考虑广播,而非 for 循环。
五、小结
- 向量化运算是 NumPy 的性能核心,直接对数组操作,避开 Python 循环开销;
- 广播机制是向量化运算的扩展,遵循「维度对齐、补 1、可扩展」三大规则,实现不同形状数组的运算;
- 矩阵乘法与元素级乘法的区别:@ 是矩阵乘法,* 是元素级乘法;
- 利用向量化和广播,可以高效实现数据标准化、线性代数运算等实战需求。
下一篇预告:《NumPy 常用工具:统计、排序、缺失值处理》------ 我们将聚焦实际数据处理中的高频需求,掌握统计分析、排序去重、缺失值处理的核心技巧。