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有多强大”只讲“在银行、保险、支付这类强监管、高时效、多角色协同的行业里你怎么用pandas把业务语言翻译成机器可执行、人可理解、系统可集成的聚合逻辑”。它面向的不是刚学完df.head()的新手也不是只写算法不碰生产环境的研究员而是每天被业务方追着要“那个带颜色的交叉表”、被运维同事提醒“你的job又把集群内存打满了”的一线数据工程师和分析师。接下来的内容全部来自我踩过的坑、压测过的参数、上线后被反复验证过的写法。没有理论推导只有实操现场。2. 核心设计思路为什么放弃“分步计算”选择“一次聚合”2.1 业务驱动的性能瓶颈从45分钟到9秒的真实代价先说一个血泪教训。2022年Q3我们为信用卡中心搭建一套实时商户风险评分看板。原始方案是典型的“分步流”第一步按merchant_id分组算sum(amount)和count(*)第二步按merchant_category分组算std(amount)和max(amount)-min(amount)第三步按datemerchant_category分组算滚动7天均值……最后用pd.merge()把七八个DataFrame拼起来。这套逻辑在测试环境10万条记录跑得飞快不到2秒。但上线首日面对生产库每小时新增的800万笔交易整个ETL pipeline卡在聚合环节平均耗时45分钟导致看板数据延迟超6小时风控策略完全失效。问题出在哪表面看是数据量大根子上是计算冗余和内存爆炸。每一次groupbypandas都要重新扫描全量数据、重建索引、分配新内存块。更致命的是merge操作本身需要对齐索引当左右表的分组键不完全一致比如A表有1000个商户B表只有950个就会触发笛卡尔积式的匹配尝试内存占用呈指数级增长。我们用memory_profiler抓取峰值发现单次merge就占用了12GB内存而服务器总内存才32GB。解决方案强制收敛到单次groupby入口。把所有需要的维度、所有需要的指标全部塞进一个agg()调用里。这不是炫技是物理定律决定的必然选择。CPU缓存友好数据只被顺序读取一次中间结果保留在高速缓存中内存可控所有聚合结果共享同一个分组索引无需额外索引对齐逻辑原子一次计算失败整个任务回滚避免部分成功导致的指标割裂。提示agg()的字典映射语法{col1: [func1, func2], col2: func3}其底层实现是pandas对每个列-函数组合进行向量化计算共享同一套分组哈希表。这比手动循环for col in cols: for func in funcs:快3-5倍且内存占用降低60%以上。这是pandas 1.3版本针对多聚合场景做的深度优化必须用。2.2 维度爆炸的治理如何避免“groupby([a,b,c,d,e])”的灾难多维聚合的另一个陷阱是盲目堆砌分组键。我见过最夸张的案例是某支付公司为分析“用户设备类型操作系统版本APP渠道地理位置精度网络类型”五维交叉写出groupby([device, os_version, channel, geo_precision, network])。结果呢分组后产生230万个唯一组合生成的DataFrame内存占用达18GB下游任何可视化工具都打不开。正确的解法是维度分层与业务裁剪。回到银行场景分析“客户盈利能力”核心维度永远是customer_id和product_line。region可以作为二级维度但必须明确其粒度——是省级10个值还是地市级300个值time维度同理按日聚合365个值和按小时聚合8760个值完全是两个量级。我的经验是在groupby之前先用value_counts().head(10)探查每个候选维度的基数cardinality。如果某个维度的唯一值超过总记录数的5%或者其业务含义过于琐碎如“用户最后一次点击的按钮ID”就必须剔除或降维例如将“iOS 16.4.1”、“iOS 16.4.2”统一为“iOS 16.4”。更关键的是用unstack()替代无脑多维groupby。比如要分析“各地区各产品线的平均交易额”不要写df.groupby([region, product])[amount].mean()返回MultiIndex Series而是写df.groupby([region, product])[amount].mean().unstack()返回DataFrame。前者有2个索引层级后者只有1个且天然适配Excel、Tableau等工具。unstack()的本质是维度折叠它把一个索引层级“转置”为列极大降低了下游处理复杂度。我坚持一个原则任何需要被业务方直接查看的聚合结果必须是二维DataFrame不能是MultiIndex结构。2.3 逻辑耦合的破局为什么自定义函数必须“可解释、可审计、可复用”标准聚合函数sum,mean,std之所以安全是因为它们有明确定义、无歧义、可跨平台验证。但业务逻辑永远比数学定义复杂。比如风控要求的“近30天交易金额的加权移动平均权重按时间倒序线性衰减”这没法用内置函数表达。这时候很多人会写一个lambda x: np.average(x, weightsnp.linspace(1, 0.1, len(x)))。看起来简洁但埋下了三个雷不可读半年后你自己再看得花两分钟想明白linspace(1, 0.1, len(x))到底是什么意思不可审计合规检查时审计师问“权重衰减系数0.1的业务依据是什么”你答不上来不可复用下次要算“手续费”的加权均值还得重写一遍无法继承逻辑。我的做法是所有业务逻辑必须封装为带完整docstring的命名函数。函数名要直指业务意图比如calculate_fraud_risk_weighted_avg而不是weighted_avg。docstring里必须包含三要素业务场景“用于识别高频小额交易中的异常模式”、参数说明“decay_factor0.1表示最近一笔交易权重为130天前为0.1”、合规依据“依据《XX银行反洗钱操作指引》第5.2条”。这样当审计师抽查代码时一眼就能确认逻辑合规性当新同事接手时看函数名和注释就能理解用途无需翻历史会议纪要。注意自定义函数内部严禁使用全局变量或外部状态。所有参数必须通过Series传入。这是保证函数纯度pure function的底线否则在分布式环境如Dask下会因状态不一致导致结果错误。3. 实操细节解析从代码到生产的七道关卡3.1 多重聚合的“列字典”语法不只是语法是数据契约agg()的字典语法{col1: [func1, func2], col2: func3}表面看是便捷写法实则是定义数据契约data contract的关键接口。它明确告诉后续所有环节“这个结果里col1字段将提供func1和func2两种计算视角col2字段只提供func3一种”。这种契约是保障下游系统稳定性的基石。我们来看一个真实案例。某次迭代中业务方要求在原有“交易金额均值”基础上增加“交易金额中位数”。开发同学直接在agg()里加了amount: [mean, median]。代码测试通过上线后却导致BI看板报错。排查发现BI工具的元数据管理模块只认amount_mean这个字段名新加的amount_median未被注册导致查询失败。解决方案在agg()之后立即用rename()标准化列名并用reset_index()固化结构。这是生产环境的铁律# 错误示范依赖pandas默认的多层列名 result df.groupby(category).agg({amount: [mean, median]}) # 正确示范主动定义清晰、扁平、可预测的列名 result (df.groupby(category) .agg({amount: [mean, median], fee: [min, max]}) .rename(columns{mean: avg_amount, median: med_amount, min: min_fee, max: max_fee}) .reset_index())这样产出的DataFrame列名是category,avg_amount,med_amount,min_fee,max_fee全是扁平字符串没有任何嵌套层级。BI工具、数据库INSERT语句、甚至Excel导入都能无缝对接。我团队的代码规范强制要求所有生产级agg()操作必须伴随rename()和reset_index()且rename()的映射字典需在代码注释中说明业务含义。3.2 自定义函数的“边界防御”如何让lambda不成为线上事故的导火索lambda函数写起来快但在生产环境它是“优雅的定时炸弹”。最大的风险在于空值NaN和极短序列的处理。看这个例子# 危险的lambda当x为空Series时x.max() - x.min()会返回NaN但业务上这可能意味着“无数据”需要特殊标记 df.groupby(category)[amount].agg(lambda x: x.max() - x.min()) # 更危险的lambda当x长度为1时np.average(x, weights...)会因权重数组长度不匹配而报错 df.groupby(category)[amount].agg(lambda x: np.average(x, weightsnp.linspace(0.5, 1.5, len(x))))我的做法是所有自定义函数第一行必须是防御性检查。以计算交易范围range为例def safe_transaction_range(series): 计算交易金额范围max-min带空值和极短序列保护 业务规则少于2笔交易视为数据不足返回-1明确标识异常 if series.isna().all(): # 全是NaN return -1 if len(series.dropna()) 2: # 有效值少于2个 return -1 clean_series series.dropna() return clean_series.max() - clean_series.min() # 使用 result df.groupby(category)[amount].agg(safe_transaction_range)这个函数看似多写了5行但它把“数据质量”这个隐性业务规则显性化、可配置、可监控。上线后我们通过监控result[result -1]的数量能第一时间发现某类商户的数据采集故障。而lambda做不到这点——它要么静默返回NaN难以追踪要么直接抛异常导致整个job失败。提示在金融场景我习惯用-1、-999等负数标记业务异常而非np.nan。因为np.nan在数据库存储、JSON序列化、甚至某些BI工具中行为不一致而整数-1在所有系统中含义明确、零兼容成本。3.3 滚动窗口的“起点哲学”为什么window7不等于“过去7天”滚动窗口rolling()是时间序列分析的利器但它的默认行为常被误解。df.rolling(window7).mean()的含义是“对当前行及往前6行共7行数据求均值”。这在等间隔、无缺失的时间序列中成立比如按日统计的daily_revenue。但现实数据充满陷阱数据缺失某天没有交易daily_revenue为0或NaNrolling(7)仍会把这天计入窗口导致均值失真非等间隔交易日志是事件驱动的date列可能有重复同一秒多笔交易或跳跃周末无数据rolling(7)按行数算而非按时间跨度算业务语义错位风控要求的“过去7个自然日”和rolling(7)的“过去7行”在数据稀疏时完全不是一回事。正确解法必须用rolling(7D)时间窗口而非rolling(7)行数窗口。但这要求date列是datetime64类型且设为索引# 确保date是datetime并设为索引 df_ts df_ts.set_index(date).sort_index() # 使用时间窗口7D表示7个日历日自动跳过无数据日期 df_ts[rolling_7day_avg] df_ts.groupby(category)[daily_revenue].rolling(7D).mean()但这就引出新问题rolling(7D)遇到周末或节假日窗口内数据不足7天结果是NaN。业务上这通常需要“向前填充”ffill或“用最小周期替代”。我的经验是在rolling()后立即跟.fillna(methodffill)并设置一个兜底值# 兜底策略先ffill若仍为NaN则用该category的历史均值填充 category_mean df_ts.groupby(category)[daily_revenue].mean() df_ts[rolling_7day_avg] (df_ts.groupby(category)[daily_revenue] .rolling(7D).mean() .fillna(methodffill) .fillna(df_ts[category].map(category_mean)))这个兜底逻辑把技术上的NaN转化成了业务上可解释的“沿用最新有效值”或“采用长期基准值”消除了下游系统的处理负担。3.4 展开窗口的“累积陷阱”为什么expanding().sum()不是简单的累加expanding()窗口看似简单df.expanding().sum()就是从第一行开始逐行累加。但生产环境中它有两个致命陷阱初始值污染expanding().sum()的第一行就是第一行的原始值。但如果第一行数据是异常值比如测试数据、脏数据整个累积曲线都会被带偏。业务上我们更希望“从第二个有效点开始累积”即忽略首行。分组内的独立性df.groupby(category)[revenue].expanding().sum()要求每个category的累积必须严格在其内部独立计算。如果数据未按category排序expanding()会跨组计算导致结果完全错误。我的标准写法包含三重防护def robust_cumulative_sum(series): 健壮的累积求和带首行过滤和空值处理 # 1. 过滤掉首行业务规则累积值从第二笔交易开始有意义 if len(series) 1: series series.iloc[1:] # 跳过第一个值 # 2. 处理空值用前向填充确保连续性 series series.fillna(methodffill) # 3. 执行累积求和 return series.cumsum() # 使用必须先按分组键排序 df_sorted df_ts.sort_values([category, date]) df_sorted[cumulative_revenue] (df_sorted.groupby(category)[daily_revenue] .apply(robust_cumulative_sum) .explode() # 因apply返回Series需explode展开 .values) # 转为numpy数组对齐这个函数把“业务规则”首行无效、“数据质量”空值填充、“计算逻辑”累积求和全部封装在一起。它比原生expanding().sum()多几行代码但换来的是结果的可预测性和可审计性。在银行系统里每一个数字背后都有监管责任容不得半点模糊。3.5 多级分组的“unstack”艺术从MultiIndex到业务语言的翻译unstack()是把MultiIndex Series转为DataFrame的魔法棒但用不好就是一场灾难。最常见的错误是unstack()后出现大量NaN或者列名变成(amount, mean)这样的元组下游系统根本无法识别。根源在于unstack()默认展开最内层索引。如果你的groupby是[region, product]那么unstack()会展开product内层把region留在行索引。这符合“地区为行、产品为列”的业务直觉。但如果groupby顺序写反了比如[product, region]unstack()就会把region展开为列结果变成“产品为行、地区为列”和业务预期相反。我的黄金法则groupby的键顺序必须和你期望的“行-列”布局严格一致unstack()的参数必须显式指定要展开的层级# 明确目标行region列product # 步骤1groupby顺序必须是 [region, product] result_series df_sales.groupby([region, product])[revenue].mean() # 步骤2unstack时指定level1即product索引的第二层从0开始计数 result_df result_series.unstack(level1, fill_value0) # fill_value0避免NaN # 步骤3重命名列使其业务含义清晰 result_df.columns.name Product_Line # 列头名称 result_df.index.name Region # 行头名称fill_value0是关键。在银行报表中“某地区某产品无交易”业务上就是“0”不是“未知”。用0填充既符合会计准则也避免下游做isna()判断。我团队的模板代码里unstack()后面永远跟着fill_value0和columns.name/index.name的设置这是交付给业务方的“成品”不是给程序员看的“中间态”。4. 完整实操构建一个银行级客户交易分析流水线4.1 数据准备与探查拒绝“上来就写agg”很多新手一拿到需求就急着写groupby。这是大忌。真实生产环境80%的调试时间花在数据质量探查上。我给自己定的铁律任何聚合分析前必须完成三件事基础分布探查用describe()看数值型字段的count,mean,std,min,max,25%,50%,75%。重点关注count是否等于总行数有无缺失、min是否为负交易金额不可能为负、max是否离谱比如单笔10亿大概率是脏数据。维度基数检查用nunique()检查每个分组键的唯一值数量。df[category].nunique()如果返回500而业务上只应有5个餐饮、零售等说明分类体系混乱必须先清洗。时间序列完整性如果是时序数据用date_range生成完整日期序列再用set.difference()找出缺失日期。缺失意味着数据采集断点所有基于时间的聚合rolling, expanding都可能失效。以下是我们银行信用卡数据的标准探查模板def probe_transaction_data(df): print( 数据基础探查 ) print(f总记录数: {len(df)}) print(f时间范围: {df[date].min()} 到 {df[date].max()}) print(f客户数: {df[customer_id].nunique()}) print(f商户类别数: {df[category].nunique()}) print(\n 数值字段分布 ) num_cols [amount, fee] print(df[num_cols].describe()) print(\n 关键字段空值检查 ) print(df[num_cols [category, customer_id]].isna().sum()) print(\n 商户类别分布Top 10) print(df[category].value_counts().head(10)) # 时间完整性检查 if date in df.columns: full_dates pd.date_range(df[date].min(), df[date].max(), freqD) missing_dates set(full_dates) - set(df[date].dt.date) print(f\n 时间完整性 ) print(f应有日期数: {len(full_dates)}, 实际日期数: {df[date].dt.date.nunique()}, 缺失日期数: {len(missing_dates)}) if missing_dates: print(f缺失日期示例: {sorted(missing_dates)[:5]}) # 调用 probe_transaction_data(df_transactions)这个探查函数能在30秒内告诉你数据能不能用、哪里有问题、要不要先清洗。它是我所有分析项目的第一个单元测试。4.2 分析1客户-品类双维度聚合多重聚合实战目标回答“每个客户在每个商户类别的平均交易额、中位数、交易笔数以及手续费的最小值和最大值”。这是客户经理最常用的视图。# 步骤1定义聚合字典明确每个字段的计算逻辑 agg_dict { amount: [mean, median, count], # 交易额均值、中位数、笔数 fee: [min, max] # 手续费最小值、最大值 } # 步骤2执行聚合注意groupby顺序[customer_id, category] - 行客户列品类 multi_agg df_transactions.groupby([customer_id, category]).agg(agg_dict) # 步骤3重命名列扁平化结构 multi_agg.columns [avg_amount, med_amount, transaction_count, min_fee, max_fee] multi_agg multi_agg.reset_index() # 步骤4业务化处理中位数和均值差异过大可能暗示异常添加警示列 multi_agg[mean_med_diff_pct] ((multi_agg[avg_amount] - multi_agg[med_amount]) / multi_agg[avg_amount] * 100).round(1) # 业务规则差异超过30%标为高波动 multi_agg[volatility_flag] np.where(multi_agg[mean_med_diff_pct].abs() 30, High, Normal) print(Analysis 1: Customer-Category Transaction Summary) print(multi_agg.head(10))输出解读C001在Dining类别的avg_amount314.52med_amount307.01差异仅2.4%属正常而C002在Groceries类别avg_amount368.27med_amount351.13差异4.7%也正常。但如果某客户某品类差异达50%volatility_flag会标为High提示客户经理重点核查该客户是否存在刷单或异常消费。4.3 分析2品类风险范围计算自定义函数实战目标计算每个商户类别的交易金额范围max-min用于设定动态风控阈值。def calculate_category_range(series): 计算品类交易范围带业务规则 # 规则1有效交易笔数少于5不计算范围返回-1数据不足 valid_count series.count() if valid_count 5: return -1 # 规则2剔除明显异常值3倍标准差外 mean_val series.mean() std_val series.std() lower_bound mean_val - 3 * std_val upper_bound mean_val 3 * std_val clean_series series[(series lower_bound) (series upper_bound)] # 规则3范围必须为正否则返回-1 if len(clean_series) 2: return -1 range_val clean_series.max() - clean_series.min() return range_val if range_val 0 else -1 # 执行聚合 range_analysis df_transactions.groupby(category)[amount].agg(calculate_category_range) range_analysis range_analysis.reset_index(nametransaction_range) # 添加业务解释列 range_analysis[risk_level] pd.cut(range_analysis[transaction_range], bins[-2, 0, 200, 400, float(inf)], labels[Data Insufficient, Low, Medium, High]) print(Analysis 2: Category Risk Range Assessment) print(range_analysis)输出解读Dining范围464.69属High风险意味着该品类交易金额从几十元到几百元不等需设置更灵敏的欺诈检测规则Travel范围399.51也属High但略低于Dining。这个结果直接输入风控引擎作为动态阈值的输入。4.4 分析3客户滚动均值时间窗口实战目标计算每个客户近7个自然日的平均交易额用于识别消费习惯突变。# 确保date是datetime并设为索引 df_ts df_transactions.copy() df_ts[date] pd.to_datetime(df_ts[date]) df_ts df_ts.set_index(date).sort_index() # 步骤1按客户分组应用滚动窗口 # 注意必须用7D不是7且groupby后要reset_index(level0, dropTrue)对齐索引 rolling_result (df_ts.groupby(customer_id)[amount] .rolling(7D).mean() .reset_index(level0, dropTrue)) # 步骤2合并回原DataFrame用fillna(ffill)处理起始NaN df_ts[rolling_7day_avg] rolling_result df_ts[rolling_7day_avg] df_ts[rolling_7day_avg].fillna(methodffill) # 步骤3业务化计算与长期均值的偏离度 long_term_avg df_ts.groupby(customer_id)[amount].mean() df_ts[deviation_from_long_term] (df_ts[rolling_7day_avg] - df_ts[customer_id].map(long_term_avg)) print(Analysis 3: Customer Rolling 7-Day Average (with Business Context)) print(df_ts[[customer_id, amount, rolling_7day_avg, deviation_from_long_term]].head(15))输出解读C001在2024-01-07的rolling_7day_avg264.09而其长期均值是262.82偏离仅1.27属正常波动但如果某天偏离超过50系统会自动触发预警通知客户经理联系客户确认。4.5 分析4客户累计消费展开窗口实战目标计算每个客户的累计交易总额用于评估客户生命周期价值LTV。# 步骤1按客户和日期排序确保时间顺序 df_sorted df_ts.sort_values([customer_id, date]) # 步骤2分组后对每个客户独立计算累积和 # 使用robust_cumulative_sum跳过首行 def robust_cumsum(series): if len(series) 1: series series.iloc[1:] return series.cumsum() cumulative_result df_sorted.groupby(customer_id)[amount].apply(robust_cumsum) # explode展开因为apply返回的是Series of Series df_sorted[cumulative_spend] cumulative_result.explode().values print(Analysis 4: Customer Cumulative Spend (Robust Version)) print(df_sorted[[customer_id, date, amount, cumulative_spend]].head(15))输出解读C001在2024-01-01的cumulative_spend是NaN被跳过2024-01-04是657.84前三笔之和2024-01-07是792.26前四笔之和。这个序列是计算LTV模型的基础特征。4.6 分析5客户-品类交叉表unstack实战目标生成一个直观的交叉表展示每个客户在各商户类别的平均消费额。# 步骤1基础聚合返回MultiIndex Series crosstab_series df_transactions.groupby([customer_id, category])[amount].mean() # 步骤2unstack展开category为列fill_value0 crosstab_df crosstab_series.unstack(level1, fill_value0) # 步骤3业务化添加总计行和列 crosstab_df[Total] crosstab_df.sum(axis1) # 每个客户的总均值 crosstab_df.loc[Grand_Total] crosstab_df.sum(axis0) # 每个品类的总均值 # 步骤4格式化保留两位小数 crosstab_df crosstab_df.round(2) print(Analysis 5: Customer vs Category Average Spend Matrix) print(crosstab_df)输出解读表格清晰显示C001在Dining均值最高314.52C002在Groceries最高368.27C003在Retail最高239.29。Grand_Total行显示Groceries品类整体均值最高285.23是重点运营品类。这张表直接导出为Excel就是销售总监晨会的材料。4.7 分析6高管摘要综合聚合与列工程目标为CEO办公室生成一页纸的高管摘要包含总消费、均值、笔数、手续费等核心指标。# 步骤1基础聚合 summary df_transactions.groupby(customer_id).agg({ amount: [sum, mean, count], fee: sum }) # 步骤2扁平化列名 summary.columns [total_spend, avg_transaction, transaction_count, total_fees] # 步骤3业务列工程 summary[avg_fee_percent] ((summary[total_fees] / summary[total_spend]) * 100).round(2) summary[spend_per_transaction] (summary[total_spend] / summary[transaction_count]).round(2) summary[fee_efficiency_score] (100 - summary[avg_fee_percent]).round(2) # 手续费越低效率越高 # 步骤4排序按总消费降序便于高管快速抓住重点客户 summary summary.sort_values(total_spend, ascendingFalse) print(Analysis 6: Executive Summary Dashboard) print(summary)输出解读C002总消费最高5714.98但手续费占比也是最高2.50%fee_efficiency_score97.50