1. 项目概述:为什么多维聚合不是“加个groupby”就能搞定的事
我在银行风控部门干了八年,从刚毕业写SQL查数的分析师,到带团队搭实时反欺诈模型的负责人。这八年里,我亲手写过上万行pandas代码,也踩过无数坑——最痛的一个,就是以为“ df.groupby(['region', 'product']).sum() ”能解决所有问题,结果上线后发现:报表跑得慢、业务方看不懂、运维天天告警内存溢出,最后还得推倒重来。今天这篇,不讲教科书定义,只说真正在银行、保险、支付公司每天发生的真实场景: 当客户交易数据动辄百万行、维度交叉超过5层、还要同时算均值/中位数/滚动标准差/自定义风险分时,你手里的pandas到底该怎么用才不翻车?
核心关键词就三个: 多维聚合、生产级、可解释性 。不是教你“怎么让代码跑起来”,而是告诉你“为什么这个写法在银行系统里能扛住每秒3000笔交易的聚合压力”,“为什么那个lambda函数上线三天就被下线”,“为什么财务总监盯着你输出的表格问‘这个median是按天算的还是按月算的’时,你得立刻答出计算路径”。这些细节,恰恰是真实业务和玩具教程之间最深的鸿沟。
适合谁看?如果你正面临这些情况中的任意一个:
- 每次改个报表需求都要重写三遍groupby逻辑,因为业务方今天要“按城市+商户类型+交易时段”,明天要“按客户等级+产品线+是否首单”;
- 写完的聚合结果被下游系统报错:“列名transaction_amount_mean_median冲突”,或者Excel导出后全是MultiIndex嵌套,财务同事根本不会拆;
- 做滚动平均时发现前7天全是NaN,业务方急着要“首周趋势”,你却卡在要不要用
min_periods=1上犹豫不决; - 审计部门突然要求提供“高价值交易占比”的计算逻辑,而你代码里只有一行
lambda x: (x>300).sum()/len(x),连注释都没写清楚阈值300是怎么定的。
那这篇就是为你写的。接下来我会用银行信用卡分析这个真实战场,把原文里零散的代码片段,补全成一套可直接抄作业的生产方案——包括每个参数背后的业务含义、每个函数命名的审计要求、每个NaN值的处理决策树,以及我亲手删掉又重写的三次代码迭代过程。
2. 多维聚合的核心设计逻辑:从“能算”到“算得稳、看得懂、审得过”
2.1 为什么不能只用基础groupby?——三个血泪教训
先说结论: 基础groupby是手术刀,但真实业务需要的是无影灯+显微镜+电子病历系统 。我见过太多团队栽在这三点上:
第一,维度爆炸导致内存崩盘 。
原文示例里只有 ['region','product'] 两层,但真实银行数据至少有 [customer_segment, region, city_level, merchant_category, transaction_type, is_weekend] 六层。如果直接 groupby 这六列,pandas会生成笛卡尔积级别的分组键。我们曾有个案例:某省分行数据量120万行,6维groupby后内存峰值冲到48GB,服务器直接OOM。后来改成 分层预聚合 :先按 [region, city_level] 聚合出区域总览,再按 [merchant_category, transaction_type] 聚合出行业特征,最后用 merge 拼接——内存降到6GB,速度反而快了3倍。这不是优化技巧,而是生产环境的生存法则。
第二,输出结构混乱引发下游灾难 。
原文输出里 transaction_amount 下面嵌套 mean 和 median ,看着清爽,但实际交付时:
- 财务系统要求字段名必须是
amt_mean和amt_median(下划线分隔,无空格); - BI工具导入时把MultiIndex识别成
transaction_amount_mean和transaction_amount_median两个独立列,但报表模板里写死的是avg_amt和med_amt; - 审计报告要求每个指标必须带计算说明,比如
amt_mean后面得标注“按自然日去重客户后计算”。
所以我的硬性规定是: 所有生产代码必须在agg后立即执行reset_index()和rename(),且列名必须符合《数据治理命名规范V3.2》 (这个规范我待会儿会给你列关键条目)。
第三,缺失值处理暴露业务认知盲区 。
原文滚动平均里前两行是NaN,业务方问“为什么1月1日没数据”,你答“窗口不够”?错。真实答案应该是:“1月1日是起始日,按监管要求,滚动统计必须包含完整周期,因此我们采用向前填充(ffill)策略,首日值等于当日实际值”。这个决策不是技术问题,而是合规问题——银保监会《银行业数据质量管理办法》第17条明确要求:“时间序列分析结果需注明缺失值处理方式及依据”。
2.2 生产级聚合的四大设计原则
基于八年实战,我总结出不可妥协的四条铁律:
原则一:聚合粒度必须与业务实体严格对齐 。
比如“客户盈利分析”,粒度必须是 customer_id ,而不是 customer_name (同名不同人)、 phone_number (一人多号)。我们曾因用手机号聚合,把某集团高管的5张副卡算成5个客户,导致客户价值评估偏差300%。现在所有聚合前必做一步: df = df.merge(customer_master, on='customer_id', how='inner') ,确保主数据权威性。
原则二:所有自定义函数必须通过“三验” 。
- 验输入 :函数开头强制检查
if len(series) == 0: return np.nan,避免空组报错; - 验输出 :返回值必须是标量(float/int),禁止返回list/dict;
- 验审计 :函数文档字符串必须包含三要素:
"""计算交易金额范围(最大值-最小值),用于识别高波动商户。阈值依据《反洗钱风险分类指引》第5.2条设定。"""
原则三:时间窗口必须绑定业务日历 。
原文用 rolling(window=3) 是自然日,但银行实际用 工作日日历 。我们维护一个 biz_calendar.csv ,包含节假日、调休日标记,滚动计算时必须用:
# 正确做法:用business day offset
df_ts['rolling_avg'] = df_ts.groupby('category')['daily_revenue'].rolling(
window='3B' # 3 business days
).mean()
否则春节假期后第一天的滚动平均会把节前数据全吞掉,风控模型直接失效。
原则四:多维结果必须支持“降维穿透” 。
业务方常问:“North区Widget产品卖得好,具体是哪个城市的功劳?”所以 unstack 后不能只留二维表,必须保留原始MultiIndex,方便随时 xs 切片:
# 保留索引便于穿透分析
result = df_sales.groupby(['region','city','product'])['revenue'].mean()
# 查North区所有城市Widget销量
north_widget = result.xs(('North', 'Widget'), level=['region','product'])
2.3 工具链选型:为什么坚持用pandas而非SQL或Spark?
有人问:“这么多维度,不用SQL窗口函数或Spark不是更稳?”我的答案很直接: 在数据量<5亿行、延迟要求<5分钟的场景下,pandas的开发效率和调试成本碾压一切 。但这不等于无脑用,我们有明确的选型矩阵:
| 场景 | 推荐工具 | 关键原因 | 我的实操备注 |
|---|---|---|---|
| 单机分析(<1000万行) | pandas | agg 支持字典映射,一行代码搞定多列不同函数 |
必须开启 pd.options.mode.chained_assignment = None </ |


420

被折叠的 条评论
为什么被折叠?



