跳到主要内容

数据合并

实际工作中,数据往往分散在多个表格中,需要将它们合并到一起进行分析。Pandas 提供了多种数据合并方法:mergejoinconcat

合并方法概览

方法用途特点
pd.merge()基于列值合并类似 SQL JOIN,最灵活
DataFrame.join()基于索引合并merge 的简化版本
pd.concat()沿轴拼接堆叠数据,不基于键匹配

concat 拼接

concat 函数用于沿指定轴将多个对象拼接到一起。

纵向拼接(axis=0)

import pandas as pd
import numpy as np

# 创建示例数据
df1 = pd.DataFrame({
'A': ['A1', 'A2', 'A3'],
'B': ['B1', 'B2', 'B3']
}, index=[0, 1, 2])

df2 = pd.DataFrame({
'A': ['A4', 'A5', 'A6'],
'B': ['B4', 'B5', 'B6']
}, index=[3, 4, 5])

# 纵向拼接
result = pd.concat([df1, df2])
print(result)
# A B
# 0 A1 B1
# 1 A2 B2
# 2 A3 B3
# 3 A4 B4
# 4 A5 B5
# 5 A6 B6

# 忽略原索引,重新生成
result = pd.concat([df1, df2], ignore_index=True)
print(result)
# A B
# 0 A1 B1
# 1 A2 B2
# 2 A3 B3
# 3 A4 B4
# 4 A5 B5
# 5 A6 B6

横向拼接(axis=1)

# 创建示例数据
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['a', 'b', 'c'])
df2 = pd.DataFrame({'B': [4, 5, 6]}, index=['a', 'b', 'c'])
df3 = pd.DataFrame({'C': [7, 8, 9]}, index=['b', 'c', 'd'])

# 横向拼接(对齐索引)
result = pd.concat([df1, df2], axis=1)
print(result)
# A B
# a 1 4
# b 2 5
# c 3 6

# 索引不完全匹配时
result = pd.concat([df1, df3], axis=1)
print(result)
# A C
# a 1.0 NaN
# b 2.0 7.0
# c 3.0 8.0
# d NaN 9.0

# 只保留共有的索引(内连接)
result = pd.concat([df1, df3], axis=1, join='inner')
print(result)
# A C
# b 2 7.0
# c 3 8.0

添加标识键

# 使用 keys 参数标识数据来源
df1 = pd.DataFrame({'value': [1, 2, 3]})
df2 = pd.DataFrame({'value': [4, 5, 6]})

result = pd.concat([df1, df2], keys=['df1', 'df2'])
print(result)
# value
# df1 0 1
# 1 2
# 2 3
# df2 0 4
# 1 5
# 2 6

# 可以通过键选择数据
print(result.loc['df1'])
# value
# 0 1
# 1 2
# 2 3

列不完全匹配

# 列不完全相同的数据
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'C': [7, 8]})

# 默认:缺失列填充 NaN
result = pd.concat([df1, df2])
print(result)
# A B C
# 0 1.0 3.0 NaN
# 1 2.0 4.0 NaN
# 0 5.0 NaN 7.0
# 1 6.0 NaN 8.0

# 只保留共有的列
result = pd.concat([df1, df2], join='inner')
print(result)
# A
# 0 1
# 1 2
# 0 5
# 1 6

append 方法(已弃用)

# ⚠️ append 方法在 Pandas 2.0 已弃用
# 旧写法:df1.append(df2)
# 新写法:
result = pd.concat([df1, df2])

merge 合并

merge 是最常用的合并方法,类似于 SQL 的 JOIN 操作。

基本用法

# 创建示例数据
employees = pd.DataFrame({
'emp_id': [1, 2, 3, 4, 5],
'name': ['张三', '李四', '王五', '赵六', '钱七'],
'dept_id': [101, 102, 101, 103, 102]
})

departments = pd.DataFrame({
'dept_id': [101, 102, 104],
'dept_name': ['技术部', '销售部', '财务部']
})

# 内连接(默认)
result = pd.merge(employees, departments, on='dept_id')
print(result)
# emp_id name dept_id dept_name
# 0 1 张三 101 技术部
# 1 3 王五 101 技术部
# 2 2 李四 102 销售部
# 3 5 钱七 102 销售部
# 注意:赵六(dept_id=103)和财务部(dept_id=104)未匹配

连接类型

# 内连接(inner)- 只保留两边都有的
inner = pd.merge(employees, departments, on='dept_id', how='inner')

# 左连接(left)- 保留左表所有行
left = pd.merge(employees, departments, on='dept_id', how='left')
print(left)
# emp_id name dept_id dept_name
# 0 1 张三 101 技术部
# 1 2 李四 102 销售部
# 2 3 王五 101 技术部
# 3 4 赵六 103 NaN # 右表无匹配,填充 NaN
# 4 5 钱七 102 销售部

