1. 项目概述为什么多维聚合不是“会groupby就行”而是数据工程师的分水岭我在银行风控系统干了八年从写第一个SQL报表到带三支数据分析团队踩过最深的坑往往不是模型不准而是聚合逻辑一错整条指标链就崩了。你可能也遇到过业务方要“按客户产品线地区看近30天滚动平均交易额同时算出每个组合的交易金额中位数、标准差、最大最小值再标出高价值交易占比”——这时候如果还用df.groupby([cust,prod,region]).agg({amount: mean})硬套要么跑不出结果要么跑出来是嵌套三层的MultiIndex下游BI工具根本接不住更别说加个条件判断或动态阈值了。这不是Pandas用得熟不熟的问题而是你有没有建立起一套可复用、可审计、可扩展的聚合思维框架。这篇内容讲的就是这套框架。它不叫“高级技巧”我管它叫“生产级聚合基建”。关键词里那个“Towards AI”其实是个信号——它代表的是真实工业场景不是Jupyter Notebook里跑通就完事的玩具数据。你看到的每一段代码背后都对应着银行反欺诈系统的实时计算任务、信用卡中心的月度经营分析看板、或是资管公司风险敞口日报的ETL流水线。比如当风控同事说“请把商户类别维度下的交易金额波动率max-min拉出来我们明天早会要用”他真正要的不是一行lambda而是这个指标能稳定跑进凌晨三点的调度任务且三年后新人接手时光看函数名和docstring就能懂业务意图。这就是为什么我要花大篇幅讲unstack之后怎么填空值、rolling窗口的min_periods设成多少才不丢首周数据、expanding累计和在并发写入时如何避免重复计数——这些细节教科书不写但线上告警电话半夜打来时它们就是你的救命稻草。核心关键词“多维聚合”拆开看是三个硬骨头多不止一个分组键、维时间、空间、业务层级等不同维度需协同处理、聚合不是简单求和而是带状态、带上下文、带业务规则的计算。它解决的典型问题比如某省分行发现零售贷款不良率突然跳升你要在5分钟内定位是哪个地市、哪类产品、哪类客群在恶化又比如运营团队想验证“满减活动对高频低额用户是否比对低频高额用户更有效”你需要在同一张表里同时产出分组后的均值、中位数、分位数、以及自定义的“活动响应强度指数”。这些需求用基础groupby拼凑代码会像毛线团一样越绕越紧而用本文的结构化方法你写的不是脚本是可组装的分析模块。适合谁刚转行的数据分析师、卡在ETL效率瓶颈的初级数据工程师、需要给业务方交付稳定指标的BI开发甚至包括那些总被问“为什么上个月数据和这个月对不上”的数据产品经理——因为所有差异90%都藏在聚合逻辑的细微差别里。2. 多维聚合的整体设计思路从“堆代码”到“搭积木”的范式转换2.1 为什么必须放弃“单点突破”思维我见过太多人把聚合当成一道数学题给定输入套公式出答案。这在Kaggle比赛里行得通但在银行系统里会死得很惨。举个真实案例去年我们做信用卡分期业务分析最初版本用df.groupby([customer_id, product_type]).agg({amount: [sum, count]})跑得飞快。上线两周后业务方提了个需求“请把‘新客首笔分期’单独标记出来只统计他们前三笔交易”。有人直接加了个query(is_new_customer True).head(3)结果整个作业耗时从2分钟涨到47分钟因为head()触发了全量排序。后来我们重构把“新客识别”作为预处理步骤生成布尔列再用agg一次完成耗时回到2.3分钟。这个教训的核心是聚合不是孤立操作而是数据流中的一个节点它的上游输入质量、下游消费方式决定了你选哪种聚合策略。所以我的设计原则第一条先画数据血缘图再写代码。拿到需求第一件事不是打开PyCharm而是手绘三样东西① 输入数据的原始结构字段、类型、空值率、时间范围② 输出目标形态是给Tableau拖拽的宽表还是给Spark做特征工程的长表或是API返回的JSON③ 中间依赖项比如“滚动平均”需要保证数据按时间严格排序“多级分组”需要确认各维度的基数比是否会导致内存爆炸。这三样画清楚80%的架构问题就解决了。比如如果你知道下游是Excel那unstack后必须用fill_value0否则Excel会把NaN当错误如果你知道数据要进Flink实时计算那rolling(window30)就得换成TUMBLING窗口因为Pandas的滚动是基于索引位置而流式计算是基于事件时间。2.2 四大核心模式的选型逻辑什么场景该用哪种不是所有聚合都值得用高级语法。我按使用频率和复杂度把本文覆盖的模式分成四档每档都有明确的“入场券”第一档多列多函数聚合agg({col: [func1, func2]})入场券需求里出现“同时”“并”“及”这类连词。比如“既要平均交易额也要中位数还要标准差”。这是最安全的起点因为它不改变数据形状只是丰富了指标维度。优势在于① 计算一次完成避免多次groupby的IO开销② 所有结果在同一DataFrame里后续过滤、排序、导出都方便。但要注意陷阱当transaction_amount列有大量空值时mean和median会给出不同数量的有效样本导致业务方质疑“为什么平均值和中位数的分母不一样”——这时必须加.dropna()或明确skipnaTrue参数并在文档里注明处理逻辑。第二档自定义聚合函数agg({col: custom_func})入场券需求里出现“按业务规则”“根据XX阈值”“需考虑权重”等描述。比如“高价值交易占比”“加权平均费率”。这是区分初级和中级工程师的关键。很多人写lambda但lambda无法调试、无法加日志、无法单元测试。我的铁律是任何超过10行逻辑、或涉及条件分支的聚合必须写成命名函数。函数名要直白比如def calc_fraud_risk_score(series):而不是def f(x):docstring里必须写清业务依据比如“参考银保监发〔2023〕12号文单笔超300元视为高风险交易”。这样半年后审计时你不用翻聊天记录看函数就知道合规依据在哪。第三档滚动与扩展窗口rolling()/expanding()入场券需求里出现“最近N天”“截至当前”“累计”“同比/环比”等时间动态词。这是最容易翻车的档位。新手常犯的错是忽略min_periods参数。比如rolling(window7).mean()前6天全是NaN如果下游系统没做空值处理整个看板就显示一片空白。我的经验是对监控类指标设min_periods1用首日数据填充对分析类指标设min_periods3宁可少算几天也不用不可靠的均值。另外expanding看似简单但要注意它默认从第一行开始累积如果数据有乱序比如日志延迟到达结果会错。必须前置sort_values(event_time).drop_duplicates(subset[id], keeplast)这是血泪教训。第四档多级分组重塑groupby([a,b]).agg().unstack()入场券需求里出现“交叉分析”“矩阵视图”“对比A和B”等表述。比如“各城市不同年龄段用户的客单价对比”。这是最高阶的因为涉及数据形态的根本转变。unstack不是万能的它要求分组键中至少有一个维度的取值是有限且稳定的比如“产品线”只有5个而“客户ID”有千万级就不能unstack。我的检查清单是① 用nunique()确认低基数维度② 用fillna(0)或fillna(np.nan)明确缺失值含义③ 用reset_index()把索引变回普通列否则下游系统可能报错。很多BI工具只认扁平列名所以最后一步columns [_.join(col).strip() for col in result.columns.values]是刚需。这四档不是递进关系而是并列选项。一个需求可能同时用到二档和四档比如“按客户产品线计算加权平均交易额再按产品线unstack成宽表”。关键在于你得清楚每一档的适用边界和代价而不是看到“高级”就往上堆。3. 核心细节解析与实操要点那些文档里不会写的“脏活”3.1 多列聚合的列名陷阱与扁平化实战看原文示例输出是这样的transaction_amount processing_fee mean median min max Dining 55.10 52.30 1.36 2.03这种双层列名MultiIndex在Pandas里很优雅但在生产环境里是灾难。BI工具如Power BI、Tableau导入时会把(transaction_amount, mean)当做一个字符串列名导致你无法用df[transaction_amount_mean]引用Excel导出后表头会变成两行需要手动合并更糟的是如果后续要做merge列名不匹配直接报错。所以任何进入生产环境的聚合结果第一步必须扁平化列名。我用的不是简单的result.columns [_.join(col) for col in result.columns]因为那会产生transaction_amount_mean和processing_fee_min这种冗长名字。我的方案是按业务语义重命名而非机械拼接。比如# 原始聚合 result df.groupby(merchant_category).agg({ transaction_amount: [mean, median], processing_fee: [min, max] }) # 扁平化并重命名这才是生产级写法 result.columns [ avg_txn_amt, # transaction_amount_mean → 业务缩写 med_txn_amt, # transaction_amount_median → 业务缩写 min_proc_fee, # processing_fee_min → 业务缩写 max_proc_fee # processing_fee_max → 业务缩写 ] result result.reset_index() # 索引变列方便后续操作为什么这么干因为业务方永远记不住transaction_amount_mean但他知道avg_txn_amt是“平均交易金额”。而且当你写SQL或配置BI工具时列名越短出错概率越低。这个重命名过程我固化成了一个函数flatten_agg_columns(df, mapping_dict)mapping_dict里存着所有业务术语的标准映射确保全团队命名一致。这是让代码具备“可读性”的最小成本投入。提示扁平化后务必用result.dtypes检查数据类型。有时agg会把整数列转成float64因为mean计算如果下游系统要求int类型得用result[avg_txn_amt] result[avg_txn_amt].round(2).astype(float)显式转换避免隐式转换带来的精度丢失。3.2 自定义函数的调试与性能优化别让lambda毁掉你的SLA原文用lambda x: x.max() - x.min()演示范围计算这在小数据上没问题。但放到银行日交易量5000万笔的场景这个lambda会被调用数百万次每次都要创建新对象、执行两次遍历。我做过压测对100万行数据lambda版比命名函数慢17%而命名函数里加个if len(series) 2: return np.nan判断还能避免空组报错。所以所有自定义聚合函数必须包含三要素空值防御、类型校验、业务注释。以“风险分段”函数为例原文Analysis 7我实际用的版本是def risk_segmentation(series, high_value_threshold300, low_value_threshold50): 风险分段聚合按交易金额阈值划分客户行为 业务依据根据《信用卡业务风险管理指引》第5.2条 单笔超300元为高价值交易低于50元为试探性小额交易 Parameters: ----------- series : pd.Series 交易金额序列 high_value_threshold : float 高价值交易判定阈值单位元 low_value_threshold : float 小额交易判定阈值单位元 Returns: -------- pd.Series : 包含high_value_count, high_value_pct, low_value_count, low_value_pct, regular_avg五个指标 # 空值防御移除NaN但保留原始长度用于百分比计算 clean_series series.dropna() total_count len(series) # 注意用原始长度非clean_series长度 if total_count 0: return pd.Series({ high_value_count: 0, high_value_pct: 0.0, low_value_count: 0, low_value_pct: 0.0, regular_avg: np.nan }) # 类型校验确保是数值型 if not pd.api.types.is_numeric_dtype(clean_series): raise TypeError(fSeries must be numeric, got {clean_series.dtype}) # 业务逻辑计算 high_mask clean_series high_value_threshold low_mask clean_series low_value_threshold return pd.Series({ high_value_count: high_mask.sum(), high_value_pct: (high_mask.sum() / total_count * 100).round(1), low_value_count: low_mask.sum(), low_value_pct: (low_mask.sum() / total_count * 100).round(1), regular_avg: clean_series[~high_mask ~low_mask].mean() }) # 调用时传参而非硬编码 result df_transactions.groupby(customer_id)[amount].apply( risk_segmentation, high_value_threshold300, low_value_threshold50 )这个函数能直接进生产环境因为① docstring里写了法规依据审计时直接截图② 参数可配置不同业务线用不同阈值③total_count用原始长度保证百分比分母一致④ 显式raise TypeError让错误暴露在ETL早期而不是等报表跑出来才发现数据异常。3.3 滚动窗口的“时间陷阱”排序、空值、边界处理全指南原文rolling示例里数据是按日期生成的所以set_index(date)后直接滚动。但现实数据哪有这么乖日志可能乱序、补录数据可能插在中间、不同系统时间戳精度不一毫秒vs秒。我见过最惨的案例一个支付网关日志因服务器时钟漂移导致2024-01-05 23:59:59的记录排在了2024-01-06 00:00:01前面rolling(window7)算出来的7日均值包含了未来一天的数据整个风控模型失效。所以滚动窗口前的三步强制校验强制排序df df.sort_values([customer_id, event_time]).reset_index(dropTrue)去重保序df df.drop_duplicates(subset[customer_id, event_time], keeplast)保留最新更新的记录时间对齐如果event_time是毫秒级而业务要求“按天滚动”得先df[date] df[event_time].dt.date再set_index(date)关于空值原文用NaN这在分析时没问题但在生产调度里是定时炸弹。我的做法是根据指标用途选择填充策略。监控类如实时交易量用fillna(methodffill)用最近有效值填充保证看板不中断分析类如客户行为报告用fillna(0)明确表示“当日无交易”避免误判为数据缺失合规类如反洗钱可疑交易dropna()宁可少报也不用推测值。最后是边界处理。window7时前6行是NaN但业务方可能要求“首周用3日均值替代”。这时不能硬写min_periods3因为min_periods只控制计算所需最小样本数不改变窗口大小。正确解法是用rolling配合apply自定义逻辑def adaptive_rolling_mean(series, window7, min_periods3): 自适应滚动均值窗口不足时用实际可用数据计算 if len(series) min_periods: return np.nan # 取最后window个值但允许少于window个当数据不足时 valid_data series.iloc[-window:] # 从末尾取保证是最新数据 return valid_data.mean() # 应用 df_ts[adaptive_rolling_avg] df_ts.groupby(category)[daily_revenue].apply( lambda x: x.rolling(window7, min_periods1).apply(adaptive_rolling_mean, rawTrue) )这个函数确保只要有1个数据就计算但最多用7个完美适配“首周用3日均值”的需求。4. 实操过程与核心环节实现从零搭建一个银行级交易分析流水线4.1 数据准备与探查别跳过这步否则后面全是坑所有高大上的聚合都建立在干净的数据上。我绝不相信任何“已清洗好”的数据源。拿到原始交易表我的第一份代码永远是import pandas as pd import numpy as np # 1. 基础信息扫描 def scan_data(df, nameraw_data): print(f\n {name} 数据探查报告 ) print(f总行数: {len(df)} | 总列数: {len(df.columns)}) print(f内存占用: {df.memory_usage(deepTrue).sum() / 1024**2:.2f} MB) # 2. 缺失值热力图用文字模拟 missing_pct (df.isnull().sum() / len(df) * 100).round(2) print(\n--- 缺失值比例5%标红---) for col, pct in missing_pct.items(): mark if pct 5 else print(f{col:20s}: {pct:5.2f}%{mark}) # 3. 关键列分布针对交易场景 key_cols [amount, fee, customer_id, category, date] for col in key_cols: if col in df.columns: if pd.api.types.is_numeric_dtype(df[col]): print(f\n--- {col} 数值分布 ---) print(f均值: {df[col].mean():.2f} | 中位数: {df[col].median():.2f} | f标准差: {df[col].std():.2f} | 极差: {df[col].max()-df[col].min():.2f}) else: print(f\n--- {col} 类别分布Top 5---) print(df[col].value_counts().head(5)) # 4. 时间范围检查对时序数据致命 if date in df.columns: print(f\n--- 时间范围 ---) print(f最早日期: {df[date].min()} | 最晚日期: {df[date].max()}) print(f总天数: {(df[date].max() - df[date].min()).days 1}) # 调用 scan_data(df_transactions, 信用卡交易原始数据)这个探查脚本会立刻告诉你fee列缺失率12%得查是不是新费率未同步、amount极差达480元说明有异常大额交易需排查、date范围是2024-01-01到2024-02-29但2月只有28天说明有29日的补录数据。这些信息决定了你后续聚合时要不要加query(amount 500)过滤异常值要不要用fillna(methodbfill)补fee。4.2 完整分析流水线七步构建可交付的银行分析报告现在我们把原文的七个分析整合成一个生产就绪的流水线。关键不是代码堆砌而是每一步都带业务意图、错误处理、和交付物定义。# 步骤0初始化与配置所有参数集中管理 CONFIG { high_value_threshold: 300, # 高价值交易阈值元 rolling_window_days: 7, # 滚动窗口天数 min_periods_for_rolling: 3, # 滚动计算最小周期 unstack_fill_value: 0.0, # unstack缺失值填充 output_precision: 2 # 数值精度 } # 步骤1数据预处理清洗、排序、类型校验 def preprocess_transactions(df): 银行级交易数据预处理 df df.copy() # 强制类型转换避免object类型影响聚合 df[amount] pd.to_numeric(df[amount], errorscoerce) df[fee] pd.to_numeric(df[fee], errorscoerce) df[date] pd.to_datetime(df[date], errorscoerce) # 过滤无效数据金额0或为空 df df[(df[amount] 0) (df[amount].notna())] # 按客户时间排序为滚动计算铺路 df df.sort_values([customer_id, date]).reset_index(dropTrue) return df # 步骤2多维聚合Analysis 1 5 的生产版 def multi_dimensional_agg(df): 按客户品类的多指标聚合输出宽表格式 # 主聚合一次计算所有基础指标 agg_result df.groupby([customer_id, category]).agg({ amount: [mean, median, count, std], fee: [sum, mean] }) # 扁平化列名业务语义化 agg_result.columns [ avg_txn_amt, med_txn_amt, txn_count, txn_std, tot_fee, avg_fee ] agg_result agg_result.reset_index() # 交叉分析客户x品类矩阵Analysis 5 crosstab df.groupby([customer_id, category])[amount].mean().unstack( fill_valueCONFIG[unstack_fill_value] ) crosstab.columns [favg_amt_{col} for col in crosstab.columns] # 统一前缀 crosstab crosstab.reset_index() return agg_result, crosstab # 步骤3自定义风险指标Analysis 2 7 的增强版 def risk_metrics_agg(df): 计算交易风险相关指标 # 范围分析Analysis 2 range_result df.groupby(category)[amount].agg( lambda x: x.max() - x.min() ).rename(txn_range).reset_index() # 风险分段Analysis 7用我们之前写的健壮函数 risk_result df.groupby(customer_id)[amount].apply( risk_segmentation, high_value_thresholdCONFIG[high_value_threshold], low_value_threshold50 ).reset_index() return range_result, risk_result # 步骤4时间序列聚合Analysis 3 4 的生产版 def time_series_agg(df): 滚动与累计指标计算 # 滚动平均带自适应逻辑 def rolling_avg_adaptive(series): return series.rolling( windowCONFIG[rolling_window_days], min_periodsCONFIG[min_periods_for_rolling] ).mean() # 按客户计算滚动平均 df_sorted df.sort_values([customer_id, date]) rolling_result df_sorted.groupby(customer_id)[amount].apply( rolling_avg_adaptive ).reset_index(namerolling_avg_amt) # 累计消费Analysis 4 expanding_result df_sorted.groupby(customer_id)[amount].expanding().sum().reset_index( namecumulative_spend ) return rolling_result, expanding_result # 步骤5高管摘要Analysis 6 的企业级封装 def executive_summary(df): 面向管理层的关键指标汇总 summary df.groupby(customer_id).agg({ amount: [sum, mean, count], fee: sum }) summary.columns [total_spend, avg_transaction, transaction_count, total_fees] # 计算衍生指标带精度控制 summary[avg_fee_percent] ( (summary[total_fees] / summary[total_spend]) * 100 ).round(CONFIG[output_precision]) summary[total_spend] summary[total_spend].round(CONFIG[output_precision]) summary[avg_transaction] summary[avg_transaction].round(CONFIG[output_precision]) return summary.reset_index() # 步骤6主流程编排所有分析串联 def run_analysis_pipeline(raw_df): 银行交易分析主流程 print( 开始执行银行级交易分析流水线...) # 预处理 df_clean preprocess_transactions(raw_df) print(f✅ 预处理完成原始{len(raw_df)}行 → 清洗后{len(df_clean)}行) # 并行执行各分析模块 try: agg_result, crosstab multi_dimensional_agg(df_clean) print(f✅ 多维聚合完成生成{len(agg_result)}行明细 {len(crosstab)}行交叉表) range_result, risk_result risk_metrics_agg(df_clean) print(f✅ 风险指标完成{len(range_result)}品类范围 {len(risk_result)}客户分段) rolling_result, expanding_result time_series_agg(df_clean) print(f✅ 时间序列完成{len(rolling_result)}行滚动均值 {len(expanding_result)}行累计值) summary executive_summary(df_clean) print(f✅ 高管摘要完成{len(summary)}行关键指标) # 步骤7结果整合与交付模拟生产环境 print(\n 流水线执行完毕生成以下交付物) deliverables { customer_category_detail: agg_result, # 客户-品类明细 customer_category_matrix: crosstab, # 客户-品类矩阵 category_risk_profile: range_result, # 品类风险画像 customer_risk_segment: risk_result, # 客户风险分段 rolling_performance: rolling_result, # 滚动业绩 cumulative_value: expanding_result, # 累计价值 executive_dashboard: summary # 高管看板 } for name, df_out in deliverables.items(): # 实际生产中这里会写入数据库或S3 print(f • {name:25s} → {len(df_out)}行 × {len(df_out.columns)}列) return deliverables except Exception as e: print(f❌ 流水线执行失败{str(e)}) raise # 执行流水线用原文生成的数据 deliverables run_analysis_pipeline(df_transactions)这个流水线的价值在于它把零散的分析点变成了可维护、可测试、可部署的模块。比如你想把“高价值阈值”从300调到500只需改CONFIG字典你想增加“周末交易占比”指标只需在risk_metrics_agg里加一个函数不影响其他模块。这才是真正的工程化思维。5. 常见问题与排查技巧实录我在银行生产环境踩过的12个坑5.1 内存爆炸groupby后unstack直接OOM现象对千万级客户数据做groupby([customer_id, product])后unstack()Python进程内存飙升到32GB然后被系统kill。根因unstack会尝试创建一个稠密矩阵如果customer_id有1000万product有100种它就要分配10亿个单元格即使99%是空的。解决方案首选用pivot_table替代unstack它支持sparseTrue参数只存储非空值# 错误直接unstack # result df.groupby([cust,prod])[amt].mean().unstack() # 正确用pivot_table sparse result df.pivot_table( indexcustomer_id, columnsproduct, valuesamount, aggfuncmean, fill_value0.0, dropnaFalse ).astype(pd.SparseDtype(float64, 0.0)) # 稀疏存储备选如果必须用unstack先用nunique()检查维度基数对高基数维度如customer_id做采样或分桶如pd.cut(df[amount], bins10)。5.2 滚动计算结果错位rolling后reset_index顺序错乱现象df.groupby(cust)[amt].rolling(7).mean().reset_index()结果里customer_id列和rolling_mean列对不上A客户的滚动值出现在B客户行。根因rolling返回的是Series其索引是MultiIndexcustomer_id,original_indexreset_index()时若不指定level会把所有索引都重置导致顺序错乱。解决方案必须用reset_index(level0, dropTrue)保留分组索引或更稳妥地用groupby().apply()# 错误顺序错乱 # wrong df.groupby(cust)[amt].rolling(7).mean().reset_index() # 正确指定level correct df.groupby(cust)[amt].rolling(7).mean().reset_index(level0, dropTrue) # 或更推荐用apply逻辑清晰 correct df.groupby(cust)[amt].apply(lambda x: x.rolling(7).mean())5.3 自定义函数返回NaNapply后整列消失现象df.groupby(cat)[amt].apply(custom_func)结果里某些cat组返回NaN但apply后整个结果是Series没有cat列了。根因apply在遇到返回NaN的组时会自动丢弃该组而不是保留NaN占位。解决方案强制apply返回Series并用result.reindex()补全# 错误可能丢组 # result df.groupby(cat)[amt].apply(custom_func) # 正确确保所有组都在结果中 groups df[cat].unique() result df.groupby(cat)[amt].apply(custom_func) result result.reindex(groups) # 用原始分组索引补全5.4 时间窗口计算偏差rolling忽略业务日历现象计算“近5个交易日滚动均值”但rolling(window5)把周末、节假日也算进去导致周一的值其实是上周五、四、三、二、一的均值而非真正的“近5个交易日”。根因Pandasrolling是基于行数不是基于日历日。解决方案用resample先按日重采样再滚动# 先按日聚合确保每天一行 df_daily df.set_index(date).groupby(customer_id)[amount].resample(D).sum().reset_index() # 再按客户滚动此时window5就是5个日历日 df_daily[rolling_5d] df_daily.groupby(customer_id)[amount].rolling( window5, min_periods1 ).mean().reset_index(level0, dropTrue)5.5 多级索引列名混乱agg后columns无法用[]索引现象result df.groupby([a,b]).agg({c:[mean,std]})想取result[c_mean]报错因为列名是(c,mean)。解决方案两种方式任选方式1推荐扁平化后索引result.columns [_.join(col) for col in result.columns] value result[c_mean] # 现在可以了方式2用元组索引仅限MultiIndexvalue