从零开始的数据分析之旅
现实中的数据很少存在一张表里。学生信息在一张表,成绩在另一张表,课程又在第三张表。数据合并(Merge/Join/Concat)就是把这些分散的数据按关系拼在一起,这是数据准备中最常见的操作。
import pandas as pd
# 学生表
students = pd.DataFrame({
'学号': ['S001', 'S002', 'S003', 'S004', 'S005'],
'姓名': ['张三', '李四', '王五', '赵六', '孙七'],
'班级': ['A班', 'B班', 'A班', 'C班', 'B班']
})
# 成绩表
scores = pd.DataFrame({
'学号': ['S001', 'S002', 'S003', 'S004', 'S006'],
'数学': [85, 92, 78, 95, 88],
'英语': [90, 85, 82, 88, 76]
})
# 四种连接
inner = pd.merge(students, scores, on='学号', how='inner') # 4行交集
left = pd.merge(students, scores, on='学号', how='left') # 5行左连接
right = pd.merge(students, scores, on='学号', how='right') # 5行右连接
outer = pd.merge(students, scores, on='学号', how='outer') # 6行并集
| 连接类型 | SQL等价 | 保留规则 | 结果行数 |
|---|---|---|---|
| inner | INNER JOIN | 两表都有的键 | 4 |
| left | LEFT JOIN | 左表所有键 | 5 |
| right | RIGHT JOIN | 右表所有键 | 5 |
| outer | FULL OUTER JOIN | 所有键 | 6 |
# 多个列作为连接键
pd.merge(df1, df2, on=['键1', '键2'])
# 不同列名
pd.merge(df1, df2, left_on='左键', right_on='右键')
# 三表连接
result = (enrollment
.merge(students, on='学号')
.merge(courses, on='课程ID'))
# 查看每行的来源
pd.merge(students, scores, on='学号', how='outer', indicator=True)
# _merge列: both / left_only / right_only
# 纵向拼接(增加行)
vertical = pd.concat([df_a, df_b], ignore_index=True)
# 横向拼接(增加列)
horizontal = pd.concat([df_a, df_c], axis=1)
# 多对象拼接
result = pd.concat([df1, df2, df3], keys=['A', 'B', 'C'])
# 基于索引的连接
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['a', 'b', 'c'])
df2 = pd.DataFrame({'B': [4, 5]}, index=['a', 'b'])
result = df1.join(df2, how='left')
merge,简单堆叠用concat,索引连接用join。# 陷阱1: 重复列名
df1 = pd.DataFrame({'key':['A','B'], 'value':[1,2]})
df2 = pd.DataFrame({'key':['A','B'], 'value':[3,4]})
result = pd.merge(df1, df2, on='key', suffixes=('_左','_右'))
# 列: key, value_左, value_右
# 陷阱2: 数据类型不匹配
df1 = pd.DataFrame({'id':['001','002'], 'v':[1,2]})
df2 = pd.DataFrame({'id':[1,2], 'v':[3,4]})
df2['id'] = df2['id'].astype(str) # 先统一类型
# 陷阱3: 多对多 → 笛卡尔积!
# 合并前检查: df.duplicated(subset=['key']).sum()
def safe_merge(left, right, on, how='inner'):
# 检查键列存在
for col in ([on] if isinstance(on, str) else on):
assert col in left.columns, f"左表缺少: {col}"
assert col in right.columns, f"右表缺少: {col}"
result = pd.merge(left, right, on=on, how=how)
print(f"合并: {len(left)} x {len(right)} -> {len(result)}")
return result
#!/usr/bin/env python3
# 合并连接 — 完整实战
import pandas as pd
import numpy as np
# ============ 创建数据 ============
students = pd.DataFrame({
'学号': ['S001','S002','S003','S004','S005'],
'姓名': ['张三','李四','王五','赵六','孙七'],
'班级': ['A班','B班','A班','C班','B班']
})
scores = pd.DataFrame({
'学号': ['S001','S002','S003','S004','S006'],
'数学': [85,92,78,95,88],
'英语': [90,85,82,88,76]
})
courses = pd.DataFrame({
'课程ID': ['C01','C02','C03'],
'课程名': ['高等数学','英语写作','数据结构']
})
enrollment = pd.DataFrame({
'学号': ['S001','S001','S002','S003','S004'],
'课程ID': ['C01','C03','C02','C01','C03']
})
# ============ 四种连接 ============
print(f"inner merge:\n{pd.merge(students, scores, on='学号', how='inner')}")
print(f"\nleft merge:\n{pd.merge(students, scores, on='学号', how='left')}")
print(f"\nright merge:\n{pd.merge(students, scores, on='学号', how='right')}")
print(f"\nouter merge:\n{pd.merge(students, scores, on='学号', how='outer')}")
# ============ 多表连接 ============
multi = enrollment.merge(students, on='学号').merge(courses, on='课程ID')
print(f"\n多表连接:\n{multi}")
# ============ join ============
df1 = pd.DataFrame({'A': [1,2,3]}, index=['a','b','c'])
df2 = pd.DataFrame({'B': [4,5]}, index=['a','b'])
print(f"\njoin:\n{df1.join(df2, how='left')}")
# ============ concat ============
df_a = pd.DataFrame({'A': [1,2], 'B': [3,4]})
df_b = pd.DataFrame({'A': [5,6], 'B': [7,8]})
print(f"\n纵向拼接:\n{pd.concat([df_a, df_b], ignore_index=True)}")
print(f"\n横向拼接:\n{pd.concat([df_a, pd.DataFrame({'C': [9,10]})], axis=1)}")
# ============ merge指示器 ============
indicator = pd.merge(students, scores, on='学号', how='outer', indicator=True)
print(f"\nmerge指示:\n{indicator[['学号','_merge']]}")
print("\n✅ Python验证通过 — merge/join/concat正确")
| SQL | Pandas | 说明 |
|---|---|---|
| INNER JOIN | merge(how='inner') | 只保留匹配行 |
| LEFT JOIN | merge(how='left') | 保留左表所有行 |
| RIGHT JOIN | merge(how='right') | 保留右表所有行 |
| FULL OUTER JOIN | merge(how='outer') | 保留所有行 |
| CROSS JOIN | merge(how='cross') | 笛卡尔积 |
| UNION ALL | concat(axis=0) | 纵向拼接不去重 |
| UNION | concat().drop_duplicates() | 纵向拼接去重 |
# 合并速查
# merge (数据库风格)
pd.merge(df1, df2, on='键', how='inner')
# concat (堆叠)
pd.concat([df1, df2], axis=0) # 纵向
pd.concat([df1, df2], axis=1) # 横向
# join (索引连接)
df1.join(df2, how='left')
# 安全检查
df.duplicated(subset=['键']).sum() # 检查键唯一性
pd.merge(df1, df2, on='键', indicator=True) # 追踪来源
validate参数检查合并类型(1:1, 1:m, m:1) ④索引列merge比列值merge更快。