← 返回数据分析目录
← 返回学习笔记首页
NumPy文件IO与序列化
数据分析专题 · 高效保存与加载数组数据
专题: Python数据分析系统学习
关键词: 数据分析, NumPy, 文件IO, npz, 内存映射, memmap, HDF5, loadtxt, savetxt
一、NumPy文件IO概述
NumPy提供了丰富的文件输入输出接口,用于将数组数据持久化到磁盘或从磁盘加载。这些接口分为两大类:二进制格式 和文本格式 。二进制格式以npy/npz为扩展名,具有极高的读写速度和较小的磁盘占用,适合大规模数值数据的存储与交换。文本格式则支持CSV等通用格式,便于与其他数据分析工具(如Excel、R、Pandas)进行数据交换。
核心函数总览: np.save / np.savez / np.load(二进制)、np.savetxt / np.loadtxt(文本)、np.memmap(内存映射)、np.savez_compressed(压缩存储)。根据数据规模和后续使用场景选择合适的IO方式,可以显著提升工作效率。
函数名 格式 适用场景
np.save 二进制 (.npy) 保存单个数组
np.savez 二进制 (.npz) 保存多个数组(未压缩)
np.savez_compressed 二进制 (.npz) 保存多个数组(压缩)
np.load 二进制 加载 .npy / .npz 文件
np.savetxt 文本 保存为CSV/文本文件
np.loadtxt 文本 从CSV/文本文件加载
np.memmap 内存映射 处理超大数组
二、二进制格式:save/savez/load
2.1 保存单个数组 (npy格式)
np.save()将单个数组以NumPy专有的npy二进制格式保存到磁盘。npy格式包含数组的形状、数据类型和数据本身的完整信息,加载时无需额外指定参数。文件扩展名通常为.npy,如果未指定则自动添加。
import numpy as np
# 创建示例数组
arr = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.float32)
# 保存到npy文件
np.save('array_data.npy', arr)
# 等价于 np.save('array_data', arr) 自动添加.npy扩展名
# 加载npy文件
loaded = np.load('array_data.npy')
print(loaded)
# [[1. 2. 3.]
# [4. 5. 6.]
# [7. 8. 9.]]
# 验证数组一致性
print(np.allclose(arr, loaded)) # True
print(loaded.dtype) # float32
性能提示: npy二进制格式的读写速度远快于文本格式。对于1000x1000的float64数组,二进制读写比CSV快约10-50倍,磁盘占用仅为文本格式的1/3到1/5。在追求性能的场景中应优先选择二进制格式。
2.2 保存多个数组 (npz格式)
np.savez()可以将多个数组打包保存到一个npz文件中。每个数组通过关键字参数命名,加载后可以通过字典方式访问。npz本质上是多个npy文件的ZIP归档包,因此不提供压缩。
import numpy as np
# 创建多个数组
x = np.linspace(0, 10, 100)
y = np.sin(x)
z = np.cos(x)
metadata = np.array(['训练集', '验证集', '测试集'])
# 保存多个数组到npz文件
np.savez('waveform_data.npz',
x_coords=x,
y_values=y,
z_values=z,
labels=metadata)
# 加载npz文件(返回类字典对象)
data = np.load('waveform_data.npz')
# 查看所有键
print(list(data.keys())) # ['x_coords', 'y_values', 'z_values', 'labels']
# 通过键名访问数组
x_loaded = data['x_coords']
y_loaded = data['y_values']
print(x_loaded.shape) # (100,)
print(y_loaded[:5]) # [0. 0.101...]
2.3 压缩存储 (savez_compressed)
当数据中存在大量重复值或冗余信息时,可以使用np.savez_compressed()进行压缩存储。该函数在归档前对数据进行gzip压缩,能显著减小文件体积,代价是写入和读取速度略有下降。
import numpy as np
# 创建包含大量重复值的稀疏风格数组
sparse_like = np.zeros((1000, 1000))
sparse_like[::10, ::10] = 1 # 仅有1%的非零元素
# 未压缩保存
np.savez('sparse_uncompressed.npz', data=sparse_like)
# 压缩保存
np.savez_compressed('sparse_compressed.npz', data=sparse_like)
# 对比文件大小
import os
uncomp_size = os.path.getsize('sparse_uncompressed.npz')
comp_size = os.path.getsize('sparse_compressed.npz')
print(f'未压缩: {uncomp_size / 1024:.1f} KB')
print(f'压缩后: {comp_size / 1024:.1f} KB')
print(f'压缩比: {comp_size / uncomp_size * 100:.1f}%')
# 未压缩: ~3906 KB
# 压缩后: ~21 KB
# 压缩比: ~0.5%
选择建议: 对于非稀疏数据且需要频繁读写的场景,使用savez(未压缩)以换取最佳读写性能;对于磁盘空间敏感或需要通过网络传输数据的场景,优先使用savez_compressed。对于单个数组,save(npy格式)是最轻量高效的选择。
三、文本格式:savetxt/loadtxt
3.1 CSV读写基础
np.savetxt()和np.loadtxt()是NumPy提供的文本格式读写函数,支持CSV、TSV等常见文本格式。文本格式的优点是通用性强,可以被任何文本编辑器打开浏览,也方便与其他数据分析工具交换数据。
import numpy as np
# 创建数据集
data = np.array([
[1.0, 2.5, 3.2],
[4.1, 5.3, 6.8],
[7.2, 8.9, 9.0],
[10.5, 11.2, 12.7]
])
# 保存为CSV格式
np.savetxt('data.csv', data,
delimiter=',',
header='特征A,特征B,特征C',
comments='# ',
fmt='%.4f')
# 查看保存的文件内容
with open('data.csv', 'r') as f:
print(f.read())
# # 特征A,特征B,特征C
# 1.0000,2.5000,3.2000
# 4.1000,5.3000,6.8000
# 7.2000,8.9000,9.0000
# 10.5000,11.2000,12.7000
import numpy as np
# 从CSV加载数据
loaded = np.loadtxt('data.csv',
delimiter=',',
skiprows=1) # 跳过表头行
print(loaded)
print(loaded.shape) # (4, 3)
3.2 常用参数详解
loadtxt和savetxt提供了丰富的参数来控制读写行为。理解这些参数对于处理各类文本格式数据至关重要。
import numpy as np
# ========== loadtxt 参数演示 ==========
# 1. skiprows - 跳过文件开头的行
# 2. max_rows - 只读取前N行(适合大数据预览)
# 3. usecols - 选择读取哪些列
# 4. dtype - 指定数据类型
# 5. unpack - 是否转置(True时将各列作为独立变量返回)
# 示例文件 sample_data.csv 内容:
# # 数据集: 测试样本
# # 创建时间: 2026-01-01
# id,身高,体重,年龄,性别
# 1,170.5,65.2,28,男
# 2,165.0,55.8,25,女
# 3,180.2,78.0,35,男
# 4,158.0,48.5,30,女
# 跳过2个注释行和1个表头行,只读取身高和体重两列
# 将第1列(身高)和第2列(体重)分别赋值给两个变量
height, weight = np.loadtxt('sample_data.csv',
delimiter=',',
skiprows=3,
usecols=(1, 2),
unpack=True)
print(f'身高: {height}') # [170.5 165. 180.2 158. ]
print(f'体重: {weight}') # [65.2 55.8 78. 48.5]
# 混合数据类型 - 使用结构化dtype
structured = np.loadtxt('sample_data.csv',
delimiter=',',
skiprows=3,
dtype={
'names': ('id', 'height', 'weight', 'age', 'gender'),
'formats': ('i4', 'f8', 'f8', 'i4', 'U4')
})
print(structured['height']) # [170.5 165. 180.2 158. ]
print(structured['gender']) # ['男' '女' '男' '女']
3.3 处理缺失值与不规则数据
现实中的数据文件常包含缺失值或不规则格式。虽然loadtxt本身对缺失值处理能力有限,但可以通过组合genfromtxt函数和参数配置来处理。
import numpy as np
# genfromtxt - loadtxt的增强版,支持缺失值处理
# 示例文件 messy.csv:
# 日期,温度,湿度,风速
# 2026-01-01,23.5,,3.2
# 2026-01-02,22.1,65%,N/A
# 2026-01-03,,70%,2.8
# 2026-01-04,25.0,68%,4.1
# 使用genfromtxt并指定缺失值标记
data = np.genfromtxt('messy.csv',
delimiter=',',
skip_header=1,
missing_values=['', 'N/A'],
filling_values=np.nan,
usecols=(1, 2, 3))
print(data)
# [[23.5 nan 3.2]
# [22.1 65. nan]
# [ nan 70. 2.8]
# [25. 68. 4.1]]
# 使用np.isnan检测缺失值
print(np.isnan(data).sum(axis=0))
# [1, 0, 1] 第1列和第3列各有1个缺失值
注意事项: 对于包含大量缺失值或复杂异质数据类型的文件,推荐使用Pandas的read_csv() 进行处理,其缺失值处理机制更为强大和灵活。NumPy的loadtxt和genfromtxt更适合类型统一、格式规整的数值数据。
3.4 性能对比与选择
二进制格式优势
读写速度快(10-50倍)
磁盘占用小
精度无损
自动保存dtype信息
支持压缩存储
文本格式优势
人类可读,可用编辑器打开
跨语言/跨工具兼容
支持版本控制(Git可diff)
标准格式(CSV)
调试友好
四、结构化数组的保存与加载
4.1 结构化数组基础
NumPy的结构化数组(structured array)允许在一个数组中存储不同类型的字段,类似于数据库表或Pandas DataFrame的列式存储。这种结构特别适合保存表格型数据。
import numpy as np
# 定义结构化dtype
employee_dtype = np.dtype([
('name', 'U20'), # Unicode字符串,最大长度20
('age', 'i4'), # 32位整数
('salary', 'f8'), # 64位浮点数
('department', 'U15'), # 部门名称
('hire_year', 'i4') # 入职年份
])
# 创建结构化数组
employees = np.array([
('张三', 32, 15000.0, '技术部', 2019),
('李四', 28, 12000.0, '市场部', 2020),
('王五', 45, 22000.0, '技术部', 2015),
('赵六', 35, 18000.0, '财务部', 2017)
], dtype=employee_dtype)
# 保存结构化数组
np.savez('employees.npz', employees=employees)
# 加载
loaded_emp = np.load('employees.npz')['employees']
print(loaded_emp['name'])
# ['张三' '李四' '王五' '赵六']
print(loaded_emp['salary'].mean())
# 16750.0
4.2 按字段筛选与查询
结构化数组支持类似SQL的字段筛选和条件查询,可以高效地进行数据过滤和分析。
import numpy as np
# 从刚才保存的文件加载
data = np.load('employees.npz')['employees']
# 筛选技术部员工
tech_dept = data[data['department'] == '技术部']
print(tech_dept['name']) # ['张三' '王五']
print(tech_dept['salary']) # [15000. 22000.]
# 筛选薪资高于中位数的员工
median_salary = np.median(data['salary'])
high_earners = data[data['salary'] > median_salary]
print(f'高薪员工: {high_earners["name"]}')
# 多条件筛选
condition = (data['department'] == '技术部') & (data['salary'] > 15000)
senior_tech = data[condition]
print(f'技术部高薪: {senior_tech["name"]}')
# 按年龄排序
sorted_by_age = np.sort(data, order='age')
print('按年龄排序:')
for emp in sorted_by_age:
print(f' {emp["name"]}: {emp["age"]}岁')
与Pandas DataFrame对比: 结构化数组在内存使用上比Pandas DataFrame更高效(约节省30-50%内存),适合大规模数值计算场景。但Pandas在数据清洗、缺失值处理、分组聚合等操作上更为便捷。实际工作中常将NumPy结构化数组作为Pandas DataFrame的后端存储引擎。
五、内存映射:memmap处理超大数组
5.1 memmap原理
当数组大小超过可用物理内存时,传统的np.load()会因内存不足而失败。np.memmap通过操作系统的内存映射机制,将磁盘文件直接映射到虚拟地址空间,允许以按需加载 的方式访问超大数组的部分数据,而无需将整个数组读入内存。
import numpy as np
import tempfile
import os
# 模拟创建超大数组(3GB)
# 实际使用中,只需指定shape和dtype即可创建一个memmap对象
shape = (20000, 20000) # 约3.2GB (float64)
filename = 'large_array.dat'
# 创建内存映射文件(默认模式 'r+' 可读写)
mmap_arr = np.memmap(filename,
dtype='float64',
mode='w+', # w+ 创建新文件,可读写
shape=shape)
# 写入部分数据(不会一次性加载整个数组到内存)
mmap_arr[0:100, 0:100] = 1.0 # 左上角区域
mmap_arr[10000:10100, 10000:10100] = 2.0 # 中心区域
# 关闭memmap(写入数据到磁盘)
del mmap_arr
# 重新打开进行读取(只读模式)
mmap_read = np.memmap(filename,
dtype='float64',
mode='r',
shape=shape)
# 读取特定区域(仅加载需要的部分到内存)
patch = mmap_read[0:10, 0:10]
print(patch)
# [[1. 1. 1. ... 1. 1. 1.]
# [1. 1. 1. ... 1. 1. 1.]
# ...]
# 读取中心区域
center_patch = mmap_read[10000:10010, 10000:10010]
print(f'中心区域均值: {center_patch.mean():.2f}') # 2.00
# 关闭
del mmap_read
# 清理
if os.path.exists(filename):
os.remove(filename)
5.2 分块处理策略
memmap的真正威力体现在与分块计算相结合的场景中。通过按块遍历大数组,可以在有限内存下完成全数组的统计分析。
import numpy as np
# 创建超大数组的memmap
shape = (50000, 50000) # 约18.6GB (float64)
data = np.memmap('huge_dataset.dat',
dtype='float64',
mode='w+',
shape=shape)
# 初始化部分数据(模拟真实数据分布)
np.random.seed(42)
chunk_size = 5000
for i in range(0, shape[0], chunk_size):
end_i = min(i + chunk_size, shape[0])
data[i:end_i, :] = np.random.randn(end_i - i, shape[1])
# === 分块计算统计量 ===
total_sum = 0.0
total_count = 0
min_val = float('inf')
max_val = float('-inf')
for i in range(0, shape[0], chunk_size):
end_i = min(i + chunk_size, shape[0])
chunk = data[i:end_i, :] # 仅加载当前块到内存
total_sum += chunk.sum()
total_count += chunk.size
min_val = min(min_val, chunk.min())
max_val = max(max_val, chunk.max())
print(f'处理进度: {end_i / shape[0] * 100:.1f}%',
end='\r')
mean_val = total_sum / total_count
print(f'\n总数据量: {total_count}')
print(f'均值: {mean_val:.4f}')
print(f'最小值: {min_val:.4f}')
print(f'最大值: {max_val:.4f}')
del data
import os
if os.path.exists('huge_dataset.dat'):
os.remove('huge_dataset.dat')
最佳实践: 使用memmap时需要注意以下几点:(1) 块大小(chunk_size)应根据可用内存调整,一般为内存的10-20%;(2) memmap在访问不规则模式时性能较差,应尽量使用连续的行切片进行访问;(3) 操作完成后务必将memmap对象删除(del)以确保数据刷新到磁盘;(4) 对于频繁随机访问的场景,memmap可能不如将数据分段加载到内存中高效。
六、h5py与HDF5格式
6.1 HDF5格式概述
HDF5(Hierarchical Data Format version 5)是一种专为大规模科学数据设计的文件格式。通过h5py库,NumPy数组可以高效地存储和读取HDF5文件。HDF5支持内部目录结构(类似文件系统)、数据压缩、分块存储、以及多种数据类型,是深度学习模型权重和大型数据集的常用存储格式。
import numpy as np
import h5py
# 创建HDF5文件
with h5py.File('dataset.h5', 'w') as f:
# 存储多个数据集
f.create_dataset('train/images', data=np.random.randn(1000, 64, 64))
f.create_dataset('train/labels', data=np.random.randint(0, 10, 1000))
f.create_dataset('test/images', data=np.random.randn(200, 64, 64))
f.create_dataset('metadata/description',
data='示例数据集:64x64灰度图像')
# 存储属性(元数据)
f.attrs['version'] = '1.0'
f.attrs['author'] = '数据分析学习笔记'
# 为数据集添加属性
f['train/images'].attrs['shape_desc'] = '(样本数, 高度, 宽度)'
f['train/images'].attrs['dtype'] = 'float32'
# 读取HDF5文件
with h5py.File('dataset.h5', 'r') as f:
# 查看文件结构
def print_structure(name, obj):
print(f'{name}: {obj.shape if hasattr(obj, "shape") else "Group"}')
f.visititems(print_structure)
# 读取数据(支持切片,不加载全部)
train_images = f['train/images']
print(f'训练图像形状: {train_images.shape}') # (1000, 64, 64)
print(f'数据类型: {train_images.dtype}')
# 读取前10张图像(懒加载)
batch = train_images[:10]
print(f'批次形状: {batch.shape}') # (10, 64, 64)
# 读取属性
print(f'版本: {f.attrs["version"]}')
import os
if os.path.exists('dataset.h5'):
os.remove('dataset.h5')
6.2 分块存储与压缩
HDF5支持在创建数据集时指定分块大小和压缩算法,这对于优化大规模数据的访问性能至关重要。
import numpy as np
import h5py
# 使用分块存储和gzip压缩
shape = (10000, 1000)
chunks = (1000, 1000) # 每块1000行
data = np.random.randn(*shape).astype(np.float32)
with h5py.File('compressed_data.h5', 'w') as f:
# chunks参数指定存储块大小
# compression参数指定压缩算法
# shuffle启用重排序以提高压缩率
f.create_dataset('large_matrix',
data=data,
chunks=chunks,
compression='gzip',
compression_opts=6, # 1-9,压缩级别
shuffle=True,
scaleoffset=0)
# 对比:与无压缩版本
f.create_dataset('uncompressed_matrix',
data=data)
# 查看文件大小
import os
file_size = os.path.getsize('compressed_data.h5')
print(f'HDF5文件大小: {file_size / 1024 / 1024:.2f} MB')
# float32: 10000*1000*4 = ~38.1 MB 原始
# 压缩后通常可减少30-70%
HDF5 vs NumPy原生格式: NumPy原生格式(npy/npz)适合单个数组或少量数组的存储,配置简单易用;HDF5适合大规模、多层级数据集的管理,支持复杂的内部组织结构、丰富的元数据、灵活的压缩选项和高效的子集访问。对于需要长期维护和频繁部分访问的大型数据集,HDF5是更优的选择。
七、NumPy与Pandas数据交换
7.1 NumPy数组与DataFrame互转
NumPy和Pandas是Python数据分析生态中最重要的两个库,它们之间的无缝数据交换是高效工作流的基础。Pandas的DataFrame底层使用NumPy数组作为存储后端,因此转换效率极高。
import numpy as np
import pandas as pd
# NumPy数组 → Pandas DataFrame
arr = np.array([
[1, '张三', 85.5],
[2, '李四', 92.0],
[3, '王五', 78.5],
[4, '赵六', 88.0]
])
df = pd.DataFrame(arr, columns=['学号', '姓名', '成绩'])
print(df)
# 学号 姓名 成绩
# 0 1 张三 85.5
# 1 2 李四 92.0
# 2 3 王五 78.5
# 3 4 赵六 88.0
# DataFrame → NumPy数组
arr_back = df.to_numpy()
print(arr_back.dtype) # object(混合类型)
print(arr_back)
# [['1' '张三' '85.5']
# ['2' '李四' '92.0']
# ['3' '王五' '78.5']
# ['4' '赵六' '88.0']]
# 提取纯数值列
scores = df['成绩'].to_numpy(dtype=np.float64)
print(scores) # [85.5 92. 78.5 88. ]
print(scores.mean()) # 86.0
7.2 使用Pandas IO读写NumPy数据
Pandas提供了比NumPy更丰富的文件读写接口,可以间接实现NumPy数据的高效读写。通过Pandas作为中介,可以处理更复杂的数据格式和缺失值情况。
import numpy as np
import pandas as pd
# 创建数值数据
data = np.random.randn(1000, 5)
# 方式1:Pandas保存为Parquet格式(高效列式存储)
df = pd.DataFrame(data, columns=['A', 'B', 'C', 'D', 'E'])
df.to_parquet('data.parquet')
# 读取回到NumPy
df_loaded = pd.read_parquet('data.parquet')
arr_loaded = df_loaded.to_numpy()
print(f'Parquet读写一致: {np.allclose(data, arr_loaded)}')
print(f'形状: {arr_loaded.shape}') # (1000, 5)
# 方式2:Pandas保存为Feather格式(极速读写)
df.to_feather('data.feather')
df_feather = pd.read_feather('data.feather')
arr_feather = df_feather.to_numpy()
print(f'Feather读写一致: {np.allclose(data, arr_feather)}')
# 方式3:Pandas保存为Pickle
df.to_pickle('data.pkl')
df_pkl = pd.read_pickle('data.pkl')
arr_pkl = df_pkl.to_numpy()
print(f'Pickle读写一致: {np.allclose(data, arr_pkl)}')
# 文件大小对比
import os
for fmt in ['parquet', 'feather', 'pkl', 'csv']:
fname = f'data.{fmt}'
size = os.path.getsize(fname) / 1024
print(f'{fmt}: {size:.1f} KB')
import os
for f in ['data.parquet', 'data.feather', 'data.pkl', 'data.csv']:
if os.path.exists(f):
os.remove(f)
推荐方案: 对于大规模数值数据的跨语言交换或长期归档,推荐使用Parquet 格式(按列压缩,支持谓词下推,兼容Spark/Hive等大数据工具);对于Python生态内的高速重复读写,推荐使用Feather 或Pickle ;对于与其他非Python工具的交互,使用CSV/HDF5 格式;Python内部快速数据交换,npy/npz 格式最轻量。
八、大数据分块加载策略
8.1 分块处理的核心思想
当单个数据集无法完整加载到内存时,分块加载是必选的解决方案。核心思想是:将大文件划分为多个小块,逐个加载处理,汇总结果。NumPy本身不提供内置的分块迭代器,但可以借助Pandas或迭代读取的方式实现。
import numpy as np
import pandas as pd
# ====== 方案1: 使用Pandas分块读取 ======
chunk_size = 10000
total_sum = 0.0
total_count = 0
# 模拟一个大型CSV文件
# 实际使用时替换为真实文件路径
for chunk in pd.read_csv('large_dataset.csv',
chunksize=chunk_size,
usecols=['feature_1', 'feature_2']):
# chunksize指定每块的行数
# usecols只加载需要的列,减少内存占用
# 转换为NumPy数组进行计算
chunk_arr = chunk.to_numpy(dtype=np.float64)
total_sum += chunk_arr.sum()
total_count += chunk_arr.size
print(f'总计: {total_sum:.2f}')
print(f'均值: {total_sum / total_count:.4f}')
import numpy as np
# ====== 方案2: 手动迭代二进制文件 ======
def process_large_npy(filename, chunk_rows=1000):
"""
对大型npy文件进行分块处理。
先通过mmap获取元信息,再逐块读取。
"""
# 使用memmap获取形状信息
mmap = np.memmap(filename, dtype='float64', mode='r')
# 注意:这里假设数组是2D的
# 对于store True,需要知道原始形状
mmap = np.memmap(filename, dtype='float64', mode='r',
shape=(mmap.size // 100, 100))
n_rows = mmap.shape[0]
n_cols = mmap.shape[1]
results = []
for start in range(0, n_rows, chunk_rows):
end = min(start + chunk_rows, n_rows)
chunk = mmap[start:end, :]
# 对该块执行各类计算
chunk_mean = chunk.mean()
chunk_std = chunk.std()
chunk_min = chunk.min()
chunk_max = chunk.max()
results.append({
'start': start,
'end': end,
'mean': chunk_mean,
'std': chunk_std,
'min': chunk_min,
'max': chunk_max
})
print(f'处理行 [{start}:{end}],均值={chunk_mean:.4f}')
del mmap
return results
8.2 大规模数据工作流设计
在实际的生产环境中,数据量可能达到数十GB甚至TB级别。此时需要设计一个完整的处理流水线,将数据加载、转换、计算、存储等环节衔接起来。
import numpy as np
import os
from glob import glob
class LargeDataProcessor:
"""大规模数据分块处理器"""
def __init__(self, data_dir, chunk_size_mb=100):
self.data_dir = data_dir
self.chunk_size = chunk_size_mb * 1024 * 1024 # 转换为字节
self.files = sorted(glob(os.path.join(data_dir, '*.npy')))
def scan_files(self):
"""扫描并汇总所有文件信息"""
file_info = []
for fpath in self.files:
fsize = os.path.getsize(fpath)
file_info.append({
'path': fpath,
'size_mb': fsize / 1024 / 1024
})
print(f'{os.path.basename(fpath)}: '
f'{fsize / 1024 / 1024:.2f} MB')
return file_info
def stream_arrays(self):
"""逐个加载文件并返回数组生成器"""
for fpath in self.files:
yield np.load(fpath)
def compute_summary(self):
"""计算所有文件的全局统计摘要"""
total_count = 0
global_sum = 0.0
global_min = float('inf')
global_max = float('-inf')
for array in self.stream_arrays():
global_sum += array.sum()
total_count += array.size
global_min = min(global_min, array.min())
global_max = max(global_max, array.max())
return {
'n_files': len(self.files),
'n_elements': total_count,
'mean': global_sum / total_count,
'min': global_min,
'max': global_max,
'total_size_mb': sum(
os.path.getsize(f) for f in self.files
) / 1024 / 1024
}
# 使用示例
# processor = LargeDataProcessor('/path/to/data')
# summary = processor.compute_summary()
# print(summary)
8.3 内存管理最佳实践
内存管理策略总结:
及时释放: 使用del关键字手动删除不再需要的数组对象,或利用with语句自动管理资源
数据类型优化: 尽可能使用低精度类型(如float32替代float64),在不影响精度的情况下可节省50%内存
视图优先: 使用切片操作产生的视图(view)而非副本(copy),避免不必要的数据复制
生成器模式: 对于需要逐个处理的大量文件,使用生成器(yield)而非一次性全部加载到列表
内存监控: 使用psutil或memory_profiler库监控内存使用情况
import numpy as np
import gc # 垃圾回收模块
# 内存管理演示
def memory_efficient_pipeline():
"""展示内存高效处理流程"""
result_buffers = []
for i in range(5):
# 模拟处理第i个数据块
chunk = np.random.randn(1000, 1000).astype(np.float32)
processed = chunk * 2.0 + 1.0
# 只保留聚合结果(标量),不保留整个数组
result_buffers.append({
'mean': processed.mean(),
'std': processed.std(),
'min': processed.min(),
'max': processed.max(),
})
# 显式释放大数组
del chunk, processed
gc.collect() # 触发垃圾回收
# 汇总结果
means = [r['mean'] for r in result_buffers]
return np.mean(means)
# 查看内存使用
import tracemalloc
tracemalloc.start()
result = memory_efficient_pipeline()
current, peak = tracemalloc.get_traced_memory()
print(f'结果: {result:.4f}')
print(f'当前内存: {current / 1024:.2f} KB')
print(f'峰值内存: {peak / 1024:.2f} KB')
tracemalloc.stop()
# 良好的内存管理可将峰值内存控制在合理范围内
九、核心要点总结
NumPy文件IO与序列化知识体系总结:
二进制格式: np.save/np.savez/np.load是NumPy最核心的IO接口,npy/npz格式读写速度极快,应作为首选持久化方案
文本格式: np.savetxt/np.loadtxt支持CSV等通用格式,适合跨工具数据交换,但性能远低于二进制格式
参数控制: loadtxt的skiprows/usecols/delimiter参数可灵活解析格式化文本文件,genfromtxt支持缺失值处理
结构化数组: 支持混合数据类型的类表格存储,是连接NumPy数值计算与Pandas数据分析的桥梁
内存映射: np.memmap通过操作系统的虚拟内存机制,允许处理远大于物理内存的数据集
HDF5格式: h5py库提供了层级化的数据存储能力,支持压缩、分块、元数据,是大规模科学数据的标准格式
Pandas互操作: NumPy数组与Pandas DataFrame之间可高效互转,借助Pandas可访问Parquet/Feather等现代列式存储格式
分块加载: 对于超大文件,使用分块(chunk)策略结合生成器模式,实现有限内存下的大数据处理
选择策略: 根据数据规模、访问模式、兼容性需求和性能要求,选择合适的IO方案
十、进一步思考与实践
10.1 场景驱动的IO方案选择
在实际的数据分析项目中,没有放之四海皆准的IO方案。需要根据具体场景的特点进行选择:
原型开发阶段: 使用npy格式快速保存中间结果,利用其极快的读写速度缩短迭代周期
团队协作项目: 使用HDF5或Parquet格式,利用其自描述特性减少沟通成本
生产系统数据管道: 使用Feather格式实现高速数据交换,必要时结合压缩降低存储成本
长期归档: 使用HDF5(带压缩)或Parquet格式,同时保存元数据和数据字典
Web API服务: 内存映射(memmap)+ 分块读取确保低延迟响应和大并发支持
文件格式速查表: 小数据(MB级)→ npy;中等数据(GB级)→ npz/HDF5;超大文件(TB级)→ memmap/HDF5分块;跨语言交换 → HDF5/Parquet;人类可读 → CSV;Python内快速交换 → Feather/Pickle。根据数据特征和使用场景灵活选择,才能构建高效的数据分析管道。
10.2 扩展学习资源
NumPy官方文档: IO章节详细介绍了所有读写函数的参数和用法
h5py官方文档: 提供了HDF5格式的完整API参考和最佳实践
PyTables库: 基于HDF5的高级封装,提供类似数据库的查询能力
Apache Arrow/Parquet: 现代列式存储标准,支持多种编程语言的高效数据交换
Dask库: 支持大于内存的数据集并行计算,底层兼容NumPy和Pandas接口