Pandas数据分组与透视表详解:从groupby到pivot_table的底层逻辑与实战

发布时间:2026/9/15 9:11:47
Pandas数据分组与透视表详解:从groupby到pivot_table的底层逻辑与实战 第一次带实习生的时候我发现一个特别普遍的现象Pandas 的基本操作大家学得都挺快read_csv、loc、iloc、merge这些都能磕磕绊绊用起来但一到“数据分组”这个坎就卡住了。比如需求是“统计每个地区的销售额”有人下意识想写 for 循环去遍历 DataFrame有人会把 Excel 透视表的思维硬套过来结果代码写出一大坨速度还慢得感人。而“透视表”这个词很多人只在 Excel 里点过菜单换到 Pandas 里就不知道该用哪个函数、参数怎么传。这篇文章就是把 Python 数据分析里最核心的两块——数据分组groupby和透视表pivot_table——彻底讲清楚。我会从它们各自的底层逻辑讲起再一步步拆解参数和写法最后用一个完整案例把它们串起来。文章面向新手但里面也会穿插一些老手才关注到的细节比如transform和filter的适用场景、pivot和pivot_table的区别、多级索引怎么处理。相信我把这些搞明白之后你处理“按某某统计某某”这类需求会顺手非常多。1. 为什么分组和透视是两套思路先搞清楚你的数据要经历什么变化1.1 一个贯穿全文的案例数据后面的所有讲解我都会围绕同一份销售订单数据。建议你打开 Jupyter 或者 VS Code把这段代码原封不动跑一遍后面每个例子都能直接复现。import pandas as pd import numpy as np df pd.DataFrame({ 订单号: [fO-{i:03d} for i in range(1, 13)], 日期: pd.to_datetime([ 2024-01-05, 2024-01-12, 2024-01-20, 2024-02-03, 2024-02-15, 2024-02-21, 2024-03-02, 2024-03-08, 2024-03-14, 2024-03-22, 2024-04-01, 2024-04-02 ]), 地区: [华东, 华北, 华东, 华北, 华南, 华东, 华南, 华东, 华北, 华南, 华东, 华北], 产品: [A, B, A, A, B, C, A, B, C, A, A, B], 销售额: [1200, 2300, 1500, 1900, 2600, 900, 2400, 1300, 3100, 1800, 2700, 3400], 数量: [2, 5, 3, 4, 6, 1, 5, 2, 7, 3, 6, 8] }) df数据很简单12 行每一行是一笔订单字段有订单号、日期、地区、产品、销售额、数量。为什么要先用一个小数据集因为小数据方便你观察每一步输出的形状变化理解了“形状变化”分组和透视的大逻辑就通了一大半。1.2 分组是“纵向压扁”透视是“纵横翻转”很多新手混淆这两个概念本质上是没想清楚数据经过操作之后会变成什么形状。分组做的事情用一句话说把若干行按照某个标签归并成若干个“组”然后对每一组做同样的计算。以“按地区统计销售额”为例输入12 行数据每行是订单粒度。操作按地区列分成 3 组华东、华北、华南。输出3 行数据每行是一个地区的汇总结果。这个过程我习惯叫“纵向压扁”——行数变少了但结构还是“竖着长”的表格每行对应一个组。透视表做的事情则是另一回事它把某一个列的唯一取值变成新表格的列名再把另一个列的唯一取值变成新表格的行名中间填的值来自第三列。比如“各地区 × 各产品的销售额交叉表”输入12 行数据。操作行的方向按地区分类列的方向按产品分类单元格里填销售额的汇总值。输出3 行 × 3 列的交叉表。这个操作是“纵横翻转”——原本竖着排列的产品字段被横着铺开了。数据不再是一列一列顺着往下排而是行列都变成了分类维度。所以遇到需求时先问自己一句我只关心一个维度还是关心两个维度交叉需求数据结构变化首选工具每个地区的销售额总和行数压扁每地区一行groupby每个月的订单量行数压扁每月一行groupby每个地区、每个产品的销售额交叉报表行变地区列变产品pivot_table每个产品在不同月份的销售趋势行变产品列变月份pivot_table当然groupby配合unstack()也能做出透视表两者可以互通这个后面第 5 章细讲。但你先建立这个直觉看需求是“一维汇总”还是“二维交叉”。2. 别急着敲代码GroupBy 的真正工作机制2.1 分组键不一定是列名但分组结果一定是个懒对象我先问一个很多新手没想过的问题df.groupby(地区)返回的到底是什么答案是一个DataFrameGroupBy对象不是什么最终结果。这个对象是**惰性lazy**的它只是记下了“按地区分组”这个指令还没有真正开始分组计算。所以你会发现在 Jupyter 里单独跑df.groupby(地区)输出的是一段对象说明而不是分组后的表格。只有当你在这个对象后面接上.sum()、.mean()、.agg()、.count()这类聚合方法时它才会真正执行“拆分—应用—合并”的完整流程。分组键的传入方式非常灵活这里列几种常见情况# 按单列分组 df.groupby(地区) # 按多列分组得到一个层级索引MultiIndex df.groupby([地区, 产品]) # 用一列Series作为分组键和按列名等价 df.groupby(df[地区]) # 按日期的月份分组这是实际需求里非常高频的写法 df.groupby(df[日期].dt.month)[销售额].sum() # 按自定义规则分组传入一个函数 df.groupby(lambda idx: 大单 if df.loc[idx, 销售额] 2000 else 普通单)[销售额].sum()最后一个自定义函数的写法可能有人第一次见。它的逻辑是groupby 会把每一行的行索引传给这个函数函数返回什么值这行就被归到哪个组。比如O-001那行销售额是 1200小于 2000就被归到“普通单”。这个能力在处理连续变量分桶时非常有用。那怎么确认一个分组对象内部长什么样用get_group()调试这招我强烈推荐新手掌握df.groupby(地区).get_group(华东)它会返回“华东”这个组对应的所有原始行。我在讲解 groupby 时只要不确定分组结果就会先get_group看一眼比盲猜高效太多。2.2 拆分—应用—合并分组计算为什么快groupby的完整流程是所有讲解里最值得用三个词概括的拆分Split、应用Apply、合并Combine。拆分Pandas 根据分组键把整个 DataFrame 拆成若干个子表每个子表对应一个组。应用对每个子表执行你指定的操作求和、均值、最大值、自定义函数等。合并把每个子表的处理结果重新拼成一张表返回给你。这里的“拆分”并不是真的复制数据到内存里而是通过索引标记快速定位所以效率比你自己写 for 循环要高得多。新手喜欢写这种代码# 反例能用 groupby 却偏要手写循环 for region in df[地区].unique(): sub df[df[地区] region] print(region, sub[销售额].sum())功能上没错但问题在于代码啰嗦、容易写错、数据量大时性能差。groupby在 C 层面做了大量优化你写df.groupby(地区)[销售额].sum()一行它内部是向量化计算速度是 for 循环的几十倍上百倍。养成“能分组就不要手写循环”的习惯是 Pandas 使用水平的一个重要分水岭。2.3 agg 聚合的三种写法第三种最实用分组之后的聚合操作最灵活的手段是.agg()。新手常看到sum()和agg(sum)两种写法它们大部分时候等价但agg能做的远不止一个函数那么简单。写法一对一列做一种聚合df.groupby(地区)[销售额].sum()输出是这样地区 华东 7600 华北 10700 华南 6800 Name: 销售额, dtype: int64注意输出里地区变成了索引想让它变回普通列要么reset_index()要么在groupby时加as_indexFalse。这个坑在第 6 章会专门展开。写法二对一列做多种聚合df.groupby(地区)[销售额].agg([sum, mean, count])输出会是每个地区一行三列分别为sum、mean、count。列名默认是函数名比较直观。写法三对不同列做不同的聚合df.groupby(地区).agg({销售额: sum, 数量: mean})意思是销售额看总和数量看平均。这种写法的输出列名会保留原来的列名如果想去掉函数名的影响或者给结果重新取个更容易读的名字用NamedAggdf.groupby(地区).agg( 总销售额pd.NamedAgg(column销售额, aggfuncsum), 平均数量pd.NamedAgg(column数量, aggfuncmean) )这样输出就是两列总销售额、平均数量不需要再做繁琐的重命名。如果你的 Pandas 版本比较新1.3 都支持这个写法在工作里非常实用。还有一个容易混淆的小点count()和size()看起来都是计数但count()会忽略 NaN 值size()不会。假如某列存在缺失值这两个结果就可能不一样。在新手阶段我建议统一用count()因为“统计有多少条非空记录”通常更符合业务含义。2.4 分组取每组最大/最小/第一条高频业务需求“分组取出每组最大的一条数据”这个问题在 MySQL 里是一道经典难题换成 Pandas 反而简单但我见过很多人绕了一大圈。比如想找“每个地区销售额最高的那笔订单”通常有两种思路思路一idxmax()方案df.loc[df.groupby(地区)[销售额].idxmax()]逻辑拆解一下df.groupby(地区)[销售额].idxmax()会返回每个地区销售额最大那一行的行索引再把索引列表交给df.loc[]就能把对应的完整行取出来。思路二排序后去重方案df.sort_values(销售额, ascendingFalse).drop_duplicates(地区)先按销售额从大到小排序然后按地区去重因为排序后每个地区第一行就是该地区的最大值。这两种方案的结果基本一样但有个细节差异方案一会保留原始数据的顺序按地区一开始出现的顺序方案二会按销售额从高到低排序。如果你希望结果按业务逻辑展示比如“重点看大单”就选方案二如果你想保持原始表的地区顺序就选方案一。类似的场景还有“取每组前 3 条”“取每组最后一条”思路都是groupbyapply或者提前排序 分组切片。先掌握上面这两种绝大多数需求就够用了。3. transform 和 filter比聚合更细粒度的“组内操作”3.1 transform让聚合结果回到每一行前面讲的.sum()、.agg()都会把行数变少得到的是“组级别”的结果。但业务里经常有一种需求我想在原始每一行旁边加一列“该行所属地区的销售总额”然后算“这笔订单占该地区销售额的百分比”。这时候直接用聚合就不行因为聚合结果行数对不上。解决办法就是.transform()df[地区销售总额] df.groupby(地区)[销售额].transform(sum) df[地区占比] df[销售额] / df[地区销售总额]transform(sum)的结果是一个和原 DataFrame等长的 Series它会把这个组的总和“广播”回组内的每一行。这样每一行都能看到自己所在组的汇总值而不需要先聚合再合并。为什么不用merge当然也可以。先groupby(...).sum().reset_index()得到一个汇总表再和原表merge回去。但transform一行就能完成代码更简洁而且因为整体操作在 Pandas 内部处理性能通常会更好。transform还有一个实用场景组内标准化。比如想计算每个订单的销售额相对于所在地区平均水平的差异df[销售额_地区偏差] df[销售额] - df.groupby(地区)[销售额].transform(mean)这样结果有正有负正数说明高于该地区平均水平负数说明低于。这类特征工程操作在数据处理里经常用到。3.2 filter先筛组再往下操作filter和前面所有方法不太一样它不是按照“每一行是否满足条件”来过滤那是普通的布尔索引而是按照“整个组是否满足条件”来决定保留还是丢弃整组数据。举个例子我想删除总销售额低于 7000 的地区相当于只保留那些“整体业绩达标”的地区再分析这些地区的订单明细。filtered df.groupby(地区).filter(lambda g: g[销售额].sum() 7000) filteredfilter接收一个函数这个函数的参数g是每组的数据子表返回值必须是布尔值。True就保留这组的全部行False就把这组全部删掉。我们这个例子里华东 7600、华北 10700 会被保留华南 6800 会被剔除。新手最容易搞混的是filter和普通布尔索引的区别。普通布尔索引是“行级”过滤df[df[销售额] 2000] # 只看销售额超过2000的订单行这是按行条件筛选大单会保留小单会被删掉和组无关。而filter是“组级”决策哪怕某地区里有一笔 300 的小订单只要整个地区总销售额达标这单也会被保留。一个是切行一个是切组这是理解filter的关键。4. pivot_table 透视表的四根支柱和一个隐藏开关4.1 四个核心参数决定了表格的骨架pivot_table的签名很长但新手只需要先吃透四个参数index、columns、values、aggfunc。它们的关系可以用一个坐标来理解index哪些列变成新表格的行索引。columns哪些列变成新表格的列名。values哪些列的值填进表格单元格。aggfunc当同一个格子有多个值时怎么合并计算。拿“各地区 × 各产品的销售额交叉表”举例pt pd.pivot_table( df, index地区, columns产品, values销售额, aggfuncsum ) pt输出长这样产品 A B C 地区 华东 5400 1300 900 华北 1900 5700 3100 华南 4200 2600 NaN每个格子的含义是某个地区销售的某个产品总计卖出了多少钱。比如华东地区的 A 产品三笔订单加起来是 5400。这里有一个特别容易踩的默认值aggfunc的默认是mean平均值不是求和。Excel 透视表默认也是求和所以从 Excel 转过来的人第一反应会觉得“为什么结果都对不上”多半就是忘了指定aggfuncsum。这个坑我见过太多次了。四个参数都可以传列表用于构建更复杂的透视表。比如pd.pivot_table( df, index[地区, 产品], columnsNone, values销售额, aggfuncsum )这会得到一个每个地区、每个产品的纵向汇总表等价于df.groupby([地区, 产品])[销售额].sum().reset_index()的一部分效果。核心思路是把需求翻译成“什么是行、什么是列、什么是值、怎么汇总”透视表就成功了一大半。4.2 margins、fill_value、dropna三个容易被忽略的细节真实做报表的时候光有上述基础表格还不够通常还要“总计行/列”。marginsTrue可以一键加上pt pd.pivot_table( df, index地区, columns产品, values销售额, aggfuncsum, marginsTrue, margins_name总计 )这样表格右侧会出现“总计”列每个地区所有产品合计底部会出现“总计”行每个产品所有地区合计右下角是全表总计。注意margins_name可以自定义默认叫All中文报表里改成“总计”更好看。fill_value0的作用是把透视结果里因为“该组合不存在”而产生的 NaN 填充为 0。上面的透视表里华南的 C 产品就是 NaN加了这个参数会变成 0。它不是真正“算了”一个 0 出来只是一个显示/填充层面的处理如果你后续还要做计算可能还是想保留 NaN 或者处理成 0看业务需要。dropnaFalse则是一个更冷门但有用的参数默认情况下透视表会自动忽略全是 NaN 的行或列。如果你希望某些维度即使没数据也出现在表格里比如保证每月都有 12 个月份就设置dropnaFalse。还有一个参数叫observed它只对“分类类型Categorical”的分组键生效。observedTrue表示只展示数据里实际出现过的分类False会把所有潜在分类都列出来。等你用到pd.Categorical时再回来看这个参数就明白了新手阶段先不深究。4.3 pivot、pivot_table、crosstab 三兄弟别用混Pandas 里带 pivot 名字的函数有好几个新手经常混淆我把它们的区别一次性讲清楚。df.pivot()是一个纯结构重塑工具它不做任何聚合。如果数据里只有“每个地区 每个产品”唯一组合那pivot可以直接把宽表抽出来summary df.groupby([地区, 产品], as_indexFalse)[销售额].sum() summary.pivot(index地区, columns产品, values销售额)这里为什么先groupby一次因为pivot要求每个“行列组合”必须唯一如果存在重复它会直接报错ValueError: Index contains duplicate entries, cannot reshape。而pivot_table自带聚合能力遇到重复会自动用aggfunc合并所以更省心。新手阶段我建议先无脑用pivot_table等理解清楚了再用pivot。pd.crosstab()看名字像是“交叉表”它的本质是统计频次的专用工具。比如想知道“每个地区分别卖了多少笔 A、B、C 产品”pd.crosstab(df[地区], df[产品])输出产品 A B C 地区 华东 3 1 1 华北 1 2 1 华南 2 1 0它默认计算的是“每组的行数”也就是计数。其实它内部就是在调用pivot_table(aggfuncsize)但调用方式更简洁非常适合做分类变量之间的关联计数。如果想让结果显示比例可以加normalizeindex参数那样每行加起来是 100%。我的建议是这样纯转置用pivot聚合汇总用pivot_table分类频次统计用crosstab。记住这个顺口溜三兄弟就不会再用混了。5. 分组和透视的“底层互通”groupby unstack 就是透视表5.1 unstack把内层索引抬成列很多教程把unstack讲得特别玄其实它的作用就是“把层次化索引MultiIndex中的某一层索引抬升为列名”。前面我们在“分组键”里见识过df.groupby([地区, 产品])[销售额].sum()的结果它是一个带有两层索引的 Series地区 产品 华东 A 5400 B 1300 C 900 华北 A 1900 B 5700 C 3100 华南 A 4200 B 2600 Name: 销售额, dtype: int64这时在这个结果后面接一个.unstack()df.groupby([地区, 产品])[销售额].sum().unstack()得到产品 A B C 地区 华东 5400 1300 900 华北 1900 5700 3100 华南 4200 2600 NaN是不是和第 4 章pivot_table的输出一模一样对groupby sum unstack本质上就是透视表。unstack把原来“地区 产品”两层的竖排索引把内层产品抬升成了列而stack就是反向操作把列压回索引层。明白这个底层关系后很多新手常问的问题就迎刃而解了“为什么我的 groupby 结果想变成宽表就那么难”——因为你缺的不是pivot_table而是一个.unstack()。5.2 什么时候用 groupby 链式什么时候用 pivot_table既然两者能互相转换那实际开发中到底用哪种我的个人判断标准有两条第一看代码意图是否清晰。如果业务需求是“统计每个地区的总销售额”df.groupby(地区)[销售额].sum()比pivot_table(index地区, values销售额, aggfuncsum)更直观因为后者要写一堆参数本质上只做了一件很简单的事。优先选择表达力最强的写法。第二看后续操作需求。如果你接下来想做的是“把这张宽表导出到 Excel 给业务方看”那pivot_table一步到位如果你还要接着做复杂的链式操作比如“先按月汇总再算环比”groupby链式更容易接续。举一个典型场景我要统计“每个地区每个月的销售额趋势”。用pivot_table做一张“行地区、列月份”的交叉表非常合适pd.pivot_table( df, index地区, columnsdf[日期].dt.month, values销售额, aggfuncsum, fill_value0 )输出日期 1 2 3 4 地区 华东 2700 900 1300 2700 华北 2300 1900 3100 3400 华南 0 2600 4200 0这个表格用来“人眼看趋势”非常爽但如果你告诉你接下来要画折线图、要透视回长表继续运算那可能更适合用groupbyunstack或者直接保持长表结构。数据结构没有绝对好坏只看你的下一步动作是什么。5.3 stack 和 melt长表和宽表的互相翻译既然提到长表和宽表就顺带提一句stack和它更常用的兄弟melt。stack是把宽表变长表列变行melt则是把多列“融化”成两列变量名 变量值它也是 Pandas 里做数据清洗时的高频函数之一。比如上面那张透视表如果要用melt还原成长表格式pt_reset pt.reset_index() pt_long pt_reset.melt(id_vars地区, var_name产品, value_name销售额)理解宽表和长表的互转是 Pandas 数据处理里一个很重要的底层能力。很多同学做数据可视化时发现“图怎么画都不对”大概率是因为数据是宽表而绘图库希望长表。先把数据变成长表再画图一切就顺了。6. 新手最容易踩的五个坑我一个个给你列出来6.1 分组键“消失”了as_index 与 reset_index第一次跑df.groupby(地区)[销售额].sum()时不少新手会愣住分组用的地区列跑哪去了它没有消失只是变成了索引。Pandas 默认把分组键设置成结果的索引as_indexTrue这是一种“标记”机制方便你后续用.loc快速定位。问题在于如果你想把这个结果to_csv导出或者和别的表merge多级索引往往会带来麻烦。两个解决方案# 方案一分组时直接关掉索引 df.groupby(地区, as_indexFalse)[销售额].sum() # 方案二分组后手动恢复 df.groupby(地区)[销售额].sum().reset_index()注意这两种写法有一个细微差异当分组键是多列时第一种写法得到的结果会展开成普通列第二种写法reset_index()默认也会展开基本等价。但如果原索引有名字且和数据列重名reset_index()偶尔会带着额外索引列遇到时检查一下选最直观的一种即可。6.2 求和变成了字符串拼接先看数据类型这是一个我亲眼见过多次的“灵异事件”明明想对一列数值求和结果输出是一串字符串连在一起。原因多半是这一列在读取或创建时被当成了object/string类型。比如你从 Excel 或 CSV 读进来的“销售额”列里面混了“1200元”这样的文本或者单元格前后有空格pandas 就会把整列推断为字符串。字符串做“加法”自然就是拼接。解决方式分两步先看类型再转换df[销售额].dtype # 确认类型 df[销售额] pd.to_numeric(df[销售额], errorscoerce)to_numeric可以把字符串列转成数值列遇到不能转换的会变成 NaN取决于errors参数。转换后再groupby().sum()结果就正常了。分组之前养成检查 dtype 的习惯能避开大量莫名其妙的问题。6.3 分组键里有缺失值dropna 可能悄悄删掉了你的一组默认情况下groupby会忽略分组键为 NaN 的行。也就是说如果某条订单的地区列为空分组统计时这一行会直接“消失”。这到底是不是问题取决于业务场景。如果地区为空本身代表一种业务状态比如“尚未分配区域”你可能希望单独把它归为一组那就需要显式告诉 Pandas 不要丢弃缺失值df.groupby(地区, dropnaFalse)[销售额].sum()dropnaFalse会让 NaN 也成为一个分组结果里会多出一行索引显示为 NaN。默认dropnaTrue的行为适合大多数情况但当你发现“统计结果对不上总账少了几笔”的时候优先检查这里。6.4 透视后的多级索引/多级列名操作时经常报错用pivot_table做多列index或多列columns时结果会带着 MultiIndex。新手常遇到的后续操作错误是pt[销售额] # 报错因为列名是两层如果columns传了[产品, 某字段]pt的列就是一个 MultiIndex直接pt[产品]会找不到。处理方式有两个# 方法一透视结果后重置索引把行索引变回普通列 pt.reset_index() # 方法二看清列层级后用 get_level_values 处理 pt.columns.get_level_values(0)建议新手在透视完成后养成reset_index()的习惯把表格变回“普通得不能再普通”的一张大宽表后续处理会顺手很多。6.5 装了 Pandas 却 import 失败环境问题排查标题虽然聚焦分组和透视但排名很靠前的提问里有一类属于环境问题。这里快速给一个排查路线如果你在 Jupyter 里能 import pandas但在终端脚本里报ModuleNotFoundError九成是 Python 环境搞混了——终端用的是系统 Python而 Jupyter 用的是虚拟环境里的 Python。先执行which python或where python看当前用的是哪个解释器再在同一个环境下用pip install pandas安装一般就解决了。Pandas 2.x 要求 Python 3.9如果你的 Python 版本太老3.7 以下建议先升级 Python否则最新版 Pandas 装不上。7. 一个完整案例从订单明细到销售报表7.1 需求一个月度、地区、产品三维视角的报表光讲函数不用在业务场景里很容易“学完就忘”。这一节我用一份订单明细完整走一遍从需求到结果的流程。假设业务方提出了三个问题看每个月销售总额判断整体趋势。看每个地区、每个产品的销售额交叉表找出贡献最大的组合。把“地区 × 月份”的销售数据整理成一张可以导出给老板看的宽表。这三问分别对应一维趋势、二维交叉、宽表导出。刚好覆盖前面所有核心知识点。7.2 用 groupby 做月度趋势月度趋势是一维汇总直接用groupbymonthly df.groupby(df[日期].dt.to_period(M))[销售额].sum() monthlydt.to_period(M)会把日期归并到“月”这个周期比dt.month更严谨跨年时不会把不同年份的同一月混在一起。输出结果日期 2024-01 5000 2024-02 5400 2024-03 8600 2024-04 6100 Freq: M, Name: 销售额, dtype: int64可以看到 3 月是高峰。如果之后要画折线图直接.plot()就行Pandas 的 Series 自带绘图接口会自动把索引画成 x 轴坐标。7.3 用 pivot_table 做地区 × 产品交叉表这个需求就是第 4 章的经典场景直接透视cross pd.pivot_table( df, index地区, columns产品, values销售额, aggfuncsum, marginsTrue, margins_name总计, fill_value0 ) cross表格会清晰地显示每个地区每个产品的销售额以及行总计、列总计。如果哪一格的数字特别显眼就是核心贡献组合。在这个例子里华北的 B 产品 5700 是单格最高值说明 B 产品在华北市场存在明显优势。7.4 用 pivot_table 做地区 × 月份宽表并导出 Excel第三个需求要一张“行地区、列月份”的宽表。月份这里我用dt.to_period(M)或者直接dt.month都行先做透视monthly_region pd.pivot_table( df, index地区, columnsdf[日期].dt.month, values销售额, aggfuncsum, fill_value0 ) monthly_region输出日期 1 2 3 4 地区 华东 2700 900 1300 2700 华北 2300 1900 3100 3400 华南 0 2600 4200 0华南 1 月和 4 月没有订单所以是 0。这张表如果要导出到 Excel 给业务方monthly_region.reset_index().to_excel(地区月度销售报表.xlsx, indexFalse)reset_index()把地区变回普通列导出后格式更友好。如果想一个 Excel 里放多张表可以借助pd.ExcelWriter在这张总表和交叉表之间用不同的 sheet_name 区分。这一步在真实需求里非常常见。7.5 扩展Excel 里“透视表只能选一个 sheet”Pandas 里怎么合并多个 sheet经常有人问“数据透视表可以选择两个 sheet 吗”这是 Excel 里的痛点一张透视表默认只能基于一个数据区域。如果你手里是一份多 sheet 的销售台账比如 1 月、2 月、3 月各占一个 sheet想跨 sheet 透视Pandas 的做法是先把这些 sheet 合并成一张长表再透视。假设有一个各月销售.xlsx里面有三个 sheet 分别叫1月、2月、3月每张表结构相同all_dfs [] for sheet_name, sheet in pd.read_excel(各月销售.xlsx, sheet_nameNone).items(): sheet[月份] sheet_name all_dfs.append(sheet) all_data pd.concat(all_dfs, ignore_indexTrue)pd.read_excel(..., sheet_nameNone)会把整个工作簿读成“sheet 名 → DataFrame”的字典遍历后给每个 sheet 加一列来源月份再用concat拼起来。拼完这张大表后面不管是groupby还是pivot_table都和单表完全一样。这个合并思路也适用于多个 CSV 文件、多个 DataFrame 需要一起分析的场景。核心就是先把“多源”变成“一表”再谈分组和透视。这个习惯能避免很多不必要的复杂度。我自己在实际操作中的体会是分组和透视不应该是需要背的两个孤立知识点它们的本质都指向同一个能力——你能不能控制表格的“形状”。拿到任何需求先问自己原始数据是什么形状最后要什么形状中间差的那一步就是你要调用的函数。如果你想变得再稳一点就把本文的案例数据改成你自己工作里的真实字段把每个例子都手动跑一遍。哪怕只是改个列名你也已经把这个能力内化成自己的了。