4、NumPy数组操作:形状变换(reshape、resize)、拼接与分割(concatenate、split)、转置与轴交换
在数据分析与科学计算中,数据的存储形态往往与计算逻辑的形态不一致。原始数据可能是一维的传感器时序流,而模型需要的是二维的特征矩阵;或者多个数据源需要按列合并成宽表,再按特定维度进行聚合。NumPy 作为 Python 数据科学生态系统的基石,其数组操作能力直接决定了数据预处理阶段的效率与代码质量。本章将深入剖析形状变换、拼接分割以及转置与轴交换三大类核心操作,帮助学习者建立“数据形状即计算视图”的思维模式。
4.1 形状变换:reshape 与 resize 的核心原理与差异
核心原理:NumPy 数组在内存中是以一维连续块(C 顺序)或 Fortran 顺序存储的。形状变换的本质是重新解释同一块内存数据的维度结构,而非修改数据本身。reshape 操作返回的是原数组的一个视图(View),这意味着新数组与原始数组共享底层数据缓冲区,对视图的修改会直接反映到原数组上。而 resize 则分为两种情况:ndarray.resize() 方法会原地修改数组本身,直接改变其形状和内存布局;而 np.resize() 函数则会返回一个新数组,且当新形状元素个数大于原数组时,会循环填充原数据。
| 操作 | 返回类型 | 是否原地 | 数据填充规则 | 典型应用场景 |
|---|---|---|---|---|
| ndarray.reshape(new_shape) | 视图(View) | 否 | 元素总数必须一致,否则报错 | 特征矩阵重构、图像展平 |
| ndarray.resize(new_shape) | None(修改自身) | 是 | 元素不足时用 0 填充,多余时截断 | 需要释放原数组引用时 |
| np.resize(arr, new_shape) | 新数组(Copy) | 否 | 元素不足时循环原数组数据 | 需要保留原数组且允许重复数据 |
操作流程与判断方法:在使用 reshape 时,最常用的技巧是使用 -1 作为占位符。例如 arr.reshape(-1, 4) 表示自动计算行数,使得列数为 4。但需注意,-1 只能出现一次。对于 resize,若使用 ndarray.resize(),需确保没有其他变量引用该数组,否则会引发 ValueError。判断何时使用哪种方法的核心依据是:是否需要保留原数组不变,以及是否允许数据复制。
实战案例:图像数据预处理中的形状变换
假设我们有一批灰度图像数据,原始存储形状为 (num_samples, height, width),例如 (1000, 64, 64)。在输入卷积神经网络前,需要将其展平为二维矩阵 (1000, 4096),或者增加通道维度变为 (1000, 1, 64, 64)。
import numpy as np
# 模拟 1000 张 64x64 的灰度图
images = np.random.randint(0, 255, size=(1000, 64, 64), dtype=np.uint8)
# 场景1:展平为特征矩阵(使用 reshape,返回视图,零拷贝)
flat_images = images.reshape(1000, -1) # 自动计算 64*64=4096
print(f"展平后形状: {flat_images.shape}") # (1000, 4096)
# 场景2:增加通道维度(使用 np.expand_dims 或 reshape)
channels_first = images.reshape(1000, 1, 64, 64)
print(f"通道前置形状: {channels_first.shape}") # (1000, 1, 64, 64)
# 场景3:当需要循环填充数据时(例如数据增强中的重复)
expanded = np.resize(images[:10], (15, 64, 64)) # 从10张循环填充到15张
print(f"循环填充后形状: {expanded.shape}") # (15, 64, 64)
该案例中,reshape 操作没有产生任何数据复制,内存效率极高。而 np.resize 则创建了新数组,且第 11-15 张图是前 5 张的重复,这在某些数据均衡策略中非常实用。
常见误区:初学者常混淆 reshape 与 resize 的返回值。请牢记:ndarray.reshape() 返回新视图,原数组不变;ndarray.resize() 直接修改原数组并返回 None。若误用 arr = arr.resize(2,3),arr 会变成 None 导致程序崩溃。此外,reshape 要求元素总数严格一致,而 resize 允许元素数量变化。
4.2 拼接与分割:concatenate、stack 与 split 的灵活运用
核心原理:拼接操作的本质是沿指定轴将多个数组的元素合并。NumPy 提供了 np.concatenate 用于沿现有轴拼接,以及 np.stack 用于沿新轴堆叠。分割则是拼接的逆操作,np.split 将数组均等分割,np.hsplit 和 np.vsplit 分别沿水平轴和垂直轴分割。理解轴(axis)参数是掌握这些操作的关键:对于二维数组,axis=0 表示行方向(垂直),axis=1 表示列方向(水平)。
操作流程与判断方法:选择哪种拼接方式,取决于目标数组的维度需求。若两个数组形状为 (3,4) 和 (3,4),使用 concatenate(axis=0) 得到 (6,4),使用 concatenate(axis=1) 得到 (3,8)。而 stack 会在指定位置插入一个新维度,例如 np.stack([a,b], axis=0) 得到 (2,3,4)。分割时需注意数组长度必须能被分割份数整除,否则会报错。
| 函数 | 轴参数含义 | 输入要求 | 输出形状示例 |
|---|---|---|---|
| np.concatenate((a,b), axis=0) | 沿第 0 轴拼接 | 除 axis 外其他维度一致 | (m1+m2, n) |
| np.vstack((a,b)) | 垂直堆叠(等价 axis=0) | 列数一致 | (m1+m2, n) |
| np.hstack((a,b)) | 水平堆叠(等价 axis=1) | 行数一致 | (m, n1+n2) |
| np.stack((a,b), axis=2) | 沿新轴堆叠 | 形状完全相同 | (m, n, 2) |
| np.split(arr, 3, axis=0) | 沿第 0 轴均分 3 份 | 长度可整除 | 3 个 (m/3, n) 数组 |
实战案例:多传感器数据融合
某工业设备同时采集温度、振动和压力三种信号,每秒钟各产生 100 个采样点。我们需要将三个一维数组合并为一个二维矩阵,每行代表同一时刻的三种传感器读数。
import numpy as np
# 模拟 1 秒内的数据,各 100 个采样点
temp = np.random.rand(100) * 100 # 温度 0-100°C
vib = np.random.rand(100) * 5 # 振动幅度 0-5mm
pressure = np.random.rand(100) * 10 # 压力 0-10kPa
# 方法1:使用 column_stack(hstack 的变体)
sensor_matrix = np.column_stack((temp, vib, pressure))
print(f"传感器矩阵形状: {sensor_matrix.shape}") # (100, 3)
# 方法2:使用 stack 增加样本维度(模拟 100 个时间步,每个步有 3 个特征)
time_series = np.stack((temp, vib, pressure), axis=1) # (100, 3)
print(f"时间序列形状: {time_series.shape}")
# 分割:将 100 个时间步分为 4 段,每段 25 步
segments = np.split(time_series, 4, axis=0)
print(f"分割段数: {len(segments)}, 每段形状: {segments[0].shape}") # (25, 3)
此案例展示了拼接在特征融合中的应用。column_stack 自动将一维数组按列拼接,而 stack 则更灵活地控制新轴位置。分割操作则便于后续按时间段进行滑动窗口分析。
经验提醒:当拼接的数组数量较多时,建议先使用列表推导式生成数组列表,再一次性调用 np.concatenate(list_of_arrays),避免在循环中反复拼接导致性能下降。此外,np.split 只能均分,若需要不等分,可使用 np.array_split,它允许最后一段长度不同。
4.3 转置与轴交换:transpose 与 swapaxes 的维度重排
核心原理:转置操作的本质是交换数组的轴顺序。对于二维数组,arr.T 等价于 arr.transpose(1,0),实现行列互换。对于高维数组,transpose 接受一个轴顺序元组,例如 arr.transpose(2,0,1) 将第 2 轴移到最前。而 swapaxes 则只交换两个指定的轴,例如 arr.swapaxes(0,2) 交换第 0 轴和第 2 轴。两者都返回视图,不复制数据。
操作流程与判断方法:理解轴编号是关键。假设数组形状为 (2,3,4),轴 0 长度为 2,轴 1 长度为 3,轴 2 长度为 4。执行 transpose(1,2,0) 后,新形状变为 (3,4,2)。判断方法:新形状的第 i 个维度等于原形状的第 axes[i] 个维度。swapaxes 则更直观,只需指定两个轴编号即可。
| 操作 | 语法 | 效果 | 典型场景 |
|---|---|---|---|
| 二维转置 | arr.T 或 arr.transpose(1,0) | 行列互换 | 矩阵乘法、协方差计算 |
| 高维转置 | arr.transpose(2,0,1) | 轴顺序重排 | 图像通道调整 (H,W,C)→(C,H,W) |
| 轴交换 | arr.swapaxes(0,2) | 仅交换两轴 | 批量数据维度调整 |
| 逆序转置 | arr.transpose()[::-1] 或 arr.T | 所有轴逆序 | 张量维度反转 |
实战案例:深度学习中的张量维度调整
在图像处理任务中,PyTorch 模型通常期望输入形状为 (Batch, Channel, Height, Width),即通道前置(NCHW)。而 OpenCV 读取的图像形状为 (Height, Width, Channel),即通道后置(HWC)。转换过程如下:
import numpy as np
# 模拟 OpenCV 读取的 BGR 图像,高度 480,宽度 640,3 通道
image_hwc = np.random.randint(0, 255, size=(480, 640, 3), dtype=np.uint8)
print(f"原始形状 (HWC): {image_hwc.shape}")
# 方法1:使用 transpose 调整轴顺序
image_chw = image_hwc.transpose(2, 0, 1) # (3, 480, 640)
print(f"转置后形状 (CHW): {image_chw.shape}")
# 方法2:使用 swapaxes 分两步完成
# 先交换 H 和 C,再交换 W 和 C(不推荐,但演示原理)
temp = image_hwc.swapaxes(0, 2) # (3, 640, 480)
image_chw_v2 = temp.swapaxes(1, 2) # (3, 480, 640)
print(f"swapaxes 结果: {image_chw_v2.shape}")
# 验证两种方法结果一致
print(f"结果一致: {np.array_equal(image_chw, image_chw_v2)}") # True
该案例展示了 transpose 在深度学习数据预处理中的核心地位。使用 transpose 一步到位,而 swapaxes 需要两步且容易混淆,实际开发中优先使用 transpose。
风险提示:transpose 和 swapaxes 返回的是视图而非副本。如果后续操作需要修改数据且不希望影响原数组,必须显式调用 .copy() 方法。例如 arr_transposed = arr.transpose(1,0).copy()。此外,对于非连续内存的视图,某些 NumPy 操作(如 np.ascontiguousarray)可能会触发隐式复制,需注意内存开销。
4.4 综合复盘与性能考量
总结复盘:本章三大操作的核心逻辑可归纳为:reshape 改变“解释方式”,concatenate/split 改变“组合方式”,transpose 改变“轴顺序”。三者均不改变底层数据的“值”,只改变数据的“组织视图”。在实际数据分析流程中,这些操作往往组合使用。例如,先使用 reshape 将一维数据变为二维矩阵,再使用 concatenate 合并多个特征块,最后使用 transpose 调整维度顺序以满足模型输入要求。
性能建议:在数据量较大时(如超过 1GB),应优先使用返回视图的操作(reshape、transpose、swapaxes),避免不必要的内存复制。而 concatenate 和 stack 必然产生新数组,应尽量减少拼接次数,采用预分配内存或列表收集后一次性拼接的策略。使用 np.memory_usage 或 arr.nbytes 可以监控数组内存占用。
经验口诀:“reshape 看总数,resize 可增减;拼接看轴号,stack 加新维;转置看轴序,swap 换两轴;视图副本要分清,内存效率记心间。”
通过本章的学习,学习者应能够熟练地在不同数据形态之间切换,理解视图与副本的本质区别,并能在实际项目中根据内存和性能约束选择最合适的操作方式。掌握这些基础操作,将为后续的 Pandas 数据处理和复杂数据分析打下坚实基础。