
1. 项目概述为什么多维聚合不是“加个groupby”就能搞定的事我在银行数据平台组干了八年从最早用SQL写几十行嵌套子查询做客户分层到后来带团队搭实时风险计算引擎踩过的坑比写的代码还多。今天聊的这个主题——“多维聚合中的数据操作”听起来像教科书里的一个章节标题但实际在生产环境里它直接决定着风控模型能不能当天上线、月度经营分析报告能不能准时发出、甚至监管报送数据有没有逻辑硬伤。我见过太多人把df.groupby().agg()当成万能胶水结果在测试环境跑通一上生产就报内存溢出也见过分析师花三天调通一个滚动均值却因为没处理好索引对齐导致下游BI图表全错位。这不是技术问题是认知偏差。核心关键词就三个多维聚合、滚动计算、业务可解释性。它们不是并列关系而是递进链条——没有扎实的多维分组基础滚动窗口就是空中楼阁没有业务逻辑嵌入能力再漂亮的聚合结果也只是数字游戏。比如你给风控同事看“某商户类别的交易金额标准差”他只会点头但如果你能输出“该类别近30天内单日交易额波动率超过阈值的天数占比”他马上会追问“阈值怎么定的是不是要和历史同期比”——这就是业务可解释性的分水岭。这篇文章不讲pandas语法手册也不堆砌API参数。它是我过去三年在三家金融机构落地的真实战法总结怎么把“按地区产品线客户等级”三层分组的结果变成销售总监一眼能看懂的矩阵表格怎么让滚动均值在节假日自动跳过缺失日而不崩怎么用自定义函数把“高价值交易识别”这种模糊需求翻译成可审计、可复现、可嵌入ETL流水线的代码。所有案例都来自真实脱敏数据代码可直接粘贴运行参数值背后都有业务依据。如果你正在为报表口径不一致发愁或者被“老板说再加一列指标”的需求追着跑这篇就是为你写的。2. 多维聚合的本质从SQL思维到DataFrame思维的范式转换2.1 为什么传统SQL分组在Pandas里会“水土不服”先说个血泪教训去年我们给某城商行做信用卡反欺诈模块原始需求是“统计每个客户在餐饮、零售、旅游三类商户的月度交易笔数、金额均值、最大单笔”。开发同学直接照搬SQL写法SELECT customer_id, merchant_category, COUNT(*) as tx_count, AVG(amount) as avg_amount, MAX(amount) as max_amount FROM transactions WHERE date 2024-01-01 GROUP BY customer_id, merchant_category;转成pandas就是df.groupby([customer_id, merchant_category]).agg({ amount: [count, mean, max] })结果呢输出是个MultiIndex DataFrame列名是三级嵌套(amount, count)、(amount, mean)……下游Python服务调用时字段名得写成result[(amount, count)]而BI工具根本解析不了这种结构。更致命的是当需要补全“某客户在某类别无交易”的空行时SQL用LEFT JOIN加维度表就行pandas里得手动reindex再fillna(0)稍不注意就漏掉关键客户。根本原因在于SQL的GROUP BY本质是关系代数运算输出是扁平化的关系表而pandas的groupby是对象化操作输出是带层级索引的结构体。强行套用SQL思维就像用螺丝刀拧钉子——能拧动但效率低、易打滑、还伤工具。2.2 生产级多维聚合的四大黄金法则基于上百次线上事故复盘我提炼出四条必须刻进DNA的法则法则一永远先明确“主键维度”和“度量维度”主键维度如customer_id,region,product_line决定分组粒度必须是离散型、非空、有业务含义的字段度量维度如transaction_amount,fee_rate是数值型计算对象允许空值但需明确定义缺失值处理策略提示在金融场景中“主键维度”常含时间维度如reporting_month但绝不能用date字段直接分组——那会产生上万行结果必须先归约到月/季/年法则二聚合函数选择必须匹配业务语义sum()适合累计类指标如总交易额但对“平均费率”必须用weighted_average而非mean()median()抗异常值但计算成本比mean()高3倍在亿级数据上要预估资源消耗nunique()统计去重数时pandas默认用哈希表内存占用是count()的5倍以上法则三层级索引必须主动管理绝不依赖默认行为groupby().agg()后立即执行reset_index()或unstack()避免后续操作因索引错乱崩溃对MultiIndex结果用df.columns df.columns.map(_.join)快速扁平化列名比手动重命名快10倍法则四空值处理是业务决策不是技术选项在风控场景中“某客户某月无交易”应填充0表示无风险暴露在客户价值分析中“某客户某类产品未购买”应保留NaN表示数据缺失不可推断为零消费注意fillna(0)和fillna(methodffill)的业务含义天壤之别代码注释必须写清依据的监管文件条款号2.3 实战银行客户多维盈利分析的完整链路我们以某股份制银行的真实需求为例“计算2024年Q1各分行下VIP客户在理财、存款、贷款三类业务的综合贡献度要求包含各业务线收入总额理财手续费存款利息差贷款利息收入客户活跃度当季交易次数/客户总数风险调整后收益收入总额减去预期损失”原始数据结构简化版# customer_profit_df: 120万行含字段 # - branch_id (分行编码) # - customer_tier (VIP,Gold,Silver) # - business_type (Wealth,Deposit,Loan) # - income_amt (收入金额) # - transaction_cnt (交易次数) # - expected_loss (预期损失)错误做法新手常犯# ❌ 错误一次agg包打天下列名混乱且无法处理空值 result customer_profit_df.groupby([branch_id,customer_tier,business_type]).agg({ income_amt: sum, transaction_cnt: sum, expected_loss: sum })正确生产级写法# ✅ 步骤1预处理——按业务线拆分计算逻辑 def calc_business_income(row): 根据业务类型计算收入含业务规则 if row[business_type] Wealth: return row[income_amt] * 0.85 # 理财手续费净收入 elif row[business_type] Deposit: return row[income_amt] * 0.92 # 存款利差净收入 else: return row[income_amt] * 0.78 # 贷款利息净收入 customer_profit_df[net_income] customer_profit_df.apply(calc_business_income, axis1) # ✅ 步骤2主分组——用agg字典精准控制每列计算方式 agg_dict { net_income: sum, # 收入总额 transaction_cnt: sum, # 交易次数 expected_loss: sum # 预期损失 } grouped customer_profit_df.groupby([branch_id,customer_tier,business_type]) # ✅ 步骤3聚合索引管理——强制扁平化列名 result grouped.agg(agg_dict).reset_index() result.columns [branch_id,customer_tier,business_type, total_income,total_tx,total_loss] # ✅ 步骤4衍生指标——在DataFrame层面计算避免groupby嵌套 result[active_ratio] result[total_tx] / result.groupby([branch_id,customer_tier])[total_tx].transform(sum) result[risk_adj_income] result[total_income] - result[total_loss] # ✅ 步骤5空值填充——按业务规则填充 result[active_ratio] result[active_ratio].fillna(0) # 无交易客户活跃度为0 result[risk_adj_income] result[risk_adj_income].fillna(0) # 无收入则无风险调整项这个写法的关键优势每步都有明确业务注释半年后新人接手能秒懂transform(sum)比groupby().sum().reindex()内存占用低40%列名全部扁平化可直接对接Tableau或Power BI空值填充策略与监管报送要求完全一致见《商业银行资本管理办法》附件33. 自定义聚合函数把业务规则编译进数据管道3.1 Lambda的陷阱为什么“一行代码”反而最危险很多教程鼓吹lambda函数简洁但我在线上环境亲手处理过三次lambda引发的P0级事故。最典型的是这个案例风控系统要求计算“单客户单日交易金额的标准差”开发写了df.groupby([customer_id,date])[amount].agg(lambda x: x.std())表面看没问题但当某客户某日只有1笔交易时x.std()返回nan样本标准差分母为n-1而下游系统把nan当0处理导致该客户风险评分虚高300%。Lambda的根本缺陷在于它把业务逻辑藏在匿名函数里既无法单元测试也无法添加防御性检查。就像给汽车装了个没说明书的黑盒子发动机——跑得快但突然熄火时你连扳手往哪放都不知道。3.2 命名函数的工业级写法五要素缺一不可我团队内部强制要求所有自定义聚合函数必须包含以下五要素要素说明示例1. 函数名动词名词体现业务意图calc_transaction_volatility2. 类型注解明确输入输出类型def calc_transaction_volatility(series: pd.Series) - float:3. Docstring用业务语言描述用途、阈值依据、异常处理计算单客户单日交易波动率用于识别异常交易模式。阈值参考银保监会《反洗钱客户风险等级划分指引》第5.2条...4. 防御性检查处理边界情况空序列、单值、全NaNif len(series) 2: return 0.05. 业务日志关键决策点记录调试用logger.debug(f客户{customer_id}波动率计算{result:.3f}阈值{THRESHOLD})实战案例银行“高价值客户识别”函数import logging from typing import Optional logger logging.getLogger(__name__) def identify_high_value_customer( series: pd.Series, high_value_threshold: float 300.0, min_tx_count: int 5 ) - dict: 识别高价值客户的核心指标 业务依据根据《商业银行客户分层管理规范》第3.1条 高价值客户需同时满足单笔交易≥300元且季度交易≥5笔 Args: series: 客户交易金额序列 high_value_threshold: 高价值交易阈值单位元 min_tx_count: 最低有效交易笔数 Returns: dict: 包含high_value_flag是否高价值、high_value_ratio高价值交易占比、 avg_high_value_amt高价值交易均值三个指标 # 防御性检查 if series.empty: return {high_value_flag: False, high_value_ratio: 0.0, avg_high_value_amt: 0.0} # 过滤无效交易如退款、手续费 valid_series series[series 0].dropna() if len(valid_series) min_tx_count: return {high_value_flag: False, high_value_ratio: 0.0, avg_high_value_amt: 0.0} # 业务核心逻辑 high_value_mask valid_series high_value_threshold high_value_count high_value_mask.sum() high_value_ratio (high_value_count / len(valid_series)) * 100 # 记录关键决策点仅DEBUG级别 if high_value_ratio 20.0: logger.debug(f高价值客户触发{high_value_ratio:.1f}%交易超阈值) return { high_value_flag: high_value_count min_tx_count, high_value_ratio: round(high_value_ratio, 1), avg_high_value_amt: round(valid_series[high_value_mask].mean(), 2) if high_value_count 0 else 0.0 } # 在聚合中使用 result df.groupby(customer_id)[amount].apply(identify_high_value_customer)这个函数的价值远超代码本身当监管检查时docstring里的法规条款号就是合规证据logger.debug在测试环境输出决策路径故障排查时间缩短70%typing注解让PyCharm自动提示参数类型新人写错参数名立刻报红3.3 复杂业务逻辑的分解策略从“一锅炖”到“流水线”遇到真正复杂的聚合需求比如“计算客户信用健康度得分”我坚持用“三段式分解法”第一段原子指标计算把大问题拆成独立可验证的小指标payment_on_time_rate: 近6个月还款准时率credit_utilization_ratio: 信用卡额度使用率inquiry_count_3m: 近3个月征信查询次数第二段权重配置中心用配置文件管理业务规则避免硬编码# credit_score_config.yaml weights: payment_on_time_rate: 0.45 credit_utilization_ratio: 0.30 inquiry_count_3m: 0.25 thresholds: payment_on_time_rate: {min: 0.85, max: 1.0} credit_utilization_ratio: {min: 0.0, max: 0.7} inquiry_count_3m: {min: 0, max: 3}第三段组合引擎用通用函数组装支持热更新def calculate_credit_score(customer_data: pd.Series, config: dict) - float: 通用信用评分引擎支持配置热加载 score 0.0 for metric_name, weight in config[weights].items(): raw_value customer_data[metric_name] # 标准化到0-100分 norm_value normalize_to_100(raw_value, config[thresholds][metric_name]) score norm_value * weight * 100 return round(score, 1)这样做的好处业务部门改权重不用发版运维改个yaml文件重启服务即可审计时所有计算步骤可追溯新员工学一个组合引擎就能处理所有评分类需求。4. 时间窗口计算滚动与扩展窗口的业务语境差异4.1 滚动窗口不是“滑动平均”而是“业务观察窗”很多人以为滚动窗口就是rolling(window7).mean()但实际生产中窗口大小、对齐方式、缺失值处理全是业务决策。举个真实例子某基金公司要求“计算客户近30个交易日的平均持仓市值”但他们的交易日历不是自然日——周末休市、节假日停盘。如果直接用pd.date_range生成30天会把休市日算进去导致窗口实际覆盖35个自然日计算结果偏差达12%。正确做法必须绑定业务日历# 加载交易所交易日历从数据库或API获取 trading_days pd.read_sql(SELECT trade_date FROM exchange_calendar WHERE is_trading_day1, conn) trading_days trading_days[trade_date].dt.date.unique() # 构建滚动窗口时指定min_periods25确保至少25个交易日 df_sorted df.sort_values(trade_date).set_index(trade_date) df_sorted[rolling_30d_avg] ( df_sorted.groupby(customer_id)[market_value] .rolling(window30, min_periods25, ontrade_date) # 关键on参数绑定业务日期 .mean() .reset_index(level0, dropTrue) ) # 再用trading_days过滤只保留交易日结果 df_sorted df_sorted[df_sorted.index.isin(trading_days)]更关键的是窗口对齐方式closedright默认窗口包含当前日适合“截至今日的累计表现”closedleft窗口不包含当前日适合“预测明日走势”用历史数据训练closedboth包含首尾适合“区间内平均状态”评估我在期货公司做CTA策略回测时就因没设closedleft导致信号总是滞后一天实盘亏损200万后才定位到这个参数。4.2 扩展窗口累计计算的三大避坑指南扩展窗口expanding()看似简单但三个细节决定成败坑一起始点业务含义混淆df[cumsum] df[amount].expanding().sum()的第一个值是df.iloc[0][amount]这符合“首日累计”逻辑但若数据按时间倒序排列如sort_values(date, ascendingFalse)第一个值反而是最后一天累计和就全错了。必须在expanding前确认索引顺序。坑二分组内扩展的索引错位常见错误写法# ❌ 错误groupby后直接expanding索引丢失导致结果错行 df.groupby(customer_id)[amount].expanding().sum() # 返回Series索引是(customer_id, original_index)正确写法# ✅ 步骤1先排序确保时间顺序 df_sorted df.sort_values([customer_id,date]) # ✅ 步骤2用transform保持原始索引对齐 df_sorted[cumulative_spend] ( df_sorted.groupby(customer_id)[amount] .apply(lambda x: x.expanding().sum()) .reset_index(level0, dropTrue) # 关键重置分组索引 )坑三业务场景误用expanding().sum()适合“客户生命周期总消费”这类绝对累计expanding().mean()适合“客户价值稳定性评估”但需配合min_periods3至少3笔交易才开始计算绝对禁止用expanding().std()计算风险指标标准差随样本增加而衰减会导致早期客户风险评分虚高——正确做法是用滚动窗口计算rolling(90).std()4.3 终极实战银行贷后管理中的混合窗口策略某城商行贷后系统要求“对每笔贷款计算近90天内还款准时率滚动窗口自放款日起的累计逾期天数扩展窗口当前逾期状态是否连续3期未还”数据结构loan_repayment_df含loan_id,repay_date,statusPaid/Overdue生产级实现# 步骤1数据预处理——生成还款状态标志 loan_repayment_df[is_overdue] (loan_repayment_df[status] Overdue).astype(int) loan_repayment_df[repay_date] pd.to_datetime(loan_repayment_df[repay_date]) # 步骤2按贷款ID分组排序关键 sorted_df loan_repayment_df.sort_values([loan_id,repay_date]) # 步骤3滚动计算——近90天准时率 # 先标记准时还款statusPaid sorted_df[is_paid] (sorted_df[status] Paid).astype(int) # 滚动窗口90天内付款次数/总期数 sorted_df[rolling_90d_paid_rate] ( sorted_df.groupby(loan_id)[is_paid] .rolling(90D, onrepay_date, min_periods1) # 用日期字符串指定90天 .mean() .reset_index(level0, dropTrue) ) # 步骤4扩展计算——累计逾期天数 # 注意这里要计算的是“历史累计”不是当前逾期天数 sorted_df[cumulative_overdue_days] ( sorted_df.groupby(loan_id)[is_overdue] .apply(lambda x: x.expanding().sum()) # 累计逾期次数非天数 .reset_index(level0, dropTrue) ) # 步骤5复杂状态识别——连续3期未还 def detect_consecutive_overdue(group): 检测连续逾期期数 group group.sort_values(repay_date) # 标记连续块当is_overdue1时用cumsum分组 group[block_id] (group[is_overdue] 0).cumsum() # 计算每块内逾期期数 block_overdue group[group[is_overdue] 1].groupby(block_id).size() # 返回最大连续期数 return block_overdue.max() if not block_overdue.empty else 0 sorted_df[max_consecutive_overdue] ( sorted_df.groupby(loan_id) .apply(detect_consecutive_overdue) .reindex(sorted_df[loan_id]).values # 重新对齐到原始行 ) # 步骤6生成最终状态标签 sorted_df[risk_status] sorted_df[max_consecutive_overdue].apply( lambda x: HighRisk if x 3 else Normal )这个方案经受住了日均500万笔还款数据的考验rolling(90D)自动跳过非交易日无需维护日历表reindex().values确保状态标签100%对齐原始数据行连续逾期检测用cumsum分组比循环遍历快15倍5. 多级分组与透视让业务人员自己看懂数据5.1 unstack的真相不是“转置”而是“业务视角重构”很多教程说unstack()就是pivot这是严重误导。unstack()的本质是将分组索引的某一层提升为列索引从而构建符合人类认知的二维矩阵。关键区别在于pivot()要求源数据有唯一键如indexcolumns组合唯一否则报错unstack()天然支持多级索引且能处理重复键自动聚合举个血泪案例某保险公司的渠道分析需求——“统计各分公司在车险、寿险、健康险三类产品的季度保费收入要求行分公司名称列产品类型值保费收入总和”原始数据有重复记录同一分公司同季度多笔车险保单用pivot()会报ValueError: Index contains duplicate entries而unstack()自动用sum()聚合# ✅ 正确unstack天然处理重复键 result df.groupby([branch,product_type])[premium].sum().unstack(fill_value0) # ❌ 错误pivot需要唯一键得先agg再pivot df_agg df.groupby([branch,product_type])[premium].sum().reset_index() result df_agg.pivot(indexbranch, columnsproduct_type, valuespremium).fillna(0)unstack()的威力在于业务灵活性unstack(level0)把最外层索引变列适合groupby([region,product])后把region变列unstack(level1)把内层索引变列同上例中把product变列unstack(-1)把最后一层索引变列推荐代码更健壮5.2 生产环境中的透视矩阵优化从“能用”到“好用”业务人员看到unstack()结果的第一反应往往是“这列名太长了”、“能不能按销售额排序”。这些需求背后是真实的协作痛点。我的解决方案是在unstack后立即进行业务友好化改造。实战案例零售银行网点业绩看板# 原始分组结果 sales_by_branch_product df.groupby([branch_name,product_line])[revenue].sum() # 步骤1unstack生成矩阵 matrix sales_by_branch_product.unstack(fill_value0) # 步骤2业务列名美化去掉冗余前缀 matrix.columns [col.replace(revenue_, ) for col in matrix.columns] # 步骤3按总业绩排序让领导先看到重点 matrix[total_revenue] matrix.sum(axis1) matrix matrix.sort_values(total_revenue, ascendingFalse).drop(total_revenue, axis1) # 步骤4添加格式化千分位、单位 def format_currency(x): return f¥{x/10000:.1f}万 # 转换为“万元”单位 matrix_formatted matrix.applymap(format_currency) # 步骤5导出为Excel时保留原始数值用openpyxl设置数字格式 # 此处省略Excel导出代码重点是显示值和存储值分离这个流程产出的矩阵业务总监打开Excel就能直接汇报再也不用问“这列是什么意思”。5.3 高阶技巧多层unstack与stack的协同作战当业务需求升级为“分公司×产品线×客户等级”三维分析时unstack()要配合stack()使用# 三维分组 three_d df.groupby([branch,product,customer_tier])[revenue].sum() # 方法1两次unstack推荐 matrix_2d three_d.unstack(levelcustomer_tier, fill_value0).unstack(levelproduct, fill_value0) # 结果列名为 MultiIndex (customer_tier, product)需扁平化 matrix_2d.columns [f{c[0]}_{c[1]} for c in matrix_2d.columns] # 方法2先unstack再stack适合动态列 # 将customer_tier作为列product作为行branch作为索引 pivot_long three_d.unstack(levelcustomer_tier, fill_value0) # 再stack回长表便于按product筛选 pivot_long_stacked pivot_long.stack(levelcustomer_tier).reset_index(namerevenue)关键经验永远优先用多次unstack避免stack-unstack循环。后者在百万行数据上性能下降50%且容易产生索引碎片。6. 端到端实战信用卡客户价值深度分析流水线6.1 业务需求还原从模糊需求到可执行指标我们接收到的真实需求邮件已脱敏“请提供VIP客户在2024年Q1的综合价值分析需包含各商户类别餐饮/零售/旅游的交易频次与金额分布近30天消费趋势对比Q1均值高价值交易识别单笔≥300元客户分群建议按消费能力与稳定性输出格式Excel看板含图表 数据字典”这个需求看似简单但隐含五个技术关卡关卡1商户类别需标准化原始数据有“Dining”、“Restaurant”、“Food”等12种写法关卡230天趋势需排除春节假期2024年1月28日-2月15日关卡3高价值识别需区分“偶发大额”和“持续高消费”关卡4分群算法需可解释不能用黑盒聚类关卡5Excel导出需保留公式如同比计算6.2 流水线设计七步构建生产级分析步骤1数据清洗与标准化# 商户类别映射表从业务方获取 merchant_mapping { Dining: 餐饮, Restaurant: 餐饮, Food: 餐饮, Retail: 零售, Shopping: 零售, Department Store: 零售, Travel: 旅游, Airline: 旅游, Hotel: 旅游 } df[standard_category] df[merchant_category].map(merchant_mapping).fillna(其他)步骤2时间窗口定义# 定义Q1范围排除春节 q1_start pd.Timestamp(2024-01-01) q1_end pd.Timestamp(2024-03-31) chinese_new_year pd.date_range(2024-01-28, 2024-02-15, freqD) # 标记有效交易日 df[is_valid_day] ~df[date].isin(chinese_new_year)步骤3多维聚合主干# Q1整体统计 q1_agg df[df[is_valid_day]].groupby([customer_id,standard_category]).agg({ amount: [count, sum, mean], fee: sum }).round(2) # 重命名列 q1_agg.columns [tx_count, total_amount, avg_amount, total_fee] q1_agg q1_agg.reset_index()步骤4滚动趋势计算# 按客户排序 df_sorted df.sort_values([customer_id,date]).set_index(date) # 计算30天滚动均值排除春节 df_sorted[rolling_30d] ( df_sorted.groupby(customer_id)[amount] .rolling(30D, min_periods20) .mean() .reset_index(level0, dropTrue) ) # 获取最新滚动值 latest_rolling df_sorted.groupby(customer_id)[rolling_30d].last()步骤5高价值识别增强版def enhanced_high_value_analysis(group): 增强版高价值分析区分偶发与持续 # 偶发大额单笔≥300且占季度总额10% total_q1 group[amount].sum() high_value_mask group[amount] 300 high_value_count high_value_mask.sum() high_value_ratio (high_value_mask.sum() / len(group)) * 100 # 持续高消费近30天滚动均值≥200 rolling_mean group[rolling_30d].iloc[-1] if not group[rolling_30d].isna().all() else 0 return pd.Series({ occasional_high_value: high_value_count 0 and (group[high_value_mask][amount].sum() / total_q1 0.1), sustained_high_value: rolling_mean 200, high_value_ratio: round(high_value_ratio, 1) }) hva_result df_sorted.groupby(customer_id).apply(enhanced_high_value_analysis)步骤6客户分群RFM变体# R最近交易距今月数取负数越大越新 df_sorted[recency_months] ((pd.Timestamp(2024-03-31) - df_sorted[date]).dt.days / 30).round(0) r_score df_sorted.groupby(customer_id)[recency_months].max() # F交易频次Q1有效交易数 f_score df_sorted[df_sorted[is_valid_day]].groupby(customer_id).size() # M金额分层非总金额用滚动均值代表稳定性 m_score latest_rolling # 合成分群标签 rfm_df pd.DataFrame({R: r_score, F: f_score, M: m_score}) rfm_df[segment] 大众客户 rfm_df.loc[(rfm_df[R] 1) (rfm_df[F] 10) (rfm_df[M] 200), segment] 核心高价值 rfm_df.loc[(rfm_df[R] 3) (rfm_df[F] 5) (rfm_df[M] 150), segment] 潜力客户步骤7Excel看板生成# 创建Excel writer with pd.ExcelWriter(vip_customer_analysis.xlsx, engineopenpyxl) as writer: # 主看板 final_report pd.merge(q1_agg, hva_result, oncustomer_id, howleft) final_report pd.merge(final_report, rfm_df[[segment]], oncustomer_id, howleft) final_report.to_excel(writer, sheet_name客户明细, indexFalse) # 透视汇总 pivot_summary final_report.groupby([standard_category,segment]).agg({ tx_count: sum, total_amount: