Pandas Melt:将宽表数据重塑为长表格式(完整指南)
Updated on
你有一张电子表格,每个月都是一个单独的列——January、February、March,一直到 December。收入数字分布在 12 列里,而你需要画时间序列、做 groupby,或者把数据喂给机器学习模型。这些操作在宽格式(wide-format)数据上都不太好用。你需要一列存放月份名称,另一列存放收入数值。这个转换——把“列变成行”——正是 pandas melt 的作用。
而且这个问题会迅速恶化。在问卷数据、传感器读数、财务报表,以及从 Excel 或 SQL 导出的 pivot 表中,包含几十甚至上百列的宽表很常见。手动重构既繁琐又容易出错。如果没有合适的工具,每多一列要反透视(unpivot),就意味着更多样板代码。
pd.melt() 函数用一次调用就能解决。它接收一个宽表 DataFrame,并通过把选定的列转换为行,将其“融化(melt)”为长格式,同时完整保留标识列(identifier columns)。本指南将覆盖完整语法、所有参数、来自真实场景的实用示例,并与 pivot、stack、wide_to_long 等相关重塑函数进行对比。
pd.melt() 的作用是什么
pd.melt() 会把 DataFrame 从宽格式反透视(unpivot)为长格式。在宽格式中,每个变量都有自己的一列。在长格式(也称为 “tidy” 格式)中,会有一列存放变量名,另一列存放变量值。
下面是概念上的转换:
宽格式(melt 前):
| student | math | science | english |
|---|---|---|---|
| Alice | 90 | 85 | 88 |
| Bob | 78 | 92 | 80 |
长格式(melt 后):
| student | subject | score |
|---|---|---|
| Alice | math | 90 |
| Alice | science | 85 |
| Alice | english | 88 |
| Bob | math | 78 |
| Bob | science | 92 |
| Bob | english | 80 |
student 列作为标识符被保留,而三个科目列被融化为两列:一列存放科目名称,另一列存放分数。
pd.melt() 语法与参数
pd.melt(frame, id_vars=None, value_vars=None, var_name=None,
value_name='value', col_level=None, ignore_index=True)你也可以作为 DataFrame 的方法来调用:
df.melt(id_vars=None, value_vars=None, var_name=None,
value_name='value', col_level=None, ignore_index=True)参数说明
| Parameter | Description | Default |
|---|---|---|
frame | 要 melt 的 DataFrame(使用 df.melt() 时不需要) | Required |
id_vars | 需要保留为标识变量的列(不会被 melt) | None |
value_vars | 需要反透视为行的列。如果省略,则使用所有不在 id_vars 中的列 | None |
var_name | 新列名:用于保存原始列名 | 'variable' |
value_name | 新列名:用于保存数值 | 'value' |
col_level | 如果列是 MultiIndex,指定要 melt 的层级 | None |
ignore_index | 若为 True,结果使用新的整数索引;若为 False,保留原索引 | True |
基础示例:学生成绩
从上面的学生成绩示例开始:
import pandas as pd
grades = pd.DataFrame({
'student': ['Alice', 'Bob', 'Charlie'],
'math': [90, 78, 85],
'science': [85, 92, 88],
'english': [88, 80, 91]
})
long = pd.melt(grades, id_vars=['student'], value_vars=['math', 'science', 'english'],
var_name='subject', value_name='score')
print(long)输出:
student subject score
0 Alice math 90
1 Bob math 78
2 Charlie math 85
3 Alice science 85
4 Bob science 92
5 Charlie science 88
6 Alice english 88
7 Bob english 80
8 Charlie english 91现在每一行代表一个“学生-科目”的组合。原始 3x4 的 DataFrame(3 行 4 列)变成了 9x3 的 DataFrame(9 行 3 列)。
省略 value_vars
如果你不传 value_vars,pandas 会 melt 所有未包含在 id_vars 里的列:
long = grades.melt(id_vars=['student'], var_name='subject', value_name='score')
print(long)这会得到与前一个示例相同的结果。当你想 melt 所有非标识列时,省略 value_vars 很方便。
不使用 id_vars 进行 melt
你也可以不指定任何标识列进行 melt。这样每一列都会进入 variable/value 这对列中:
temperatures = pd.DataFrame({
'Jan': [30, 28],
'Feb': [32, 31],
'Mar': [45, 42]
})
long = temperatures.melt(var_name='month', value_name='temp_f')
print(long)输出:
month temp_f
0 Jan 30
1 Jan 28
2 Feb 32
3 Feb 31
4 Mar 45
5 Mar 42当每一列都是测量值、并且没有需要保留的标识信息时,这种方式很有用。
多个标识列(Identifier Columns)
真实数据集往往不止一个标识列。你可以给 id_vars 传入列名列表:
sales = pd.DataFrame({
'region': ['North', 'South', 'North', 'South'],
'product': ['Widget', 'Widget', 'Gadget', 'Gadget'],
'q1_revenue': [1200, 1500, 800, 950],
'q2_revenue': [1400, 1600, 900, 1100],
'q3_revenue': [1100, 1450, 850, 1000],
'q4_revenue': [1500, 1700, 1000, 1200]
})
long_sales = sales.melt(
id_vars=['region', 'product'],
var_name='quarter',
value_name='revenue'
)
print(long_sales)输出:
region product quarter revenue
0 North Widget q1_revenue 1200
1 South Widget q1_revenue 1500
2 North Gadget q1_revenue 800
3 South Gadget q1_revenue 950
4 North Widget q2_revenue 1400
5 South Widget q2_revenue 1600
6 North Gadget q2_revenue 900
7 South Gadget q2_revenue 1100
8 North Widget q3_revenue 1100
9 South Widget q3_revenue 1450
10 North Gadget q3_revenue 850
11 South Gadget q3_revenue 1000
12 North Widget q4_revenue 1500
13 South Widget q4_revenue 1700
14 North Gadget q4_revenue 1000
15 South Gadget q4_revenue 1200region 和 product 都会在每一行中被保留。四个季度列被折叠为两列。
Melt 后的清理工作
melt 之后,variable 列里通常包含你想清理的字符串。在上面的示例里,quarter 列的值是 q1_revenue 这种形式,而不是 Q1。可以使用字符串操作来处理:
long_sales['quarter'] = long_sales['quarter'].str.replace('_revenue', '').str.upper()
print(long_sales.head())输出:
region product quarter revenue
0 North Widget Q1 1200
1 South Widget Q1 1500
2 North Gadget Q1 800
3 South Gadget Q1 950
4 North Widget Q2 1400只 melt 选定的列
有时你只想 melt 一部分列。此时在 value_vars 中显式指定:
survey = pd.DataFrame({
'respondent': ['R1', 'R2', 'R3'],
'age': [25, 34, 42],
'q1_satisfaction': [4, 5, 3],
'q2_satisfaction': [3, 4, 5],
'q3_satisfaction': [5, 3, 4],
'income': [50000, 75000, 60000]
})
# Only melt the satisfaction columns, keep age and income as identifiers
long_survey = survey.melt(
id_vars=['respondent', 'age', 'income'],
value_vars=['q1_satisfaction', 'q2_satisfaction', 'q3_satisfaction'],
var_name='question',
value_name='rating'
)
print(long_survey)输出:
respondent age income question rating
0 R1 25 50000 q1_satisfaction 4
1 R2 34 75000 q1_satisfaction 5
2 R3 42 60000 q1_satisfaction 3
3 R1 25 50000 q2_satisfaction 3
4 R2 34 75000 q2_satisfaction 4
5 R3 42 60000 q2_satisfaction 5
6 R1 25 50000 q3_satisfaction 5
7 R2 34 75000 q3_satisfaction 3
8 R3 42 60000 q3_satisfaction 4即使 income 不在 melt 的列里,它仍作为标识列被保留。
真实场景示例:时间序列数据
金融与宏观经济数据经常以“日期作为列名”的宽格式出现。melt 可以把它转换成便于绘图的时间序列格式:
import pandas as pd
gdp = pd.DataFrame({
'country': ['USA', 'UK', 'Germany'],
'2020': [20.94, 2.71, 3.89],
'2021': [23.00, 3.12, 4.26],
'2022': [25.46, 3.07, 4.07],
'2023': [27.36, 3.33, 4.46]
})
gdp_long = gdp.melt(id_vars=['country'], var_name='year', value_name='gdp_trillion_usd')
gdp_long['year'] = gdp_long['year'].astype(int)
gdp_long = gdp_long.sort_values(['country', 'year']).reset_index(drop=True)
print(gdp_long)输出:
country year gdp_trillion_usd
0 Germany 2020 3.89
1 Germany 2021 4.26
2 Germany 2022 4.07
3 Germany 2023 4.46
4 UK 2020 2.71
5 UK 2021 3.12
6 UK 2022 3.07
7 UK 2023 3.33
8 USA 2020 20.94
9 USA 2021 23.00
10 USA 2022 25.46
11 USA 2023 27.36现在你可以轻松画 GDP 的时间趋势图、按国家 group by,或计算同比增长。
对 MultiIndex 列进行 melt
如果你的 DataFrame 有多层级列头(multi-level column headers),可以用 col_level 指定要 melt 的层级:
arrays = [['score', 'score', 'attendance', 'attendance'],
['midterm', 'final', 'midterm', 'final']]
columns = pd.MultiIndex.from_arrays(arrays, names=['metric', 'exam'])
data = pd.DataFrame([[85, 90, 95, 100], [78, 82, 90, 88]],
index=['Alice', 'Bob'], columns=columns)
# Melt the top level
melted = data.melt(col_level=0, var_name='metric', value_name='value', ignore_index=False)
print(melted)在更复杂的多层级场景中,你可能需要先展平列名(例如用 droplevel(),或用下划线把层级拼接起来)再进行 melt。
Melt vs Pivot:互逆操作
melt() 和 pivot() 是互逆操作。melt 把宽表变长表;pivot 把长表变宽表。
import pandas as pd
# Start wide
wide = pd.DataFrame({
'name': ['Alice', 'Bob'],
'math': [90, 78],
'science': [85, 92]
})
# Melt: wide -> long
long = wide.melt(id_vars='name', var_name='subject', value_name='score')
print("Long format:")
print(long)
# Pivot: long -> wide (round-trip)
back_to_wide = long.pivot(index='name', columns='subject', values='score').reset_index()
back_to_wide.columns.name = None
print("\nBack to wide format:")
print(back_to_wide)输出:
Long format:
name subject score
0 Alice math 90
1 Bob math 78
2 Alice science 85
3 Bob science 92
Back to wide format:
name math science
0 Alice 90 85
1 Bob 78 92关键差异:pivot() 要求 index 与 columns 的组合必须唯一。如果你的长表数据存在重复组合,请使用带聚合函数的 pivot_table()。
Melt vs Stack vs wide_to_long
pandas 提供了多种重塑函数。下面是各自适用场景:
| Function | Direction | Best For | Key Difference |
|---|---|---|---|
melt() | wide to long | 将指定列反透视为行 | 基于列操作;对初学者最直观 |
stack() | wide to long | 把列层级压入索引层级 | 基于索引操作;适用于 MultiIndex |
wide_to_long() | wide to long | 列名具有共同前缀 + 数字后缀(如 score1, score2) | 自动解析 stub 名称 |
pivot() | long to wide | 将值展开为列(键唯一) | melt() 的逆操作 |
unstack() | long to wide | 将索引层级展开为列 | stack() 的逆操作 |
何时改用 stack()
stack() 会对列索引进行操作,并将其压入行索引。当你已经在使用 MultiIndex,且希望在索引层级进行重塑时最有用:
wide = pd.DataFrame({
'math': [90, 78],
'science': [85, 92]
}, index=['Alice', 'Bob'])
stacked = wide.stack()
print(stacked)输出:
Alice math 90
science 85
Bob math 78
science 92
dtype: int64结果是一个带 MultiIndex 的 Series,而不是像 melt() 那样带命名列的 DataFrame。当你希望得到“整洁”的 DataFrame 输出,并明确指定列名时,使用 melt() 更合适。
何时使用 wide_to_long()
wide_to_long() 专门针对列名遵循 score1、score2、score3 这种命名模式的情况:
df = pd.DataFrame({
'student': ['Alice', 'Bob'],
'score1': [90, 78],
'score2': [85, 92],
'score3': [88, 80]
})
long = pd.wide_to_long(df, stubnames='score', i='student', j='exam_num')
print(long.reset_index())输出:
student exam_num score
0 Alice 1 90
1 Alice 2 85
2 Alice 3 88
3 Bob 1 78
4 Bob 2 92
5 Bob 3 80当列名具有一致的“前缀-后缀”规律时用 wide_to_long() 很省事;否则 melt() 更灵活。
性能注意事项
对大多数数据集(少于几百万行),melt() 的速度足够快。下面是在一个 100,000 行、50 列的 DataFrame 上进行 melt 的基准(benchmark):
| Operation | Approximate Time |
|---|---|
melt() with 50 value columns | ~15 ms |
stack() equivalent | ~10 ms |
Manual loop with concat() | ~500 ms |
提升性能的小技巧:
- 显式指定
value_vars——只 melt 你需要的列,比 melt 全部列更快。 - 使用
ignore_index=True(默认)——保留原始索引会带来额外开销。 - 避免 melt 后立刻再 pivot ——如果你只是需要另一种宽表形式,考虑直接用
pivot_table()或rename(),而不是“来回转换(round-trip)”。 - 对于超大 DataFrame(melt 后达到 1e8+ 行),可以考虑 polars 或 Dask,它们支持 lazy evaluation 与并行计算。
# Only melt the columns you actually need
long = df.melt(
id_vars=['id'],
value_vars=['col_a', 'col_b', 'col_c'], # Not all 50 columns
var_name='metric',
value_name='reading'
)常见错误与解决方法
1. KeyError: Column Not Found
当 id_vars 或 value_vars 中的列名在 DataFrame 里不存在时会出现:
# Wrong: column name has a typo
long = df.melt(id_vars=['stduent']) # KeyError
# Fix: check column names first
print(df.columns.tolist())2. 出现了你意料之外的重复行
melt 本身不会“凭空制造重复”——它会为每个标识变量组合生成一行。如果你看到重复,说明原始数据中的标识行本来就重复:
# Check for duplicates in your id columns
print(df.duplicated(subset=['student']).sum())3. value 列的数据类型混杂
当你 melt 不同 dtype 的列(例如有的 int64,有的 float64)时,pandas 会把 value 列 upcast 到更通用的类型。如果数值列和字符串列混在一起 melt,value 列会变成 object dtype:
df = pd.DataFrame({
'id': [1, 2],
'score': [90, 85],
'grade': ['A', 'B']
})
long = df.melt(id_vars='id')
print(long.dtypes)
# variable object
# value object <-- both score and grade become object要避免这种情况,把数值列与字符串列分开 melt。
使用 PyGWalker 可视化 melt 后的数据
当你把数据从宽格式重塑到长格式之后,自然的下一步就是对结果进行可视化探索——查看分布、比较组间差异、或发现异常值。PyGWalker (opens in a new tab) 是一个开源 Python 库,可以在 Jupyter Notebook 内把任意 pandas DataFrame 变成交互式、类似 Tableau 的可视化探索界面。
import pandas as pd
import pygwalker as pyg
# Melt your wide data into long format
grades = pd.DataFrame({
'student': ['Alice', 'Bob', 'Charlie', 'Diana'],
'math': [90, 78, 85, 92],
'science': [85, 92, 88, 79],
'english': [88, 80, 91, 84]
})
long = grades.melt(id_vars='student', var_name='subject', value_name='score')
# Launch interactive visualization
walker = pyg.walk(long)在 PyGWalker 中,你可以把 subject 拖到 x-axis,把 score 拖到 y-axis,再按 student 着色,立刻比较不同科目的表现——不需要写任何图表代码。它支持柱状图、散点图、箱线图等,通过拖拽即可完成。
在 Google Colab (opens in a new tab) 或 Kaggle (opens in a new tab) 中试用 PyGWalker,或通过
pip install pygwalker安装。
FAQ
pandas melt 是做什么的?
pandas 的 melt() 会把 DataFrame 从宽格式重塑为长格式。它把列转换为行,并生成两列:一列保存原列名(variable),另一列保存对应的值(value)。这也被称为“反透视(unpivot)”。
pandas 中 melt 和 pivot 有什么区别?
melt() 将宽格式转换为长格式(列变行)。pivot() 则相反——把长格式转换为宽格式(行变列)。它们是互逆操作。如果你对 DataFrame 进行 melt,然后用相同参数对结果 pivot,通常可以还原回原始 DataFrame。
什么时候用 melt,什么时候用 stack?
当你希望得到一个“干净”的 DataFrame(有命名列),并且想明确控制哪些列要反透视时,用 melt()。当你在处理 MultiIndex 列,并希望把列层级推入行索引时,用 stack()。对初学者来说 melt() 更直观;而 stack() 在层级重塑上更强大。
如何在 pandas 里 melt 多列?
在 value_vars 里传入列名列表:df.melt(id_vars=['id'], value_vars=['col_a', 'col_b', 'col_c'])。所有列都会被反透视为行。如果省略 value_vars,pandas 会 melt 所有不在 id_vars 中的列。
能对列名重复的 DataFrame 进行 melt 吗?
pandas 可以对其 melt,但结果可能会令人困惑,因为 variable 列会出现重复值。建议先用 df.columns = [...] 或 df.rename() 重命名重复列,避免歧义。
如何把 melt 反向还原?
使用 pivot() 或 pivot_table() 将 melt 后的长表转换回宽表:long.pivot(index='id', columns='variable', values='value')。如果存在重复的 index-columns 组合,需要聚合时使用 pivot_table()。
总结
pandas 的 melt() 函数是 Python 中将宽格式 DataFrame 转换为长(tidy)格式的标准做法。核心要点:
- 用
id_vars指定要保留为标识符的列。 - 用
value_vars控制哪些列会被 melt;省略它则会 melt 除标识列之外的所有列。 - 用
var_name与value_name为输出列提供有意义的名字。 - melt 与 pivot 互为逆操作——
melt()用于 wide-to-long,pivot()用于 long-to-wide。 - 当你需要一个扁平的 DataFrame(而不是 MultiIndex 的 Series)并希望明确列名时,优先选择
melt()而不是stack()。 - melt 后要清理——可对 variable 列使用字符串方法,去掉前后缀或重新格式化取值。
在你完成数据重塑后,PyGWalker (opens in a new tab) 这类工具可以让你无需编写图表代码就能直观探索结果,让分析流程更快、更顺畅。