# 右连接(right)- 保留右表所有行
right = pd.merge(employees, departments, on='dept_id', how='right')
print(right)
# emp_id name dept_id dept_name
# 0 1.0 张三 101 技术部
# 1 3.0 王五 101 技术部
# 2 2.0 李四 102 销售部
# 3 5.0 钱七 102 销售部
# 4 NaN NaN 104 财务部 # 左表无匹配

# 外连接(outer)- 保留所有行
outer = pd.merge(employees, departments, on='dept_id', how='outer')
print(outer)
# emp_id name dept_id dept_name
# 0 1.0 张三 101 技术部
# 1 3.0 王五 101 技术部
# 2 2.0 李四 102 销售部
# 3 5.0 钱七 102 销售部
# 4 4.0 赵六 103 NaN
# 5 NaN NaN 104 财务部

# 交叉连接(cross)- 笛卡尔积
cross = pd.merge(
employees[['emp_id', 'name']].head(2),
departments[['dept_id', 'dept_name']].head(2),
how='cross'
)
print(cross)
# emp_id name dept_id dept_name
# 0 1 张三 101 技术部
# 1 1 张三 102 销售部
# 2 2 李四 101 技术部
# 3 2 李四 102 销售部

不同列名合并

# 列名不同时的合并
employees = pd.DataFrame({
'emp_id': [1, 2, 3],
'name': ['张三', '李四', '王五'],
'department_id': [101, 102, 101]
})

departments = pd.DataFrame({
'dept_id': [101, 102, 103],
'dept_name': ['技术部', '销售部', '财务部']
})

# 指定左右表的列名
result = pd.merge(
employees,
departments,
left_on='department_id',
right_on='dept_id'
)
print(result)
# emp_id name department_id dept_id dept_name
# 0 1 张三 101 101 技术部
# 1 3 王五 101 101 技术部
# 2 2 李四 102 102 销售部

# 删除重复的连接列
result = result.drop('dept_id', axis=1)

基于索引合并

# 设置索引后基于索引合并
employees_idx = employees.set_index('department_id')
departments_idx = departments.set_index('dept_id')

# 使用索引合并
result = pd.merge(
employees_idx,
departments_idx,
left_index=True,
right_index=True
)
print(result)
# emp_id name dept_name
# department_id
# 101 1 张三 技术部
# 101 3 王五 技术部
# 102 2 李四 销售部

# 混合使用索引和列
result = pd.merge(
employees,
departments_idx,
left_on='department_id',
right_index=True
)

多键合并

# 多列作为合并键
sales = pd.DataFrame({
'year': [2023, 2023, 2024, 2024],
'quarter': ['Q1', 'Q2', 'Q1', 'Q2'],
'sales': [100, 150, 120, 180]
})

targets = pd.DataFrame({
'year': [2023, 2023, 2024, 2024],
'quarter': ['Q1', 'Q2', 'Q1', 'Q2'],
'target': [90, 140, 130, 170]
})

# 多键合并
result = pd.merge(sales, targets, on=['year', 'quarter'])
print(result)
# year quarter sales target
# 0 2023 Q1 100 90
# 1 2023 Q2 150 140
# 2 2024 Q1 120 130
# 3 2024 Q2 180 170

处理重复列名

# 两表有相同列名但不是连接键
df1 = pd.DataFrame({
'id': [1, 2],
'value': [100, 200],
'status': ['A', 'B']
})

df2 = pd.DataFrame({
'id': [1, 2],
'value': [150, 250],
'status': ['C', 'D']
})

# 自动添加后缀
result = pd.merge(df1, df2, on='id')
print(result)
# id value_x status_x value_y status_y
# 0 1 100 A 150 C
# 1 2 200 B 250 D

# 自定义后缀
result = pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))
print(result)
# id value_left status_left value_right status_right
# 0 1 100 A 150 C
# 1 2 200 B 250 D

验证合并

# validate 参数验证合并类型
# 'one_to_one':一对一
# 'one_to_many':一对多
# 'many_to_one':多对一
# 'many_to_many':多对多

# 一对一验证
df1 = pd.DataFrame({'id': [1, 2, 3], 'name': ['A', 'B', 'C']})
df2 = pd.DataFrame({'id': [1, 2, 3], 'value': [100, 200, 300]})

# 如果验证失败会抛出 MergeError
result = pd.merge(df1, df2, on='id', validate='one_to_one')

指示列

# indicator 参数显示合并来源
result = pd.merge(
employees,
departments,
on='dept_id',
how='outer',
indicator=True
)
print(result)
# emp_id name dept_id dept_name _merge
# 0 1.0 张三 101 技术部 both
# 1 3.0 王五 101 技术部 both
# 2 2.0 李四 102 销售部 both
# 3 5.0 钱七 102 销售部 both
# 4 4.0 赵六 103 NaN left_only
# 5 NaN NaN 104 财务部 right_only

