Python 3.9多进程处理超大规模地理数据时AssertionError问题求解
我完全懂你处理超大规模地理数据的痛苦——430万个网格单元+5GB的船舶轨迹数据集,想靠多进程提速反而触发AssertionError,确实头大。先帮你梳理核心问题和可行的解决思路:
问题核心分析
你遇到的这个断言错误,本质是多进程间传递的数据量超过了系统默认限制(Windows下多进程管道默认有2GB左右的传输上限)。虽然Python官方称该bug已修复,但在地理数据场景下,超大GeoDataFrame的序列化/反序列化还是容易触发问题——毕竟你现在是把整个5GB的轨迹数据集传给每个worker进程,内存和传输压力直接拉满。
你试过的方法复盘
你已经尝试了不少方向,但都没命中核心问题:
- 拆分网格数据集:只拆分了网格,但每个worker仍要处理全量轨迹,数据传输量没减少
gp.overlay/sjoin:前者同样面临大数据传输问题,后者没法处理轨迹多次穿网格的计数需求- pandarallel:应该是环境配置问题导致
clip未找到,但即便解决了,还是绕不开大数据传输的坑
可行的解决方案
1. 让Worker按需加载/筛选轨迹数据(最推荐)
不要把整个轨迹GeoDataFrame传给每个worker,而是让worker根据当前处理的网格范围,只加载/筛选相交的轨迹数据,彻底减少进程间的数据传输量。
修改你的cell_calc函数和主逻辑:
def cell_calc(x, tracks_db, tracks_layer): # 只读取和当前网格范围相交的轨迹,bbox参数会自动过滤 tracks_gdf = read_file(tracks_db, layer=tracks_layer, Driver='FileGDB', bbox=x.total_bounds) tracks_gdf = tracks_gdf[['MYTYPE', 'MONTH', 'geometry']] df = clip(tracks_gdf.to_crs(x.crs), x, keep_geom_type=True).explode() cell = x TList = ['CARGO', 'FISHING', 'OTHER', 'PASSENGER', 'TANKER'] MList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] cell['ALL'] = len(df) for type in TList: cell[type] = len(df[df['MYTYPE']==type]) for month in MList: mdf = df[df['MONTH']==month] cell[month_name[month]+'_ALL'] = len(mdf) for type in TList: cell[month_name[month]+'_'+type] = len(mdf[mdf['MYTYPE'] == type]) return cell if __name__ == '__main__': # 其他初始化代码不变... func = partial(cell_calc, tracks_db=TRACKSDB, tracks_layer=TRACKS) p = Pool(processes=16) # 传入网格的每一行(或拆分后的小批次) pool_results = p.map(func, GRIDGDF.itertuples(index=False)) p.close() p.join() # 把结果合并成GeoDataFrame grid = gpd.GeoDataFrame(pool_results, columns=['grid_id', 'geometry'] + [你的统计字段列表])
2. 用imap代替map,分批接收结果
Pool.map()会一次性把所有worker的结果传回主进程,容易瞬间占满内存。换成imap()或imap_unordered(),可以迭代接收结果,逐步写入文件或合并,缓解内存压力:
# 替换原有的pool_results = p.map(func, split_dfs) pool_results = [] temp_count = 0 for result in p.imap(func, split_dfs): pool_results.append(result) # 每处理5批就写入临时文件,释放内存 if len(pool_results) >=5: temp_count +=1 gpd.GeoDataFrame(pool_results).to_file(f'temp_{temp_count}.gpkg', driver='GPKG') pool_results.clear() # 处理剩余的结果 if pool_results: temp_count +=1 gpd.GeoDataFrame(pool_results).to_file(f'temp_{temp_count}.gpkg', driver='GPKG') # 最后合并所有临时文件 final_grid = gpd.GeoDataFrame() for i in range(1, temp_count+1): temp_gdf = gpd.read_file(f'temp_{i}.gpkg') final_grid = pd.concat([final_grid, temp_gdf]) remove(f'temp_{i}.gpkg')
3. 改用Dask-GeoPandas处理超大规模数据
如果手动写多进程太麻烦,直接用专门的大数据地理处理框架Dask-GeoPandas,它会自动帮你分区、并行处理,不用关心底层多进程细节:
import dask_geopandas as dg # 按块加载数据,自动拆分成分区 grid_dg = dg.read_file(GRIDDB, layer=GRID, chunksize=10000) tracks_dg = dg.read_file(TRACKSDB, layer=TRACKS, chunksize=10000)[['MYTYPE', 'MONTH', 'geometry']] # 先做空间裁剪,只保留和网格相交的轨迹 clipped = dg.clip(tracks_dg, grid_dg, keep_geom_type=True).explode() # 按grid_id分组统计所有需要的指标 # 可以用循环生成agg的参数,避免重复代码 agg_params = {'ALL': ('MYTYPE', 'count')} for ship_type in ['CARGO', 'FISHING', 'OTHER', 'PASSENGER', 'TANKER']: agg_params[ship_type] = ('MYTYPE', lambda x: (x == ship_type).sum()) for month in range(1,13): agg_params[f'{month_name[month]}_ALL'] = ('MONTH', lambda x: (x == month).sum()) for ship_type in ['CARGO', 'FISHING', 'OTHER', 'PASSENGER', 'TANKER']: agg_params[f'{month_name[month]}_{ship_type}'] = (['MYTYPE','MONTH'], lambda x: ((x[0]==ship_type)&(x[1]==month)).sum()) stats = clipped.groupby('grid_id').agg(agg_params).compute() # 合并统计结果到原网格 final_grid = grid_dg.compute().merge(stats, on='grid_id') final_grid.to_file(ODB, layer='AIS_GRID', driver='GPKG')
4. 调整多进程启动方式(Windows环境可选)
Windows下Python默认用spawn启动进程,序列化开销大。如果你的OSGeo4W环境支持,可以试试改用forkserver启动:
from multiprocessing import get_context p = Pool(processes=16, context=get_context('forkserver'))
总结
核心思路就是减少进程间的大数据传输——要么让worker按需取数据,要么用框架自动处理分区。优先试试第一种方法,改动最小,效果最直接;如果数据集实在大到离谱,Dask-GeoPandas是更省心的选择。
备注:内容来源于stack exchange,提问作者MrKingsley

