在数据科学和机器学习领域,NumPy 作为 Python 的核心科学计算库,其数组操作的高效性广为人知。然而,在实际项目中,我们不仅需要在内存中操作数组,还需要将数组持久化保存到磁盘,或者从磁盘加载预处理好的数据。本文将全面探讨 NumPy 数组的各种 I/O 操作方法,涵盖从简单的文本格式到高效的二进制格式,再到处理超大规模数据的内存映射技术。
一、文本文件 I/O 操作
1.1 保存数组到文本文件
NumPy 提供了 savetxt()
函数用于将数组保存为文本文件,这是最直观的数据交换格式:
import numpy as np
# 创建一个示例数组
data = np.array([[1.12, 2.34, 3.56],
[4.67, 5.78, 6.89],
[7.91, 8.02, 9.13]])
# 保存为文本文件
np.savetxt('data.txt', data, delimiter=',', fmt='%.2f', header='X,Y,Z', comments='# ')
参数解析:
-
delimiter
:指定列分隔符,默认为空格 -
fmt
:格式字符串,控制数值的显示格式 -
header
:文件开头的描述性文字 -
comments
:注释符号,用在 header 前面
文件内容示例:
# X,Y,Z
1.12,2.34,3.56
4.67,5.78,6.89
7.91,8.02,9.13
1.2 从文本文件加载数组
对应的加载函数是 loadtxt()
:
# 从文本文件加载数据
loaded_data = np.loadtxt('data.txt', delimiter=',')
# 可以跳过首行标题
loaded_data = np.loadtxt('data.txt', delimiter=',', skiprows=1)
高级用法:
-
指定数据类型:
dtype=np.float32
-
选择加载特定列:
usecols=(0, 2)
-
处理缺失值:
missing_values='NA', filling_values=0
1.3 文本格式的优缺点分析
优点:
-
人类可读,便于调试和检查
-
通用性强,几乎所有工具都能处理文本文件
-
版本兼容性好,不受 NumPy 版本影响
缺点:
-
存储效率低,文件体积大
-
读写速度慢,特别是大数据量时
-
精度可能丢失,取决于格式化字符串
适用场景:小型数据集交换、调试阶段、需要人工检查的数据
二、二进制文件 I/O 操作
2.1 .npy 格式(单个数组)
.npy 是 NumPy 专用的二进制格式,针对数组存储进行了优化。
保存单个数组:
np.save('single_array.npy', data)
加载单个数组:
array_data = np.load('single_array.npy')
文件特点:
-
自动保存数组的 dtype、shape 等元信息
-
支持所有 NumPy 数据类型
-
文件头包含格式说明,具有版本控制
2.2 .npz 格式(多个数组)
.npz 是压缩的存档格式,可以保存多个数组。
保存多个数组:
arr1 = np.arange(10)
arr2 = np.random.rand(5,5)
np.savez('multiple_arrays.npz', array1=arr1, array2=arr2)
加载多个数组:
with np.load('multiple_arrays.npz') as data:
arr1 = data['array1']
arr2 = data['array2']
压缩版本(节省空间):
np.savez_compressed('compressed_arrays.npz', array1=arr1, array2=arr2)
2.3 二进制格式的性能优势
我们通过实验对比文本和二进制格式的性能差异:
import time
large_array = np.random.rand(10000, 10000)
# 文本格式
start = time.time()
np.savetxt('large.txt', large_array)
print(f"文本保存时间: {time.time()-start:.2f}s")
start = time.time()
_ = np.loadtxt('large.txt')
print(f"文本加载时间: {time.time()-start:.2f}s")
# 二进制格式
start = time.time()
np.save('large.npy', large_array)
print(f"二进制保存时间: {time.time()-start:.2f}s")
start = time.time()
_ = np.load('large.npy')
print(f"二进制加载时间: {time.time()-start:.2f}s")
典型结果:
文本保存时间: 45.32s
文本加载时间: 38.76s
二进制保存时间: 1.02s
二进制加载时间: 0.25s
二进制格式在速度和存储空间上通常有数量级的优势。
三、内存映射文件技术
3.1 内存映射的基本原理
内存映射(memory mapping)允许将磁盘上的文件直接映射到内存地址空间,实现以下优势:
-
处理超过物理内存限制的大型数组
-
多个进程共享同一数据
-
惰性加载,只在访问时读取相应数据
3.2 创建内存映射数组
# 创建一个新的内存映射数组
shape = (100000, 100000) # 约74.5GB的float64数组
mmap_arr = np.memmap('huge_array.dat', dtype='float64', mode='w+', shape=shape)
# 分段写入数据
for i in range(0, shape[0], 10000):
mmap_arr[i:i+10000] = np.random.rand(10000, shape[1])
# 确保数据写入磁盘
del mmap_arr
3.3 读取现有内存映射
# 以只读模式打开现有文件
mmap_arr = np.memmap('huge_array.dat', dtype='float64', mode='r', shape=shape)
# 计算每列平均值
column_means = np.mean(mmap_arr, axis=0)
3.4 内存映射的高级用法
模式选择:
-
'r'
:只读 -
'r+'
:读写(文件必须已存在) -
'w+'
:创建或覆盖 -
'c'
:写时复制
跨进程共享:
# 进程1写入数据
mmap = np.memmap('shared.dat', dtype='float32', mode='w+', shape=(1000,))
mmap[:] = np.random.rand(1000)
# 进程2读取数据
mmap2 = np.memmap('shared.dat', dtype='float32', mode='r', shape=(1000,))
print(mmap2[0]) # 访问相同数据
四、其他专业格式接口
4.1 HDF5 格式
HDF5 是科学计算中常用的层次化数据格式,需要安装 h5py
包:
import h5py
with h5py.File('data.h5', 'w') as f:
# 创建数据集
f.create_dataset('training/data', data=train_data)
f.create_dataset('training/labels', data=train_labels)
# 添加属性
f['training/data'].attrs['description'] = 'Input features'
with h5py.File('data.h5', 'r') as f:
# 读取数据
data = f['training/data'][:]
# 读取属性
desc = f['training/data'].attrs['description']
4.2 Pandas 交互
NumPy 数组与 Pandas DataFrame 的相互转换:
import pandas as pd
# NumPy 到 DataFrame
df = pd.DataFrame(data, columns=['X', 'Y', 'Z'])
# DataFrame 到 NumPy
array = df.values # 或 df.to_numpy()
4.3 图像数据
使用 imageio 或 OpenCV 处理图像数据:
import imageio
# 读取图像为 NumPy 数组
img = imageio.imread('image.jpg') # 形状为 (height, width, channels)
# 保存数组为图像
imageio.imwrite('output.png', img_array)
五、性能优化与实践建议
5.1 格式选择决策树
-
数据量小且需要人工检查? → 文本格式
-
单个大型数组? → .npy 格式
-
多个相关数组? → .npz 格式
-
数据量超过内存? → 内存映射
-
复杂层次化数据? → HDF5 格式
5.2 最佳实践
-
版本控制:保存数据时记录 NumPy 版本
np.savez('data.npz', array=data, metadata={'numpy_version': np.__version__})
-
数据验证:加载后检查形状和 dtype
assert loaded_array.shape == expected_shape assert loaded_array.dtype == np.float32
-
安全考虑:
allow_pickle=False
避免潜在的安全风险np.load('data.npy', allow_pickle=False)
-
压缩策略:权衡压缩率和速度
# 快速但压缩率低 np.savez('fast.npz', data=data) # 慢但压缩率高 np.savez_compressed('small.npz', data=data)
六、总结
NumPy 提供了丰富多样的 I/O 操作方法,从简单的文本格式到高效的二进制格式,再到处理超大规模数据的内存映射技术。在实际项目中,我们应该根据数据规模、使用场景和性能需求选择合适的存储格式:
-
小型临时数据:文本格式便于调试
-
中型生产数据:.npy/.npz 提供最佳性能
-
超大型数据集:内存映射技术突破内存限制
-
复杂结构化数据:考虑 HDF5 等专业格式
掌握这些 I/O 技术将大大提高数据处理的效率和灵活性,是每个数据科学家和工程师必备的核心技能。