如何优化pandas中非数值操作分组循环的执行性能
性能问题核心原因
你当前代码慢的根源是三个pandas开发里最常见的性能陷阱:
- 循环内逐行用
.at/.loc赋值,每次操作都有很高的Python层调度开销 - 每次循环都用
pd.concat拼接DataFrame,该操作会全量复制已有数据,数据量越大耗时呈指数级上升 - 之前尝试的
groupby().apply()本质还是在Python层逐组执行自定义函数,没有用到pandas底层C实现的向量化算子,调度开销甚至比手写循环还高
优化实现(向量化方案,无Python层循环)
这类分组取相邻行值的场景,完全可以用pandas内置的分组移位算子实现,所有计算都在C层完成,性能比手写循环高100~1000倍。
具体实现步骤:
- 先确认数据已按
CaseId、Timestamp升序排序,可加排序逻辑做兜底 - 直接用全局索引生成
EventId,不用逐组赋值 - 分组对
ActivityName做移位,取前一行、后一行的值,分别作为PreviousActivity、NextActivity的基础值 - 对每个分组的首行填充
Start Activity、末行填充End Activity - 向量化判断Loop标记:当前活动和前一个活动相同,或和后一个活动相同时标记为1,否则为0
优化后的完整代码:
import pandas as pd def get_events_df(event_log: pd.DataFrame) -> pd.DataFrame: # 兜底排序:确保同CaseId下事件按时间升序 df = event_log.sort_values(["CaseId", "Timestamp"]).reset_index(drop=True) # 生成全局EventId df["EventId"] = df.index grp_activity = df.groupby("CaseId")["ActivityName"] # 分组取相邻行的活动名(C层实现,无Python循环) df["PreviousActivity"] = grp_activity.shift(1) df["NextActivity"] = grp_activity.shift(-1) # 填充分组首尾的标记 grp_full = df.groupby("CaseId") df.loc[grp_full.cumcount() == 0, "PreviousActivity"] = "Start Activity" df.loc[grp_full.cumcount(ascending=False) == 0, "NextActivity"] = "End Activity" # 向量化计算Loop标记,不用逐行判断 df["Loop"] = ( (df["PreviousActivity"] == df["ActivityName"]) | (df["ActivityName"] == df["NextActivity"]) ).astype(int) # 按目标表列顺序返回 return df[["CaseId", "ActivityName", "PreviousActivity", "NextActivity", "Timestamp", "Loop", "EventId"]]
注:你给出的示例表中EventId=3的Receipt对应的PreviousActivity标注为Payment属于笔误——按你代码里的逻辑,上一行活动是Receipt,PreviousActivity应为Receipt,Loop=1的判定也符合该逻辑。如果需要取上一个非重复的活动,只需要对相邻重复活动做向前/向后填充即可,依然可以用向量化实现。
额外性能提示
- 所有能用pandas内置分组算子(
shift/diff/cumsum/内置agg方法)实现的逻辑,绝对不要用groupby().apply(),后者性能差两个数量级以上 - 禁止在循环中反复执行
pd.concat/pd.append,如果必须循环拼接,先把所有子DataFrame存到列表里,最后一次性用pd.concat(list_of_dfs)拼接 - 逐行赋值的操作能省则省,尽量用整列的向量化判断、赋值替代。
内容的提问来源于stack exchange,提问作者Yevhenii
相关产品推荐
相关产品推荐

