Pandas: 合并连接
最后更新:2026-08-26
真实数据从来不在一张表里——用户信息一张表,订单记录一张表,商品详情又一张表。分析需要把它们关联起来,这就是 merge 的工作。Pandas 的 merge 实现了 SQL 的所有 JOIN 类型,本节用 Mermaid 图解 4 种 join 的效果,让你彻底理解"左表保留什么、右表保留什么"。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ merge 4 种连接类型
- ❷ on / left_on / right_on
- ❸ 多键合并
- ❹ suffixes 与 indicator
- ❺ validate 与交叉连接
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 图解
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 万行!
📖 小节
- merge 4 种 join:inner(只匹配)/ left(保留左表)/ right(保留右表)/ outer(两边全保留)
- 多键合并用 on=['key1','key2'],列名不同用 left_on/right_on
- suffixes 处理重名列,indicator 追踪行来源
- validate 验证合并关系(1:1 / 1:m / m:1),防止意外
- cross join 产生笛卡尔积,只用于小表组合
- join 是 merge 的快捷方式(默认 Index + left join)
- 合并后务必检查行数和 indicator,确认结果正确
📝 作业
- 基础题(难度⭐):创建学生表和成绩表(共享 student_id),分别用 inner/left/outer merge,观察行数变化。
- 进阶题(难度⭐⭐):创建部门表(dept_id/dept_name)和员工表(emp_id/dept_id/salary),用 merge 关联后按部门统计平均工资,用 indicator 找出没有员工的部门。
- 挑战题(难度⭐⭐⭐):模拟三表(用户/订单/商品),完成:inner merge 关联→计算每用户消费总额→left merge 找出零消费用户→validate 验证关系→indicator 诊断。