You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 22:06:24