3.1 数组重塑、堆叠与拆分
第 2 章把形状视为数组含义的一部分。本节要把这个观念变成一条工作规则:改变形状之前,先写清改变后每条轴代表什么。NumPy 可以验证元素数量,却无法判断某一行究竟代表日期、班次还是配送区域。
我们从包含 24 条测量值的一维班次日志开始:
import numpy as np
flat = np.arange(24)假设这些测量值按如下顺序到达:2 天,每天 3 个班次,每个班次 4 个区域。一维数组保留了全部数值,却隐藏了这些坐标。
reshape 改变坐标,不改变观测值
重塑(reshape) 会在元素总数不变的前提下返回指定形状的数组:
shift_log = flat.reshape(2, 3, 4)
print(shift_log.shape) # (2, 3, 4)
print(shift_log[1, 2, 1])这里第 0 轴是日期,第 1 轴是班次,第 2 轴是区域。因此,坐标 [1, 2, 1] 表示第 2 天、第 3 个班次、第 2 个区域。在默认的行优先顺序(C order)中,最后一条轴变化最快。它在一维数组中的位置为
因此该值就是 flat[21]。当数据看起来合理、但可能使用了错误的轴契约进行重塑时,这个计算很适合用于排查问题。
新旧形状各维度的乘积必须相等:
flat.reshape(4, 6) # 有效:4 * 6 = 24
flat.reshape(3, 8) # 有效:3 * 8 = 24
flat.reshape(5, 5) # ValueError:5 * 5 != 24可以把一个维度写为 -1,让 NumPy 根据其他已知维度推断它:
by_zone = flat.reshape(3, -1)
print(by_zone.shape) # (3, 8)只能推断一个维度,因为两个未知维度无法确定唯一形状。还要注意:数学上有效的重塑,业务含义仍可能错误。(2, 3, 4) 与 (4, 3, 2) 都能容纳 24 个值,却表达完全不同的坐标系统。
展平与转置回答不同的问题
ravel 与 flatten 都能得到一维结果,但它们表达了不同的内存所有权意图:
possibly_shared = shift_log.ravel()
independent = shift_log.flatten()ravel 会在可能时返回视图(view),因此结果可能与源数组共享存储;flatten 总是返回副本。第 3.4 节会把这个差异直观呈现出来,并介绍如何检测内存共享。
转置(transpose)并不是先展平、再重新填充。它会重新排列轴:
zone_first = shift_log.transpose(2, 0, 1)
print(zone_first.shape) # (4, 2, 3)新的轴契约是“区域 × 日期 × 班次”。之前写作 shift_log[1, 2, 1] 的同一条观测,现在写作 zone_first[1, 1, 2]。轴标签随轴移动,观测值本身并没有获得新的含义。
concatenate 延长已有轴,stack 创建新轴
假设两个周文件已经过验证,并转换为形状为 (2, 3) 的数组。行代表日期,列代表三个区域:
week_a = np.array([
[31, 38, 44],
[29, 41, 47],
])
week_b = np.array([
[33, 39, 42],
[35, 40, 46],
])连接(concatenation)会沿一条已经存在的轴拼接数组:
longer_week = np.concatenate([week_a, week_b], axis=0)
more_columns = np.concatenate([week_a, week_b], axis=1)
print(longer_week.shape) # (4, 3)
print(more_columns.shape) # (2, 6)沿 axis=0 连接时,所有未被连接的维度都必须匹配,因此两个数组都要有 3 列;沿 axis=1 连接时,两者都要有 2 行。若尺寸不匹配,NumPy 会报错,而不会悄悄虚构对齐关系。
堆叠(stacking)则会插入一条新轴:
weekly_batches = np.stack([week_a, week_b], axis=0)
print(weekly_batches.shape) # (2, 2, 3)此时三条轴表示“批次 × 日期 × 区域”。np.vstack 与 np.hstack 是便捷函数,但其行为会受输入维数影响。在分析代码中,明确写出 concatenate(..., axis=...) 或 stack(..., axis=...) 往往更能传达意图。
合并数组之前,检查三件事:
- 对应的轴是否描述相同实体和单位?
- 除连接轴以外,其他维度是否全部一致?
- 需要新增来源或批次轴,还是延长已有的行轴或列轴?
使用能解释清楚的边界拆分
当分界准确时,np.split 可以逆转连接过程:
first_two_days, last_two_days = np.split(longer_week, [2], axis=0)
rebuilt = np.concatenate([first_two_days, last_two_days], axis=0)
assert np.array_equal(rebuilt, longer_week)列表 [2] 包含的是边界位置,不是片段大小。多个边界会产生多个片段:
first, middle, final = np.split(np.arange(10), [3, 7])
# first:位置 0:3;middle:3:7;final:7:10若传入整数,则表示请求等数量的片段。当所选轴长度不能被 3 整除时,np.split(values, 3) 会失败;np.array_split(values, 3) 允许片段大小不等,并把多出的元素放在靠前的片段中。只有当业务允许分组不等时,这种便利才合适。
不要只凭方便的数组算术推断业务分区。若训练集/测试集边界、客户分组或时间截止点很重要,就应保存该边界,并说明选择依据。
现在,你可以在不丢失轴契约的前提下改变数组布局。下一节将使用可复用的置换和有序边界来重新排列记录,同时维持各列之间的关系。