3.6 数据合并与重塑
"The art of data wrangling is the art of knowing what shape your data should be in.""数据整理的艺术在于知道数据应该是什么形状。"— Wes McKinney, Creator of pandas (pandas创始人)
整合多源数据,构建分析数据集
本节目标
- 掌握数据合并(merge、join、concat)
- 理解宽格式与长格式转换
- 学习面板数据的构建
- 避免常见的合并错误
数据合并(Merge & Join)
四种合并类型
| 类型 | 说明 | SQL 等价 | 使用场景 |
|---|---|---|---|
inner | 交集(两边都有) | INNER JOIN | 只保留匹配的 |
left | 保留左表所有 | LEFT JOIN | 主数据集完整 |
right | 保留右表所有 | RIGHT JOIN | 少用(改用 left) |
outer | 并集(所有记录) | FULL OUTER JOIN | 完整性检查 |
基本合并
python
import pandas as pd
# 示例:个人数据 + 地区 GDP
individuals = pd.DataFrame({
'person_id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Charlie', 'David'],
'region': ['East', 'West', 'East', 'South']
})
region_gdp = pd.DataFrame({
'region': ['East', 'West', 'North'],
'gdp': [100, 120, 90]
})
# Left join(保留所有个人)
merged = pd.merge(
individuals,
region_gdp,
on='region',
how='left'
)
print(merged)
# person_id name region gdp
# 0 1 Alice East 100.0
# 1 2 Bob West 120.0
# 2 3 Charlie East 100.0
# 3 4 David South NaN # South 在 region_gdp 中不存在多键合并
python
# 基于多个列合并
df1 = pd.DataFrame({
'year': [2018, 2018, 2019, 2019],
'region': ['A', 'B', 'A', 'B'],
'sales': [100, 120, 110, 130]
})
df2 = pd.DataFrame({
'year': [2018, 2019],
'region': ['A', 'A'],
'gdp': [500, 520]
})
merged = pd.merge(
df1, df2,
on=['year', 'region'],
how='left'
)不同列名合并
python
# 左表用 'region_code',右表用 'code'
merged = pd.merge(
df1, df2,
left_on='region_code',
right_on='code',
how='left'
)索引合并
python
# 基于索引合并
merged = pd.merge(
df1, df2,
left_index=True,
right_index=True,
how='left'
)
# 或者直接用 join
merged = df1.join(df2, how='left')合并验证(重要!)
python
# 验证合并关系
merged = pd.merge(
individuals,
region_gdp,
on='region',
how='left',
validate='m:1' # 多对一关系
)
# 常见关系类型:
# '1:1' - 一对一
# '1:m' - 一对多
# 'm:1' - 多对一
# 'm:m' - 多对多(通常不推荐)** 常见错误:笛卡尔积**
python
# 错误示例:region 在两个表中都不唯一
df1 = pd.DataFrame({'region': ['A', 'A', 'B'], 'x': [1, 2, 3]})
df2 = pd.DataFrame({'region': ['A', 'A'], 'y': [10, 20]})
merged = pd.merge(df1, df2, on='region')
# 结果:4 行(2×2 的笛卡尔积)
# region x y
# 0 A 1 10
# 1 A 1 20 # 重复!
# 2 A 2 10 # 重复!
# 3 A 2 20 # 重复!
# 解决:先去重或使用 validate 参数数据拼接(Concat)
纵向拼接(Stacking)
python
# 合并多年数据
df_2018 = pd.read_csv('survey_2018.csv')
df_2019 = pd.read_csv('survey_2019.csv')
df_2020 = pd.read_csv('survey_2020.csv')
# 纵向堆叠
df_panel = pd.concat(
[df_2018, df_2019, df_2020],
ignore_index=True
)
# 添加年份标识
for year, df in zip([2018, 2019, 2020], [df_2018, df_2019, df_2020]):
df['year'] = year
df_panel = pd.concat([df_2018, df_2019, df_2020], ignore_index=True)横向拼接
python
# 合并列(确保索引对齐)
df1 = pd.DataFrame({'A': [1, 2, 3]})
df2 = pd.DataFrame({'B': [4, 5, 6]})
df_combined = pd.concat([df1, df2], axis=1)
# A B
# 0 1 4
# 1 2 5
# 2 3 6宽格式 ↔ 长格式
长格式 → 宽格式(Pivot)
python
# 长格式(面板数据)
df_long = pd.DataFrame({
'person_id': [1, 1, 1, 2, 2, 2],
'year': [2018, 2019, 2020, 2018, 2019, 2020],
'income': [50000, 52000, 55000, 60000, 62000, 65000]
})
# 转换为宽格式
df_wide = df_long.pivot(
index='person_id',
columns='year',
values='income'
)
print(df_wide)
# year 2018 2019 2020
# person_id
# 1 50000 52000 55000
# 2 60000 62000 65000宽格式 → 长格式(Melt)
python
# 宽格式
df_wide = pd.DataFrame({
'person_id': [1, 2],
'income_2018': [50000, 60000],
'income_2019': [52000, 62000],
'income_2020': [55000, 65000]
})
# 转换为长格式
df_long = pd.melt(
df_wide,
id_vars=['person_id'],
value_vars=['income_2018', 'income_2019', 'income_2020'],
var_name='year',
value_name='income'
)
# 清理年份列
df_long['year'] = df_long['year'].str.extract(r'(\d{4})').astype(int)
print(df_long)
# person_id year income
# 0 1 2018 50000
# 1 2 2018 60000
# 2 1 2019 52000
# 3 2 2019 62000
# 4 1 2020 55000
# 5 2 2020 65000何时使用?
| 格式 | 适用场景 |
|---|---|
| 长格式 | 面板数据回归、时间序列分析、可视化 |
| 宽格式 | Excel 报表、截面数据分析、人类阅读 |
面板数据构建
完整流程
python
import pandas as pd
import numpy as np
# 步骤 1:读取多年数据
df_2018 = pd.read_csv('survey_2018.csv')
df_2019 = pd.read_csv('survey_2019.csv')
df_2020 = pd.read_csv('survey_2020.csv')
# 添加年份
df_2018['year'] = 2018
df_2019['year'] = 2019
df_2020['year'] = 2020
# 步骤 2:纵向合并
df_panel = pd.concat([df_2018, df_2019, df_2020], ignore_index=True)
# 步骤 3:排序(重要!)
df_panel = df_panel.sort_values(['person_id', 'year'])
# 步骤 4:设置多层索引
df_panel = df_panel.set_index(['person_id', 'year'])
# 步骤 5:检查平衡性
obs_per_person = df_panel.groupby('person_id').size()
if obs_per_person.nunique() == 1:
print(" 平衡面板")
else:
print("️ 非平衡面板")
print(obs_per_person.value_counts())
# 步骤 6:创建滞后项
df_panel = df_panel.sort_index()
df_panel['income_lag1'] = df_panel.groupby('person_id')['income'].shift(1)
# 步骤 7:面板回归
from linearmodels.panel import PanelOLS
model = PanelOLS(
df_panel['income'],
df_panel[['education', 'experience', 'income_lag1']],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', cluster_entity=True)
print(model)小结
核心函数
| 任务 | 函数 | 示例 |
|---|---|---|
| 合并 | pd.merge() | 基于键合并 |
df.join() | 基于索引合并 | |
| 拼接 | pd.concat() | 纵向/横向拼接 |
| 重塑 | df.pivot() | 长 → 宽 |
pd.melt() | 宽 → 长 |
最佳实践
- 使用 validate 参数:防止意外的笛卡尔积
- 检查合并结果:比较合并前后的行数
- 处理缺失值:合并后检查 NaN
- 保留原始数据:使用
df.copy() - 文档化合并逻辑:注释说明合并的键和类型
常见错误
| 错误 | 后果 | 解决 |
|---|---|---|
| 未排序就创建滞后项 | 滞后项错误 | 先 sort_values() |
| 重复键导致笛卡尔积 | 行数爆炸 | 使用 validate |
| 合并键格式不一致 | 无法匹配 | 统一大小写、去空格 |
忘记设置 how 参数 | 丢失数据 | 明确指定 how='left' |
练习题
合并学生信息表(student_id, name)和成绩表(student_id, subject, score),要求保留所有学生。
将宽格式的面板数据(id, income_2018, income_2019, income_2020)转换为长格式。
构建一个平衡面板数据集,包含 100 个个体,3 年数据,并创建收入的 1 期滞后项。
下一步
最后一节:完整案例研究
参考:
- Python for Data Analysis (3rd Ed) - Chapter 8
- Pandas 官方文档:Merge, Join, Concatenate