RSS
菜单
全部文章快讯开发科技深度热点

第 12 章 数据重塑与透视表

内容摘要

数据有两种典型形态:**长格式**(一行一个观测)和**宽格式**(一行多个变量)。分析时经常需要在两者之间切换,并生成 Excel 式的**透视表**。本篇文章系统讲解 pandas 的重塑武器:**pivot / pivot_table / melt / stack / unstack / crosstab**。

数据有两种典型形态:长格式(一行一个观测)和宽格式(一行多个变量)。分析时经常需要在两者之间切换,并生成 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 最强大的统计分析武器,没有之一。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 12 章 数据重塑与透视表》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。