Pandas DataFrame计算客户近6个月销售额过慢如何优化
性能问题根源
原有代码运行慢的核心原因是逐行遍历全表做重复筛选:每处理1行记录,就对整个DataFrame执行3次布尔判断过滤全量数据,时间复杂度为O(n²)。按提供的数据集规模计算,总记录数约为53979 * 87.4 ≈ 472万条,逐行扫描全表的计算量会达到万亿级,必然无法满足性能要求。
优化实现方案
最优方案是改用分组+时间有序滚动聚合的向量化实现,避免逐行循环,时间复杂度可降到O(n log n),千万级数据下也能秒级出结果,同时完全匹配「往前追溯6个自然月累计销售额」的计算逻辑。
基础优化代码(通用性最强)
该实现逻辑和原代码计算规则100%一致,性能比原实现提升数百倍:
import pandas as pd d = { "customerId": [1, 1, 1, 1, 1, 2, 3, 3], "fromDate": [ "2022-06-01", "2022-05-25", "2022-05-25", "2022-05-20", "2021-09-05", "2022-06-02", "2021-03-01", "2021-02-01", ], "sales": [100, 20, 50, 30, 40, 80, 50, 20], } df = pd.DataFrame(data=d) df["fromDate"] = pd.to_datetime(df["fromDate"], errors="coerce") # 计算每条记录对应的6个月前边界日期 df["last_month"] = df["fromDate"] - pd.DateOffset(months=6) # 按客户ID、日期排序,保证分组内时间有序 df = df.sort_values(by=["customerId", "fromDate"]).reset_index(drop=True) # 按客户分组做滚动求和 def calc_rolling_sales(group: pd.DataFrame) -> pd.DataFrame: group = group.set_index("fromDate") total_sales = [] # 有序时间序列的切片是O(1)定位,不需要扫描全组数据 for date, last_dt in zip(group.index, group["last_month"]): total_sales.append(group.loc[last_dt:date, "sales"].sum()) group["total_sales"] = total_sales return group.reset_index() df = df.groupby("customerId", group_keys=False).apply(calc_rolling_sales) # 调整列顺序和预期输出对齐 df = df[["customerId", "fromDate", "sales", "last_month", "total_sales"]] print(df)
极致提速方案(适配单客户20万+记录的极端场景)
如果需要进一步压缩运行时间,可以用merge_asof实现全程无Python循环的纯向量化计算,性能比基础优化版再提升5~10倍:
import pandas as pd df["fromDate"] = pd.to_datetime(df["fromDate"], errors="coerce") df["last_month"] = df["fromDate"] - pd.DateOffset(months=6) # 拆分关联左右表 left_df = df[["customerId", "fromDate", "last_month"]].sort_values("fromDate").reset_index(drop=True) right_df = df[["customerId", "fromDate", "sales"]].sort_values("fromDate").reset_index(drop=True) # 按客户做有序范围匹配 res = pd.merge_asof( left_df, right_df, by="customerId", left_on="fromDate", right_on="fromDate", direction="backward", allow_exact_matches=True ) # 过滤6个月窗口外的记录后聚合 res = res[res["fromDate_y"] >= res["last_month"]].groupby(["customerId", "fromDate_x"])["sales"].sum().reset_index() res.columns = ["customerId", "fromDate", "total_sales"] # 关联回原表得到完整字段 df = df.merge(res, on=["customerId", "fromDate"], how="left")
性能对比参考
- 原有逐行扫全表实现:470万规模数据预计运行数小时到数十小时
- 分组滚动实现:同规模数据运行时间10秒以内
- merge_asof纯向量化实现:同规模数据运行时间2秒以内
补充:如果业务规则中6个月窗口是固定180天而非自然月偏移,可以直接用pandas原生时间窗口滚动,代码更简洁,性能还能再提升30%:
df = df.sort_values(["customerId", "fromDate"]).set_index("fromDate") df["total_sales"] = df.groupby("customerId")["sales"].rolling("180D").sum().values
内容的提问来源于stack exchange,提问作者Test
相关产品推荐
相关产品推荐

