跳到主要内容

透视表与数据重塑

透视表是数据分析中的强大工具,可以将长格式数据转换为宽格式,方便进行多维分析。Pandas 提供了 pivotpivot_tablemeltstackunstack 等多种数据重塑方法。

pivot 与 pivot_table

pivot 方法

pivot 用于将长格式数据转换为宽格式,不进行聚合操作。当数据中存在重复的索引-列组合时会报错。

import pandas as pd
import numpy as np

# 创建示例数据
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02', '2024-01-03'],
'city': ['北京', '上海', '北京', '上海', '北京'],
'temperature': [5, 10, 3, 12, 6],
'humidity': [30, 45, 35, 40, 32]
})
print(df)
# date city temperature humidity
# 0 2024-01-01 北京 5 30
# 1 2024-01-01 上海 10 45
# 2 2024-01-02 北京 3 35
# 3 2024-01-02 上海 12 40
# 4 2024-01-03 北京 6 32

# 使用 pivot 转换
result = df.pivot(index='date', columns='city', values='temperature')
print(result)
# city 北京 上海
# date
# 2024-01-01 5 10
# 2024-01-02 3 12
# 2024-01-03 6 NaN

# 转换多列值
result = df.pivot(index='date', columns='city')
print(result)
# temperature humidity
# city 北京 上海 北京 上海
# date
# 2024-01-01 5 10 30 45
# 2024-01-02 3 12 35 40
# 2024-01-03 6 NaN 32 NaN

# 访问特定值
print(result['temperature']['北京'])

pivot_table 方法

pivot_tablepivot 更强大,支持聚合函数处理重复数据:

# 创建有重复值的数据
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02'],
'city': ['北京', '北京', '上海', '北京', '上海'],
'category': ['A', 'A', 'B', 'A', 'B'],
'sales': [100, 150, 200, 120, 180]
})
print(df)
# date city category sales
# 0 2024-01-01 北京 A 100
# 1 2024-01-01 北京 A 150 # 重复:同一天的北京A类
# 2 2024-01-01 上海 B 200
# 3 2024-01-02 北京 A 120
# 4 2024-01-02 上海 B 180

# 使用 pivot_table 聚合重复值
result = pd.pivot_table(
df,
values='sales',
index='date',
columns='city',
aggfunc='mean' # 默认值
)
print(result)
# city 北京 上海
# date
# 2024-01-01 125.0 200.0
# 2024-01-02 120.0 180.0

# 多种聚合函数
result = pd.pivot_table(
df,
values='sales',
index='date',
columns='city',
aggfunc=['sum', 'mean', 'count']
)
print(result)
# sum mean count
# city 北京 上海 北京 上海 北京 上海
# date
# 2024-01-01 250 200 125 200 2 1
# 2024-01-02 120 180 120 180 1 1

pivot_table 详细参数

# 创建更完整的示例数据
np.random.seed(42)
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=100).repeat(3),
'city': np.random.choice(['北京', '上海', '广州', '深圳'], 300),
'category': np.random.choice(['电子产品', '服装', '食品'], 300),
'sales': np.random.randint(100, 1000, 300),
'quantity': np.random.randint(1, 20, 300)
})

# 完整参数示例
result = pd.pivot_table(
df,
values=['sales', 'quantity'], # 要聚合的值列
index=['date', 'city'], # 行索引
columns='category', # 列索引
aggfunc={'sales': 'sum', 'quantity': 'mean'}, # 聚合函数
fill_value=0, # 填充缺失值
margins=True, # 添加汇总行/列
margins_name='总计', # 汇总名称
dropna=True, # 删除全为 NaN 的列
observed=False # 是否只显示观测值
)
print(result.head())

# 使用多个值列和聚合函数
result = pd.pivot_table(
df,
values=['sales', 'quantity'],
index='city',
columns='category',
aggfunc=['sum', 'mean']
)
print(result)

添加汇总(margins)

