☰
数据重塑:melt、stack 与 unstack 的使用方法
2026/10/15 15:40:14 网站建设 项目流程

数据重塑:melt、stack 与 unstack 的使用方法

摘要

同一份业务数据,可能以适合人工阅读的宽表保存,也可能以适合程序分析的长表保存。宽表便于查看不同月份、渠道或指标的横向对比,长表则更适合分组、筛选、绘图和写入数据库。数据分析过程中,经常需要在两种结构之间转换。

本文介绍 pandas 中melt、stack和unstack的使用方法,说明长表与宽表的区别、标识列和度量列的划分方式,以及pivot、pivot_table与数据重塑之间的关系。最后通过销售数据案例完成宽转长、长转宽、MultiIndex 重塑和结果校验。

一、背景与问题

1. 宽表和长表

宽表通常把同一类指标展开到多个列中:

region channel 一月 二月 三月 华东 线上 120 150 180 华东 门店 90 110 125 华南 线上 80 95 130

长表则把“月份”从列名变成一列:

region channel month sales 华东 线上 一月 120 华东 线上 二月 150 华东 线上 三月 180

两种结构没有绝对的优劣,关键在于使用场景:

数据结构更适合的场景
宽表人工查看、报表展示、横向对比
长表分组聚合、筛选、可视化、建模、入库

2. 数据重塑不是数据聚合

数据重塑主要改变行列的组织方式,通常不会主动计算总和、平均值或最大值。数据聚合则会把多条记录压缩成统计结果。

例如,把“一月、二月、三月”转换为month列属于重塑;按照地区计算销售总额属于聚合。混淆二者,容易在转换过程中无意丢失明细。

3. 需要先明确一行代表什么

执行重塑前,要先写清楚数据粒度:

一行 = 一个地区 + 一个渠道 + 一个月份的销售记录

如果region + channel + month不能唯一确定一行,直接使用pivot就可能报错;如果强行使用pivot_table,还需要明确重复数据应该如何聚合。

二、核心概念

1.melt:把宽表转换为长表

melt的核心思想是:

保留标识列 ↓ 把多个指标列展开 ↓ 生成变量列和数值列

常用参数如下:

参数作用
id_vars需要原样保留的标识列
value_vars需要展开的列
var_name展开后变量列的名称
value_name展开后数值列的名称
ignore_index是否重新生成连续索引

例如,region和channel是标识列,一月、二月、三月是需要展开的度量列。

2.stack:把列层级移动到行索引

stack主要面向索引和列索引,作用是把列轴上的一个层级移动到行索引中。转换之后,结果通常会出现MultiIndex。

列轴:一月、二月、三月 ↓ stack 行索引:地区、渠道、月份

当 DataFrame 的列是普通单层列时,stack通常返回 Series;当列本身是多层列索引时,stack可以返回仍然带有多个列的 DataFrame。

3.unstack:把行索引层级移动到列

unstack与stack方向相反,它会把行索引中的某一层移动到列轴:

行索引:地区、渠道、月份 ↓ unstack("month") 列轴:一月、二月、三月

使用unstack前,通常需要先通过set_index创建多层索引,或者已经拥有一个MultiIndex。

4.pivot与pivot_table

方法是否允许重复键是否聚合
pivot不允许不聚合,重复键会报错
pivot_table允许会按照aggfunc聚合
unstack依赖索引组合不负责业务聚合

如果业务上确认每个键组合只有一条记录,优先使用pivot,这样可以尽早发现重复数据。如果重复数据本来就有业务意义,则应使用pivot_table并明确聚合规则。

三、工作原理

1.melt如何计算结果行数

如果原始表有n行,选择了m个value_vars,且没有过滤缺失值,那么melt通常会生成n × m行。

例如:

原始数据:3 行 展开列:3 列 结果数据:3 × 3 = 9 行

因此,展开前后行数变化是可预期的。不能把行数增加直接判断为重复数据。

2.stack与 MultiIndex

stack的关键不是简单地“把列变成行”,而是将列标签写入索引层级。一个二维表在重塑后可能变成:

region channel month 华东 线上 一月 120 二月 150 华东 门店 一月 90 二月 110

如果索引或列轴中存在多个层级,就要通过层级名称或位置指定重塑对象,例如stack("month")或unstack("channel")。

3. 缺失值与零值不是一回事

