Pandas: 合并连接

最后更新:2026-08-26

真实数据从来不在一张表里——用户信息一张表,订单记录一张表,商品详情又一张表。分析需要把它们关联起来,这就是 merge 的工作。Pandas 的 merge 实现了 SQL 的所有 JOIN 类型,本节用 Mermaid 图解 4 种 join 的效果,让你彻底理解"左表保留什么、右表保留什么"。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


2. Bob 的用户订单关联

(1) 痛点:两表分开,无法分析

Bob 有用户表和订单表,想看"每个用户买了什么":

PYTHON
import pandas as pd

users = pd.DataFrame({
    'user_id': [1, 2, 3, 4],
    'name': ['Alice', 'Bob', 'Charlie', 'Carol']
})

orders = pd.DataFrame({
    'order_id': ['O001', 'O002', 'O003'],
    'user_id': [1, 2, 2],
    'amount': [120, 85, 200]
})

# How to combine? Need to link by user_id
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) 解法:merge 一行关联

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:merge 基础关联(难度⭐)

PYTHON
import pandas as pd

users = pd.DataFrame({
    'user_id': [1, 2, 3, 4],
    'name': ['Alice', 'Bob', 'Charlie', 'Carol']
})

orders = pd.DataFrame({
    'order_id': ['O001', 'O002', 'O003'],
    'user_id': [1, 2, 2],
    'amount': [120, 85, 200]
})

# Inner join (default) — only matching user_ids
result = pd.merge(users, orders, on='user_id')
print(result)
#    user_id     name order_id  amount
# 0        1    Alice     O001     120
# 1        2      Bob     O002      85
# 2        2      Bob     O003     200
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

3. 4 种 join 类型

(1) Mermaid 图解

100%
graph TB
    subgraph Inner["inner — 只保留匹配"]
        I1["user_id 1: Alice+O001"]
        I2["user_id 2: Bob+O002"]
        I3["user_id 2: Bob+O003"]
    end
    subgraph Left["left — 保留左表全部"]
        L1["user_id 1: Alice+O001"]
        L2["user_id 2: Bob+O002"]
        L3["user_id 2: Bob+O003"]
        L4["user_id 3: Charlie+NaN"]
        L5["user_id 4: Carol+NaN"]
    end
    subgraph Right["right — 保留右表全部"]
        R1["user_id 1: Alice+O001"]
        R2["user_id 2: Bob+O002"]
        R3["user_id 2: Bob+O003"]
    end
    subgraph Outer["outer — 保留两边全部"]
        O1["user_id 1: Alice+O001"]
        O2["user_id 2: Bob+O002"]
        O3["user_id 2: Bob+O003"]
        O4["user_id 3: Charlie+NaN"]
        O5["user_id 4: Carol+NaN"]
    end
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) 4 种 join 对比

类型 how 左表无匹配 右表无匹配 行数
inner 'inner' 丢弃 丢弃 ≤min(左,右)
left 'left' 保留(填NaN) 丢弃 =左表行数×匹配
right 'right' 丢弃 保留(填NaN) =右表行数×匹配
outer 'outer' 保留(填NaN) 保留(填NaN) ≥max(左,右)

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:4 种 join 对比(难度⭐⭐)

PYTHON
import pandas as pd

left = pd.DataFrame({
    'id': [1, 2, 3],
    'name': ['Alice', 'Bob', 'Charlie']
})

right = pd.DataFrame({
    'id': [2, 3, 4],
    'score': [85, 92, 78]
})

# Inner — only id 2, 3 (both tables have them)
print("INNER:")
print(pd.merge(left, right, on='id', how='inner'))
#    id    name  score
# 0   2     Bob     85
# 1   3 Charlie     92

# Left — all left rows, right fills NaN where no match
print("\nLEFT:")
print(pd.merge(left, right, on='id', how='left'))
#    id     name  score
# 0   1    Alice    NaN  ← no match in right
# 1   2      Bob   85.0
# 2   3  Charlie   92.0

# Right — all right rows
print("\nRIGHT:")
print(pd.merge(left, right, on='id', how='right'))
#    id    name  score
# 0   2     Bob     85
# 1   3 Charlie     92
# 2   4    NaN     78  ← no match in left

# Outer — all rows from both sides
print("\nOUTER:")
print(pd.merge(left, right, on='id', how='outer'))
#    id     name  score
# 0   1    Alice    NaN
# 1   2      Bob   85.0
# 2   3  Charlie   92.0
# 3   4     NaN    78.0
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

4. 多键合并与不同列名

(1) 多键合并

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:多键合并(难度⭐⭐)

PYTHON
import pandas as pd

sales = pd.DataFrame({
    'region': ['North', 'North', 'South', 'South'],
    'category': ['Electronics', 'Clothing', 'Electronics', 'Clothing'],
    'revenue': [5000, 800, 2000, 600]
})

