Skip to content

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()宽 → 长

最佳实践

  1. 使用 validate 参数:防止意外的笛卡尔积
  2. 检查合并结果:比较合并前后的行数
  3. 处理缺失值:合并后检查 NaN
  4. 保留原始数据:使用 df.copy()
  5. 文档化合并逻辑:注释说明合并的键和类型

常见错误

错误后果解决
未排序就创建滞后项滞后项错误sort_values()
重复键导致笛卡尔积行数爆炸使用 validate
合并键格式不一致无法匹配统一大小写、去空格
忘记设置 how 参数丢失数据明确指定 how='left'

练习题

  1. 合并学生信息表(student_id, name)和成绩表(student_id, subject, score),要求保留所有学生。

  2. 将宽格式的面板数据(id, income_2018, income_2019, income_2020)转换为长格式。

  3. 构建一个平衡面板数据集,包含 100 个个体,3 年数据,并创建收入的 1 期滞后项。


下一步

最后一节:完整案例研究


参考

  • Python for Data Analysis (3rd Ed) - Chapter 8
  • Pandas 官方文档:Merge, Join, Concatenate

基于 MIT 许可证发布。内容版权归作者所有。