深入理解Python中的常用数据格式(如csv、json、pickle、npz、h5等):存储机制与性能解析
在数据科学与工程领域,数据的存储与读取是日常工作中不可或缺的一部分。选择合适的数据格式不仅影响数据处理的效率,还关系到存储空间的利用与后续分析的便捷性。本文将以通俗易懂的方式,深入探讨Python中几种常用的数据读写格式(如.csv
、.json
、.pickle
、.npz
、.h5
等)的存储方式,并分析它们在速度与性能上的优劣。
1. 常用数据格式概览
在Python中,常见的数据读写格式包括但不限于:
接下来,我们将逐一详细解析这些格式的存储机制及其性能特点。
2. 详解存储机制与性能分析
CSV(Comma-Separated Values)
存储机制:
CSV是一种纯文本格式,每一行代表一条记录,字段之间使用逗号(或其他分隔符)分隔。例如:
csv
复制代码
id,value,category
1,0.123,A
2,0.456,B
...
性能分析:
为什么速度较慢:
CSV作为纯文本格式,数据需要逐行解析,且每个字段都需要从字符串转换为相应的数据类型。这种逐行、逐字段的处理方式在数据量大时会显著降低读写速度。
JSON(JavaScript Object Notation)
存储机制:
JSON也是一种纯文本格式,使用键值对来表示数据,支持嵌套的对象和数组。例如:
json
复制代码
[
{"id": 1, "value": 0.123, "category": "A"},
{"id": 2, "value": 0.456, "category": "B"},
...
]
性能分析:
为什么速度较慢:
类似于CSV,JSON也是纯文本格式,数据解析需要逐字符解析和解析嵌套结构,导致在处理大规模数据时效率较低。此外,复杂的嵌套结构增加了数据解析的复杂性,进一步影响了读写速度。
Pickle
存储机制:
Pickle是Python特有的序列化协议,用于将Python对象转化为字节流,并可将其恢复为原始对象。支持几乎所有Python数据类型,包括自定义对象。
性能分析:
为什么速度适中:
Pickle使用二进制格式进行序列化,相较于纯文本格式,读写速度和存储效率更高。然而,由于其复杂的序列化机制和对Python对象的高度兼容,性能在某些情况下可能不如专门优化的二进制格式(如HDF5或Parquet)。
NPZ(NumPy Zip)
存储机制:
NPZ是NumPy提供的一种压缩文件格式,用于存储多个NumPy数组。它实际上是一个ZIP归档,包含多个.npy
文件,每个.npy
文件存储一个数组。
性能分析:
为什么速度较快:
NPZ基于NumPy的.npy
格式,直接存储内存中的数组数据,避免了中间的解析步骤。此外,支持压缩可以有效减少文件大小,进一步提升读写效率。然而,专注于数值数组的设计也限制了其应用范围。
HDF5(Hierarchical Data Format version 5)
存储机制:
HDF5是一种用于存储和管理大规模数据的二进制文件格式,支持分层结构。它允许在一个文件中存储多种类型的数据集和元数据,类似于文件系统中的目录和文件结构。
性能分析:
h5py
和PyTables
。h5py
或PyTables
)才能使用。为什么速度高效:
HDF5采用分块存储和压缩技术,可以有效地管理和访问大规模数据。其底层使用二进制格式,减少了数据解析的开销。此外,支持并行I/O使其在多线程或分布式环境中具备显著的性能优势。
Parquet
存储机制:
Parquet是一种开源的列式存储格式,基于Apache Arrow项目开发,专为大规模数据处理和分析优化。它将数据按列而非按行存储,有利于高效的压缩和快速的列式查询。
性能分析:
pyarrow
或fastparquet
等第三方库。为什么速度高效:
Parquet的列式存储方式使其在进行按列操作时无需读取整个数据集,显著提升了查询效率。高效的压缩算法减少了磁盘I/O的开销,加快了读写速度。此外,基于Apache Arrow的内存映射技术,使其在内存中处理数据时具备高效性。
Feather
存储机制:
Feather是基于Apache Arrow的高性能列式存储格式,旨在提供跨语言(主要是Python与R)之间的高效数据传输。它采用二进制格式,优化了读写速度和内存布局。
性能分析:
为什么速度极快:
Feather利用Apache Arrow的内存映射和零拷贝技术,使得数据在内存与磁盘之间的传输几乎不需要额外的处理步骤。这大幅减少了读写时间,特别适用于需要频繁在不同编程环境之间传输数据的场景。
SQLite
存储机制:
SQLite是一个轻量级的嵌入式关系型数据库管理系统,使用单一的文件来存储整个数据库。它支持标准的SQL查询语句,具备事务支持和数据完整性保障。
性能分析:
为什么速度适中:
SQLite通过单一文件管理数据,适合轻量级应用。然而,其设计目标并非高性能的并发读写,而是提供一个简单可靠的嵌入式数据库解决方案。在处理大规模数据时,由于缺乏分布式架构和高级索引优化,性能可能受限。
3. 为什么不同格式速度快慢不一
不同数据格式在读写速度和性能上的差异,主要源于以下几个方面:
- 存储方式:
- 文本 vs 二进制:文本格式(如CSV、JSON)需要逐字符解析,速度较慢;二进制格式(如Pickle、HDF5、Parquet、Feather)直接存储内存数据,读写速度更快。
- 行式 vs 列式:行式存储(如CSV、JSON)适合逐行读取,但在进行列操作时效率低下;列式存储(如Parquet、Feather)适合按列读取和操作,提升了分析效率。
- 压缩与编码:
- 有无压缩:支持压缩的格式(如HDF5、Parquet)可以显著减少文件大小,但可能增加读写时的压缩与解压开销。
- 压缩算法:不同格式支持不同的压缩算法,影响存储效率与读写速度的平衡。
- 数据类型支持:
- 类型丰富度:支持丰富数据类型和结构的格式(如HDF5、Parquet)能够更高效地存储复杂数据,但可能导致解析过程更复杂。
- 类型限制:如NPZ仅支持NumPy数组,优化了特定数据类型的存储与访问效率。
- 并行与并发支持:
- 并行读写:支持并行I/O操作的格式(如HDF5)在多线程或分布式环境中具备显著的性能优势。
- 单线程限制:不支持并行操作的格式在高负载场景下可能成为瓶颈。
- 内存布局与访问模式:
- 内存映射:利用内存映射技术的格式(如Feather)减少了数据在内存与磁盘之间的拷贝,提升了读写速度。
- 缓存优化:优化缓存访问模式的格式能够更高效地利用CPU缓存,提升性能。
- 元数据与索引:
- 丰富的元数据:支持存储和利用元数据(如HDF5、Parquet)的格式,可以优化数据访问路径,提升查询性能。
- 缺乏索引:不支持索引的格式在进行复杂查询时需要全表扫描,影响性能。
4. 选择合适的数据格式的建议
根据上述分析,不同数据格式适用于不同的场景。以下是一些选择建议:
(1) 小规模数据或需要人类可读的场景:
(2) 需要高效读写和数值计算:
(3) 大规模数据分析与按列查询:
(4) 需要数据库功能与复杂查询:
(5) 需要跨语言支持和高性能传输:
(6) 需要序列化复杂Python对象:
5. 亿级数据量下常用数据格式的读写性能排名
在处理亿级(108)数据量时,选择合适的数据存储格式对于提升数据处理效率、节省存储空间以及优化内存使用至关重要。不同的数据格式在读写速度、文件大小和内存占用等方面表现各异。本文将基于综合读写性能,对常用的数据格式进行从优到差的排序,并解释其背后的原因。
5.1 综合读写性能排名
以下是常用数据格式在处理亿级数据量时,综合读写性能从优到差的排序:
- Feather
- Parquet
- HDF5 (.h5)
- NPZ (.npz)
- Pickle (.pkl)
- SQLite (.sqlite)
- JSON (.json)
- CSV (.csv)
5.2 排名详解
No.1 Feather
综合性能:最优
适用场景:需要在不同编程环境(如Python与R)之间快速传输大量数据,或者需要极高读写速度的分析任务。
No.2 Parquet
综合性能:极佳
适用场景:大规模数据分析、数据仓库、需要高效列式查询的环境(如Apache Spark)。
No.3 HDF5 (.h5)
综合性能:优良
适用场景:科学计算、工程应用、大规模多维数组存储、需要存储复杂数据结构的场景。
No.4 NPZ (.npz)
综合性能:良好
适用场景:存储和共享多个NumPy数组,科学计算和机器学习任务中常用。
No.5 Pickle (.pkl)
综合性能:中等
适用场景:序列化和反序列化复杂的Python对象,内部数据持久化(需注意安全性)。
No.6 SQLite (.sqlite)
综合性能:较好
适用场景:嵌入式应用、小型项目、需要使用SQL进行复杂查询的场景。
No.7 JSON (.json)
综合性能:较差
适用场景:需要存储嵌套数据结构、配置文件或在不同系统间交换数据(但数据量不大时更为合适)。
No.8 CSV (.csv)
综合性能:最差
适用场景:简单、轻量级的数据交换与存储,数据量较小或对可读性要求较高的场景。
5.3 排名总结
综合考虑亿级数据量下的读写速度、文件大小和内存占用,常用数据格式的性能从优到差排序如下:
- Feather:最快的读写速度,适合跨语言高效传输。
- Parquet:高效的列式存储与压缩,适合大规模数据分析。
- HDF5 (.h5):强大的数据管理能力,适用于科学计算和工程应用。
- NPZ (.npz):专为NumPy数组优化,适合数值计算任务。
- Pickle (.pkl):灵活的Python对象序列化,适合内部数据持久化。
- SQLite (.sqlite):嵌入式数据库,适合需要SQL查询的小型应用。
- JSON (.json):灵活的文本格式,适合嵌套数据和配置文件。
- CSV (.csv):最简单的文本格式,适合小规模、轻量级的数据交换。
5.4选择建议
5.5 注意事项
h5py
、pyarrow
)。通过合理选择和应用上述数据格式,可以在处理亿级数据量时显著提升数据处理效率,优化存储和内存使用,为后续的数据分析和应用奠定坚实基础。
6. 千万级数据实验
本节通过模拟千万级数据,验证各种数据格式:npy, npz, h5, pkl, csv, txt, feather, parquet
这八种数据格式在读取、写入和内存占用上的性能对比。
6.1 实验设计
Step1: 数据生成
本节通过 data = np.random.rand(10**7, 5)
生成数据维度为 (1kw* 5) 的数据;
Step2: 读写和内存性能测评
记录这批数据分别保存为各种格式的时间,以及读取保存后的数据的时间,记录内存占用;每测评完一个格式后,就进行缓存刷新,尽量保证公平性;共测试三轮,计算平均值。
具体操作请看后面的完整代码。
6.2 效果展示
读写性能对比:
内存占用对比:
6.3 结果分析
在千万级数据集上,除了csv和txt外,其余格式没有质的差别,感觉是数据量较小导致的,上述效果仅供参考!
之所以不模拟亿级别的数据,因为我的电脑只能计算千万级的数据量,当我扩大到亿级时,直接崩了。代码在下面,有更好设备的朋友,可以尝试扩大规模,验证效果,要是乐于分享,也可以发给我,我进行补充。
6.4 实验代码
import numpy as np
import pandas as pd
import os
import time
import h5py
import pickle
import pyarrow as pa
import pyarrow.feather as feather
import pyarrow.parquet as pq
import matplotlib.pyplot as plt
def write_h5(file_path, data):
with h5py.File(file_path, 'w') as f:
f.create_dataset('data', data=data)
def test_read_write(file_path, write_func, read_func):
# 写入
start_time = time.time()
write_func()
write_time = time.time() - start_time
os.sync() # 刷新缓存
# 读取
start_time = time.time()
read_func()
read_time = time.time() - start_time
os.sync() # 刷新缓存
# 文件大小
file_size = os.path.getsize(file_path) / (1024 * 1024) # 转换为 MB
return {'write_time': write_time, 'read_time': read_time, 'size': file_size}
def run_experiments(n=5):
# 生成数据
data = np.random.rand(10**7, 5)
df = pd.DataFrame(data)
# 文件路径
file_paths = {
'npy': 'data.npy',
'npz': 'data.npz',
'h5': 'data.h5',
'pkl': 'data.pkl',
'csv': 'data.csv',
'txt': 'data.txt',
'feather': 'data.feather',
'parquet': 'data.parquet',
}
# 写入函数
formats = {
'npy': lambda: np.save(file_paths['npy'], data),
'npz': lambda: np.savez(file_paths['npz'], data=data),
'h5': lambda: write_h5(file_paths['h5'], data),
'pkl': lambda: df.to_pickle(file_paths['pkl']),
'csv': lambda: df.to_csv(file_paths['csv'], index=False),
'txt': lambda: np.savetxt(file_paths['txt'], data),
'feather': lambda: feather.write_feather(df, file_paths['feather']),
'parquet': lambda: pq.write_table(pa.Table.from_pandas(df), file_paths['parquet']),
}
results = {fmt: {'write_time': [], 'read_time': [], 'size': []} for fmt in formats}
# 执行N次实验
for i in range(n):
print(f"第 {i} 次执行开始")
for fmt, writer in formats.items():
read_func = None
if fmt == 'npy':
read_func = lambda: np.load(file_paths['npy'])
elif fmt == 'npz':
read_func = lambda: np.load(file_paths['npz'])
elif fmt == 'h5':
read_func = lambda: h5py.File(file_paths['h5'], 'r')['data'][:]
elif fmt == 'pkl':
read_func = lambda: pd.read_pickle(file_paths['pkl'])
elif fmt == 'csv':
read_func = lambda: pd.read_csv(file_paths['csv'])
elif fmt == 'txt':
read_func = lambda: np.loadtxt(file_paths['txt'])
elif fmt == 'feather':
read_func = lambda: feather.read_feather(file_paths['feather'])
elif fmt == 'parquet':
read_func = lambda: pq.read_table(file_paths['parquet']).to_pandas()
res = test_read_write(file_paths[fmt], writer, read_func)
results[fmt]['write_time'].append(res['write_time'])
results[fmt]['read_time'].append(res['read_time'])
results[fmt]['size'].append(res['size'])
# 计算平均值,增强可信度
avg_results = {}
for fmt in results:
avg_results[fmt] = {
'avg_write_time': np.mean(results[fmt]['write_time']),
'avg_read_time': np.mean(results[fmt]['read_time']),
'avg_size': np.mean(results[fmt]['size']),
}
# 绘制结果
df_avg_results = pd.DataFrame(avg_results).T
plt.figure(figsize=(12, 6))
# 绘制写入时间
plt.plot(df_avg_results.index, df_avg_results['avg_write_time'], marker='o', label='Average Write Time', color='blue')
for i, write_time in enumerate(df_avg_results['avg_write_time']):
plt.annotate(f"{write_time:.2f}", (df_avg_results.index[i], write_time), textcoords="offset points", xytext=(0,10), ha='center')
# 绘制读取时间
plt.plot(df_avg_results.index, df_avg_results['avg_read_time'], marker='o', label='Average Read Time', color='green')
for i, read_time in enumerate(df_avg_results['avg_read_time']):
plt.annotate(f"{read_time:.2f}", (df_avg_results.index[i], read_time), textcoords="offset points", xytext=(0,-10), ha='center')
plt.title('Average Read/Write Time Comparison')
plt.xlabel('File Format')
plt.ylabel('Time (seconds)')
plt.xticks(rotation=45)
plt.grid()
plt.legend()
plt.savefig('read_write_time_comparison.jpg', format='jpg', bbox_inches='tight')
plt.close() # 关闭图形以释放内存
# 绘制文件大小
plt.figure(figsize=(12, 6))
plt.plot(df_avg_results.index, df_avg_results['avg_size'], marker='o', color='orange', label='Average Size (MB)')
for i, size in enumerate(df_avg_results['avg_size']):
plt.annotate(f"{size:.2f} MB", (df_avg_results.index[i], size), textcoords="offset points", xytext=(0,10), ha='center')
plt.title('Average File Size Comparison')
plt.xlabel('File Format')
plt.ylabel('Size (MB)')
plt.xticks(rotation=45)
plt.grid()
plt.legend()
plt.savefig('file_size_comparison.jpg', format='jpg', bbox_inches='tight')
plt.close() # 关闭图形以释放内存
# 调用实验
run_experiments(n=3)
7. 结论
在Python的数据处理过程中,选择合适的数据读写格式至关重要。不同格式在存储机制、读写速度、存储效率和功能支持上各有千秋。理解各格式的存储原理和性能特点,可以帮助开发者在实际应用中做出更明智的选择,提升数据处理的效率与效果。
总结建议:
通过合理地选择和应用这些数据格式,可以显著优化数据处理流程,提升工作效率,满足不同应用场景的需求。
作者:陈壮实的搬砖日记