targets = pd.DataFrame({
    'region': ['North', 'North', 'South', 'South'],
    'category': ['Electronics', 'Clothing', 'Electronics', 'Clothing'],
    'target': [4500, 1000, 2500, 500]
})

# Merge on multiple keys
result = pd.merge(sales, targets, on=['region', 'category'])
print(result)
#   region    category  revenue  target
# 0  North  Electronics     5000    4500
# 1  North     Clothing      800    1000
# 2  South  Electronics     2000    2500
# 3  South     Clothing      600     500

# Calculate achievement rate
result['achievement'] = (result['revenue'] / result['target'] * 100).round(1)
print(result[['region', 'category', 'achievement']])
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) 不同列名合并

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:left_on/right_on(难度⭐⭐)

PYTHON
import pandas as pd

employees = pd.DataFrame({
    'emp_id': [1, 2, 3],
    'name': ['Alice', 'Bob', 'Charlie']
})

salaries = pd.DataFrame({
    'employee_id': [1, 2, 3],
    'salary': [75000, 92000, 68000]
})

# Column names differ: emp_id vs employee_id
result = pd.merge(
    employees, salaries,
    left_on='emp_id', right_on='employee_id',
    how='inner'
)
print(result)
#    emp_id     name  employee_id  salary
# 0       1    Alice            1   75000
# 1       2      Bob            2   92000
# 2       3  Charlie            3   68000

# Drop redundant column
result = result.drop(columns='employee_id')
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

5. suffixes 与 indicator

(1) 列名冲突处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:suffixes 处理重名列(难度⭐)

PYTHON
import pandas as pd

df1 = pd.DataFrame({
    'id': [1, 2],
    'value': [100, 200]
})

df2 = pd.DataFrame({
    'id': [1, 2],
    'value': [300, 400]
})

# Default suffixes: _x and _y
result = pd.merge(df1, df2, on='id')
print(result)
#    id  value_x  value_y
# 0   1      100      300
# 1   2      200      400

# Custom suffixes
result2 = pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))
print(result2)
#    id  value_left  value_right
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) indicator 诊断合并来源

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:indicator 追踪来源(难度⭐⭐)

PYTHON
import pandas as pd

left = pd.DataFrame({'id': [1, 2, 3], 'name': ['Alice', 'Bob', 'Charlie']})
right = pd.DataFrame({'id': [2, 3, 4], 'score': [85, 92, 78]})

# Add _merge column to see where each row came from
result = pd.merge(left, right, on='id', how='outer', indicator=True)
print(result)
#    id     name  score      _merge
# 0   1    Alice    NaN   left_only
# 1   2      Bob   85.0        both
# 2   3  Charlie   92.0        both
# 3   4     NaN    78.0  right_only

# Filter by merge source
only_left = result[result['_merge'] == 'left_only']
print(f"Only in left table: {len(only_left)} rows")  # 1 (Alice)

# Validate data integrity — find unmatched records
unmatched = result[result['_merge'] != 'both']
print(f"Unmatched records: {len(unmatched)}")  # 2
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

6. validate 与 cross join

(1) validate 验证合并类型

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:validate 验证(难度⭐⭐)

PYTHON
import pandas as pd

users = pd.DataFrame({'user_id': [1, 2, 3], 'name': ['Alice', 'Bob', 'Charlie']})
orders = pd.DataFrame({'order_id': ['O1', 'O2', 'O3'], 'user_id': [1, 2, 2]})

# Validate: each user should have at most one order (1:1)
# This will FAIL because user 2 has 2 orders
try:
    result = pd.merge(users, orders, on='user_id', validate='1:1')
except Exception as e:
    print(f"Validation failed: {e}")

# Validate: each user can have many orders (1:m) — passes
result = pd.merge(users, orders, on='user_id', validate='1:m')
print(result)  # OK

# Validation types: '1:1', '1:m', 'm:1', 'm:m'
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) cross join 交叉连接

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:cross join(难度⭐)

PYTHON
import pandas as pd

colors = pd.DataFrame({'color': ['Red', 'Blue']})
sizes = pd.DataFrame({'size': ['S', 'M', 'L']})

# Every combination (cartesian product)
result = pd.merge(colors, sizes, how='cross')
print(result)
#   color size
# 0   Red    S
# 1   Red    M
# 2   Red    L
# 3  Blue    S
# 4  Blue    M
# 5  Blue    L
# 6 rows = 2 colors × 3 sizes
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

7. merge vs join

特性 merge join
调用方式 pd.merge(df1, df2) df1.join(df2)
默认连接 on 指定列 Index
多列连接 ✅ on=['a','b'] ❌ 仅 Index
连接类型 inner/left/right/outer left(默认)
灵活性
适用 通用关联 Index 对齐

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:join 快捷方式(难度⭐)

PYTHON
import pandas as pd

df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['x', 'y', 'z'])
df2 = pd.DataFrame({'B': [4, 5]}, index=['x', 'y'])