# 添加汇总行和列
result = pd.pivot_table(
df,
values='sales',
index='city',
columns='category',
aggfunc='sum',
margins=True,
margins_name='总计'
)
print(result)
# category 电子产品 服装 食品 总计
# city
# 北京 12500 9800 11200 33500
# 上海 10800 10500 8900 30200
# 广州 11200 10100 9500 30800
# 深圳 9800 9200 10200 29200
# 总计 44300 39600 39800 123700

melt 方法

meltpivot 的逆操作,将宽格式数据转换为长格式:

# 宽格式数据
df_wide = pd.DataFrame({
'name': ['张三', '李四', '王五'],
'math': [90, 85, 92],
'english': [88, 92, 85],
'physics': [95, 80, 90]
})
print(df_wide)
# name math english physics
# 0 张三 90 88 95
# 1 李四 85 92 80
# 2 王五 92 85 90

# 转换为长格式
df_long = pd.melt(
df_wide,
id_vars=['name'], # 保留的标识列
value_vars=['math', 'english', 'physics'], # 要转换的列
var_name='subject', # 变量名列名
value_name='score' # 值列名
)
print(df_long)
# name subject score
# 0 张三 math 90
# 1 李四 math 85
# 2 王五 math 92
# 3 张三 english 88
# 4 李四 english 92
# 5 王五 english 85
# 6 张三 physics 95
# 7 李四 physics 80
# 8 王五 physics 90

# 省略 value_vars 时,转换所有非 id_vars 的列
df_long = df_wide.melt(id_vars='name', var_name='subject', value_name='score')

# 保留原始索引
df_long = df_wide.melt(id_vars='name', ignore_index=False)

melt 实际应用

# 多指标宽格式数据
df = pd.DataFrame({
'city': ['北京', '上海', '广州'],
'sales_2023': [1000, 1200, 800],
'sales_2024': [1100, 1300, 900],
'profit_2023': [100, 150, 80],
'profit_2024': [120, 160, 95]
})

# 先拆分年份和指标
df_long = df.melt(id_vars='city', var_name='metric_year', value_name='value')

# 再拆分指标和年份
df_long[['metric', 'year']] = df_long['metric_year'].str.split('_', expand=True)
df_long = df_long.drop('metric_year', axis=1)

# 重新透视
df_final = df_long.pivot_table(
index=['city', 'year'],
columns='metric',
values='value'
).reset_index()
print(df_final)

stack 与 unstack

stackunstack 用于层次化索引的转换:

stack 方法

stack 将列"堆叠"到行索引,产生更窄更长的数据:

# 创建多级列索引
df = pd.DataFrame({
('A', 'x'): [1, 2, 3],
('A', 'y'): [4, 5, 6],
('B', 'x'): [7, 8, 9],
('B', 'y'): [10, 11, 12]
})
df.columns = pd.MultiIndex.from_tuples(df.columns)

# stack 将最内层列索引转为行索引
stacked = df.stack()
print(stacked)
# A B
# 0 x 1 7
# y 4 10
# 1 x 2 8
# y 5 11
# 2 x 3 9
# y 6 12

# 指定堆叠的级别
stacked = df.stack(level=0) # 堆叠第一层

# 简单 DataFrame 的 stack
df_simple = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6]
})
stacked = df_simple.stack()
print(stacked)
# 0 A 1
# B 4
# 1 A 2
# B 5
# 2 A 3
# B 6

unstack 方法

unstackstack 的逆操作,将行索引"展开"到列:

# 创建多级行索引
index = pd.MultiIndex.from_tuples([
('北京', '2023'), ('北京', '2024'),
('上海', '2023'), ('上海', '2024')
])
df = pd.DataFrame({
'sales': [100, 120, 150, 180],
'profit': [10, 15, 20, 25]
}, index=index)

print(df)
# sales profit
# 北京 2023 100 10
# 2024 120 15
# 上海 2023 150 20
# 2024 180 25

