1. 项目概述为什么“多维聚合”不是Pandas进阶技巧而是业务分析的生存技能我在银行风控部门干了七年从刚毕业写SQL查数的分析师到带三个人小团队做反欺诈模型的数据架构师。这七年里我亲手重构过四套核心报表系统也给二十多个业务部门做过数据赋能培训。最常被问到的问题不是“怎么建模”而是“老师这个指标能不能按客户产品时间三个维度一起算现在跑三次groupby再merge一跑就是四十分钟领导在催。”——这句话背后藏着的是真实世界里每天都在发生的效率损耗、逻辑错位和决策延迟。“Part 20: Data Manipulation in Multi-Dimensional Aggregation”这个标题听起来像教科书里的章节编号但在我日常工作中它对应的是一个具体、高频、高价值的场景用一份代码同时回答五个不同角色的问题。财务总监要看各区域各产品的毛利总和与波动率风险经理要盯住某类商户交易金额的极差max-min是否突破阈值运营总监需要滚动30天的客单价均值来判断营销活动效果客户经理则想快速拉出自己名下客户在餐饮和旅游类目的消费偏好矩阵而CEO办公室的BI看板要求所有这些结果必须在凌晨两点前自动刷新完毕。这些需求绝不是df.groupby(region).sum()能解决的。它们共同指向一个核心能力在单次计算中对同一份数据按不同维度、施加不同逻辑、产出异构结果并保证结构可读、下游可用。这就是“多维聚合”的本质——它不是语法糖而是业务复杂度在数据层的映射。你看到的agg({amount: [mean, std], fee: [min, max]})背后是财务部和风控部两个会议纪要的合并你写的rolling(window7).mean()其实是把“过去一周是否异常”这个业务判断固化成了可复用、可审计、可回溯的计算单元而unstack()之后那个整齐的表格不是为了好看是为了让销售总监不用打开Jupyter Notebook直接复制粘贴进他明天早会的PPT里。我见过太多团队因为没吃透这些模式硬生生把一个本该200行代码搞定的分析流程拆成七八个独立脚本中间靠Excel手工拼接每次数据源更新都要花半天时间校验一致性。这种低效最终都会变成业务响应慢、指标口径乱、决策依据弱。所以这篇文章不讲“pandas有多强大”只讲“你在什么场景下必须用哪一种聚合方式以及为什么其他方式在这里会翻车”。我会用银行信用卡分析这个贯穿全文的真实案例把每一段代码背后的业务意图、参数选择的权衡、生产环境的坑掰开揉碎讲清楚。你不需要记住所有函数名但你要知道当业务方说“我要看每个客户在不同行业的消费集中度还要标出哪些人最近三个月大额交易明显增多”你脑子里立刻能跳出对应的聚合组合——这才是真正能让你在数据岗位上站稳脚跟的硬功夫。2. 核心思路拆解五种聚合模式如何精准匹配五大业务场景2.1 多列多函数聚合解决“一个动作多方诉求”的协同难题先看一个最典型的翻车现场。某次给零售银行做月度经营分析财务部要各分行的贷款余额平均值用于考核风控部要同一组数据的标准差用于评估资产质量稳定性而运营部则盯着手续费收入的最小值和最大值找异常网点。三个部门各自发来SQL脚本我们数据组照着跑结果发现三个脚本用的都是GROUP BY branch_id但数据源版本、过滤条件、甚至日期字段的时区处理都不一致。最后合并出来的报告光是核对三个“XX分行”的数值是否一致就花了两天。问题根源在于把一个逻辑上紧密耦合的分析任务强行拆解成多个孤立的计算步骤。这不仅增加IO开销反复读取同一张表更致命的是引入了人为不一致的风险。pandas的agg()字典映射正是为解决这个痛点而生。它的设计哲学非常务实一次分组多次计算结果同源。你看原文中的例子result df.groupby(merchant_category).agg({ transaction_amount: [mean,median], processing_fee: [min,max] })这里的关键不是语法而是背后的约束力。transaction_amount的mean和median必然来自完全相同的分组后子集processing_fee的min和max也共享同一份数据切片。这意味着当你在财务报告里看到“零售类目平均交易额150.78元”在风控简报里看到“零售类目交易额中位数125.50元”这两个数字的分母即参与计算的交易笔数是100%一致的。这种确定性在跨部门协作中价值千金。我实际项目中会把这个模式用得更彻底。比如在构建客户健康度评分卡时一个groupby(customer_id)后面会同时挂载age:[min, max]判断客户生命周期阶段transaction_amount:[sum, mean, std, lambda x: x.quantile(0.95)]总消费、平均、波动、高净值门槛days_since_last_transaction:[max]流失预警product_count:[nunique]产品渗透率这样一个agg()调用就生成了评分卡所需的全部基础特征。后续的权重计算、规则引擎触发都基于这份“原子级一致”的结果。避免了因特征计算时间点不同步导致的评分漂移——这是很多模型上线后效果衰减的隐形杀手。2.2 自定义聚合函数把业务规则从文档里“编译”进代码标准聚合函数sum, mean, count等覆盖了80%的统计需求但剩下那20%往往是决定分析成败的关键。比如原文提到的“交易金额范围max-min”在风控领域叫“波动率”它直接关联到欺诈检测模型的阈值设定。一个餐饮类商户如果日交易额在50-100元之间稳定波动和另一个在100-5000元之间剧烈震荡的商户其风险等级天壤之别。这个range计算无法用内置函数一步到位必须自定义。但自定义函数的价值远不止于实现一个新算法。它本质上是一种业务知识的编码化封装。我见过太多团队把复杂的业务逻辑写在Word文档里或者藏在分析师的脑子里。当新人接手、或需要复盘历史决策时只能去翻邮件、找聊天记录。而一个命名清晰、有完整docstring的自定义函数就是活的、可执行的业务手册。以原文的weighted_average为例它的docstring明确指出“Weight recent transactions more heavily”。这短短一句话背后可能是一条经过验证的业务规则客户最近的行为比半年前的行为更能预测其未来动向。这个规则不是拍脑袋定的而是通过A/B测试对比了不同权重方案对客户流失预测准确率的影响后确定的。把它写成函数意味着可复现任何人运行这段代码得到的结果都一样可审计六个月后审计师问“为什么这个客户评分这么高”你可以直接指向函数名weighted_recent_activity并展示其内部逻辑可迭代当业务规则更新比如改成“最近30天权重翻倍”你只需要修改函数内部所有调用它的分析脚本自动升级。我在实际项目中会强制要求所有自定义聚合函数满足三个条件函数名即业务语义如calculate_risk_score_by_variance而不是my_funcdocstring包含业务背景说明这个计算解决了什么问题依据是什么如“参考2023年Q4反欺诈策略白皮书第3.2节”有边界处理如原文中if len(series) 2: return series.mean()这是生产环境的生命线。空数据、单条数据、极端异常值必须有明确的fallback策略否则整个pipeline会因一个客户的异常数据而崩溃。2.3 滚动窗口聚合给静态数据装上“时间感知”的眼睛如果说基础聚合是给数据拍一张“快照”那么滚动窗口Rolling Window就是给它装上了一台“摄像机”。它回答的核心问题是“相对于最近一段时间当前状态是否异常”原文用3天滚动均值分析日营收这个例子很经典但我想补充一个更贴近金融实战的细节窗口大小的选择从来不是技术问题而是业务问题。为什么是3天不是5天或7天因为这家银行的运营SOP规定连续3天日均交易量低于阈值系统自动触发预警工单。这个“3”是业务流程定义的不是数据科学家拍的。我曾在一个支付清算项目中看到团队纠结于“用15分钟还是30分钟滚动窗口计算TPS每秒交易数”。最后是运维总监一锤定音“我们的故障自愈SLA是5分钟所以窗口必须小于5分钟选3分钟最稳妥。”——你看技术参数最终由业务SLA决定。滚动窗口在生产环境的最大陷阱是对NaN值的处理。原文输出中前两行是NaN这是正确的数学行为窗口不足但在业务系统里这往往意味着“数据不可用”。我的经验是必须根据下游用途做显式决策如果是实时监控大屏NaN会显示为空白影响观感此时用fillna(methodffill)向前填充保持曲线连续如果是生成预警信号NaN应被视作“无数据”不能填充否则会掩盖数据采集故障更稳健的做法是使用min_periods参数。比如rolling(window3, min_periods2)表示只要有2个有效值就计算降低对数据完整性的苛刻要求。还有一个容易被忽略的点滚动窗口必须与时间索引严格对齐。原文中df_ts.set_index(date)是关键一步。如果你的数据时间戳是字符串或者索引未排序rolling()会按物理顺序而非时间顺序计算结果完全错误。我踩过的最深的坑是在一个跨境支付项目中数据按“交易ID”排序但ID生成时间与实际交易时间错位导致滚动计算出的“最近一小时成功率”反映的其实是“最近一小时入库的订单成功率”而非真实业务表现。血的教训永远先sort_index()再rolling()。2.4 扩展窗口聚合构建“累积视角”的业务叙事扩展窗口Expanding Window与滚动窗口是硬币的两面滚动窗口关注“近期”扩展窗口关注“全程”。它的核心价值在于构建一种累积性的业务叙事——不是“今天怎么样”而是“从开始到现在一路走来怎么样”。原文的累计营收例子很直观但我想强调它在金融领域的两个高阶应用客户生命周期价值CLV追踪对每个客户计算其从开户至今的累计交易额、累计手续费、累计投诉次数。这不是一个静态指标而是一个随时间增长的动态轨迹。expanding().sum()能完美捕捉这个过程且性能远超循环累加。模型性能的在线监控在部署一个信用评分模型后你需要持续监控其预测效果。一个关键指标是“滚动AUC”但更稳健的是“扩展AUC”——即用模型上线以来所有已知好坏样本计算其整体AUC。随着样本量增大这个指标会逐渐收敛告诉你模型的长期稳定性。expanding().apply(lambda x: roc_auc_score(x[y_true], x[y_pred]))就是实现它的简洁方式。扩展窗口有一个隐藏优势它天然支持“增量计算”。假设你每天新增1万条交易记录用expanding().sum()pandas只需在昨天的累计值基础上加上今天的增量就能得到新的累计值。而如果用groupby().sum()重新计算全量计算量是O(n²)。在大数据量下这个差异就是几分钟和几小时的区别。但要注意一个陷阱扩展窗口默认从序列第一个元素开始。如果你的数据有缺失或者你想从某个特定时间点如“模型上线日”开始计算必须先用loc或query筛选数据子集再应用expanding()。否则早期的无效数据会污染整个累计结果。2.5 多级分组与Unstack让数据结构匹配人类思维最后这个模式解决的是一个看似简单、实则致命的问题如何让机器计算的结果一眼就被业务方看懂技术上groupby([region, product])生成的是一个MultiIndex Series结构是region product North Widget 15000 Gadget 12000 South Widget 18000 Gadget 14000这对程序员很清晰但对销售总监来说这就像看天书。他需要的是一个表格行是地区列是产品单元格里是数字。unstack()就是这个翻译官。它的价值远超格式美化。unstack()后的DataFrame具备了即插即用的特性可以直接喂给matplotlib或plotly画热力图颜色深浅直观显示高低可以无缝导入Excel业务方双击单元格就能做二次计算在BI工具如Tableau, Power BI中它天然匹配“行/列/值”的拖拽逻辑无需额外配置透视。我在一个跨国银行项目中曾用这个模式解决了一个棘手问题。业务方要求“按国家、按产品线、按季度展示净利息收入NII”这是一个三维分析。pandas原生不支持三维透视但我们用groupby([country, product, quarter])[nii].sum().unstack([product, quarter])生成了一个MultiIndex列的DataFrame。虽然列名是(Widget, Q1)这样的元组但BI工具能完美解析。最终交付的看板业务方可以自由切换“国家”作为行、“产品”作为列或者反过来完全自助。unstack()还有一个重要变体pivot_table()。当你的分组键中有重复值比如同一个国家同一产品在同一个季度有多条记录unstack()会报错而pivot_table()可以指定聚合函数如aggfuncsum来处理。这是生产环境的必备常识。3. 实操要点与避坑指南从代码到生产的最后一公里3.1 多列聚合的“扁平化”艺术告别让人头疼的层级列名原文输出中result的列名是层级结构transaction_amount processing_fee mean median min max这在探索性分析中没问题但一旦进入生产流水线这种结构会让下游开发抓狂。想象一下一个Java服务要读取这个CSV它的字段名会是transaction_amount_mean还是transaction_amount-mean答案是都不对它会是(transaction_amount, mean)一个Python元组在其他语言里根本无法直接映射。我的解决方案是在agg()之后立即执行列名扁平化。这不是可选项而是生产规范。# 原始输出 result df.groupby(merchant_category).agg({ transaction_amount: [mean,median], processing_fee: [min,max] }) # 扁平化用下划线连接层级 result.columns [_.join(col).strip() for col in result.columns.values] # 结果列名变为 transaction_amount_mean, transaction_amount_median, processing_fee_min, processing_fee_max # 更进一步重命名成业务友好名 result result.rename(columns{ transaction_amount_mean: avg_txn_amt, transaction_amount_median: med_txn_amt, processing_fee_min: min_proc_fee, processing_fee_max: max_proc_fee })这个操作看似简单却能避免90%的上下游集成故障。我坚持在所有对外交付的分析结果中执行此步骤并将其写入团队的《数据交付规范》第一条。3.2 自定义函数的“防御性编程”让代码在脏数据面前屹立不倒真实世界的金融数据充满了各种“惊喜”空值、无穷大、字符串混入数值列、时间戳格式错乱……一个没有防御的自定义函数就是一颗定时炸弹。原文的weighted_average函数只处理了len(series) 2的情况这远远不够。一个生产就绪的自定义聚合函数必须包含三层防御输入清洗将输入Series转换为统一类型并处理常见异常。计算保护在核心计算中用try...except捕获可能的数学错误。输出兜底无论发生什么都返回一个符合预期类型的值绝不让NaN或None穿透出去。以下是我常用的模板def robust_weighted_avg(series): 计算加权平均值具备强健的异常处理能力。 业务背景客户近期交易权重更高用于预测活跃度。 # 第一层输入清洗 if not isinstance(series, pd.Series): raise TypeError(fExpected pd.Series, got {type(series)}) # 转换为数值强制将非数字转为NaN numeric_series pd.to_numeric(series, errorscoerce) # 过滤掉NaN和无穷大 valid_mask numeric_series.notna() np.isfinite(numeric_series) clean_series numeric_series[valid_mask].copy() # 第二层边界检查 if len(clean_series) 0: return 0.0 # 空数据返回0而非NaN if len(clean_series) 1: return float(clean_series.iloc[0]) # 单值直接返回 # 第三层核心计算带保护 try: # 生成权重越靠后权重越高 weights np.linspace(0.5, 1.5, len(clean_series)) weighted_avg np.average(clean_series, weightsweights) # 防止计算结果溢出或为NaN if not np.isfinite(weighted_avg): weighted_avg clean_series.mean() return float(round(weighted_avg, 2)) except Exception as e: # 任何未预期错误降级为简单均值 return float(round(clean_series.mean(), 2)) # 使用 result df.groupby(customer_id)[amount].agg(robust_weighted_avg)这个函数我已经在十几个项目中稳定运行超过三年从未因数据质量问题导致pipeline中断。它的核心思想是永远假设数据是坏的然后优雅地处理它。3.3 滚动与扩展窗口的“索引对齐”生死线这是我在无数项目中反复强调、也反复看到新人踩坑的点。rolling()和expanding()的计算结果其索引必须与原始数据的索引严格对齐否则一切分析都是空中楼阁。最常见的错误有三种索引未排序数据按customer_id分组后date列是乱序的。rolling()会按物理顺序计算而非时间顺序。索引非时间类型date列是字符串set_index(date)后索引是object类型rolling()无法识别时间间隔。索引有重复同一时间戳出现多次rolling()会将它们视为同一时刻的多个观测值计算逻辑混乱。我的标准化操作流程SOP如下# 步骤1确保date列是datetime64类型 df[date] pd.to_datetime(df[date]) # 步骤2按分组键和时间双重排序 df_sorted df.sort_values([customer_id, date]).reset_index(dropTrue) # 步骤3设置复合索引可选但推荐 df_indexed df_sorted.set_index([customer_id, date]) # 步骤4进行滚动计算注意groupby后必须reset_index rolling_result ( df_indexed.groupby(customer_id)[amount] .rolling(window7D) # 使用字符串窗口如7D更鲁棒 .mean() .reset_index(namerolling_7day_avg) # reset_index确保索引干净 ) # 步骤5与原始数据merge确保一一对应 final_df df_sorted.merge(rolling_result, on[customer_id, date], howleft)关键点在于window7D7天而不是window77行。前者是基于时间间隔后者是基于行数。在交易数据中周末无交易window7会跨过周末计算出的“7天均值”实际覆盖了9天而window7D则严格保证是最近7个自然日的数据。这是业务准确性与技术实现的分水岭。3.4 多级分组的“空值填充”策略让交叉表真正可用unstack()后如果某个地区没有某类产品对应单元格就是NaN。在业务报表中NaN通常被解读为“数据缺失”但真实情况可能是“该地区确实不销售此产品”。这两种含义必须明确区分。我的做法是永远显式指定fill_value并赋予其业务意义。# 错误让NaN自然存在 crosstab df_sales.groupby([region,product])[revenue].mean().unstack() # 正确用0填充并注释说明 crosstab df_sales.groupby([region,product])[revenue].mean().unstack(fill_value0) # 注释fill_value0 表示该地区该产品无销售记录非数据缺失 # 更高级用业务默认值填充 # 例如新设地区首月无数据但按规划应有最低销售目标 planned_min_revenue {North: 10000, South: 12000} crosstab crosstab.fillna(pd.Series(planned_min_revenue))此外unstack()可能产生大量空列如某个产品在所有地区都无销售这会污染报表。我会在unstack()后立即删除全零列crosstab crosstab.loc[:, crosstab.sum() 0] # 删除所有行为0的列这些细节决定了你的分析结果是被业务方信任并依赖的“仪表盘”还是被质疑为“数据不准”的“可疑文件”。4. 全流程实战构建一个银行信用卡客户分析流水线4.1 数据准备与探查从模拟数据到真实感原文的模拟数据已经很好但为了更贴近生产环境我增加了几个关键维度customer_segment客户分群、is_fraud_flag是否欺诈标记、channel交易渠道。这些字段在真实风控系统中至关重要。import pandas as pd import numpy as np from datetime import datetime, timedelta np.random.seed(42) customers [fC{str(i).zfill(3)} for i in range(1, 101)] # 100个客户 dates pd.date_range(2024-01-01, 2024-03-31, freqD) # 构建更丰富的交易数据 data [] for date in dates: # 每天随机生成100-300笔交易 n_txns np.random.randint(100, 301) for _ in range(n_txns): customer np.random.choice(customers) # 客户分群影响交易习惯 segment np.random.choice([Premium, Standard, Student], p[0.2, 0.6, 0.2]) category np.random.choice([Groceries, Dining, Travel, Retail, Utilities], p[0.25, 0.25, 0.15, 0.25, 0.10]) # Premium客户交易额更高 base_amt {Premium: 300, Standard: 150, Student: 80}[segment] amount np.random.normal(base_amt, base_amt * 0.3) # 加入波动 amount max(10, round(amount, 2)) # 保证最小值 # 欺诈标记Premium客户欺诈率更低 is_fraud np.random.random() {Premium: 0.001, Standard: 0.005, Student: 0.01}[segment] # 渠道线上交易更多但欺诈率略高 channel np.random.choice([Online, POS], p[0.7, 0.3]) fee_rate 0.025 if channel Online else 0.015 fee round(amount * fee_rate, 2) data.append({ date: date, customer_id: customer, customer_segment: segment, category: category, amount: amount, fee: fee, is_fraud_flag: is_fraud, channel: channel }) df pd.DataFrame(data) print(f总交易笔数: {len(df)}) print(f时间范围: {df[date].min()} 到 {df[date].max()}) print(f客户数: {df[customer_id].nunique()}) print(\n客户分群分布:) print(df[customer_segment].value_counts(normalizeTrue))这个数据生成逻辑模拟了真实的业务规律高净值客户Premium交易额大、欺诈率低学生客群交易额小、欺诈率高线上渠道交易频次高、手续费率高。这为后续的多维分析提供了真实的业务土壤。4.2 分析1客户-产品-时间三维聚合滚动多函数这是最核心的分析回答“每个客户在每个产品类别的消费趋势如何”# 步骤1按客户和产品分组计算基础统计 base_stats df.groupby([customer_id, category]).agg({ amount: [count, sum, mean, std], fee: [sum], is_fraud_flag: [sum] # 欺诈笔数 }).round(2) # 扁平化列名 base_stats.columns [_.join(col).strip() for col in base_stats.columns.values] base_stats base_stats.reset_index() # 步骤2计算滚动指标需先按时间排序 df_sorted df.sort_values([customer_id, date]).copy() df_sorted[date_ordinal] pd.to_datetime(df_sorted[date]).map(pd.Timestamp.toordinal) # 为每个客户计算7天滚动均值和标准差 rolling_window df_sorted.groupby(customer_id).apply( lambda x: x.set_index(date).resample(D)[amount].mean().rolling(7D).agg([mean, std]) ).reset_index() # 合并回主表 df_with_rolling df_sorted.merge( rolling_window, left_on[customer_id, date], right_on[customer_id, date], howleft ) # 步骤3生成最终宽表客户为行产品为列指标为值 # 先计算每个客户每个产品的平均交易额 crosstab_avg df.groupby([customer_id, category])[amount].mean().unstack(fill_value0) # 再计算每个客户每个产品的欺诈率 crosstab_fraud ( df.groupby([customer_id, category])[is_fraud_flag].mean().unstack(fill_value0) * 100 ).round(2) # 转为百分比 print( 客户-产品平均交易额矩阵 (前5行) ) print(crosstab_avg.head()) print(\n 客户-产品欺诈率矩阵 (前5行) ) print(crosstab_fraud.head())这个分析产出的两个矩阵是风控模型的黄金特征。crosstab_avg揭示了客户偏好如某客户在Travel类目消费远高于均值crosstab_fraud则标识了高风险组合如某客户在Dining类目的欺诈率高达5%。这两张表可以直接作为XGBoost模型的输入特征。4.3 分析2自定义风险分层识别“高危高价值”客户这是业务方最关心的“行动项”。我们需要找出那些既贡献高额收入又带来高欺诈风险的客户对他们实施精细化管理。def risk_segmentation(series): 对客户交易额序列进行风险分层。 返回高价值客户数、高风险客户数、混合客户数高价值且高风险 if len(series) 5: return pd.Series({high_value_count: 0, high_risk_count: 0, hybrid_count: 0}) # 定义阈值业务规则 high_value_threshold series.quantile(0.8) # 80分位数以上为高价值 high_risk_threshold series.quantile(0.95) # 95分位数以上为高风险 high_value_mask series high_value_threshold high_risk_mask series high_risk_threshold return pd.Series({ high_value_count: high_value_mask.sum(), high_risk_count: high_risk_mask.sum(), hybrid_count: (high_value_mask high_risk_mask).sum() }) # 应用到每个客户 risk_profile df.groupby(customer_id)[amount].apply(risk_segmentation) risk_profile risk_profile.sort_values(hybrid_count, ascendingFalse) print( 高危高价值客户TOP 10 ) print(risk_profile.head(10)) # 关联客户分群信息 customer_info df.groupby(customer_id).agg({ customer_segment: first, amount: sum, is_fraud_flag: sum }).rename(columns{amount: total_spend, is_fraud_flag: fraud_count}) top_hybrid risk_profile.merge(customer_info, left_indexTrue, right_indexTrue) print(\n TOP 10 混合客户详细信息 ) print(top_hybrid.head(10)[[customer_segment, total_spend, fraud_count, hybrid_count]])这个分析直接输出了可执行的客户名单。业务团队可以据此对hybrid_count 3的客户启动人工尽调对customer_segment Premium且hybrid_count 0的客户提供专属风控服务包对fraud_count 5的所有客户自动加入高风险监控队列。4.4 分析3渠道效能分析POS vs Online的ROI对比这是运营团队的KPI。我们需要比较不同渠道的投入产出比。# 计算每个渠道的总体指标 channel_summary df.groupby(channel).agg({ amount: [sum, mean, count], fee: sum, is_fraud_flag: sum }).round(2) # 扁平化 channel_summary.columns [_.join(col).strip() for col in channel_summary.columns.values] channel_summary channel_summary.reset_index() # 计算ROI手续费收入 - 欺诈损失/ 交易总额 # 假设欺诈损失 欺诈交易额 * 100%简化 fraud_loss_rate 1.0 channel_summary[roi_pct] ( (channel_summary[fee_sum] - channel_summary[is_fraud_flag_sum] * channel_summary[amount_mean]) / channel_summary[amount_sum] * 100 ).round(2) print( 渠道效能分析 ) print(channel_summary[[channel, amount_sum, fee_sum, is_fraud_flag_sum, roi_pct]]) # 进阶按客户分群交叉分析 channel_segment df.groupby([channel, customer_segment]).agg({ amount: sum, is_fraud_flag: sum }).unstack(fill_value0) print(\n 渠道-客户分群交易额矩阵 ) print(channel_segment)这个分析揭示了一个反直觉的结论虽然Online渠道的手续费率2.5%高于POS1.5%但其ROI可能更低因为欺诈损失更大。这直接指导了资源分配——是否应该加大对POS渠道的推广力度这个决策就源于这一行代码的输出。4.5 分析4构建自动化报告流水线最后把所有分析整合成一个可调度的流水线。这是从“能跑通”到“能生产”的关键一步。def generate_daily_report(): 每日自动报告生成函数。 输出一个包含多个Sheet的Excel文件。 # 1. 基础统计 base_stats df.groupby([customer_id, category]).agg({ amount: [sum, mean], fee: sum, is_fraud_flag: sum }).round(2) base_stats.columns [_.join(col).strip() for col in base_stats.columns.values] # 2. 风险客户名单 risk_customers risk_profile[risk_profile[hybrid_count] 0].index.tolist() high_risk_df df[df[customer_id].isin(risk_customers)].copy() # 3. 创建Excel writer with pd.ExcelWriter(daily_credit_card_report.xlsx, engineopenpyxl) as writer: base_stats.to_excel(writer, sheet_nameCustomer_Product_Summary) crosstab_avg.to_excel(writer, sheet_nameAvg_Amt_Matrix) crosstab_fraud.to_excel(writer, sheet_nameFraud_Rate_Matrix) high_risk_df.to_excel(writer, sheet_nameHigh_Risk_Customers, indexFalse) # 添加汇总页 summary_data { Metric: [Total_Trans