重塑可能产生两类“空”:

  • 原始数据中本来没有这条组合记录;
  • 这条组合存在,但业务值确实为零;

两者不能直接用fillna(0)混为一谈。前者可能表示没有销售、未采集或不适用,后者表示确实发生了记录值为零的业务事件。

4. 重塑后的排序

melt会按照value_vars的顺序生成结果。stack和unstack可能改变索引顺序,因此进行结果比较时,应该明确排序字段,而不是依赖当前显示顺序。

四、实战示例

1. 准备宽表

下面的示例使用 Python 3.10+ 和 pandas 2.1+。各代码块按顺序运行,可以直接使用交互式解释器或 Jupyter Notebook。

python -m pip install "pandas>=2.1"
importpandasaspd wide=pd.DataFrame({"region":["华东","华东","华南"],"channel":["线上","门店","线上"],"一月":[120,90,80],"二月":[150,110,95],"三月":[180,125,130],})print(wide)

这张表中,region和channel用来识别一条业务记录,月份列是需要展开的销售指标。

2. 使用melt转成长表

long=wide.melt(id_vars=["region","channel"],value_vars=["一月","二月","三月"],var_name="month",value_name="sales",)long=long.sort_values(["region","channel","month"],ignore_index=True,)print(long)

结果结构如下:

region channel month sales 0 华东 线上 一月 120 1 华东 线上 三月 180 2 华东 线上 二月 150 ...

如果需要按自然月份排序,不要依赖中文月份的字符串排序,可以显式设置分类顺序:

month_order=["一月","二月","三月"]long["month"]=pd.Categorical(long["month"],categories=month_order,ordered=True,)long=long.sort_values(["region","channel","month"],ignore_index=True,)

3. 从长表恢复宽表

因为region + channel + month在本例中是唯一键,所以可以使用pivot:

restored=(long.pivot(index=["region","channel"],columns="month",values="sales",).reindex(columns=month_order).reset_index())restored.columns.name=Noneprint(restored)

pivot不会替你处理重复键。如果同一个地区、渠道和月份出现多条记录,可以改用:

summary=long.pivot_table(index=["region","channel"],columns="month",values="sales",aggfunc="sum",fill_value=0,observed=True,).reindex(columns=month_order).reset_index()summary.columns.name=None

这里的sum是明确的业务选择,不应为了消除报错而随意添加。

4. 使用stack和unstack

下面把宽表的标识列设置为多层索引,再将月份列堆叠到索引中:

indexed=wide.set_index(["region","channel"])stacked=indexed.stack(future_stack=True)stacked.name="sales"stacked.index=stacked.index.set_names(["region","channel","month"])print(stacked)

再把month这一层索引移回列轴:

unstacked=stacked.unstack("month")unstacked=unstacked.reindex(columns=month_order)print(unstacked)

future_stack参数从 pandas 2.1 开始提供。这里显式使用新的堆叠实现,不再依赖旧实现的默认缺失值处理;使用它时,不要同时传入dropna和sort。如果项目仍使用更早的 pandas,请查阅对应版本文档或先升级,不要直接复制混用版本的参数。

对于本例这种单层列结构,新实现会保留原表中已有位置上的缺失值:

with_missing=indexed.copy()with_missing.loc[("华东","线上"),"二月"]=float("nan")missing_stacked=with_missing.stack(future_stack=True)assertlen(missing_stacked)==with_missing.sizeassertpd.isna(missing_stacked.loc[("华东","线上","二月")])

“保留已有缺失值”不等于“自动补齐所有可能的索引组合”。对多层列索引,仍要区分原表存在的列组合和业务上根本不存在的组合。

5. 验证重塑是否保持数据

仅仅看到结果形状正确还不够,还应该验证键和数值是否一致:

expected=(wide.set_index(["region","channel"]).reindex(columns=month_order).sort_index())actual=(restored.set_index(["region","channel"]).reindex(columns=month_order).sort_index())pd.testing.assert_frame_equal(expected,actual,check_names=False,)

在生产脚本中,可以额外检查:

assertlen(long)==len(wide)*len(month_order)assertnotlong.duplicated(["region","channel","month"]).any()assertlong["sales"].sum()==wide[month_order].sum().sum()

这些检查分别验证了行数、键唯一性和总量是否保持。

五、常见问题与实践建议

1.pivot报重复键错误

先定位重复组合:

