6.11. 归约

归约(reduction)会沿一个或多个轴对数组进行折叠,比如求和、求平均、取最小值等等。每次归约都是针对整个数组的一次库调用,比等价的 Python 循环要快得多。numpy 涵盖了日常常用的归约操作:

  • sum() —— 所有元素的总和

  • mean() —— 算术平均值(总和除以元素个数)

  • std() —— 标准差,ddof= 用于调整除数(N - ddof

  • min() / max() —— 最小和最大的元素

  • median() —— 元素排序后的中间值(第 50 百分位数)

  • argmin() / argmax() —— 最小或最大元素的索引

  • all() / any() —— 对布尔数组进行真值归约

6.11.1. 不带 axis 关键字

在不带 axis= 调用时,归约会返回一个覆盖整个数组的标量:

a = np.array([1, 2, 3, 4], dtype=np.float)
np.sum(a)           # 10.0
np.mean(a)          # 2.5
np.std(a)           # 1.118...
np.median(a)        # 2.5

b = np.array([40, 10, 30, 20], dtype=np.float)
np.max(b)           # 40.0
np.argmax(b)        # 0  (index of the maximum)

6.11.2. axis 关键字

axis= 会收缩指定的某个轴而保留其余的轴。结果是一个比输入低一阶的数组:

m = np.arange(12, dtype=np.float).reshape((3, 4))

np.sum(m)               # 66.0          - scalar
np.sum(m, axis=0)       # length-4      - column sums
np.sum(m, axis=1)       # length-3      - row sums

同样的形状规则适用于所有归约:axis=0 折叠第一个轴,axis=1 折叠第二个轴,以此类推。例如,沿一行求平均值 / 标准差分别写作 np.mean(m, axis=1)np.std(m, axis=1)。结果的长度由另一个轴决定。

keepdims=True 关键字会把被收缩的轴以长度 1 的形式保留在原处,而不是将其丢弃。当归约后的结果需要广播回原数组时,这一区别就很重要:keepdims 保持了阶数,从而让广播规则逐轴对齐。

从每一行中减去该行的均值是其典型用途:

m = np.arange(12, dtype=np.float).reshape((3, 4))
row_means = np.mean(m, axis=1, keepdims=True)
# row_means has shape (3, 1)
centred = m - row_means
# (3, 4) - (3, 1) -> (3, 4), each row centred on its own mean

如果不带 keepdimsnp.mean(m, axis=1) 会返回形状为 (3,) 的一维结果。广播 (3, 4) - (3,) 时,(3,) 在前置补阶后被对齐为 (1, 3),这与 (3, 4) 不兼容:最后一个轴不一致(4 对 3)且都不为 1,因此 numpy 会抛出 ValueError。正是 keepdims=True 让这次减法保持有效。

6.11.3. 布局很重要

结合 形状与步幅 中介绍的行优先布局,沿最后一个轴归约是开销最小的情况。归约会按数据存储的方向遍历数据块,无需在行与行之间跳转:

m = np.arange(2000, dtype=np.float).reshape((2, 1000))
np.sum(m, axis=1)       # cheap - long axis is the inner one
np.sum(m, axis=0)       # has to jump rows on every step

当应用程序可以选择缓冲区的布局方式时,应把长轴放在最后,这样沿它归约时就能以快速方向运行。

6.11.4. 以可迭代对象作为输入

大多数归约都接受 Python 可迭代对象(listrange、元组)来代替 ndarray。这种便利性会因隐式转换而花费几微秒——在循环中累积起来会很快变多。当同一份数据被多次归约时,应只构建一次 ndarray 并传递它。