# join uses Index by default
result = df1.join(df2)  # how='left' by default
print(result)
#    A    B
# x  1  4.0
# y  2  5.0
# z  3  NaN  ← left join keeps all left rows

# join with how parameter
result2 = df1.join(df2, how='inner')
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

8. 完整示例:三表关联分析

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:三表 merge 全流程(难度⭐⭐⭐)

PYTHON
import pandas as pd

# ============================================
# Comprehensive example: 3-table merge
# users + orders + products → full analysis
# ============================================

# 1. Users table
users = pd.DataFrame({
    'user_id': [1, 2, 3, 4, 5],
    'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
    'region': ['North', 'South', 'East', 'North', 'West']
})

# 2. Orders table
orders = pd.DataFrame({
    'order_id': ['O001', 'O002', 'O003', 'O004', 'O005', 'O006'],
    'user_id': [1, 2, 2, 3, 1, 5],
    'product_id': ['P01', 'P02', 'P03', 'P01', 'P04', 'P02'],
    'quantity': [2, 1, 3, 1, 5, 2]
})

# 3. Products table
products = pd.DataFrame({
    'product_id': ['P01', 'P02', 'P03', 'P04', 'P05'],
    'product_name': ['Laptop', 'Phone', 'Tablet', 'Mouse', 'Keyboard'],
    'price': [999, 699, 349, 29, 79]
})

# Step 1: orders + products → order details with price
order_details = pd.merge(orders, products, on='product_id', how='left')
order_details['total'] = order_details['quantity'] * order_details['price']

# Step 2: order_details + users → full picture
full = pd.merge(order_details, users, on='user_id', how='left')

# Step 3: Analyze by region
region_sales = full.groupby('region')['total'].sum().sort_values(ascending=False)
print("=== Sales by Region ===")
print(region_sales)

# Step 4: Top customers
top_customers = full.groupby('name')['total'].sum().sort_values(ascending=False)
print("\n=== Top Customers ===")
print(top_customers)

# Step 5: Unmatched products (in products but never ordered)
unmatched = pd.merge(
    products, orders[['product_id']].drop_duplicates(),
    on='product_id', how='left', indicator=True
)
never_ordered = unmatched[unmatched['_merge'] == 'left_only']
print(f"\n=== Never Ordered Products ===")
print(never_ordered[['product_id', 'product_name']])
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

❓ 常见问题

Q inner 和 left 用哪个?
A 默认用 inner(只保留匹配的,数据干净)。想保留左表全部(如"所有用户无论是否有订单")用 left。right 很少用(换成 left 交换表即可)。outer 用于找差异("哪些用户没订单+哪些订单没用户")。日常 80% 场景用 inner。
Q 多键合并键名不同怎么办?
A 用 left_on 和 right_on 分别指定两表的键列。如 pd.merge(a, b, left_on='emp_id', right_on='employee_id')。合并后两列都保留,用 drop 删掉冗余列。如果一列是 Index,用 left_index=True / right_index=True。
Q suffixes 冲突怎么处理?
A 两表有同名列(非键列)时,merge 自动加 _x 和 _y 后缀。用 suffixes=('_left','_right') 自定义。建议合并前先重命名列,避免歧义:df.rename(columns={'value': 'value_a'})
Q merge 和 join 区别?
A merge 更通用——支持列连接、多键、4 种 join。join 更简洁——默认用 Index 连接、默认 left join。需要多列关联用 merge,两个 Index 对齐用 join。推荐优先用 merge(功能完整),join 只做快捷方式。
Q 如何检查合并结果正确?
A 三步检查:① 合并前行数 vs 合并后行数(inner 应≤min,left 应=左表×匹配);② 用 indicator=True 查看每行来源(both/left_only/right_only);③ 用 validate='1:1' 或 '1:m' 指定预期关系,违反则报错。
Q merge 导致行数爆炸怎么办?
A 行数爆炸说明是 m:m(多对多)合并——右表每个键有 N 行匹配时,结果=左表×N。检查:合并前对右表去重或聚合(groupby+agg),减少匹配行数。用 validate='m:1' 可以提前检测。
Q cross join 什么时候用?
A cross join 产生笛卡尔积(每行×每行),行数=左表×右表。只用于生成组合场景(如颜色×尺码=所有规格组合)。绝不要对大数据表用 cross join——1000×1000=100 万行!

📖 小节


📝 作业

  1. 基础题(难度⭐):创建学生表和成绩表(共享 student_id),分别用 inner/left/outer merge,观察行数变化。
  2. 进阶题(难度⭐⭐):创建部门表(dept_id/dept_name)和员工表(emp_id/dept_id/salary),用 merge 关联后按部门统计平均工资,用 indicator 找出没有员工的部门。
  3. 挑战题(难度⭐⭐⭐):模拟三表(用户/订单/商品),完成:inner merge 关联→计算每用户消费总额→left merge 找出零消费用户→validate 验证关系→indicator 诊断。

← 上一课:分组聚合 · 下一课:拼接追加 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