数据有两种典型形态:长格式(一行一个观测)和宽格式(一行多个变量)。分析时经常需要在两者之间切换,并生成 Excel 式的透视表。本篇文章系统讲解 pandas 的重塑武器:pivot / pivot_table / melt / stack / unstack / crosstab。
目录
1. 长格式与宽格式
2. pivot:宽化(长→宽)
3. pivot_table:透视表(带聚合)
4. melt:熔化(宽→长)
5. stack / unstack:索引层级升降
6. crosstab:交叉表
7. set_index / reset_index 进阶
8. 重塑实战案例
9. 常见坑与注意事项
10. 本章小结与练习
1. 长格式与宽格式
先理解两种形态:
宽格式(wide):每个主体一行,变量是列。
姓名 语文 数学 英语
0 张三 90 85 95
1 李四 88 92 80
长格式(long):每个观测一行,变量名放在一列。
姓名 科目 成绩
0 张三 语文 90
1 张三 数学 85
2 张三 英语 95
3 李四 语文 88
...
| 形态 | 适合 | 举例 |
|------|------|------|
| 宽格式 | 直接看表、做模型特征 | Excel 报表 |
| 长格式 | 分组统计、绘图、数据库存储 | tidy data |
pandas 提供了两种方向的转换:
宽格式 --pivot/pivot_table--> 长格式? (不对)
宽格式 <--pivot-- 长格式
长格式 <--melt-- 宽格式
准确地说:pivot 把长数据变宽,melt 把宽数据变长。
2. pivot:宽化(长→宽)
pivot 用于"把某列的值变成新列,另一列的值变成新值"。
import pandas as pd
df = pd.DataFrame({
"姓名": ["张三", "张三", "张三", "李四", "李四", "李四"],
"科目": ["语文", "数学", "英语", "语文", "数学", "英语"],
"成绩": [90, 85, 95, 88, 92, 80],
})
print(df)
姓名 科目 成绩
0 张三 语文 90
1 张三 数学 85
2 张三 英语 95
3 李四 语文 88
4 李四 数学 92
5 李四 英语 80
# pivot:index=行索引,columns=新列名,values=值
wide = df.pivot(index="姓名", columns="科目", values="成绩")
print(wide)
科目 数学 英语 语文
姓名
张三 85 95 90
李四 92 80 88
pivot要求数据没有重复(每个 index+columns 组合唯一),否则报ValueError: Index contains duplicate entries。有重复时用pivot_table。
pivot 的其他写法
# 不指定 values:结果变成多层列(MultiIndex columns)
df.pivot(index="姓名", columns="科目")
# 不指定 index:用原索引
df.pivot(columns="科目", values="成绩")
3. pivot_table:透视表(带聚合)
pivot_table 是 pivot 的升级版:允许重复值,并用聚合函数汇总,同时支持多个行列维度。
# 基础透视:按 城市+商品 统计销量总和
sales = pd.DataFrame({
"城市": ["北京", "北京", "北京", "上海", "上海", "上海", "北京", "上海"],
"商品": ["苹果", "香蕉", "苹果", "苹果", "香蕉", "苹果", "橙子", "橙子"],
"销量": [100, 80, 120, 90, 70, 110, 60, 50],
})
pt = sales.pivot_table(index="城市", columns="商品", values="销量", aggfunc="sum")
print(pt)
商品 苹果 橙子 香蕉
城市
北京 220 60 80
上海 200 50 70
| 参数 | 作用 | 示例 |
|------|------|------|
| index | 行维度 | index="城市" 或 index=["城市","商品"] |
| columns | 列维度 | columns="商品" |
| values | 要聚合的值列 | values="销量" |
| aggfunc | 聚合函数 | "sum"/"mean"/"count"/"max"/np.sum/... |
| fill_value | 无数据时的填充值 | fill_value=0 |
| margins | 是否加总计行列 | margins=True(显示 All) |
| dropna | 是否丢弃全 NaN 行列 | dropna=False |
多维度 + 多聚合
# 行:城市+月份;列:商品;值:销量和金额
pt2 = sales.pivot_table(
index=["城市", "月份"] if "月份" in sales.columns else "城市",
columns="商品",
values="销量",
aggfunc=["sum", "mean"],
)
# 多值列
pt3 = sales.pivot_table(
index="城市",
columns="商品",
values=["销量", "单价"] if "单价" in sales.columns else "销量",
aggfunc="sum",
)
# 加总计
pt4 = sales.pivot_table(
index="城市", columns="商品", values="销量",
aggfunc="sum", margins=True, margins_name="总计",
)
print(pt4)
商品 苹果 橙子 香蕉 总计
城市
北京 220.0 60.0 80.0 360.0
上海 200.0 50.0 70.0 320.0
总计 420.0 110.0 150.0 680.0
margins=True 会额外生成"总计"行和列,是 Excel 透视表"总计"的对应功能。4. melt:熔化(宽→长)
melt 把宽表"熔化"成长表:多列变成"变量列 + 值列"。
wide = pd.DataFrame({
"姓名": ["张三", "李四"],
"语文": [90, 88],
"数学": [85, 92],
"英语": [95, 80],
})
print(wide)
姓名 语文 数学 英语
0 张三 90 85 95
1 李四 88 92 80
# 基础 melt:id_vars 保留的列,其余列都熔化
long = wide.melt(id_vars="姓名", var_name="科目", value_name="成绩")
print(long)
姓名 科目 成绩
0 张三 语文 90
1 李四 语文 88
2 张三 数学 85
3 李四 数学 92
4 张三 英语 95
5 李四 英语 80
| 参数 | 作用 |
|------|------|
| id_vars | 保留不变的标识列 |
| value_vars | 指定哪些列熔化(默认除 id_vars 外全部) |
| var_name | 变量列的新列名(默认 variable) |
| value_name | 值列的新列名(默认 value) |
| col_level | 多层列时指定层级 |
# 只熔化部分列
wide.melt(id_vars="姓名", value_vars=["语文", "数学"],
var_name="科目", value_name="成绩")
反 melt:用 pivot 还原
# 长表 -> 宽表(pivot)
wide_back = long.pivot(index="姓名", columns="科目", values="成绩").reset_index()
# reset_index 让"姓名"回到普通列
print(wide_back)
# 科目 姓名 数学 英语 语文
# 0 张三 85 95 90
# 1 李四 92 80 88
长宽互转口诀:melt变长,pivot变宽,二者互为逆操作。
5. stack / unstack:索引层级升降
stack 把列压进索引(变长),unstack 把索引层级抬到列(变宽)。通常与 MultiIndex 配合使用。
# 多层列
df_multi = pd.DataFrame(
[[90, 85], [88, 92]],
index=["张三", "李四"],
columns=pd.MultiIndex.from_tuples([("语文", "期中"), ("语文", "期末")]),
)
print(df_multi)
# 语文
# 期中 期末
# 张三 90 85
# 李四 88 92
# stack:把最内层列变成索引
print(df_multi.stack())
# 语文
# 张三 期中 90
# 期末 85
# 李四 期中 88
# 期末 92
# unstack:把索引层级变回列
print(df_multi.stack().unstack())
# 多层索引场景
df = pd.DataFrame({
"城市": ["北京", "北京", "上海", "上海"],
"月份": ["1月", "2月", "1月", "2月"],
"销量": [100, 120, 90, 110],
}).set_index(["城市", "月份"])
print(df)
# 销量
# 城市 月份
# 北京 1月 100
# 2月 120
# 上海 1月 90
# 2月 110
# unstack:把"月份"层级变成列
print(df.unstack())
# 销量
# 月份 1月 2月
# 城市
# 北京 100 120
# 上海 90 110
# stack 还原
print(df.unstack().stack())
stack / unstack 参数
# 指定层级
df.unstack(level=0) # 把第 0 层(城市)变成列
df.stack(level=0)
# 缺失处理:fill_value
df.unstack(fill_value=0)
6. crosstab:交叉表
crosstab 用于统计两个(或多个)分类变量的交叉频数,本质是"快速透视计数"。
df = pd.DataFrame({
"性别": ["男", "女", "男", "女", "男", "女", "男", "女"],
"购买": ["是", "是", "否", "是", "否", "否", "是", "是"],
})
# 交叉频数表
print(pd.crosstab(df["性别"], df["购买"]))
# 购买 否 是
# 性别
# 女 1 3
# 男 2 2
# 带占比
print(pd.crosstab(df["性别"], df["购买"], normalize="index")) # 按行归一化
print(pd.crosstab(df["性别"], df["购买"], normalize="columns")) # 按列归一化
print(pd.crosstab(df["性别"], df["购买"], normalize="all")) # 整体占比
# 三变量 + 汇总
print(pd.crosstab(df["性别"], df["购买"], margins=True))
# 带聚合值
sales = pd.DataFrame({
"性别": ["男", "男", "女", "女"],
"城市": ["北京", "上海", "北京", "上海"],
"金额": [100, 200, 150, 250],
})
print(pd.crosstab(sales["性别"], sales["城市"], values=sales["金额"], aggfunc="sum"))
# 城市 北京 上海
# 性别
# 女 150 250
# 男 100 200
7. set_index / reset_index 进阶
df = pd.DataFrame({
"姓名": ["张三", "李四", "王五"],
"年龄": [25, 30, 28],
"城市": ["北京", "上海", "广州"],
})
# set_index:把列设为索引
df_idx = df.set_index("姓名")
print(df_idx)
# 年龄 城市
# 姓名
# 张三 25 北京
# 李四 30 上海
# 王五 28 广州
# 多列索引
df_midx = df.set_index(["城市", "姓名"])
print(df_midx.index) # MultiIndex
# 取回
df_back = df_idx.reset_index()
print(df_back.columns) # Index(['姓名', '年龄', '城市'], ...)
# reset_index(drop=True):直接丢弃索引列
df_idx.reset_index(drop=True)
8. 重塑实战案例
# 原始:宽格式月度销售
raw_wide = pd.DataFrame({
"城市": ["北京", "上海", "广州"],
"1月销量": [100, 90, 80],
"2月销量": [120, 110, 95],
"3月销量": [130, 105, 100],
})
# 1. 宽 -> 长(melt):适合绘图和 groupby
long_sales = raw_wide.melt(
id_vars="城市",
var_name="月份",
value_name="销量",
)
print(long_sales)
# 2. 长 -> 宽(pivot):生成报表
wide_report = long_sales.pivot(index="城市", columns="月份", values="销量")
print(wide_report)
# 3. 透视表:多维度聚合
pt = long_sales.pivot_table(
index="城市", columns="月份", values="销量",
aggfunc="sum", margins=True, fill_value=0,
)
print(pt)
输出:
城市 月份 销量
0 北京 1月销量 100
1 上海 1月销量 90
2 广州 1月销量 80
3 北京 2月销量 120
4 上海 2月销量 110
5 广州 2月销量 95
6 北京 3月销量 130
7 上海 3月销量 105
8 广州 3月销量 100
月份 1月销量 2月销量 3月销量
城市
北京 100 120 130
上海 90 110 105
广州 80 95 100
9. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| pivot 遇重复值 | ValueError | 改用 pivot_table 或先去重 |
| pivot_table 忘 aggfunc | 默认 mean,结果"不对劲" | 明确 aggfunc="sum" 等 |
| melt 忘 id_vars | 标识列也被熔化 | 指定 id_vars |
| unstack 后出现 NaN | 有的组合没数据 | fill_value=0 |
| reset_index 忘 drop | 多出一列索引 | 不需要时 drop=True |
| crosstab 数值列报错 | 类别列被当数值 | 先 astype(str) 或指定 values/aggfunc |
10. 本章小结与练习
小结
- 宽表 ↔ 长表:
melt变长、pivot变宽; - 透视:
pivot_table(index, columns, values, aggfunc, margins)是 Excel 透视表的对应物; - 索引层级:
stack列→索引、unstack索引→列; - 交叉统计:
crosstab快速做分类变量频数表; set_index/reset_index在索引与列之间切换。
练习题
1. 把长表 [姓名,科目,成绩] 用 pivot 变成宽表。
2. 把宽表 [姓名,语文,数学,英语] 用 melt 变成三列长表。
3. 用 pivot_table 生成"城市×月份"的销量透视表(含总计)。
4. 用 crosstab 统计"性别×是否购买"并加行列总计。
5. 用 set_index(["a","b"]) 创建 MultiIndex,再用 unstack 变宽、stack 还原。
下一篇预告:第 13 章 分组聚合:groupby —— pandas 最强大的统计分析武器,没有之一。
文章回复
0 条公开回复