🔗 合并连接

从零开始的数据分析之旅

📖 数据合并:把分散的数据拼起来

现实中的数据很少存在一张表里。学生信息在一张表,成绩在另一张表,课程又在第三张表。数据合并(Merge/Join/Concat)就是把这些分散的数据按关系拼在一起,这是数据准备中最常见的操作。

Pandas合并方法对比: pd.merge() pd.concat() df.join() ────────── ─────────── ────────── 数据库风格合并 堆叠拼接 索引连接 ├ 按列值匹配 ├ 纵向拼接(axis=0) ├ 按索引连接 ├ 支持多种连接类型 ├ 横向拼接(axis=1) ├ 左连接为主 └ 类似SQL JOIN └ 多对象同时拼接 └ 简化写法 连接类型: ──────── inner 交集 只保留两表都有的键 left 左连接 保留左表所有键 right 右连接 保留右表所有键 outer 并集 保留所有键

1. merge — 数据库风格合并

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等价保留规则结果行数
innerINNER JOIN两表都有的键4
leftLEFT JOIN左表所有键5
rightRIGHT JOIN右表所有键5
outerFULL 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'))

merge指示器

# 查看每行的来源
pd.merge(students, scores, on='学号', how='outer', indicator=True)
# _merge列: both / left_only / right_only

📚 concat与join

concat — 堆叠拼接

# 纵向拼接(增加行)
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'])

join — 索引连接

# 基于索引的连接
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

📐 2024-2025 数据合并前沿

⚠️ 合并陷阱与解决方案

# 陷阱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正确")
🏆 成就解锁:merge/join/concat正确
Python验证通过 — 四种连接类型(inner:4行, left:5行, right:5行, outer:6行)结果正确。多表链式连接5行,join结果3行,concat纵向4行横向2行,merge指示器标识正确。
思考题:
① inner join和outer join的区别是什么?
② merge的on参数和left_on/right_on的区别?
③ concat的axis=0和axis=1分别是什么效果?
④ 什么情况下用join比merge更方便?

📝 课后练习

  1. 实现一个简单的学生选课系统,涉及5张表的合并
  2. 对比merge、join、concat在相同任务上的性能差异
  3. 处理合并时列名冲突的情况(suffixes参数)
  4. 实现渐变维度(SCD Type 2)的数据合并
  5. 用DuckDB SQL实现同样的合并操作
📚 参考资料:
• Pandas Merging文档: pandas.pydata.org/docs/user_guide/merging
• SQL JOIN可视化: sql-joins.leopard.in.ua
• Python for Data Analysis (Wes McKinney, 2022)
• DuckDB Joins: duckdb.org/docs/sql/query_syntax/join

🗺️ 合并方法决策树

数据合并决策树: 如何合并? ├ 按列值匹配 → pd.merge() │ ├ 两表交集 → how='inner' │ ├ 保留左表 → how='left' │ ├ 保留右表 → how='right' │ └ 保留全部 → how='outer' ├ 简单堆叠 → pd.concat() │ ├ 上下拼接 → axis=0 │ └ 左右拼接 → axis=1 ├ 索引连接 → df.join() │ └ 默认左连接 └ 比较差异 → pd.compare() 合并前检查: ──────── □ 键列名是否一致? □ 键列类型是否一致? □ 是否有重复键?(多对多风险) □ 合并后行数是否符合预期?

SQL JOIN vs Pandas Merge对照

SQLPandas说明
INNER JOINmerge(how='inner')只保留匹配行
LEFT JOINmerge(how='left')保留左表所有行
RIGHT JOINmerge(how='right')保留右表所有行
FULL OUTER JOINmerge(how='outer')保留所有行
CROSS JOINmerge(how='cross')笛卡尔积
UNION ALLconcat(axis=0)纵向拼接不去重
UNIONconcat().drop_duplicates()纵向拼接去重

🔑 本课关键要点

数据合并核心5条:
① merge按列值匹配,concat按方向堆叠,join按索引连接
② inner交集/outer并集/left保留左表/right保留右表
③ 合并前检查键的类型一致性和唯一性
④ 多对多合并会产生笛卡尔积——行数爆炸!
⑤ 用indicator参数追踪每行的来源(both/left_only/right_only)
# 合并速查
# 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)  # 追踪来源
合并性能优化:①合并前对键列排序可加速merge ②大数据集考虑Dask merge ③用validate参数检查合并类型(1:1, 1:m, m:1) ④索引列merge比列值merge更快。