# unstack 将最内层行索引转为列
unstacked = df.unstack()
print(unstacked)
# sales profit
# 2023 2024 2023 2024
# 北京 100 120 10 15
# 上海 150 180 20 25

# 指定展开的级别
unstacked = df.unstack(level=0) # 展开第一层
print(unstacked)
# sales profit
# 北京 上海 北京 上海
# 2023 100 150 10 20
# 2024 120 180 15 25

# 指定展开级别名称
unstacked = df.unstack(level='城市') # 如果索引有名称

stack/unstack 与缺失值

# 不完整的索引会导致缺失值
df = pd.DataFrame({
'A': [1, 2],
'B': [3, 4]
})
df.index = ['x', 'y']

# 添加一个额外的行
df_extra = pd.DataFrame({'A': [5]}, index=['z'])
df = pd.concat([df, df_extra])

# unstack 会产生缺失值
result = df.unstack()
print(result)

# 使用 fill_value 填充
result = df.stack().unstack(fill_value=0)

交叉表 crosstab

crosstab 用于计算两个(或多个)因子的交叉频率表:

# 创建示例数据
df = pd.DataFrame({
'gender': ['男', '女', '男', '女', '男', '女', '男', '女'],
'education': ['本科', '本科', '硕士', '硕士', '本科', '博士', '硕士', '本科'],
'department': ['技术', '人事', '技术', '销售', '销售', '技术', '人事', '销售']
})

# 基本交叉表
result = pd.crosstab(df['gender'], df['education'])
print(result)
# education 本科 硕士 博士
# gender
# 男 1 2 0
# 女 2 1 1

# 多行索引
result = pd.crosstab([df['gender'], df['department']], df['education'])
print(result)
# education 本科 硕士 博士
# gender department
# 男 技术 1 1 0
# 销售 0 0 0
# 女 人事 1 0 0
# 技术 0 0 1
# 销售 1 1 0

# 添加汇总
result = pd.crosstab(df['gender'], df['education'], margins=True, margins_name='总计')
print(result)
# education 本科 硕士 博士 总计
# gender
# 男 1 2 0 3
# 女 2 1 1 4
# 总计 3 3 1 7

# 归一化
result = pd.crosstab(df['gender'], df['education'], normalize='index') # 按行归一化
print(result)
# education 本科 硕士 博士
# gender
# 男 0.333333 0.666667 0.000000
# 女 0.500000 0.250000 0.250000

result = pd.crosstab(df['gender'], df['education'], normalize='columns') # 按列归一化
result = pd.crosstab(df['gender'], df['education'], normalize='all') # 全局归一化

# 带值的交叉表
result = pd.crosstab(
df['gender'],
df['education'],
values=df.index, # 使用索引作为值
aggfunc='count' # 计数
)

数据重塑流程

长格式转宽格式

# 长格式数据
df_long = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=4).repeat(3),
'city': ['北京', '上海', '广州'] * 4,
'temperature': np.random.randint(-5, 15, 12)
})

# 方法 1: pivot
df_wide = df_long.pivot(index='date', columns='city', values='temperature')

# 方法 2: pivot_table(适合有重复值)
df_wide = df_long.pivot_table(
index='date',
columns='city',
values='temperature',
aggfunc='mean'
)

# 方法 3: unstack
df_wide = df_long.set_index(['date', 'city'])['temperature'].unstack()

宽格式转长格式

# 宽格式数据
df_wide = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=4),
'北京': [5, 3, 6, 4],
'上海': [10, 12, 11, 13],
'广州': [15, 16, 14, 17]
})

# 方法 1: melt
df_long = df_wide.melt(
id_vars='date',
var_name='city',
value_name='temperature'
)

# 方法 2: stack
df_long = df_wide.set_index('date').stack().reset_index()
df_long.columns = ['date', 'city', 'temperature']

实战示例

示例 1:销售数据多维分析

