一、概述
在数据分析中,透视表(Pivot Table) 是最强大的数据汇总工具之一。Pandas 提供了完整的透视表生态系统,包括 pivot_table、pivot、crosstab、melt、stack、unstack 和 explode 等函数,帮助我们对数据进行多维度的重塑与汇总分析。本文将系统讲解每个工具的用法、参数含义和最佳实践。
核心知识点:
- pivot_table:聚合透视表,支持多级行/列索引、自定义聚合函数、边际总计
- pivot:简单行转列,无聚合功能(数据必须唯一)
- crosstab:专门用于频次/交叉统计,支持归一化
- melt:宽表转长表,逆透视操作
- stack/unstack:行列层级索引转换
- explode:列表型单元格展开为多行
二、准备数据
为方便演示,我们先构造一份销售数据集,涵盖多个维度的字段:
import pandas as pd
import numpy as np
# 构造销售数据集
df = pd.DataFrame({
'日期': ['2024-01', '2024-01', '2024-01',
'2024-02', '2024-02', '2024-02',
'2024-03', '2024-03', '2024-03'],
'区域': ['华东', '华南', '华北',
'华东', '华南', '华北',
'华东', '华南', '华北'],
'产品': ['A', 'A', 'B',
'A', 'B', 'B',
'A', 'B', 'A'],
'销量': [100, 150, 80, 120, 90, 110, 130, 95, 105],
'金额': [10000, 15000, 8000, 12000, 9000,
11000, 13000, 9500, 10500]
})
print(df)
这份数据包含日期、区域、产品三个维度字段,以及销量和金额两个度量字段,非常适合演示各类透视操作。
三、pivot_table:聚合透视表
pivot_table 是 Pandas 中最核心的透视函数,它能够根据指定的行索引、列索引和聚合方式对数据进行汇总。其核心参数如下:
| 参数 | 说明 | 示例值 |
| values | 需要聚合的数值列 | '销量', ['销量','金额'] |
| index | 行索引(分组维度) | '区域', ['区域','产品'] |
| columns | 列索引(分组维度) | '产品', ['区域','产品'] |
| aggfunc | 聚合函数 | 'sum', 'mean', np.sum, {'销量':'sum'} |
| margins | 是否显示边际总计 | True / False |
| margins_name | 边际总计的名称 | '合计' |
| fill_value | 缺失值填充 | 0 |
| dropna | 是否删除全NaN行/列 | True / False |
| observed | 是否只显示观测到的类别 | True / False |
3.1 基本用法
按区域汇总总销量:
# 按区域汇总销量
pt1 = df.pivot_table(
values='销量',
index='区域',
aggfunc='sum'
)
print(pt1)
# 输出:
销量
区域
华东 350
华北 295
华南 335
3.2 多维度行索引
按区域和产品分类汇总金额:
# 区域 + 产品 双维度行索引
pt2 = df.pivot_table(
values='金额',
index=['区域', '产品'],
aggfunc='sum'
)
print(pt2)
# 输出:
区域 产品
华东 A 35000
华北 A 10500
B 19000
华南 A 15000
B 9500
3.3 行 + 列交叉透视
这是最常见的透视表形式,行索引为区域,列索引为产品,值为金额:
# 区域 x 产品 交叉透视
pt3 = df.pivot_table(
values='金额',
index='区域',
columns='产品',
aggfunc='sum'
)
print(pt3)
# 输出:
产品 A B
区域
华东 35000 NaN
华北 10500 19000
华南 15000 9500
观察结论:
华东区域只有 A 产品的数据,没有 B 产品数据(NaN)。这是因为原始数据中华东区域三个月份全部销售的是 A 产品。可以通过 fill_value=0 将 NaN 替换为 0。
3.4 fill_value 参数
用 0 填充缺失值,使表格更干净:
pt4 = df.pivot_table(
values='金额',
index='区域',
columns='产品',
aggfunc='sum',
fill_value=0
)
print(pt4)
# 输出:
产品 A B
区域
华东 35000 0
华北 10500 19000
华南 15000 9500
3.5 margins 边际总计
添加行总计和列总计,快速查看各维度的合计值:
pt5 = df.pivot_table(
values='金额',
index='区域',
columns='产品',
aggfunc='sum',
fill_value=0,
margins=True,
margins_name='合计'
)
print(pt5)
# 输出:
产品 A B 合计
区域
华东 35000 0 35000
华北 10500 19000 29500
华南 15000 9500 24500
合计 60500 28500 89000
3.6 多值列聚合
同时透视销量和金额两个字段:
pt6 = df.pivot_table(
values=['销量', '金额'],
index='区域',
columns='产品',
aggfunc='sum',
fill_value=0
)
print(pt6)
# 输出:
金额 销量
产品 A B A B
区域
华东 35000 0 350 0
华北 10500 19000 105 190
华南 15000 9500 150 95
3.7 不同列使用不同聚合函数
通过传入字典,可以为每个 values 列指定独立的聚合函数:
pt7 = df.pivot_table(
index='区域',
columns='产品',
aggfunc={'销量': 'sum', '金额': 'mean'},
fill_value=0
)
print(pt7)
# 金额使用均值聚合,销量使用求和聚合
3.8 observed 参数与类别数据
当列是 CategoricalDtype 类型时,observed=True 只显示数据中实际出现的类别,observed=False(默认)会显示所有可能的类别(包括未出现的):
# 假设产品列是类别类型
df_cat = df.copy()
df_cat['产品'] = pd.Categorical(df_cat['产品'],
categories=['A', 'B', 'C'])
pt8 = df_cat.pivot_table(
values='销量',
index='区域',
columns='产品',
aggfunc='sum',
observed=True, # 只显示A、B,不显示C
fill_value=0
)
3.9 dropna 参数
默认情况下,如果某行或某列的全部数据都是 NaN,dropna=True 会将其自动删除。设置 dropna=False 则保留所有行列:
pt9 = df.pivot_table(
values='金额',
index='区域',
columns='产品',
aggfunc='sum',
dropna=False
)
四、pivot:简单行转列
pivot 是 pivot_table 的简化版本,它不进行聚合,只是将数据从长格式重塑为宽格式。使用前提是:每个 (index, columns) 组合必须唯一,否则会报错。
# 构造唯一索引的数据
df_pivot = pd.DataFrame({
'姓名': ['张三', '张三', '李四', '李四'],
'科目': ['语文', '数学', '语文', '数学'],
'成绩': [85, 92, 78, 88]
})
# 行转列:姓名作行索引,科目作列名
pvt = df_pivot.pivot(
index='姓名',
columns='科目',
values='成绩'
)
print(pvt)
# 输出:
科目 数学 语文
姓名
李四 88 78
张三 92 85
重要区别:pivot vs pivot_table
- pivot:纯重塑,无聚合。数据必须唯一,否则 ValueError
- pivot_table:重塑 + 聚合。重复数据会自动按 aggfunc 聚合
简单记忆:数据有重复用 pivot_table,数据唯一用 pivot。
五、crosstab:交叉表
crosstab 是专用于计算两个(或多个)因子之间频次统计的函数,本质上是对 groupby + unstack 的封装,特别适合分析分类变量之间的关系。
# 扩展数据集
df2 = pd.DataFrame({
'性别': ['男', '男', '女', '女', '男', '女'],
'购买': ['是', '否', '是', '是', '否', '是'],
'等级': ['高', '中', '高', '低', '中', '低']
})
5.1 基本频次统计
# 性别与购买的交叉频次表
ct1 = pd.crosstab(
index=df2['性别'],
columns=df2['购买']
)
print(ct1)
# 输出:
购买 否 是
性别
女 0 3
男 2 1
5.2 多维度交叉
# 性别 x 购买 x 等级 三维交叉
ct2 = pd.crosstab(
index=df2['性别'],
columns=[df2['购买'], df2['等级']]
)
print(ct2)
5.3 normalize 归一化
将频次转换为比例,便于对比分布:
# 按行归一化(行百分比)
ct3 = pd.crosstab(
index=df2['性别'],
columns=df2['购买'],
normalize='index'
)
print(ct3)
# 输出:
购买 否 是
性别
女 0.000000 1.000000
男 0.666667 0.333333
normalize 可选值:
'all':占总数的百分比
'index':按行归一化(每行和为1)
'columns':按列归一化(每列和为1)
True:等同于 'all'
5.4 margins 边缘总计
ct4 = pd.crosstab(
index=df2['性别'],
columns=df2['购买'],
margins=True,
margins_name='合计'
)
print(ct4)
# 输出:
购买 否 是 合计
性别
女 0 3 3
男 2 1 3
合计 2 4 6
5.5 values + aggfunc 自定义聚合
crosstab 也支持传入数值列和聚合函数,类似于 pivot_table:
# 交叉表 + 数值聚合
ct5 = pd.crosstab(
index=df['区域'],
columns=df['产品'],
values=df['销量'],
aggfunc='sum'
)
print(ct5)
# 等价于:
# df.pivot_table(values='销量', index='区域', columns='产品', aggfunc='sum')
crosstab 的优势:
- 接口更简洁,直接传 Series 而非字符串列名
- 天然支持分类变量的频次统计
- 内置 normalize 参数,快速计算百分比
- 支持传入 DataFrame 的行切片作为参数
六、melt:数据逆透视
melt 是透视的逆操作,将宽表转换为长表(宽表转长表)。当原始数据以交叉表形式存储时,melt 可以将其还原为"行记录"格式,便于后续分析。
# 构造宽表数据
df_wide = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'语文': [85, 78, 92],
'数学': [92, 88, 76],
'英语': [88, 90, 85]
})
print(df_wide)
# 输出:
姓名 语文 数学 英语
0 张三 85 92 88
1 李四 78 88 90
2 王五 92 76 85
6.1 基本 melt
# 将宽表转长表
melted = pd.melt(
df_wide,
id_vars=['姓名'], # 标识变量(保留不转换的列)
value_vars=['语文', '数学', '英语'], # 要逆透视的列
var_name='科目', # 列名存放的字段名
value_name='成绩' # 值存放的字段名
)
print(melted)
# 输出:
姓名 科目 成绩
0 张三 语文 85
1 李四 语文 78
2 王五 语文 92
3 张三 数学 92
4 李四 数学 88
5 王五 数学 76
6 张三 英语 88
7 李四 英语 90
8 王五 英语 85
6.2 参数详解
| 参数 | 含义 | 说明 |
| id_vars | 标识变量列 | 这些列保留不动,作为每条记录的标识 |
| value_vars | 值变量列 | 需要转换为长格式的列,默认使用所有非 id_vars 列 |
| var_name | 变量名列名 | 存储原列名的列名,默认 'variable' |
| value_name | 值列名 | 存储原数值的列名,默认 'value' |
| col_level | 列层级 | 如果列是多层索引,指定用哪一层 |
| ignore_index | 是否重置索引 | 默认 True |
# value_vars 不指定时,默认使用除 id_vars 外的所有列
melted2 = pd.melt(
df_wide,
id_vars=['姓名'],
var_name='科目',
value_name='成绩'
)
print(melted2)
# 结果同上
6.3 实际应用场景
melt 的典型用途
- 数据清洗:将 Excel 导出的交叉报表转换为标准 DataFrame
- 统计分析:长格式适合 seaborn/matplotlib 绘图
- 机器学习:特征工程中转换为"样本-特征"格式
- 数据库写入:长格式符合数据库规范化设计
七、stack / unstack:行列层级转换
stack 和 unstack 用于在行索引和列索引之间进行转换,是操作多层索引(MultiIndex)的利器。
7.1 unstack:行索引转列索引
将行索引的最内层移动到列索引:
# 先创建多层索引数据
df_mi = df.set_index(['区域', '产品'])
print(df_mi)
# 将产品(内层行索引)转为列索引
unstacked = df_mi.unstack()
print(unstacked)
# 输出:
金额 销量
产品 A B A B
区域
华东 35000 NaN 350 NaN
华北 10500 19000 105 190
华南 15000 9500 150 95
# 指定层级:level=0 转换外层索引
unstacked2 = df_mi.unstack(level=0)
print(unstacked2)
7.2 stack:列索引转行索引
将列索引的最内层移动到行索引(unstack 的逆操作):
# 承接上面的 unstacked,stack 回去
stacked = unstacked.stack()
print(stacked)
# 输出恢复为多层行索引格式
金额 销量
区域 产品
华东 A 35000.0 350.0
华北 A 10500.0 105.0
B 19000.0 190.0
华南 A 15000.0 150.0
B 9500.0 95.0
# stack 也可以指定层级
stacked2 = unstacked.stack(level=0)
print(stacked2)
7.3 多层索引的透视图
结合 stack/unstack 和 pivot_table,可以实现复杂的多维分析:
# 三层透视:区域 x 产品 x 日期
pt_multi = df.pivot_table(
values='销量',
index=['区域', '日期'],
columns='产品',
aggfunc='sum',
fill_value=0
)
print(pt_multi)
# 通过 unstack 将日期从行索引转到列索引
pt_unstack = pt_multi.unstack(level='日期')
print(pt_unstack)
# 通过 stack 再将日期转回行索引
pt_stack = pt_unstack.stack(level='日期')
print(pt_stack)
stack/unstack 适用场景
- 数据可视化:某些图表库需要特定格式的数据布局
- 报表生成:按不同层级汇总后展示
- 特征工程:将多层索引展开为平坦列
- 时序分析:将时间维度在行和列之间切换
八、explode:列表展开
explode 用于将单元格中的列表(list-like)数据展开为多行,每个元素占一行,其他列的值重复。
# 构造包含列表的数据
df_list = pd.DataFrame({
'姓名': ['张三', '李四'],
'科目': [['语文', '数学'], ['英语', '物理', '化学']],
'成绩': [[85, 92], [90, 88, 76]]
})
print(df_list)
# 展开科目列
exploded = df_list.explode('科目')
print(exploded)
# 输出:
姓名 科目 成绩
0 张三 语文 [85, 92]
0 张三 数学 [85, 92]
1 李四 英语 [90, 88, 76]
1 李四 物理 [90, 88, 76]
1 李四 化学 [90, 88, 76]
# 同时展开多个列(需要对应长度一致)
df_list2 = pd.DataFrame({
'姓名': ['张三'],
'科目': [['语文', '数学']],
'成绩': [[85, 92]]
})
exploded2 = df_list2.explode(['科目', '成绩'])
print(exploded2)
# 输出:
姓名 科目 成绩
0 张三 语文 85
0 张三 数学 92
explode 的注意事项:
- 列表中的元素会展开为独立行,其他列数据重复
- 空列表([])会被展开为 NaN 行,可使用
ignore_index=True 重置索引
- 同时 explode 多列时,各列的列表长度必须相同
- 适用于 NLP 分词结果展开、JSON 嵌套数据扁平化等场景
九、pivot_table 与 groupby 的关系对比
pivot_table 本质上是对 groupby + unstack 的封装。理解二者关系有助于灵活选择工具。
# 写法一:pivot_table(简洁)
pt_a = df.pivot_table(
values='金额',
index='区域',
columns='产品',
aggfunc='sum',
fill_value=0,
margins=True
)
# 写法二:groupby + unstack(灵活)
pt_b = df.groupby(['区域', '产品'])['金额'].sum() \
.unstack(fill_value=0)
| 对比维度 | pivot_table | groupby + unstack |
| 语法简洁度 | 高,一站式完成 | 低,需多步组合 |
| margins 支持 | 内置,一行搞定 | 需手动计算和拼接 |
| 多值列 | 自动处理多层列索引 | 需额外处理 |
| 自定义聚合 | 参数或字典 | agg 灵活传入函数 |
| fill_value | 直接参数 | unstack 参数 |
| 复杂过滤 | 不支持(需先过滤数据) | 原生支持 |
| 多步转换 | 受限 | 可自由组合 |
选择建议
- 需要快速生成标准汇总表 → 用 pivot_table
- 需要灵活的中途加工(过滤、排序、变换) → 用 groupby
- 需要多层列的复杂转换 → groupby + unstack 更可控
- 需要边际总计和百分比 → pivot_table 更省事
十、综合实战案例
以下是一个综合案例,模拟真实数据分析场景:
# ========== 综合案例:销售数据分析 ==========
# 1. 生成模拟数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=100, freq='D')
data = {
'日期': np.random.choice(dates, 500),
'区域': np.random.choice(['华东', '华南', '华北', '西南'], 500),
'品类': np.random.choice(['电子产品', '服装', '食品', '家居'], 500),
'销售额': np.random.randint(100, 10000, 500),
'数量': np.random.randint(1, 50, 500)
}
df_sales = pd.DataFrame(data)
# 2. 区域 x 品类 透视表(含边际总计)
report = df_sales.pivot_table(
values=['销售额', '数量'],
index='区域',
columns='品类',
aggfunc={'销售额': 'sum', '数量': 'mean'},
fill_value=0,
margins=True,
margins_name='总计'
)
# 3. 交叉表分析:区域 vs 品类的订单频次
freq = pd.crosstab(
index=df_sales['区域'],
columns=df_sales['品类'],
margins=True,
normalize='index'
)
# 4. 宽表转长表(便于绘图)
report_long = pd.melt(
report.reset_index(),
id_vars=['区域'],
value_vars=['电子产品', '服装', '食品', '家居'],
var_name='品类',
value_name='销售额'
)
# 5. stack/unstack 多维度切换
report_stack = report.stack(level='品类')
report_unstack = report_stack.unstack(level='区域')
核心要点总结
- pivot_table:核心透视函数,支持 values/index/columns/aggfunc/margins/fill_value/dropna/observed 八大参数
- pivot:简单行转列,要求数据唯一,无聚合功能
- crosstab:交叉频次统计利器,内置 normalize 归一化,适合分类变量分析
- melt:宽表转长表,id_vars 标识列 / value_vars 值列 / var_name 列名 / value_name 值名
- stack/unstack:行索引与列索引间的层级转换,操作 MultiIndex 的瑞士军刀
- explode:列表型单元格展开为多行,处理嵌套数据的必备工具
- pivot_table vs groupby:pivot_table 是 groupby + unstack 的高层封装,前者简洁、后者灵活
十一、进一步思考
进阶方向:
- 多级透视:三层及以上 index/columns 组合,理解层级化数据结构
- 自定义聚合函数:使用 lambda 或自定义函数实现复杂聚合逻辑
- 与可视化结合:透视结果直接传入 seaborn.heatmap 绘制热力图
- 大数据优化:使用 pivot_table 的 observed 参数减少内存消耗
- pandas 2.0+:新版中对 pivot_table 的性能优化和 dtype 后向传播
- pivot 与 set_index:了解 pivot 内部如何通过 set_index 实现重建索引