# 根据来源筛选
left_only = result[result['_merge'] == 'left_only'] # 只在左表
right_only = result[result['_merge'] == 'right_only'] # 只在右表
both = result[result['_merge'] == 'both'] # 两表都有

join 方法

join 是基于索引的合并,是 merge 的简化版本。

基本用法

# 创建示例数据
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['a', 'b', 'c'])
df2 = pd.DataFrame({'B': [4, 5, 6]}, index=['a', 'b', 'c'])
df3 = pd.DataFrame({'C': [7, 8]}, index=['b', 'c'])

# 默认左连接
result = df1.join(df2)
print(result)
# A B
# a 1 4
# b 2 5
# c 3 6

# 指定连接类型
result = df1.join(df3, how='inner') # 内连接
result = df1.join(df3, how='outer') # 外连接
result = df1.join(df3, how='right') # 右连接

多表连接

# 一次连接多个 DataFrame
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['a', 'b', 'c'])
df2 = pd.DataFrame({'B': [4, 5, 6]}, index=['a', 'b', 'c'])
df3 = pd.DataFrame({'C': [7, 8, 9]}, index=['a', 'b', 'c'])

result = df1.join([df2, df3])
print(result)
# A B C
# a 1 4 7
# b 2 5 8
# c 3 6 9

指定列连接

# 基于某列连接(需要先设置索引)
employees = pd.DataFrame({
'emp_id': [1, 2, 3],
'name': ['张三', '李四', '王五'],
'dept_id': [101, 102, 101]
})

departments = pd.DataFrame({
'dept_name': ['技术部', '销售部', '财务部']
}, index=[101, 102, 103])

# 基于员工的 dept_id 和部门的索引连接
result = employees.join(departments, on='dept_id')
print(result)
# emp_id name dept_id dept_name
# 0 1 张三 101 技术部
# 1 2 李四 102 销售部
# 2 3 王五 101 技术部

合并 vs 拼接选择

# 场景 1:需要基于键值匹配 → 使用 merge
pd.merge(employees, departments, on='dept_id')

# 场景 2:基于索引合并 → 使用 join 或 merge
df1.join(df2)
pd.merge(df1, df2, left_index=True, right_index=True)

# 场景 3:简单堆叠数据 → 使用 concat
pd.concat([df1, df2, df3], axis=0) # 纵向堆叠
pd.concat([df1, df2, df3], axis=1) # 横向拼接

# 场景 4:追加新行 → 使用 concat(append 已弃用)
pd.concat([df, new_row_df], ignore_index=True)

实战示例

示例 1:订单数据分析

# 创建示例数据
orders = pd.DataFrame({
'order_id': [1, 2, 3, 4, 5],
'customer_id': [101, 102, 101, 103, 102],
'product_id': ['P1', 'P2', 'P1', 'P3', 'P2'],
'quantity': [2, 1, 3, 1, 2],
'order_date': pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03',
'2024-01-04', '2024-01-05'])
})

customers = pd.DataFrame({
'customer_id': [101, 102, 103, 104],
'name': ['张三', '李四', '王五', '赵六'],
'city': ['北京', '上海', '广州', '深圳']
})

products = pd.DataFrame({
'product_id': ['P1', 'P2', 'P3', 'P4'],
'product_name': ['笔记本', '鼠标', '键盘', '显示器'],
'price': [5000, 100, 200, 1500]
})

# 合并订单与客户
order_customer = pd.merge(orders, customers, on='customer_id')

# 再合并产品信息
full_data = pd.merge(order_customer, products, on='product_id')

# 计算订单金额
full_data['amount'] = full_data['quantity'] * full_data['price']

print(full_data[['order_id', 'name', 'product_name', 'quantity', 'amount']])
# order_id name product_name quantity amount
# 0 1 张三 笔记本 2 10000
# 1 3 张三 笔记本 3 15000
# 2 2 李四 鼠标 1 100
# 3 5 李四 鼠标 2 200
# 4 4 王五 键盘 1 200

示例 2:查找不匹配的数据

# 找出没有订单的客户
customers_with_orders = pd.merge(
customers,
orders[['customer_id']].drop_duplicates(),
on='customer_id',
how='left',
indicator=True
)

no_orders = customers_with_orders[
customers_with_orders['_merge'] == 'left_only'
][['customer_id', 'name', 'city']]
print(no_orders)
# customer_id name city
# 3 104 赵六 深圳 # 赵六没有订单

