pandas处理大数据时melt与concat触发MemoryError的解决方案
问题原因
你的报错本质是内存不足,和pd.melt()、pd.concat()的语法无关。
你之前的分块写法完全没有降低峰值内存:你把所有分块melt后的结果全部存在pivot_list列表里,最后一次性做concat时,需要在内存里申请连续空间存储总共18.2亿行、object类型的全量结果,总共需要27.1GiB内存,超出了你当前环境的可用内存上限,才触发MemoryError。
加上你用了multiprocessing多进程,每个子进程会独立拷贝一份数据副本,会进一步放大内存占用。
可行解决方案
1. 先优化数据类型,从根源降低内存占用
pandas默认的数值类型、字符串类型内存浪费非常严重,处理前先把各列转成内存占用最小的类型,通常能直接减少60%以上的内存占用:
- 整数类列(ID、Year)用
pd.to_numeric(..., downcast='integer')转成最小位宽的整数类型,不要用默认int64 - 取值重复率高的字符串列(Col2、Col3、melt后的New_Var列)转成
category类型 - 数值类的Value列用
pd.to_numeric(..., downcast='float')转成最小位宽的浮点类型,不要用默认float64或object类型
示例代码:
# 预处理原数据固定列类型 df['ID'] = pd.to_numeric(df['ID'], downcast='integer') df['Year'] = pd.to_numeric(df['Year'], downcast='integer') df[['Col2', 'Col3']] = df[['Col2', 'Col3']].astype('category')
2. 边处理边落盘,不要在内存中攒全量结果
不要用列表存储所有分块的处理结果,第一块处理完直接写入磁盘文件,后续块处理完以追加模式写入,从根本上避免全量数据占用内存:
- 优先选parquet格式存储,比csv压缩比高3-10倍,读写速度更快
- 每块处理完立刻删除临时变量、触发垃圾回收释放内存
- 如果不是必须全局按ID排序,可以去掉最后的
sort_values('ID')步骤,排序本身也会占用大量额外内存
示例代码:
import gc import pandas as pd chunk_size = 100000 output_path = "./melted_result.parquet" first_chunk = True for i in range(0, len(df), chunk_size): # 取当前分块 chunk = df.iloc[i:i+chunk_size] # melt处理 chunk_melted = pd.melt( chunk, id_vars=['ID', 'Col2', 'Col3', 'Year'], var_name='New_Var', value_name='Value' ) # 优化当前分块的列类型 chunk_melted['New_Var'] = chunk_melted['New_Var'].astype('category') chunk_melted['Value'] = pd.to_numeric(chunk_melted['Value'], downcast='float', errors='ignore') # 写入文件,第一块写表头,后续块追加 chunk_melted.to_parquet( output_path, engine="pyarrow", compression="snappy", append=not first_chunk, index=False ) # 释放当前分块占用的内存 del chunk, chunk_melted gc.collect() first_chunk = False
3. 换支持外存计算的框架处理超大数据
如果必须在流程中做全量排序、聚合等操作,不要用纯pandas,换用Dask等支持外存计算的框架,语法和pandas几乎一致,会自动分块调度,不需要把全量数据加载到内存:
示例代码:
import dask.dataframe as dd # 把pandas df转成dask分块对象,也可以直接用dask读磁盘文件 ddf = dd.from_pandas(df, npartitions=10) # 做melt操作,和pandas语法完全一致 melted_ddf = ddf.melt( id_vars=['ID', 'Col2', 'Col3', 'Year'], var_name='New_Var', value_name='Value' ).set_index("ID") # 按ID建索引即完成排序,不需要全量加载到内存 # 结果直接落盘 melted_ddf.to_parquet("./melted_dask_result/", write_index=True)
额外注意
- 多进程场景下每个子进程会独立持有数据副本,内存占用会随进程数线性增长,如果单进程能处理不要开多进程
- object类型的数组内存开销远大于数值、category类型,只要能转成其他类型就不要保留object类型
内容的提问来源于stack exchange,提问作者Peter Chen
相关产品推荐
相关产品推荐

