
在数据处理和机器学习领域,我们经常会遇到需要将多个独立的 numpy .npz 文件合并成一个统一文件的情况。.npz 文件是 numpy 提供的一种方便的归档格式,可以存储多个 numpy 数组。然而,直接使用字典的 update() 方法进行合并,往往会导致数据丢失,因为 update() 会覆盖同名键的值。本教程将详细介绍一种正确且高效的合并策略。
1. 数据存储约定:为合并做准备
在合并 .npz 文件之前,了解其内部结构至关重要。一个 .npz 文件本质上是一个包含多个 NumPy 数组的压缩包,每个数组都通过一个字符串键进行标识。为了后续能够顺利合并,建议在创建独立的 .npz 文件时遵循以下约定:
使用字典结构存储数据: 将需要保存的 NumPy 数组组织成一个字典,其中键是字符串,值是对应的 NumPy 数组。
保持键名一致性: 确保所有待合并的 .npz 文件中,代表相同类型数据的数组使用相同的键名。例如,如果每个文件都有一个特征数组和一个标签数组,可以分别命名为 'features' 和 'labels'。
-
使用 np.savez_compressed 或 np.savez 保存:
import numpy as np # 示例数据 arr_0 = np.random.rand(10, 5) arr_1 = np.random.randint(0, 2, size=(10,)) # 将数据存储在字典中 data_to_save = {'arr_0': arr_0, 'arr_1': arr_1} # 保存为 .npz 文件,使用 **data_to_save 展开字典作为关键字参数 # np.savez_compressed 会对数据进行压缩,节省存储空间 np.savez_compressed('path/to/file/filename_1.npz', **data_to_save) # 创建并保存第二个文件 arr_0_b = np.random.rand(15, 5) arr_1_b = np.random.randint(0, 2, size=(15,)) data_to_save_b = {'arr_0': arr_0_b, 'arr_1': arr_1_b} np.savez_compressed('path/to/file/filename_2.npz', **data_to_save_b)通过 **data_to_save 这种方式,字典的键会成为 .npz 文件内部数组的名称。
2. 合并 .npz 文件的核心策略
合并的核心思想是:遍历所有 .npz 文件,对于每个相同的键,收集其对应的所有数组,然后使用 np.concatenate 将这些数组沿着合适的轴拼接起来。
以下是实现这一策略的 Python 代码:
import numpy as np
import os
def merge_npz_files(file_list, output_filename='merged_data.npz'):
"""
合并多个 .npz 文件到一个新的 .npz 文件中。
参数:
file_list (list): 包含所有待合并 .npz 文件路径的列表。
output_filename (str): 合并后 .npz 文件的输出路径和名称。
"""
if not file_list:
print("文件列表为空,无法合并。")
return
# 1. 加载所有 .npz 文件
# np.load 返回一个 NpzFile 对象,可以像字典一样访问其内部数组
data_all = [np.load(fname) for fname in file_list]
merged_data = {}
# 2. 遍历第一个文件的所有键(假设所有文件有相同的键结构)
# 对于每个键,收集所有文件中的对应数组并进行拼接
for key in data_all[0].keys():
# 收集所有文件中对应当前键的数组
# 使用 d[key] 访问 NpzFile 对象中的数组
arrays_to_concatenate = [d[key] for d in data_all]
# 3. 使用 np.concatenate 拼接数组
# 默认沿轴0拼接,即在第一个维度上堆叠
try:
merged_data[key] = np.concatenate(arrays_to_concatenate, axis=0)
print(f"键 '{key}' 已成功合并。新形状: {merged_data[key].shape}")
except ValueError as e:
print(f"警告: 键 '{key}' 的数组无法合并。错误: {e}")
print(f"跳过键 '{key}' 的合并。")
# 如果无法合并,可以选择跳过或采取其他处理
# 例如,如果数组形状不兼容,可能需要进行 reshape 或 padding
# 4. 保存合并后的数据到新的 .npz 文件
if merged_data:
np.savez_compressed(output_filename, **merged_data)
print(f"所有数据已成功合并并保存到 '{output_filename}'。")
else:
print("没有可合并的数据。")
# --- 示例用法 ---
if __name__ == "__main__":
# 假设我们有以下文件:
# 创建一些示例 .npz 文件
if not os.path.exists("temp_npz_files"):
os.makedirs("temp_npz_files")
for i in range(3):
arr_features = np.random.rand(10 + i, 5) # 模拟不同数量的样本
arr_labels = np.random.randint(0, 2, size=(10 + i,))
data_dict = {'features': arr_features, 'labels': arr_labels}
np.savez_compressed(f'temp_npz_files/data_{i}.npz', **data_dict)
print(f"创建文件: temp_npz_files/data_{i}.npz (features shape: {arr_features.shape}, labels shape: {arr_labels.shape})")
# 获取所有 .npz 文件名
file_names = [os.path.join("temp_npz_files", f) for f in os.listdir("temp_npz_files") if f.endswith('.npz')]
file_names.sort() # 确保顺序一致
# 执行合并
merge_npz_files(file_names, 'merged_output.npz')
# 验证合并结果
print("\n验证合并结果:")
merged_file = np.load('merged_output.npz')
for key in merged_file.keys():
print(f"合并文件中键 '{key}' 的形状: {merged_file[key].shape}")
# 清理临时文件
import shutil
shutil.rmtree("temp_npz_files")
os.remove("merged_output.npz")
print("\n清理完成。")3. 注意事项与最佳实践
- 键名一致性是关键: 确保所有待合并的 .npz 文件中,对应相同逻辑数据的数组使用完全相同的键名。如果键名不一致,合并逻辑将无法正确识别和拼接这些数组。
- 数组维度兼容性: np.concatenate 要求所有待拼接的数组在除拼接轴之外的所有维度上都必须相同。例如,如果沿 axis=0 拼接,那么所有数组的 shape[1:] 必须一致。如果维度不兼容,np.concatenate 会抛出 ValueError。在实际应用中,您可能需要对不兼容的数组进行预处理(如填充、裁剪或重塑)。
-
内存管理: 对于非常大的数据集,一次性加载所有 .npz 文件到内存中可能会导致内存溢出(OOM)。
- 分批处理: 如果单个文件很大但总文件数量不多,可以考虑逐个文件加载并进行累积合并。
- 流式处理/惰性加载: 对于海量数据,可以考虑使用如 dask.array 这样的库,它支持惰性计算和分布式处理,可以处理超出内存的数据集。
- 压缩与性能: np.savez_compressed 会对数据进行压缩,生成的文件更小,但读写速度可能略慢于 np.savez。在存储空间和性能之间需要权衡。对于大规模数据,压缩通常是首选。
- 错误处理: 在实际应用中,增加对文件不存在、文件损坏或数组维度不兼容等情况的健壮性检查和错误处理是必要的。
总结
正确合并多个 NumPy .npz 文件的关键在于理解 .npz 文件的内部结构以及 np.concatenate 的工作原理。通过遵循一致的数据存储约定,并采用基于键的数组拼接策略,我们可以高效地将分散的数据聚合到一个统一的 .npz 文件中,为后续的数据分析和模型训练提供便利。同时,对于大规模数据集,务必考虑内存管理和性能优化。










