数据重塑是将数据在"宽格式"和"长格式"之间转换。宽格式适合呈现,长格式适合分析。

将长表不经过聚合直接变成宽表。要求索引+列的组合唯一。
import pandas as pdimport numpy as npdf = pd.DataFrame({ 'date': ['2024-01', '2024-01', '2024-02', '2024-02'], 'city': ['北京', '上海', '北京', '上海'], 'temp': [5, 8, 10, 12]})print(df)# date city temp# 2024-01 北京 5# 2024-01 上海 8# 2024-02 北京 10# 2024-02 上海 12# pivot: 长 -> 宽wide = df.pivot(index='date', columns='city', values='temp')print(wide)# city 北京 上海# date# 2024-01 5 8# 2024-02 10 12# 不指定 values: 对剩余所有列做 pivotresult = df.pivot(index='date', columns='city')# ⚠️ 如果 index + columns 组合不唯一会报错!# 如果有多余列,pivot 会用多层级列来容纳当 index + columns 组合不唯一(即需要聚合)时使用。
df = pd.DataFrame({ 'date': ['2024-01', '2024-01', '2024-01', '2024-02', '2024-02', '2024-02'], 'city': ['北京', '北京', '上海', '北京', '北京', '上海'], 'product': ['A', 'B', 'A', 'A', 'B', 'B'], 'sales': [100, 150, 200, 110, 160, 220]})# 基本用法result = df.pivot_table( values='sales', # 要聚合的值 index='city', # 行索引 columns='product', # 列索引 aggfunc='sum', # 聚合函数(默认 'mean') fill_value=0 # 用 0 填充 NaN)print(result)# product A B# city# 北京 210 310# 上海 200 220# 多个聚合函数result = df.pivot_table( values='sales', index='city', columns='product', aggfunc=['sum', 'mean', 'count'])# 对多个值列使用不同聚合result = df.pivot_table( index='city', aggfunc={'sales': 'sum', 'quantity': 'mean'})# 多层索引result = df.pivot_table( values='sales', index=['city', 'date'], # 多级行索引 columns='product', aggfunc='sum')pd.pivot_table( data, values=None, # 要聚合的列 index=None, # 行分组键 columns=None, # 列分组键 aggfunc='mean', # 聚合函数: str / list / dict fill_value=None, # 填充缺失值 margins=False, # 是否添加 'All' 汇总行/列 margins_name='All', # 汇总标签名 dropna=True, # 是否丢弃全为 NaN 的列 observed=False, # 分类数据:是否只显示数据中出现的组 sort=True # 是否对结果排序)
result = df.pivot_table( values='sales', index='city', columns='product', aggfunc='sum', margins=True, margins_name='合计')# 添加了"合计"行和列
将列名"融化"成行值,是 pivot 的逆操作。
df_wide = pd.DataFrame({ 'name': ['张三', '李四', '王五'], 'math': [85, 90, 78], 'english': [88, 82, 95], 'chinese': [92, 88, 85]})# 基本用法: 指定 id_vars(保持不变的列)long = pd.melt(df_wide, id_vars=['name'])# name variable value# 0 张三 math 85# 1 李四 math 90# ...# 7 李四 chinese 88# 8 王五 chinese 85# 自定义列名long = pd.melt( df_wide, id_vars=['name'], value_vars=['math', 'english', 'chinese'], # 指定要融化的列 var_name='科目', # 原列名变成的新列名 value_name='分数' # 原值变成的新列名)print(long)# name 科目 分数# 0 张三 math 85# ...# 只融化部分列pd.melt(df, id_vars=['id', 'date'], value_vars=['sale', 'cost'])# 不指定 value_vars: 自动使用所有非 id_vars 列pd.melt(df, id_vars=['id'])# 配合 groupby 做聚合long = pd.melt(df, id_vars=['region'], value_vars=['Q1', 'Q2', 'Q3', 'Q4'])result = long.groupby(['region', 'variable'])['value'].sum()
操作的是 MultiIndex 的行/列层级。
# 创建有多层列的 DataFramecols = pd.MultiIndex.from_product([['温度', '湿度'], ['北京', '上海']], names=['指标', '城市'])df = pd.DataFrame(np.arange(12).reshape(3, 4), index=['1月', '2月', '3月'], columns=cols)print(df)# 指标 温度 湿度# 城市 北京 上海 北京 上海# 1月 0 1 2 3# 2月 4 5 6 7# 3月 8 9 10 11# stack: 列层级 -> 行层级stacked = df.stack() # 压缩最内层列到行print(stacked)# 指标 温度 湿度# 1月 北京 0 2# 上海 1 3# 2月 北京 4 6# ...stacked = df.stack(level=0) # 压缩第 0 层列到行stacked = df.stack(level='指标') # 按名称压缩
# 创建有 MultiIndex 的 Seriesidx = pd.MultiIndex.from_product([['1月', '2月'], ['北京', '上海']], names=['月份', '城市'])s = pd.Series([5, 8, 10, 12], index=idx, name='温度')print(s)# 月份 城市# 1月 北京 5# 上海 8# 2月 北京 10# 上海 12# unstack: 行层级 -> 列层级wide = s.unstack() # 压缩最内层行到列print(wide)# 城市 北京 上海# 月份# 1月 5 8# 2月 10 12wide = s.unstack(level=0) # 压缩第 0 层wide = s.unstack(level='月份') # 按名称压缩wide = s.unstack(fill_value=0) # NaN 填充为 0
| 操作 | 方向 | 效果 |
|---|---|---|
| stack() | 列 → 行 | 数据变长、变窄 |
| unstack() | 行 → 列 | 数据变宽 |
| melt() | 列 → 行 | 将列名变为一列的值 |
| pivot() | 行 → 列 | 将一列的值变成列名 |
计算分组频率,类似 pivot_table(aggfunc='count'),但更简洁。
df = pd.DataFrame({ '性别': ['男', '女', '女', '男', '女', '男', '男'], '学历': ['本科', '硕士', '本科', '博士', '硕士', '本科', '硕士'], '部门': ['技术', '销售', '技术', '技术', '销售', '销售', '技术']})# 基本交叉表result = pd.crosstab(df['性别'], df['学历'])print(result)# 学历 博士 本科 硕士# 性别# 女 0 1 2# 男 1 2 1# 多列分组result = pd.crosstab(df['性别'], [df['学历'], df['部门']])# 归一化:百分比pd.crosstab(df['性别'], df['学历'], normalize='all') # 整体占比pd.crosstab(df['性别'], df['学历'], normalize='index') # 行占比pd.crosstab(df['性别'], df['学历'], normalize='columns') # 列占比# 带汇总pd.crosstab(df['性别'], df['学历'], margins=True, margins_name='合计')# 带值(而非计数)df2 = pd.DataFrame({ '性别': ['男', '女', '女', '男'], '产品': ['A', 'A', 'B', 'B'], '金额': [100, 200, 150, 180]})pd.crosstab(df2['性别'], df2['产品'], values=df2['金额'], aggfunc='sum')pd.crosstab( index, # 行分组 columns, # 列分组 values=None, # 要聚合的值 aggfunc=None, # 聚合函数 rownames=None, # 行名称 colnames=None, # 列名称 margins=False, # 汇总 margins_name='All', dropna=True, # 丢弃 NaN 列 normalize=False # 归一化: 'all' / 'index' / 'columns')
| 需求 | 方法 |
|---|---|
| 长→宽(无聚合,唯一键) | df.pivot(index, columns, values) |
| 长→宽(需要聚合) | df.pivot_table(values, index, columns, aggfunc) |
| 宽→长 | pd.melt(df, id_vars, var_name, value_name) |
| 列层级→行层级 | df.stack() |
| 行层级→列层级 | df.unstack() |
| 分组计数 | pd.crosstab(index, columns) |
| 行/列互换 | df.T 或 df.transpose() |