pandas跨两个DataFrame按日期对比计算字段的优化方法
Pandas关联衍生字段高效实现方案
原逐行apply实现性能差的核心原因是:每处理一行df_A的记录,都会对df_B做一次全表扫描筛选,时间复杂度是O(len(df_A)*len(df_B)),数据量上涨后耗时会快速升高。下面两种实现都避免了重复全表扫描,性能比原方案高1~2个数量级。
方案1:分组预聚合实现(代码简洁,百万行内数据首选)
核心思路是先按球员id对df_B做一次聚合,把每个球员的所有参赛日期、参赛项目提前打包,后续处理df_A时直接读取对应球员的参赛数据做判断,无需重复扫描df_B全表:
import pandas as pd # 初始化样例数据 df_A = pd.DataFrame({"id": [1,2,1,4], "texts":['text for person 1', 'text for person 2', 'text for person 1 AGAIN', 'text for person 4'], "injury_date": [pd.to_datetime('1/1/2020'),pd.to_datetime('2/1/2020'),pd.to_datetime('5/1/2021'),pd.to_datetime('1/1/2022')]}) df_B = pd.DataFrame({"id": [2,1,1], "games":['soccer', 'football', 'tennis'], 'played_on': [pd.to_datetime('1/1/2019'),pd.to_datetime('2/1/2020'),pd.to_datetime('5/1/2021')]}) # 按id预聚合参赛数据,全表仅扫描1次df_B b_agg = df_B.groupby("id").agg( play_dates=("played_on", list), play_games=("games", list) ).reset_index() # 关联到df_A,无参赛记录的球员填充空值 df_res = df_A.merge(b_agg, on="id", how="left") df_res[["play_dates", "play_games"]] = df_res[["play_dates", "play_games"]].apply( lambda col: ([], []) if pd.isna(col["play_dates"]).any() else col, axis=1 ) # 计算前后参赛标识与项目列表 def calc_field(row): injury_dt = row["injury_date"] dates = row["play_dates"] games = row["play_games"] # 拆分受伤前后的参赛项目,自动去重匹配原逻辑的groupby first效果 after_games = list({g for g, dt in zip(games, dates) if dt > injury_dt}) before_games = list({g for g, dt in zip(games, dates) if dt <= injury_dt}) return pd.Series([ len(after_games) > 0, len(before_games) > 0, after_games, before_games ], index=["played after injury", "played before injury", "games played after", "games played before"]) df_res[["played after injury", "played before injury", "games played after", "games played before"]] = df_res.apply(calc_field, axis=1) # 清理临时列得到最终结果 df_res = df_res.drop(columns=["play_dates", "play_games"])
该方案在df_A、df_B均为百万行规模时,运行耗时通常在1秒以内。
方案2:全向量化merge实现(千万级超大数据集首选)
核心思路是先按id关联两个表的所有匹配记录,再通过向量化的日期比较打标,最后按df_A的原始行粒度聚合,全程走pandas底层C实现的算子,无Python层逐行循环,性能拉满:
import pandas as pd # 初始化样例数据 df_A = pd.DataFrame({"id": [1,2,1,4], "texts":['text for person 1', 'text for person 2', 'text for person 1 AGAIN', 'text for person 4'], "injury_date": [pd.to_datetime('1/1/2020'),pd.to_datetime('2/1/2020'),pd.to_datetime('5/1/2021'),pd.to_datetime('1/1/2022')]}) df_B = pd.DataFrame({"id": [2,1,1], "games":['soccer', 'football', 'tennis'], 'played_on': [pd.to_datetime('1/1/2019'),pd.to_datetime('2/1/2020'),pd.to_datetime('5/1/2021')]}) # 给df_A加唯一行标识,避免同id同日期记录被错误聚合 df_A = df_A.reset_index(names="row_id") # 同id关联所有参赛记录 merged = df_A.merge(df_B, on="id", how="left") # 向量化打标参赛记录在受伤前/后 merged["is_after"] = merged["played_on"] > merged["injury_date"] merged["is_before"] = merged["played_on"] <= merged["injury_date"] # 按原始行粒度聚合结果 agg_df = merged.groupby("row_id").agg( **{ "played after injury": ("is_after", "any"), "played before injury": ("is_before", "any"), "games played after": ("games", lambda x: list(set(x[merged.loc[x.index, "is_after"]]))), "games played before": ("games", lambda x: list(set(x[merged.loc[x.index, "is_before"]]))) } ).reset_index() # 合并回原表清理临时列 df_res = df_A.merge(agg_df, on="row_id").drop(columns="row_id")
说明:两个方案默认对参赛项目做去重,和原代码
groupby('games').first()的逻辑完全一致,如果不需要去重,把包裹项目列表的set()去掉即可。
内容的提问来源于stack exchange,提问作者Kevin
相关产品推荐
相关产品推荐