# 创建销售数据
np.random.seed(42)
sales = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=180).repeat(4),
'region': ['华东', '华北', '华南', '西部'] * 180,
'product': np.random.choice(['A', 'B', 'C'], 720),
'channel': np.random.choice(['线上', '线下'], 720),
'sales': np.random.randint(100, 1000, 720),
'quantity': np.random.randint(1, 20, 720)
})

# 月度区域产品销售透视
sales['month'] = sales['date'].dt.to_period('M')
pivot_result = pd.pivot_table(
sales,
values='sales',
index=['month', 'region'],
columns='product',
aggfunc='sum',
margins=True
)
print(pivot_result.head(10))

# 区域渠道分析
channel_analysis = pd.pivot_table(
sales,
values=['sales', 'quantity'],
index='region',
columns='channel',
aggfunc={'sales': 'sum', 'quantity': 'mean'}
)
print(channel_analysis)

# 转换为长格式进行可视化
sales_long = sales.melt(
id_vars=['date', 'region', 'product', 'channel'],
value_vars=['sales', 'quantity'],
var_name='metric',
value_name='value'
)

示例 2:学生成绩分析

# 学生成绩数据
grades = pd.DataFrame({
'student': ['张三', '李四', '王五', '赵六', '钱七'] * 3,
'subject': ['数学'] * 5 + ['英语'] * 5 + ['物理'] * 5,
'midterm': np.random.randint(60, 100, 15),
'final': np.random.randint(60, 100, 15)
})

# 学生成绩透视表
student_pivot = grades.pivot_table(
index='student',
columns='subject',
values=['midterm', 'final'],
aggfunc='mean'
)
print(student_pivot)

# 计算总分和平均分
grades['total'] = grades['midterm'] + grades['final']
grades['average'] = grades[['midterm', 'final']].mean(axis=1)

# 转换为长格式便于分析
grades_long = grades.melt(
id_vars=['student', 'subject'],
value_vars=['midterm', 'final', 'total', 'average'],
var_name='exam_type',
value_name='score'
)

示例 3:时间序列重塑

# 创建时间序列数据
dates = pd.date_range('2023-01-01', '2024-12-31', freq='D')
ts_data = pd.DataFrame({
'date': dates,
'value': np.random.randn(len(dates)).cumsum()
})

# 添加时间维度
ts_data['year'] = ts_data['date'].dt.year
ts_data['month'] = ts_data['date'].dt.month
ts_data['day'] = ts_data['date'].dt.day

# 创建月度透视表(行:月,列:年)
monthly_pivot = ts_data.pivot_table(
index='month',
columns='year',
values='value',
aggfunc='mean'
)
print(monthly_pivot)

# 转换为月度长格式
monthly_long = ts_data.groupby(['year', 'month'])['value'].mean().reset_index()

重塑方法选择指南

需求推荐方法
长格式 → 宽格式(无重复)pivot()
长格式 → 宽格式(有重复)pivot_table()
宽格式 → 长格式melt()
多级索引 ↔ 多级列stack() / unstack()
频率交叉分析crosstab()
时间序列重塑pivot_table() + Grouper

小结

主要方法

  • pivot():简单的长到宽转换,不支持聚合
  • pivot_table():强大的透视表,支持多种聚合
  • melt():宽到长转换
  • stack():列转行
  • unstack():行转列
  • crosstab():交叉频率表

常用参数

  • index:行索引
  • columns:列索引
  • values:要聚合的值
  • aggfunc:聚合函数
  • fill_value:缺失值填充
  • margins:添加汇总

练习

  1. 创建一个包含日期、产品、地区、销售额的数据集,使用 pivot_table 分析各产品在各地区的销售情况
  2. 将练习 1 的结果用 melt 转换回长格式
  3. 使用 crosstab 分析性别与教育程度的交叉分布
  4. 创建多级索引数据,练习 stackunstack 操作
  5. 对时间序列数据按月和年进行透视分析

下一步

掌握了透视表与数据重塑后,让我们学习 时间序列