duplicates=(long.groupby(["region","channel","month"],dropna=False,observed=True,).size().reset_index(name="count").query("count > 1"))print(duplicates)

如果重复记录是脏数据,应回到数据源处理;如果重复记录表示多笔订单,则先按照业务规则聚合,再进行透视。

2. 把melt当成聚合操作

melt只是改变数据排列方式,不会自动计算每个地区的总销售额。需要统计时,应该显式使用groupby:

region_total=(long.groupby("region",as_index=False)["sales"].sum().rename(columns={"sales":"total_sales"}))

3. 月份、季度等标签被错误排序

字符串排序不一定符合业务顺序。月份、季度、等级和流程状态等字段,应使用有序分类或显式排序映射。

4. 把缺失值直接填成零

在填充之前先确认空值语义。如果空值表示“没有销售记录”,可以在明确业务规则后填零;如果表示“数据未采集”,则应保留缺失或单独增加质量标记。

5. 重塑后索引名称没有清理

pivot、stack和unstack可能保留columns.name或多层索引名称。输出报表前,可以使用:

result=unstacked.reset_index()result.columns.name=None

6. 不检查数据量变化

对melt来说,行数增加通常是正常的;对pivot来说,行数减少也可能是正常的。关键是提前计算预期粒度,并在代码中加入断言。

六、进阶思考

1. 宽表和长表应服务于不同环节

一个稳定的数据处理流程,通常可以采用如下分工:

原始文件宽表 ↓ melt 标准化长表 ↓ 清洗、关联、聚合 分析结果长表 ↓ pivot / unstack 报表展示宽表

不要为了让中间结果“看起来像 Excel”而过早恢复成宽表,否则可能增加后续筛选和聚合的复杂度。

2. 多层列索引适合表达多个维度

当列同时包含“指标”和“统计口径”时,可以使用MultiIndex:

columns=pd.MultiIndex.from_tuples([("sales","sum"),("sales","mean"),],names=["metric","agg"],)multi=pd.DataFrame([[450,150],[325,325/3]],index=pd.Index(["线上","门店"],name="channel"),columns=columns,)metric_rows=multi.stack("agg",future_stack=True)print(metric_rows)

这里把agg从列层级移到行索引,metric仍保留在列轴,因此结果是 DataFrame,而不是 Series。

这类结构适合复杂报表,但对下游系统不够直观。导出 CSV 或写入数据库前,通常需要扁平化列名,并检查扁平化后是否出现重名。

3. 注意数据量膨胀

melt会把每个原始行复制到多个度量变量。对于百万级数据,展开前要估算内存占用,必要时只选择需要的value_vars,或分批处理文件。

4. 重塑结果要保留可追溯字段

长表中应保留能够定位原始记录的字段,例如订单号、日期、地区、渠道和产品编码。只有指标值而没有业务主键的长表,很难排查异常和回溯来源。

5. 把数据契约和校验一起封装

不要仅封装一行melt。更有价值的是把输入字段、键唯一性和输出行数约束放在同一个函数中:

defreshape_sales(frame,months):required={"region","channel",*months}missing=required-set(frame.columns)ifmissing:raiseValueError(f"缺少字段:{sorted(missing)}")ifframe.duplicated(["region","channel"]).any():raiseValueError("地区与渠道组合必须唯一")result=frame.melt(id_vars=["region","channel"],value_vars=months,var_name="month",value_name="sales",)assertlen(result)==len(frame)*len(months)returnresult checked_long=reshape_sales(wide,month_order)assertlen(checked_long)==9

往返转换不保证所有元数据自动恢复:多个数值列合并到一个值列时,类型可能统一;类别顺序、轴名称也可能变化。测试除了检查总和,还应比较各业务键对应的值,并根据项目契约决定是否严格检查数据类型。

结论

melt适合把宽表转换成长表,stack适合把列轴层级移动到行索引,unstack则可以把索引层级恢复到列轴。掌握这三个操作后,可以在业务明细、统计中间表和报表展示表之间灵活转换。

实际使用时,要先明确一行的业务粒度,再检查键是否唯一、空值代表什么、重塑后数据量是否符合预期。下一步可以继续学习 pandas 的描述性统计、时间序列分析和可视化,把重塑后的标准长表用于更完整的数据分析流程。

参考资料

  • pandas DataFrame.melt 官方文档
  • pandas DataFrame.stack 官方文档
  • pandas 数据重塑与透视表官方指南

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询