如何用Dask并行执行GroupBy并分批写入磁盘以优化内存?
我来给你几个可行的方案,既能用Dask实现分组的并行处理,又能严格控制内存占用避免OOM,同时还能及时把结果写入磁盘,解决你当前的效率和内存矛盾:
先优化核心处理函数my_func
内存占用的源头之一是分组处理时的不必要数据复制,先给你的my_func做轻量化改造:
def my_func(df_): # 原地排序,减少内存复制 df_.sort_values('year', inplace=True, ignore_index=True) # 用临时变量计算差值,避免新增冗余列 diffs = df_['year'].diff() shifted_diffs = diffs.shift(-1) # 直接筛选切片,按需保留数据 x = df_.loc[shifted_diffs == 1, x_vars].copy() y = df_.loc[diffs == 1, y_vars].copy() # 手动清理临时变量,帮助GC回收内存 del diffs, shifted_diffs return (x, y)
方案一:用Dask Delayed手动控制分批并行
这个思路是把客户ID分成若干小批次,并行处理每一批,处理完就立即写入磁盘,确保内存中始终只保留当前批次的处理数据:
import dask from dask.diagnostics import ProgressBar # 先获取所有唯一客户ID(如果数据量极大,也可以用Dask来提取) unique_ids = concatenated_data['ID'].unique().tolist() # 自定义每批处理的分组数,根据你的内存承载能力调整(比如每批5个) batch_size = 5 batches = [unique_ids[i:i+batch_size] for i in range(0, len(unique_ids), batch_size)] # 定义单批次处理逻辑 def process_batch(batch_ids): batch_data = concatenated_data[concatenated_data['ID'].isin(batch_ids)] x_list, y_list = [], [] for _, group in batch_data.groupby('ID'): x, y = my_func(group) x_list.append(x) y_list.append(y) return pd.concat(x_list, ignore_index=True), pd.concat(y_list, ignore_index=True) # 用Dask Delayed包装所有批次任务 delayed_batches = [dask.delayed(process_batch)(batch) for batch in batches] # 逐批计算并写入磁盘 with open('X.csv', 'a') as x_file, open('Y.csv', 'a') as y_file: # 先写入表头 pd.DataFrame(columns=x_vars).to_csv(x_file, header=True, index=False) pd.DataFrame(columns=y_vars).to_csv(y_file, header=True, index=False) # 显示处理进度 with ProgressBar(): # 用进程调度器实现并行,内存隔离更安全 for batch_x, batch_y in dask.compute(*delayed_batches, scheduler='processes'): batch_x.to_csv(x_file, header=False, index=False) batch_y.to_csv(y_file, header=False, index=False)
优势:完全手动控制批次大小,精准匹配你的内存上限;每批处理完立即释放内存,不会累积所有结果。
方案二:用Dask DataFrame分区自动并行处理
如果你的数据可以按客户ID分区,Dask会自动帮你并行处理每个分区,且每个分区的内存占用可控:
import dask.dataframe as dd # 把Pandas数据转为Dask DataFrame,先设置初始分区数(比如20,根据数据大小调整) ddf = dd.from_pandas(concatenated_data, npartitions=20) # 按ID重新分区,确保同一个客户的所有年度数据在同一个分区内 ddf = ddf.set_index('ID').reset_index() # 定义分区内的分组处理逻辑 def process_partition(partition): x_list, y_list = [], [] for _, group in partition.groupby('ID'): x, y = my_func(group) x_list.append(x) y_list.append(y) # 处理空分区的情况 if not x_list: return pd.DataFrame(columns=x_vars), pd.DataFrame(columns=y_vars) return pd.concat(x_list, ignore_index=True), pd.concat(y_list, ignore_index=True) # 对每个分区应用处理函数 result_partitions = ddf.map_partitions( process_partition, meta=(pd.DataFrame(columns=x_vars), pd.DataFrame(columns=y_vars)) ) # 遍历分区结果写入磁盘 with open('X.csv', 'a') as x_file, open('Y.csv', 'a') as y_file: pd.DataFrame(columns=x_vars).to_csv(x_file, header=True, index=False) pd.DataFrame(columns=y_vars).to_csv(y_file, header=True, index=False) for part_x, part_y in result_partitions.compute(): part_x.to_csv(x_file, header=False, index=False) part_y.to_csv(y_file, header=False, index=False)
优势:利用Dask的原生分区机制自动并行,无需手动分批;如果原始数据是多个CSV,还可以直接用dd.read_csv('./data_*')读取,全程避免加载全量数据到内存。
额外优化建议
- 直接用Dask读取原始数据:不要先把所有CSV读成Pandas再拼接,直接用
dd.read_csv('./data_*')加载,从源头减少内存占用。 - 调整Dask内存配置:通过以下配置让Dask在内存不足时自动溢写到磁盘:
import dask.config dask.config.set({ 'distributed.worker.memory.target': 0.7, 'distributed.worker.memory.spill': 0.8 })
内容的提问来源于stack exchange,提问作者TJ_93
相关产品推荐
相关产品推荐

