2.3 向量化与广播
上一节选择了具有明确含义的数据子集。现在要转换这些值。NumPy 鼓励用表达式描述整个数组上的运算,而不是指挥 Python 逐个访问元素。
这就是向量化(vectorization):用数组运算表达重复的数值工作。它改善的不只是速度。minutes * 60 直接说明了转换意图,而循环会把这个意图与列表创建、迭代和修改混在一起。
逐元素运算
数组与标量之间的算术会应用到每个元素:
minutes = np.array([31.0, 52.0, 28.0, 44.0])
seconds = minutes * 60
with_buffer = minutes + 5
within_target = minutes <= 40形状相同的数组会按照位置逐元素配对:
total_minutes = np.array([31, 52, 28, 44])
parking_wait = np.array([3, 7, 2, 5])
travel_minutes = total_minutes - parking_waitNumPy 还提供通用函数(universal function, ufunc),用于对元素执行定义好的运算:
roots = np.sqrt(np.array([1, 4, 9, 16]))
capped = np.minimum(minutes, 40)循环并没有从世界上消失,NumPy 的编译实现仍要处理各个元素。优势在于,Python 只派发一次数组运算,不必为每个元素解释执行一次 Python 循环体。实际加速程度取决于 dtype、内存布局、运算种类、数组大小和临时数组分配。重要工作负载应实际测量,不要把“向量化永远更快”当成口号。
向量化代码仍须保留业务规则
更短的表达式仍然可能错误。如果停车等待时间因为数据缺陷而大于总时间,直接相减会产生负数行驶时间。根据已经声明的规则,可以拒绝记录、调查原因,或暂时限制输出:
travel_minutes = np.maximum(total_minutes - parking_wait, 0)限制下界不能代替验证,因为它会把非法值改成零,可能隐藏上游缺陷。可靠流程会统计边界生效了多少次,并保留足够信息供后续调查。
向量化表达式还可能创建临时数组。例如 (values - values.mean()) / values.std() 会先产生减法中间结果,再执行除法。一般情况下没有问题,但大型数组需要关注内存。第 3 章将进一步讨论性能和内存行为。
广播组合兼容形状
广播(broadcasting)是 NumPy 对不同但兼容的形状执行逐元素运算的规则。假设矩阵的行是日期,列是区域:
times = np.array([
[31, 38, 44, 36],
[29, 41, 47, 35],
[33, 39, 42, 37],
])
zone_baseline = np.array([30, 40, 45, 35])
deviation = times - zone_baseline两个形状分别是 (3, 4) 和 (4,)。NumPy 从右侧开始对齐维度,末尾维度都是 4,因此基线会被逻辑上复用于三行,结果形状仍为 (3, 4)。
广播通常不会在内存里真正创建一份完整的重复基线数组。它表现得像重复了值,底层却通过能够识别步幅的访问方式完成运算。
兼容规则
从形状的最后一个维度向左比较。两个维度在以下情况下兼容:
- 二者相等,或
- 其中一个为 1,或
- 其中一个形状在该位置没有维度,例如标量或更短的形状。
下面是与形状 (3, 4) 矩阵运算的例子:
| 另一个形状 | 是否兼容 | 含义 |
|---|---|---|
() | 是 | 每个单元格使用同一标量 |
(4,) | 是 | 每列一个值 |
(1, 4) | 是 | 明确的一行形式,向下复用 |
(3, 1) | 是 | 每行一个值,向右复用 |
(2,) | 否 | 末尾维度 4 与 2 冲突 |
(3,) | 否 | 末尾维度 4 与 3 冲突 |
若要让每行使用一个值,应通过重塑明确目标轴:
daily_adjustment = np.array([1, 2, 3]).reshape(3, 1)
adjusted = times - daily_adjustmentnp.newaxis 是插入长度为 1 的新维度的另一种写法:
daily_adjustment = np.array([1, 2, 3])[:, np.newaxis]防止意外的外部运算
广播可能产生形状合法、概念错误的结果。形状 (3, 1) 与 (4,) 组合会产生 (3, 4):三个行值中的每一个都会与四个列值中的每一个发生运算。这可能正是需要的外部比较,也可能是没有察觉的错误。
执行广播运算前,写下三项信息:
- 每条轴的业务含义。
- 运算前两个操作数的形状。
- 预期结果的形状和含义。
然后对重要预期使用断言:
assert times.shape == (3, 4)
assert zone_baseline.shape == (4,)
deviation = times - zone_baseline
assert deviation.shape == times.shape下一节会通过聚合有意折叠轴。我们将预测哪些标签保留下来,分析缺失值如何改变分母,并观察浮点表示为什么会影响看似简单的计算。