首页 / 资讯中心 / 文章详情

Pandas数据变形实战:pivot、melt、stack/unstack全解析

Pandas数据变形实战:pivot、melt、stack/unstack全解析 ★ FEATURED ARTICLE
1. 概念先行Pandas里的“变形”到底在解决什么问题Pandas学到Task04这个阶段很多人会卡在一个隐形门槛上明明函数都见过但是拿到真实数据不知道用哪个。尤其是“变形”这一块pivot、melt、stack、unstack这些词看起来不难一到自己上手就发懵。我当年学的时候也一样后来才意识到问题不出在记不住函数名而是没搞明白“变形”到底是在解决什么问题。先看一个日常场景。你在Excel里整理数据的时候经常要把行里的内容挪到列上去或者把几列堆成一列。比如一张表是“每个季度各地区的销售额”你可能想把季度从行维度变成列维度让每一行变成一个地区的全年记录。这在Excel里就是一次“复制粘贴转置”在Pandas里这就是pivot要做的事情。反过来如果你的数据里每一行都带着“季度1、季度2、季度3”三列你想把它压缩成“季度销售额”两列来方便后续的筛选和汇总这就是melt的活。说白了变形的本质只有两件事让数据变得更适合你当前的分析目的。宽表适合人眼阅读和横向对比长表适合程序处理和分组聚合。数据形态特点典型用途宽表列数多、行数少一个主体占一行日常报表、人眼阅读、透视展示长表列数少、行数多一个主体的多个属性堆叠在一起统计分析、画图、数据库存储、分组聚合问题在于很多人并不知道自己手里的数据应该用宽表还是长表所以拿到数据就卡住了。我可以给你一个非常简单的判断标准如果你下一步要做的操作是groupby是聚合是画分面图数据大概率应该先melt成长表如果你下一步要做的是对比某一行的多个属性或者给人汇报、打印数据多半应该pivot成宽表。这一篇Task04的博文我会把pivot、pivot_table、melt、stack、unstack这几个核心操作一次性讲透。我不打算只念一遍官方文档而是把我实际处理数据时怎么判断、怎么选、怎么避免踩坑的经验一起放进来。每个操作都会配可复现的代码示例你直接照抄就能跑。学完这一篇你不需要背任何函数签名只需要记住一条主线先判断你想让行变成列还是列变成行再判断你是否需要聚合最后决定用哪个函数。2. 变形的四大金刚pivot、pivot_table、melt、stack/unstack2.1 pivot把行里的唯一值变成列名pivot可能是名字最直白的一个函数——就真的是“透视”。它做的是把数据框里的某一列的值取出来当新列名另一列的值填充到新列下面。我们用一个最简单的场景来演示。假设你有一份销售数据记录的是每个销售员在每个季度的业绩import pandas as pd df pd.DataFrame({ 姓名: [张三, 张三, 李四, 李四, 王五, 王五], 季度: [Q1, Q2, Q1, Q2, Q1, Q2], 销售额: [120, 150, 130, 160, 110, 145] }) print(df)这是一份典型的长表数据同一个人的信息分布在多行里。你如果想横向对比三个人两个季度的业绩就可以用pivotdf_pivot df.pivot(index姓名, columns季度, values销售额) print(df_pivot)输出结果应该是季度 Q1 Q2 姓名 张三 120 150 李四 130 160 王五 110 145你看原来的“季度”列变成了两个新列“姓名”变成了行索引。这就是pivot最基础的行为。pivot有三个关键参数index、columns、values你只需要记住一句话index是最后要当行的列columns是要变成列名的列values是填充这些新列的值。pivot有一个非常容易踩坑的地方它要求index和columns的组合在原始数据里是唯一的。换句话说你不能有两行完全相同的“姓名季度”组合。如果出现这种情况pivot会直接报错“Index contains duplicate entries, cannot reshape”。我用一个例子来说明这个报错是怎么触发的。假如原始数据里张三在Q1出现了两条记录df_bad pd.DataFrame({ 姓名: [张三, 张三, 张三, 李四], 季度: [Q1, Q1, Q2, Q1], 销售额: [120, 90, 150, 130] }) df_bad.pivot(index姓名, columns季度, values销售额)这一跑就是报错。为什么因为“张三Q1”对应了两个不同的销售额pivot不知道120和90哪个该填进新表里。这个时候你有两个选择要么先把重复数据去重或聚合成一条要么改用pivot_table。2.2 pivot_table透视表的完全体自带聚合功能pivot_table就是在pivot的基础上加了一个聚合步骤。它的接口和pivot几乎一样只是多了一个aggfunc参数用来指定当出现重复组合时怎么处理。pivot要求数据必须唯一而pivot_table会把重复记录按你指定的方式合并起来。沿用上面这份有重复的数据我们想知道每个销售员每个季度的总销售额就可以写df_pt pd.pivot_table( df_bad, index姓名, columns季度, values销售额, aggfuncsum ) print(df_pt)这次就不会报错了输出是季度 Q1 Q2 姓名 张三 210 150 李四 130 NaN注意pivot_table还有个默认行为它会自动把聚合后为空的位置填成NaN。上面李四只有Q1的数据所以Q2就是NaN。如果你不想要这个NaN可以传fill_value0。df_pt_filled pd.pivot_table( df_bad, index姓名, columns季度, values销售额, aggfuncsum, fill_value0 )pivot_table之所以比pivot更常用是因为真实世界的数据几乎没有不重复的。同一个日期、同一个店铺、同一个商品就可能有多条记录。所以我在实际工作中pivot_table的使用频率远远高于pivot。记住一个原则有重复数据就是pivot_table没有重复数据才考虑pivot。aggfunc可以传什么它比很多人想象的要灵活得多。除了常用的sum、mean、count你还可以传一个列表同时对多个字段做不同的聚合df_multi pd.DataFrame({ 部门: [A, A, A, B, B, B], 月份: [1月, 1月, 2月, 1月, 2月, 2月], 收入: [100, 150, 200, 300, 250, 350], 成本: [60, 80, 120, 180, 150, 210] }) pd.pivot_table( df_multi, index部门, columns月份, values[收入, 成本], aggfunc[sum, mean] )这样的输出会形成一个多级列索引列的最外层是sum和mean内层是收入和成本。这个结果可以直接喂给后续的可视化代码不需要再做额外的格式调整。2.3 melt把宽表融化成可以分析的长表如果说pivot是“把长表变宽”那melt就是“把宽表变长”。melt的工作方式是把指定的多列全部“融化”成两列一列是原来的列名一列是对应的值。为什么需要这个操作因为Excel时代的报表习惯是宽表一个产品一行后面跟好几个月份的数据。但到了Python里做数据分析这种宽表非常不方便——你想按照月份做分组就做不到因为月份是列名不是数据。这时候就需要melt。我们用一份典型的宽表数据来看df_wide pd.DataFrame({ 产品: [手机, 电脑, 平板], 1月销量: [120, 80, 60], 2月销量: [130, 90, 70], 3月销量: [140, 100, 75] }) print(df_wide)这个表很直观但如果你想计算“所有产品在三个月的平均销量”或者画出每个产品随时间的折线图这个格式就不方便了。用melt把它变得规整df_long df_wide.melt( id_vars产品, value_vars[1月销量, 2月销量, 3月销量], var_name月份, value_name销量 ) print(df_long)输出结果是产品 月份 销量 0 手机 1月销量 120 1 电脑 1月销量 80 2 平板 1月销量 60 3 手机 2月销量 130 4 电脑 2月销量 90 5 平板 2月销量 70 6 手机 3月销量 140 7 电脑 3月销量 100 8 平板 3月销量 75从9行3列的宽表变成了9行3列的长表看似行数没变但结构变了——每一行变成了一个独立的“产品月份”观测值。这才是适合做分析和绘图的标准格式。melt的几个参数我用最容易理解的方式解释一下id_vars不需要变形的列保留下来作为标识变量。相当于“每一行属于谁”的身份信息。value_vars需要被融化的列列表。不写这个参数时melt会默认把除了id_vars以外的所有列都融化。var_name融化之后原列名存放的列叫什么。value_name融化之后原值存放的列叫什么。我实际使用melt时最常用的做法是只传id_vars其他都让它走默认。因为大多数情况下你确实想融化所有剩余列少写一个参数就少一处出错的可能。melt有一个容易被忽略的细节如果原始宽表里有多组属性列比如“1月销量、2月销量、1月库存、2月库存”直接melt会把它们全部堆到同一列里得到的是混合了销量和库存的长表。这种情况下最好先拆表再分别melt或者用后面讲到的stack配合MultiIndex来处理。我见过很多新手在这里栽跟头以为melt万能结果做出来的长表语义混乱。2.4 stack / unstack从索引层面做透视stack和unstack和前三个函数不太一样它们是在索引级别上做变换的处理的对象是MultiIndex多层索引。它们的逻辑一句话就能说清stack把列索引转成行索引的一部分unstack把行索引的一部分转到列上去。我用一个具体的例子。你有一个DataFrame行索引是姓名列索引是两层的科目、学期df_stack pd.DataFrame( [[85, 90, 78, 92], [88, 89, 82, 91]], index[学生A, 学生B], columnspd.MultiIndex.from_tuples([ (语文, 上学期), (语文, 下学期), (数学, 上学期), (数学, 下学期) ]) )这个表看起来就是平时考试的成绩单一个学生一行四个科目成绩排开。如果你想把科目和学期都变成行只用一次stack就能做到df_stacked df_stack.stack()得到的DataFrame行索引变成了“学生A 某个科目列名”的组合列的层级被移到了行索引的底部。如果你再对这个结果调用unstack()基本就能变回去。stack和unstack的灵活之处在于它们支持level参数。当你有三层索引的时候你想把哪一层移动到列上就指定level。比如df.unstack(level0)是把行索引的第一层移到列上df.unstack(level1)则是把第二层移到列上。不过我得说一句实在话stack和unstack这两个操作对初学者来说并不是每天都会用到。它们更多出现在一些高级场景里比如处理跨表计算、配合groupby实现复杂聚合、或者清洗乱七八槽的多层列数据。你在Task04这个阶段只要知道它们是“从索引层面变形的工具”就够了真到用的时候再回来看细节。3. 为什么变形成功但结果不是你想要的样子实操案例拆解3.1 从长表到宽表做一个标准的月度汇总报表把前面讲过的知识串到一起我们来解决一个日常工作中最常见的需求把一份每天一条记录的明细表变成按月、按部门的汇总宽表。假设原始数据是某个零售门店的每日流水长这样df_sales pd.DataFrame({ 日期: [2024-01-05, 2024-01-12, 2024-02-03, 2024-02-15, 2024-01-08, 2024-02-20, 2024-03-02, 2024-03-18], 部门: [生鲜, 生鲜, 生鲜, 生鲜, 日用品, 日用品, 日用品, 日用品], 销售额: [3200, 4100, 2800, 3900, 1500, 1800, 2100, 2300] }) print(df_sales)第一步先把日期列转成真正的日期类型再提取月份。这一步新手经常忘但如果日期还是字符串后面的排序和分组就会出问题df_sales[日期] pd.to_datetime(df_sales[日期]) df_sales[月份] df_sales[日期].dt.strftime(%Y-%m)第二步用pivot_table做透视。因为我们希望每个部门是一行每个月是一列所以index填“部门”columns填“月份”values填“销售额”df_monthly pd.pivot_table( df_sales, index部门, columns月份, values销售额, aggfuncsum, fill_value0 )最终输出月份 2024-01 2024-02 2024-03 部门 日用品 1500 1800 2100 生鲜 7300 6700 0到这里一份标准的汇总报表就出来了。如果你最后想把它导出给业务同事看再多做一步把索引变成普通列df_monthly df_monthly.reset_index() df_monthly.to_excel(月度汇总报表.xlsx, indexFalse)我在这一步上吃过亏。导出的Excel如果带上索引最左边会多出一列无表头的数字编号。业务同事不熟悉Python看到这一列就会跑来问你是什么意思。所以导出前记住一句口诀要给人看的表永远先reset_index导出时再带indexFalse。3.2 从宽表到长表清洗一份Excel抄过来的乱数据再来看一个反向的场景。假设你拿到一份别人从Excel发来的数据格式是这样每个店铺一列每个日期一行。这种表在Excel里看起来很舒服但你在Python里想做“每个日期下所有店铺的平均销售额”就会发现特别别扭。因为你没法在列上直接做groupby。df_shop_wide pd.DataFrame({ 日期: [2024-01-01, 2024-01-02, 2024-01-03], 店铺A: [1200, 1350, 1100], 店铺B: [800, 950, 900], 店铺C: [1500, 1600, 1700] })这种数据用melt一把梭就够df_shop_long df_shop_wide.melt( id_vars日期, var_name店铺, value_name销售额 )结果就是把三个店铺列融化成了“店铺销售额”两列每一行变成了“某天某店铺的销售额”。到这一步后续操作就非常轻松了# 每个店铺的平均销售额 df_shop_long.groupby(店铺)[销售额].mean() # 每个日期的总销售额 df_shop_long.groupby(日期)[销售额].sum()你用宽表直接做这两个操作会麻烦得多。所以以后看到别人发来的宽表别急着透视先想想你下一步要做什么如果要分组、聚合、画图就先melt。3.3 分步变形的组合拳先调索引再用stack有些复杂的变形需求单个函数搞不定。这时候就需要组合操作。我举个真实碰到的例子。某次我处理一份教育数据结构是这样的每个班级的数据占两行第一行是男生成绩第二行是女生成绩列名是“语文、数学、英语”。原始数据是从数据库里查出来的长这样df_class pd.DataFrame({ 班级: [一班, 一班, 二班, 二班], 性别: [男, 女, 男, 女], 语文: [78, 82, 88, 90], 数学: [85, 79, 92, 87], 英语: [90, 88, 85, 93] })我想做的是把“性别”这一列变成行把“科目”这一列变成原来的多层列。换句话说我希望最终的表格以班级为行索引以“性别 科目”为列索引df_reshaped df_class.set_index([班级, 性别]).stack().unstack(性别)我来逐步解释这行代码干了什么set_index([班级, 性别])把班级和性别都变成行索引此时剩下的语文、数学、英语就是普通列。.stack()把语文、数学、英语这三列压到行索引里形成一个三层索引的表。.unstack(性别)把性别这一层索引升为列。最终你得到一个以班级为行、性别科目为列的宽表既适合横向对比也适合继续做计算。这种组合拳在处理真实数据时非常常见。你不需要一步到位写出完美方案完全可以先拆成三步每步看一眼结果确认没问题再往下走。这也是我调试变形代码的习惯永远不要在一个单元格里写完一长串链式操作然后一把梭先逐步执行看中间结果。4. 常见问题与排查技巧实录4.1 “duplicate entries”报错查重比改参数更高效前面提过pivot遇到重复组合会直接报错。我第一次遇到这个错误时第一反应是换成pivot_table去聚合。后来我发现先搞清楚为什么有重复往往比直接换函数更重要。重复来源通常有三种原始数据就有重复记录比如同一订单被录入了两次数据虽然是唯一的但你pivot时选择的index/columns组合粒度太粗漏了关键的区分列数据存在一对多的关系本身就是个一对多结构。我的排查思路是这样的先用duplicated找出重复行看看重复行的数据是不是完全一样。如果完全一样说明是纯粹的冗余记录直接drop_duplicates就行。如果只是index和columns重复但values不同说明少了一个关键的区分维度需要把那个维度加进index或者columns里。# 找出重复组合 df_bad[df_bad.duplicated(subset[姓名, 季度], keepFalse)]这种问题不能靠换pivot_table一劳永逸因为有时候重复带来的聚合结果可能会掩盖数据的本来问题。比如某个订单被重复录入了两次你用sum聚合后数值翻倍了但你毫无察觉。4.2 变了形之后索引乱了重新set_index还是reset_index变形操作之后索引往往会变成MultiIndex。很多初学者在这一步容易晕什么时候该reset_index什么时候该set_index。我的经验法则只要下一步是要对数据做算术运算或者切片筛选索引不用太在意只要下一步是要导出给人看、或者跟其他表做按列合并必须先把索引变成普通列。比如pivot之后的表行索引是“姓名”你想把它恢复成一个普通列就做reset_index()。但如果你后续还要用这个“姓名”去left join别的表这个reset反而多余——因为merge的时候可以直接用索引当键只要加left_indexTrue, right_on姓名之类的参数。所以不要机械地记“变形后一定要reset_index”而是要看你下一步干什么。4.3 数据类型被搞乱了透视后的数值列变成object这是一个很容易踩但很少有人写出来的坑。当你用pivot_table汇总后如果某些单元格因为aggfunc没有匹配到值而产生了NaN或者你用了fill_value0但传入了一个字符串列的类型就可能是object而不是float。判断方法很简单print(df_pt.dtypes)如果你发现数值列的类型是object可以手动转换df_pt[销售额] df_pt[销售额].astype(float)或者更推荐的方法直接在创建透视表时就用fill_value0避免NaN混进去导致类型推断错乱。4.4 搞不清melt结果是长表还是宽表看行数变化有时候你自己melt完也不知道有没有搞对。一个很实用的自检方法宽表变长表之后行数应该等于value_vars列数 × 非id_vars组合数。举个例子3个产品、3个月份melt之后如果没有其他id_vars行数就是3×39行。如果你melt之后得到的行数不是这个数大概率是id_vars设置错了。这个方法能帮你快速定位问题。4.5 变形的性能问题大数据量下为什么卡顿pivot_table和melt在大数据集几百万行上会明显变慢。这不是Pandas本身的问题而是这些操作底层要做笛卡尔积式的结构重组。我的建议是先做必要的筛选把无用的行和列删掉再变形。比如只需分析某几个部门三个月的销售就先条件筛选再pivot_table。数据量从500万降到5万变形速度可以提高几十倍。如果筛选后数据依然很大同时你只是想做汇总统计而不是变换表格结构那可以考虑直接groupby配合unstack有些场景下性能比pivot_table好。5. 知识脑图怎么画Task04的学习路径梳理标题里提到了“知识梳理脑图”我讲一下我自己怎么梳理这一章的内容。脑图不是把函数名抄一遍就完事那样记不住。我画脑图画的是判断路径。我的Pandas变形脑图分了三个分支第一个分支是“目标判断”拿到一份数据先问自己想让它变宽还是变长。变宽走pivot/pivot_table变长走melt。第二个分支是“索引处理”如果涉及多层索引再考虑stack/unstack这一分支和前面的pivot/melt有交叉但核心思路完全围绕索引层级转置。第三个分支是“聚合需求”有重复数据、需要汇总统计就上pivot_table只是纯粹重排结构用pivot。这样画出来你会发现pivot、pivot_table、melt、stack/unstack不是四个孤立的知识点而是围绕同一个核心问题的四条路径。核心问题就是当前的行列结构是否符合你下一步的计算需求。我建议大家学完这一篇后自己找一份真实的Excel表格试着先用pivot_table做一张汇总宽表再把它melt回长表感受一下数据在你手里被来回揉捏的过程。这一步操作给你带来的手感比你刷一百道题的收获都大。6. 配套资源与学习建议别急着收藏跟着练才有效每次发学习类的总结都会有人留言问“有没有全套教程”“有没有带代码的文档”。实际上Task04这一块资源不需要多贵在精。我把手头常用的资源和使用思路整理一下。第一官方文档的“Reshaping and pivot tables”页面是最好的参考。它不是给你背的而是当字典查的。遇到一个记不清的参数直接搜页面里的函数名定位到那一段比你在任何博客里翻都靠谱。第二面向练习题把pivot、pivot_table、melt、stack、unstack各找几道典型题目练手。做这些题目的目的不是为了考高分而是让自己形成“看到数据结构就能自动反应出用什么功能”的条件反射。这种条件反射只能靠实际敲代码练出来。第三强烈推荐自己做一个“变形函数速查表”。格式不需要复杂就一张表格列上函数名、作用、最简单的一个一行代码示例。放在自己笔记软件里用时一搜就能找到。这张表一定是你自己亲手写的才有用因为写的过程就是理解的过程。关于学习顺序我的建议是分两天。第一天只学pivot_table和melt这两个覆盖了绝大多数日常工作场景。第二天再研究stack和unstack这两个是在前两个的基础上的进阶。一天不要贪多贪多嚼不烂。7. 写在最后的实操心得Pandas学到Task04这个阶段已经从“会读取、会筛选”进入“能摆弄数据结构”的层面了。这是分析能力的一次跃升因为数据处理这个环节超过一半的工作本质上就是在做变形。数据太宽你就想着怎么变长数据太长你就想着怎么变宽索引层级乱你就想着怎么用stack和unstack理顺。我自己学这一章的时候一开始也很抗拒这些函数觉得它们绕来绕去记不住。后来我发现与其硬记函数参数不如每次拿到数据先问自己一句“我现在这个结构做下一步操作方便吗”不方便就变形方便就直接干。这样想pivot和melt就变得非常自然了。还有一个小技巧想分享给你初学阶段每写完一个变形操作就把结果用shape打印出来看一眼。行数和列数的变化能立刻告诉你变形是否按预期发生。这一步几乎不需要额外成本但能把很多低级错误挡在门外。希望这篇Task04的梳理对你的Pandas学习有帮助。下一篇见。
阅读完成 · 觉得有帮助?
咨询建站