Pandas数据重塑实战:pivot、melt、stack与unstack详解

发布时间:2026/10/19 3:31:28
Pandas数据重塑实战:pivot、melt、stack与unstack详解
1. 先搞清楚重排和旋转到底在做什么Pandas里的Dataframe重排和旋转是数据处理中容易卡壳、但也最有价值的一环。我经常看到有人处理数据时陷入一个怪圈拿到一份订单明细想要按月看各品类销售对比得来回写循环拿到一份宽表的统计报表想做分组聚合发现一堆列名根本没法直接groupby。折腾半天其实问题都出在对长表和宽表这两个形态理解不够以及不熟悉Dataframe在不同形态之间切换的那几个核心方法。先说结论所谓重排和旋转本质上就是改变数据的形状。我们把每一行代表一次独立观测、每一列代表一个字段的格式称为长表Long Format把同一主体、不同属性的值摊开成多列的格式称为宽表Wide Format。长表适合存储、筛选、聚合宽表适合阅读、对比、特征工程。而pivot、pivot_table、melt、stack、unstack就是在这两种形态之间来回切换的五把钥匙。这篇文章就把这几个方法从原理到实操拆透适合已经学过Pandas基础、但在实际项目中遇到表结构不对这类问题的读者。1.1 长表和宽表先建立坐标系很多人上来就写代码结果被reshape成MultiIndex的Dataframe搞晕。我建议先建立一个坐标系把问题想清楚再动笔。长表的长处数据库中导出的原始记录基本都是长表每行是独立的样本点比如一个用户在一个时间点的购买行为。它天然适合SQL、适合groupby、适合大部分统计检验。宽表的长处宽表把同一主体的多个属性并排列出来直观、利于横向比较也适合直接喂给很多机器学习模型做特征矩阵。但宽表不是免费的午餐——同一个主体多行记录在转宽时很容易产生重复索引问题。这就是为什么pivot、pivot_table、melt这几个方法总是配套使用你先把明细用pivot做成矩阵看一眼再用melt把结果还原长表继续算整个过程就像把数据在两种坐标系之间来回投影。1.2 四个核心方法怎么分工先用一句话建立整体认知框架pivot / pivot_table长表转宽表。把某一列的唯一值变成新列把另一列的值变成新列的内容。melt宽表转长表。把多列内容溶回两列一列存原来的列名一列存原来的数值。stack / unstack在行和列之间平移索引层级。unstack把行的某个层级推到列上stack反方向操作把列压回行。这四个方法不是孤立的。我用一个比喻pivot是立起一堵墙melt是拆墙做平地stack和unstack则是把楼层上下移动。理解了各自的位置实际项目里看到奇怪的报表结构就不会束手无策了。2. 用pivot把长表变宽从明细到矩阵2.1 基础场景一列变多列pivot是最直观的旋转方法。它的语法其实就三个核心参数index、columns、values。意思分别是——哪些列作为新表的行索引哪些列的唯一值作为新表的列哪些列的值填进新表。我一个具体例子。假设手上是一份某电商平台三天的销售明细每条记录是某个类目在某天的销售额import pandas as pd df pd.DataFrame({ 日期: [2024-01-01, 2024-01-01, 2024-01-02, 2024-01-02, 2024-01-03, 2024-01-03], 类目: [数码, 服饰, 数码, 服饰, 数码, 服饰], 销售额: [1200, 800, 1500, 900, 1100, 1300] }) print(df)现在想看每一天、每个类目的销售额矩阵直接一行代码搞定pivot_df df.pivot(index日期, columns类目, values销售额) print(pivot_df)输出会变成这样的宽表类目 数码 服饰 日期 2024-01-01 1200 800 2024-01-02 1500 900 2024-01-03 1100 1300对比原始数据你发现pivot做了三件事把日期作为新的行索引把类目的唯一值数码、服饰变成两个新列把销售额填到了对应的行列交叉处。这就是旋转的直观感觉——原本在类目这一列里的分类值旋转90度变成了列标题。这里有个容易忽略的细节pivot之后的Dataframe列名就是原字符串而且默认会保留index的排序。如果你希望新的索引按日期排序用sort_index()如果你希望列按特定顺序直接用布尔索引调整列顺序即可。用的时候想清楚pivot返回的不是一个视图而是一个全新的Dataframe修改它不会影响原表。2.2 pivot的坑遇到重复怎么办pivot看起来很美但它有一个极其严格的前提在index和columns组成的组合下values必须唯一。换句话说pivot默认假设你不会有两行相同日期、相同类目的记录。如果出现了重复记录它就会抛出一个ValueError提示包含重复值。举例来说把刚才的数据复制一份df_dup pd.concat([df, df.iloc[[0]]], ignore_indexTrue) # 此时2024-01-01、数码这条记录出现了两次 try: df_dup.pivot(index日期, columns类目, values销售额) except ValueError as e: print(type(e).__name__, :, e)你会遇到经典的报错Index contains duplicate entries, cannot reshape。这个报错在实际项目中几乎每天都能遇到。原因很现实原始明细里不乏同一天、同一个用户、同一种商品的多次交易甚至只是因为上游数据做了union导致某些行重复。遇到这种情况你有两条路先消除重复用drop_duplicates(subset[日期, 类目])把重复行去掉再pivot。换用pivot_table做聚合pivot_table会自动按需聚合绕开唯一性限制后面我会专门讲。我个人的习惯是先查清楚为什么重复。如果到导入环节就出现重复往往说明上游ETL有bug直接丢弃可能掩盖数据质量问题。建议先df[df_dup.duplicated(subset[日期, 类目])]看一遍哪些重复、重复了几次再决定用聚合还是去重。2.3 pivot_table带聚合的升级版pivot_table可以看作pivot的放宽版。当你的数据在index和columns的组合下有多个值时pivot_table不会报错而是用aggfunc参数指定的聚合函数把它们合并。这个参数默认是np.mean你可以传入sum、max、min、count也可以传一个函数列表。沿用重复数据的例子pivot_table_df df_dup.pivot_table( index日期, columns类目, values销售额, aggfuncsum ) print(pivot_table_df)现在不会报错了2024-01-01的数码类目会显示2400因为那条记录被复制后求和了。你再传一个参数fill_value0pivot_table_df df_dup.pivot_table( index日期, columns类目, values销售额, aggfuncsum, fill_value0 )这样所有NaN都会被填成0。这在业务报表里很常用——没有销售的类目不是缺失而是0。pivot_table还有一个隐藏好用的参数叫margins设成True会在末尾追加一行总计或一列总计pivot_table_df df_dup.pivot_table( index日期, columns类目, values销售额, aggfuncsum, fill_value0, marginsTrue, margins_name合计 )这样输出的宽表自带边际汇总适合做报表直接展示。如果你做透视表时希望同时看总销量和总数aggfunc可以传列表比如aggfunc[sum, count]结果列会变成多层索引但信息量会大很多。注意pivot_table可以传多个values列比如values[销售额, 订单数]结果会生成MultiIndex列。这时如果想继续做重塑需要先levels处理或者把列名拼平切忌直接按两列名拿数值。3. 用stack和unstack在行列之间平移pivot和melt解决的是普通两维表的形态转换而stack和unstack则是专门处理MultiIndex多层索引的工具。在实际场景里这两种方法常被用在处理带复合分组、宽表统计结果、透视表结果的后续加工中。3.1 unstack收起行、展开列unstack的意思是把行索引的某一层抬起来变到列索引上去。它的方向是行转列。还是用数据说话。假设我们现在有一份按季度和地区统计销售额的数据但存成了两层行索引df_multi pd.DataFrame({ 销售额: [100, 200, 300, 400, 500, 600] }, indexpd.MultiIndex.from_product([ [Q1, Q2, Q3], [华东, 华北] ], names[季度, 地区])) print(df_multi)这个Dataframe的行索引有两层季度、地区。现在我们想把它转成每行是一个季度、每列是一个地区的形状unstacked df_multi.unstack() print(unstacked)结果是销售额 地区 华东 华北 季度 Q1 100 200 Q2 300 400 Q3 500 600其实结果里列索引还是两层只不过原来在行上的地区这一层被搬到了列上。如果你觉得列索引里的销售额这层多余还可以链式调用unstack(level...)或者用droplevel去清理unstacked2 df_multi.unstack(level地区) # 甚至可以直接指定要平移的层更直观stack和unstack最常用的场景就是当你用groupby得到了多个分组维度的统计结果时结果往往有MultiIndex的行不方便直接用列表或标准模型读取。你只需要用unstack把其中一个维度展开成列数据就变得非常规整。3.2 stack把列压回行stack和unstack方向相反它把列索引压回行索引生成更长的表。通俗讲就是把宽表变回长表但它不像melt那样基于普通列名操作而是专门处理MultiIndex列。上面的unstacked就是一个多层列索引的宽表。现在把它恢复成原来的形状stacked unstacked.stack() print(stacked)你会看到之前被摊开的地区列又回到了行索引整个表恢复成了多层索引的长表。这就是stack和unstack互相逆操作的关系。不过这里要特别提醒一个细节unstack之后如果你做一次stack可能会发现数据的顺序或者索引名字和你预想的不同因为stack默认会把剩下的列索引层级都压回去。当列索引还有多余层级时最好用level参数指定。3.3 level参数多层索引里指定往哪一层转level是stack和unstack里面最容易被忽略、但最关键的参数。默认为-1也就是最内层。实际项目中多层索引可能有三层甚至更多你经常只希望移动其中特定的一层。举个例子上面数据如果季度、地区、销售员三层索引你想只把地区这层移动到列上就要写unstack(level地区)而不是直接unstack()。写错level得到的表结构完全不是你想要的。我处理实际问题时有一个原则只要出现了MultiIndex就必须明确level是哪个名字或顺序位置。不要赌默认值。尤其是当索引层数增加时默认的-1经常会把最内层的粒度项转成列你本以为会得到一个汇总矩阵结果列数暴增数据完全变形。提示stack和unstack都不会改变数据的总量只是重新排列维度。你可以通过检查转换前后df.shape的乘积变化快速判断操作是否符合预期——比如宽表列数为K转换成长表后行数增加、列数减少但总单元格数保持不变。4. 用melt把宽表变长从矩阵回归明细4.1 基础场景把多列溶成两列如果说pivot是立墙melt就是拆墙。melt能把宽表的多个数据列熔成一个变量名列和一个数值列变成我们最爱处理的长表。假设你拿到的是一份门店每个月的销量统计每个门店一行每个月是一列df_wide pd.DataFrame({ 门店: [A店, B店, C店], 一月: [120, 200, 150], 二月: [180, 230, 170], 三月: [210, 260, 190] }) print(df_wide)现在的需求是把每个月的销量变成一行方便后续用Matplotlib画趋势图也方便直接拖进统计模型做分组。melt代码如下df_long df_wide.melt( id_vars门店, var_name月份, value_name销量 ) print(df_long)输出为门店 月份 销量 0 A店 一月 120 1 B店 一月 200 2 C店 一月 150 3 A店 二月 180 4 B店 二月 230 5 C店 二月 170 6 A店 三月 210 7 B店 三月 260 8 C店 三月 190你会发现melt把一月、二月、三月三列溶成了两列一列是原来的列名月份一列是对应的数值销量。id_vars指定保留哪些列不变var_name是新生成的列名列的名称value_name是新生成的数值列的名称。melt是我们做数据清洗时最常用的方法之一尤其是上游报表为了展示方便把时间维度摊成多列时必须先用melt还原成明细才能继续聚合、筛选、画图。4.2 与pivot互为逆操作pivot和melt正好是逆过程melt把宽表变长pivot把长表变宽。你用melt生成的df_long如果再用pivotdf_recovered df_long.pivot(index门店, columns月份, values销量) print(df_recovered)基本能恢复成原来的宽表只是一些顺序和索引细节需要调整。这种长宽互转的灵活性是Pandas重构数据最核心的思想之一我把这套操作称为数据形态互转三连先搞清楚你要的最终形状再选对应的主方法必要时用一个辅助方法做二次加工。4.3 其他变长工具wide_to_longmelt能解决90%的宽转长需求但有一种场景不太方便宽表的列名带有共同前缀比如成绩_语文、成绩_数学、身高_cm、体重_kgmelt会把所有列混在一起丢失分组关系。这时候更好的选择是pandas.wide_to_long。举个例子df_stu pd.DataFrame({ 学号: [1, 2, 3], 成绩_期中: [88, 92, 80], 成绩_期末: [90, 89, 85], 考勤_期中: [10, 9, 10], 考勤_期末: [9, 10, 8] })现在想变成每行一个学号-阶段组合包含成绩和考勤两个指标df_stu_long pd.wide_to_long( df_stu, stubnames[成绩, 考勤], i学号, j阶段, sep_, suffix.* ) print(df_stu_long)wide_to_long的设计思想挺好理解stubnames是公共前缀i是标识主键j是后缀那列生成的新列名sep是分隔符suffix是后缀的正则表达式。它不像melt那样无脑熔而是按前缀分组再变长适合列名结构规整的数据。但要注意它的默认后缀匹配是\d即只匹配数字非数字后缀比如期中期末必须指定suffix.*否则会报错或者匹配为空。这也是我踩过的坑之一。5. 综合实战一次完整的数据重塑流程方法单独看都好懂但真实项目里往往要把它们串起来用。下面我用一个虚构的业务场景完整走一遍流程。5.1 需求与初始数据某公司运营部门拿到一份各区域各品类每月销售金额的宽表部分列还有合计、空值等问题。需求是先把数据清洗成长表再去掉明显异常的合计行最后转成按日期-区域的透视表并做一个对比图的数据源。初始表大致长这样df_raw pd.DataFrame({ 区域: [北京, 上海, 广州], 品类1_1月: [100, 150, 120], 品类1_2月: [88, 140, 110], 品类2_1月: [210, 240, 220], 品类2_2月: [200, 260, 250], 合计: [598, 790, 700] })这种表就是典型的为了好看而设计的展示结构直接分析寸步难行列名里既有品类又有月份最后还混着合计根本没有统一规范字段。5.2 数据清洗与标准化第一步先把列名里的品类和月份拆成两个维度。这里适合用melt但要先处理列名。我的做法是先把列名改造成标准的品类_月份形式然后melt。df_raw df_raw.rename(columns{合计: 总计}) # 人为构造更规范的数据实际项目里你可能从Excel读进来会有各种奇怪列名规范化的第一步永远是把所有非业务字段如总计、小计识别出来并从分析范围剔除。我用的是显式指定要保留的维度列的方式这也是最安全的。df_melt df_raw.melt( id_vars区域, var_name指标, value_name销售额 ) # 把指标拆成品类和月份 df_melt[品类] df_melt[指标].str.extract(r品类(\d)_(\d)月)[0] df_melt[月份] df_melt[指标].str.extract(r品类(\d)_(\d)月)[1] df_melt[月份] df_melt[月份].astype(int)这里坑点不少正则表达式如果写错会导致品类或月份变成NaN而NaN混在维度列里后面pivot或groupby时会生成奇怪的nan分组。我在实操中的对策是每做完一步提取立刻检查df_melt[品类].isna().sum()和月份数量分布确认没有丢失数据再往下走。5.3 多步重塑长→宽→长清洗完的长表一般长这样区域、指标、销售额、品类、月份。现在做最终的聚合透视pivot_result df_melt.pivot_table( index区域, columns月份, values销售额, aggfuncsum, fill_value0 ) print(pivot_result)如果后续还要继续给统计模型喂数据把它变回长表同时保留每行完整维度信息final_long pivot_result.reset_index().melt( id_vars区域, var_name月份, value_name销售额 ) print(final_long)整个链路里有一个细节值得提pivot_table之后再melt列名和顺序会发生微妙变化比如月份会变成数字类型区域作为普通列而非索引。如果不做reset_index直接meltid_vars拿不到区域。这个小坑只有自己写过一遍才会记得。提示每次重排生成新表后我都建议立即检查df.shape和df.columns并对照原始数据总记录数来判断是否发生了丢失或膨胀。比例不对就立刻停手别等画图才发现数据错了。6. 常见问题与排查经验6.1 问题速查表把我在实际项目中遇到最多的问题整理成一个速查表供大家直接对号入座问题现象常见原因解决方案pivot报ValueError: Index contains duplicate entriesindex与columns组合有重复先用drop_duplicates或换pivot_table聚合pivot后大量NaN部分组合没有值用pivot_table的fill_value0或对NaN做业务区分melt后列名变成数字后缀原列名本身是数字先转字符串astype(str)或替换为可读名stack/unstack后索引层级不对level参数用错或者索引有重名打印df.index或df.columns检查levels显式指定levelwide_to_long结果为空sep或suffix写错确认分隔符、后缀正则是否匹配真实列名groupby结果想转宽表出错结果行索引是MultiIndex用reset_index把分组列变成普通列再pivot表转来转去之后数据类型变了数值列被当成了索引列检查dtypes必要时用astype重置类型6.2 我的三条实战建议第一永远有一个标准长表作为主数据形态。我不管原始数据是什么形状清洗阶段第一件事就是melt或stack成长表然后用标准列名统一管理。长表对聚合、筛选、画图最友好展示形态等最后需要可见时再转宽。这样做的结果是所有项目里代码风格可以高度统一遇到新数据只要套同一个模板。第二结构转换前先备份原始数据。重排和旋转一旦选错方法数据量大的时候非常容易灾难性变形——几千行数据变出几百万行或者彻底丢失分组信息。我在项目里始终保留一份raw_df副本转换之前还会打印df.shape转完再打印一次两相对比立刻能发现异常。第三善用duplicated和dtypes做校验。reshape之后我最常做的两件事df[df.duplicated(subset[...])]去看有没有不该出现的重复行以及df.dtypes确认月份、ID这类字段没有被转成object类型。数据类型的隐性变化在melt、stack、pivot操作中非常常见也是最容易导致下游报错的原因。做数据分析这几年我对比过很多处理重塑方案坦白讲Pandas这几个核心方法已经覆盖了90%以上的场景。少数复杂场景可能会引入MultiIndex的碰撞但只要心里始终装着长表、宽表、索引层这三个坐标系再配上对shape和数据类型的主动检查基本不会走偏。希望这篇经验总结能让你少走一些我走过的弯路。