Pandas从0.25.1升级至2.1.4后,千万级数据按用户分组按月FFill操作性能骤降的优化方案咨询
优化Pandas 2.x中大规模数据缺失月份填充的性能方案
我完全理解这种升级后性能暴跌的挫败感——1000万行数据从20分钟涨到8小时,确实让人头大。结合你的场景和Pandas版本变化的背景,咱们一步步拆解优化方案,把速度拉回升级前的水平。
先分析原代码的性能瓶颈
你的核心慢点在**分组后用apply(lambda x: x.asfreq('MS', method='ffill'))**这一步。Pandas 2.x对groupby.apply的内部实现做了不少调整(比如更严格的类型校验、分组逻辑优化),但对于大规模分组(比如customer+user_id的组合可能多达几十万甚至上百万),逐组的lambda操作会产生巨大的开销,这是导致时间暴增的主要原因。
另外,原代码中循环用_append合并DataFrame的方式,在Pandas新版本里效率也很低,这部分可以先优化。
优化方案一:批量读取+pd.concat替代循环追加
首先优化数据读取和合并环节,减少不必要的内存开销和循环 overhead:
import pandas as pd import os path = "your_data_directory" file_list = ['Data_202601.csv', 'Data_202602.csv', 'Data_202603.csv'] # 预定义数据类型,避免Pandas自动推断的性能损耗,同时减少内存占用 dtypes = { 'customer': 'int32', # 如果数值范围允许,用更小的整数类型 'user_id': 'int32', 'user_name': 'category', # 字符串列重复率高,转成category大幅节省内存 'status': 'int8', 'user_type': 'category', 'login_counter': 'int32', 'month': 'str' # 先读为字符串,后续统一转换为datetime } # 批量读取并预处理数据 df_list = [] for file in file_list: df = pd.read_csv( os.path.join(path, file), sep=';', dtype=dtypes, parse_dates=['last_login_timestamp'], date_parser=lambda x: pd.to_datetime(x, format='%d.%m.%Y %H:%M', exact=True) ) # 转换month列 df['month'] = pd.to_datetime(df['month'], format='%Y%m') df_list.append(df) # 用pd.concat一次性合并,比循环_append效率高10倍以上 df_all = pd.concat(df_list, ignore_index=True) # 排序保持原逻辑 df_all = df_all.sort_values( ['customer', 'user_id', 'month'], ascending=True ).reset_index(drop=True)
优化方案二:用笛卡尔积+左连接替代groupby.apply
这是解决性能问题的核心——把逐组的lambda操作替换为向量化的合并和填充,彻底避免分组循环的开销:
步骤说明:
- 生成所有存在的月度序列;
- 生成所有
customer+user_id的唯一组合; - 用笛卡尔积生成所有用户+月份的完整索引;
- 左连接原数据,再按用户组向前填充缺失值。
# 1. 获取所有涉及的月份,生成完整的月度序列 all_months = pd.date_range( start=df_all['month'].min(), end=df_all['month'].max(), freq='MS' ) # 2. 获取所有唯一的用户组(customer+user_id),并保留用户的基础静态信息 user_base_info = df_all[['customer', 'user_id', 'user_name', 'status', 'user_type']].drop_duplicates() # 3. 生成用户组和月份的笛卡尔积(所有可能的组合) full_combination = user_base_info.assign(key=1).merge( pd.DataFrame({'month': all_months, 'key': 1}), on='key' ).drop('key', axis=1) # 4. 左连接原数据,得到包含缺失行的完整数据集 df_full = pd.merge( full_combination, df_all[['customer', 'user_id', 'month', 'last_login_timestamp', 'login_counter']], on=['customer', 'user_id', 'month'], how='left' ) # 5. 按用户组向前填充缺失值(向量化操作,速度极快) df_filled = df_full.sort_values(['customer', 'user_id', 'month'])\ .groupby(['customer', 'user_id'])\ .ffill()\ .reset_index(drop=True)
这个方法的优势在于:所有核心操作都是Pandas的向量化函数,没有逐组的lambda循环,在1000万行数据的场景下,速度能提升几十倍甚至上百倍。
额外优化:内存压缩
处理大规模数据时,内存占用直接影响运行速度。除了上面代码中已经用到的category类型和小整数类型,还可以:
- 检查
last_login_timestamp是否可以用datetime64[ns]之外的类型(比如datetime64[us],但一般没必要); - 如果某些列的缺失值较多,可以用
Nullable类型(比如Int32代替int32),但这对性能影响不大; - 处理完数据后及时释放中间变量:
del df_list, full_combination, df_full; import gc; gc.collect()
验证小范围数据的正确性
在全量数据运行前,建议用你提供的3个月样本数据测试上述代码,确保填充后的结果和原代码一致,再推广到全量数据。
内容的提问来源于stack exchange,提问作者cph_sto
相关产品推荐
相关产品推荐