# 找出订单中没有的产品
products_in_orders = pd.merge(
orders[['product_id']].drop_duplicates(),
products,
on='product_id',
how='left',
indicator=True
)

missing_products = products_in_orders[
products_in_orders['_merge'] == 'left_only'
]
# 当前没有缺失的产品

示例 3:自连接

# 员工表包含经理信息
employees = pd.DataFrame({
'emp_id': [1, 2, 3, 4, 5],
'name': ['张三', '李四', '王五', '赵六', '钱七'],
'manager_id': [None, 1, 1, 2, 2] # None 表示没有上级
})

# 自连接获取员工和经理姓名
result = pd.merge(
employees,
employees[['emp_id', 'name']],
left_on='manager_id',
right_on='emp_id',
suffixes=('', '_manager'),
how='left'
)

print(result[['name', 'name_manager']])
# name name_manager
# 0 张三 NaN
# 1 李四 张三
# 2 王五 张三
# 3 赵六 李四
# 4 钱七 李四

示例 4:时间序列对齐

# 不同频率的时间序列数据
daily = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=5),
'daily_value': [10, 20, 30, 40, 50]
})

weekly = pd.DataFrame({
'date': pd.to_datetime(['2024-01-01', '2024-01-08']),
'weekly_target': [100, 200]
})

# 使用 merge_asof 进行近似匹配(用于时间序列)
result = pd.merge_asof(
daily,
weekly,
on='date',
direction='backward' # 使用最近的过去值
)
print(result)
# date daily_value weekly_target
# 0 2024-01-01 10 100
# 1 2024-01-02 20 100
# 2 2024-01-03 30 100
# 3 2024-01-04 40 100
# 4 2024-01-05 50 100

性能优化

合并键的数据类型

# 确保合并键的数据类型一致
df1['key'] = df1['key'].astype(str)
df2['key'] = df2['key'].astype(str)

# 避免在 object 类型上进行合并
# 如果键是整数,保持为整数类型

大数据集合并

# 对于大数据集,先过滤再合并
# 假设只需要最近一年的数据
recent_orders = orders[orders['order_date'] >= '2023-01-01']
result = pd.merge(recent_orders, customers, on='customer_id')

# 分块合并(超大文件)
chunk_size = 100000
results = []
for chunk in pd.read_csv('large_file.csv', chunksize=chunk_size):
merged = pd.merge(chunk, lookup_table, on='key')
results.append(merged)
final_result = pd.concat(results)

使用分类类型优化

# 对于重复值多的列,使用分类类型
df['category'] = df['category'].astype('category')

# 分类类型可以加速合并操作
# 特别适用于状态、地区等有限值集合

常见错误

错误 1:列名不一致

# ❌ 错误:列名不同导致合并失败
# pd.merge(df1, df2, on='id') # df2 中是 'ID'

# ✅ 正确:指定左右列名
pd.merge(df1, df2, left_on='id', right_on='ID')

# 或重命名列
df2 = df2.rename(columns={'ID': 'id'})
pd.merge(df1, df2, on='id')

错误 2:意外的笛卡尔积

# 如果连接键有重复值,会产生笛卡尔积
df1 = pd.DataFrame({'key': [1, 1, 2], 'value1': ['A', 'B', 'C']})
df2 = pd.DataFrame({'key': [1, 1], 'value2': ['X', 'Y']})

result = pd.merge(df1, df2, on='key')
print(result)
# key value1 value2
# 0 1 A X
# 1 1 A Y
# 2 1 B X
# 3 1 B Y
# 结果行数 = df1中key=1的行数 × df2中key=1的行数

# 检查重复值
print(df1['key'].duplicated().sum())

错误 3:索引和列混淆

# ❌ 错误:索引不是列
# pd.merge(df1, df2, on='index') # index 不是列名

# ✅ 正确:使用索引合并
pd.merge(df1, df2, left_index=True, right_index=True)

# 或重置索引后再合并
pd.merge(df1.reset_index(), df2.reset_index(), on='index')

小结

方法选择

需求推荐方法
基于列值匹配pd.merge()
基于索引匹配df.join()
简单堆叠数据pd.concat()
时间序列近似匹配pd.merge_asof()
多表连接join([df1, df2, ...])

连接类型

类型说明保留数据
inner内连接两表都有
left左连接左表全部
right右连接右表全部
outer外连接两表全部
cross交叉连接笛卡尔积

练习

  1. 创建两个 DataFrame,使用四种连接类型进行合并,观察结果差异
  2. 使用 indicator 参数找出只在一个表中存在的数据
  3. 实现一个自连接,找出所有员工的直属下属
  4. 使用 merge_asof 对两个不同频率的时间序列进行对齐
  5. 比较大表合并时,先过滤再合并和直接合并的性能差异

下一步

掌握了数据合并后,让我们学习 分组与聚合