Skip to content

Pandas Melt:将宽表数据重塑为长表格式(完整指南)

Updated on

你有一张电子表格,每个月都是一个单独的列——January、February、March,一直到 December。收入数字分布在 12 列里,而你需要画时间序列、做 groupby,或者把数据喂给机器学习模型。这些操作在宽格式(wide-format)数据上都不太好用。你需要一列存放月份名称,另一列存放收入数值。这个转换——把“列变成行”——正是 pandas melt 的作用。

而且这个问题会迅速恶化。在问卷数据、传感器读数、财务报表,以及从 Excel 或 SQL 导出的 pivot 表中,包含几十甚至上百列的宽表很常见。手动重构既繁琐又容易出错。如果没有合适的工具,每多一列要反透视(unpivot),就意味着更多样板代码。

pd.melt() 函数用一次调用就能解决。它接收一个宽表 DataFrame,并通过把选定的列转换为行,将其“融化(melt)”为长格式,同时完整保留标识列(identifier columns)。本指南将覆盖完整语法、所有参数、来自真实场景的实用示例,并与 pivotstackwide_to_long 等相关重塑函数进行对比。

📚

pd.melt() 的作用是什么

pd.melt() 会把 DataFrame 从宽格式反透视(unpivot)为长格式。在宽格式中,每个变量都有自己的一列。在长格式(也称为 “tidy” 格式)中,会有一列存放变量名,另一列存放变量值。

下面是概念上的转换:

宽格式(melt 前):

studentmathscienceenglish
Alice908588
Bob789280

长格式(melt 后):

studentsubjectscore
Alicemath90
Alicescience85
Aliceenglish88
Bobmath78
Bobscience92
Bobenglish80

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)

参数说明

ParameterDescriptionDefault
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     1200

regionproduct 都会在每一行中被保留。四个季度列被折叠为两列。

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 提供了多种重塑函数。下面是各自适用场景:

FunctionDirectionBest ForKey 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() 专门针对列名遵循 score1score2score3 这种命名模式的情况:

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):

OperationApproximate Time
melt() with 50 value columns~15 ms
stack() equivalent~10 ms
Manual loop with concat()~500 ms

提升性能的小技巧:

  1. 显式指定 value_vars ——只 melt 你需要的列,比 melt 全部列更快。
  2. 使用 ignore_index=True(默认)——保留原始索引会带来额外开销。
  3. 避免 melt 后立刻再 pivot ——如果你只是需要另一种宽表形式,考虑直接用 pivot_table()rename(),而不是“来回转换(round-trip)”。
  4. 对于超大 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_varsvalue_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_namevalue_name 为输出列提供有意义的名字。
  • melt 与 pivot 互为逆操作——melt() 用于 wide-to-long,pivot() 用于 long-to-wide。
  • 当你需要一个扁平的 DataFrame(而不是 MultiIndex 的 Series)并希望明确列名时,优先选择 melt() 而不是 stack()
  • melt 后要清理——可对 variable 列使用字符串方法,去掉前后缀或重新格式化取值。

在你完成数据重塑后,PyGWalker (opens in a new tab) 这类工具可以让你无需编写图表代码就能直观探索结果,让分析流程更快、更顺畅。

📚