连接进阶:多键、冲突与条件连接
本教程共 54 篇 · 第 35 篇 · 更新于 2026-08-11 · 约 7 分钟阅读
本节目标:掌握多键连接、suffixes 处理列名冲突、indicator 追踪行来源、validate 校验唯一性,并了解非等值连接的思路。
多键连接:on 传列表
单靠一列常常区分不出唯一身份。两个班都有”张三”,只按姓名连接会张冠李戴。把”姓名 + 班级”两列一起当键:
import pandas as pd
left = pd.DataFrame({
"姓名": ["张三", "张三"],
"班级": ["1班", "2班"],
"成绩": [85, 90],
})
right = pd.DataFrame({
"姓名": ["张三", "张三"],
"班级": ["2班", "1班"],
"身高": [170, 175],
})
result = pd.merge(left, right, on=["姓名", "班级"])
print(result)
两个张三各归各班。如果只按姓名连接,会两两配对出 4 行错误数据。多键连接同样支持 left/right/outer,键的组合决定配对:
result = pd.merge(left, right, on=["姓名", "班级"], how="outer")
print(result)
多键 + outer 时,两边独有的键组合也会保留,缺失列填 NaN。
Tip连接前先确认键的”唯一性粒度”:要么单列真唯一,要么用多列组合保证唯一。这是连接结果正确性的第一道保险。
列名冲突:suffixes 自动改名
两表都有”成绩”列(一个数学、一个语文),合并后 pandas 自动加后缀区分:
math_scores = pd.DataFrame({"姓名": ["张三"], "成绩": [85]})
chinese_scores = pd.DataFrame({"姓名": ["张三"], "成绩": [90]})
result = pd.merge(math_scores, chinese_scores, on="姓名")
print(result) # 成绩_x、成绩_y
默认后缀 _x 和 _y 含义不明,用 suffixes 自定义:
result = pd.merge(
math_scores,
chinese_scores,
on="姓名",
suffixes=("_数学", "_语文"),
)
print(result)
suffixes 只处理”两个表都有的列”,键列本身不会加后缀。想彻底避免冲突,也可以在合并前用 rename 把列名改好。
Warning合并结果里冒出意料之外的 _x/_y 列,说明两表有同名列没被处理。先把要保留的列改好名,或显式指定 suffixes,别让结果混着含义不明的列。
indicator:追踪每行来源
想知道每行是两边都有、还是只有一边有,用 indicator=True:
s1 = pd.DataFrame({"学号": ["S001", "S002"], "成绩": [85, 92]})
s2 = pd.DataFrame({"学号": ["S002", "S003"], "成绩": [90, 78]})
result = pd.merge(s1, s2, on="学号", how="outer", indicator=True)
print(result)
结果多出 _merge 列,三种取值:both(两边都有)、left_only(只有左表)、right_only(只有右表)。排查”为什么左连接后一堆 NaN”时很有用——把 _merge 列按取值统计一下,一眼就能看出哪些键没配上:
print(result["_merge"].value_counts())
indicator 也能传字符串当列名:indicator=“来源”。
validate:合并前先校验
想提前发现键不唯一,用 validate 声明期望的连接类型:
- “one_to_one” / “1:1”:两边键都必须唯一;
- “one_to_many” / “1:m”:左表键唯一,右表可以重复;
- “many_to_one” / “m:1”:右表键唯一,左表可以重复。
不满足就抛 MergeError,合并不会执行:
left = pd.DataFrame({"班级": ["1班", "1班"], "人数": [40, 42]})
right = pd.DataFrame({"班级": ["1班", "2班"], "教室": ["A101", "A102"]})
try:
pd.merge(left, right, on="班级", validate="one_to_one")
except Exception as e:
print(type(e).__name__, ":", e)
报错信息里会明确指出是哪边的键重复了,省去自己查的功夫。左表键重复但右表唯一时,声明 m:1 就能正常通过:
result = pd.merge(left, right, on="班级", validate="many_to_one")
print(result)
validate 在连接前做检查,比连完再人工核对高效,还能防止多对多导致的意外膨胀。
非等值连接:条件匹配的思路
merge 默认只做”键相等”匹配。现实里还有”成绩 ≥ 分数线""时间落在区间内”这类条件连接。pandas 没有通用的非等值连接函数,但有几种思路:
- cross join 加过滤:how=“cross” 生成笛卡尔积,再用布尔条件筛掉不合格的行,适合小数据;
- merge_asof:按有序键做”最近匹配”,对左表每一行取右表中键不大于它的最后一行,两表都要先按键排序;
- merge_ordered:按有序键做外连接,还能用 fill_method 顺带前向填充缺失值,适合时间序列。
思路一的完整写法:
students = pd.DataFrame({"姓名": ["张三", "李四"], "成绩": [85, 70]})
lines = pd.DataFrame({"分数线": [60, 80]})
all_pairs = pd.merge(students, lines, how="cross")
passed = all_pairs[all_pairs["成绩"] >= all_pairs["分数线"]]
print(passed)
先两两组合,再按条件筛,等效于”成绩 ≥ 分数线”的条件连接。数据一大,笛卡尔积会很吓人,所以只适合小数据。
merge_asof 的例子,交易表匹配最近的报价:
trades = pd.DataFrame({
"时间": pd.to_datetime(["2026-08-11 09:30:01", "2026-08-11 09:30:05"]),
"价格": [51.95, 51.98],
})
quotes = pd.DataFrame({
"时间": pd.to_datetime(["2026-08-11 09:30:00", "2026-08-11 09:30:03"]),
"买价": [51.90, 51.96],
})
result = pd.merge_asof(trades, quotes, on="时间")
print(result)
两笔交易分别匹配到 09:30:00 和 09:30:03 的报价——取的是”时间不晚于交易时刻”的最后一条报价。
merge_ordered 的例子:两表按有序键外连接,缺失值前向填充:
mo_left = pd.DataFrame({"k": ["K0", "K1", "K2"], "lv": [1, 2, 3]})
mo_right = pd.DataFrame({"k": ["K1", "K3"], "rv": [10, 30]})
result = pd.merge_ordered(mo_left, mo_right, fill_method="ffill")
print(result)
K2 没有对应的 rv,fill_method=“ffill” 把上一行的 10 前向填充下来;K0 是第一行没有前值,保持 NaN。
Notemerge_asof 还有 by 参数支持分组匹配(比如按股票代码分组后各自找最近报价)、tolerance 限制最大时间差。这些都是金融数据处理的常用武器。
索引参与连接
merge 的键不一定是普通列,也可以是索引层级。on / left_on / right_on 里直接写索引层级名就行:
stu = pd.DataFrame({"姓名": ["张三", "李四"], "年龄": [20, 21]})
score = pd.DataFrame({"姓名": ["张三", "李四"], "数学": [90, 88]})
result = pd.merge(
stu.set_index("姓名"),
score.set_index("姓名"),
left_index=True,
right_index=True,
)
print(result)
两边都按同一 MultiIndex 层级连接时,结果会保留该索引层级;只按部分层级连时,多出来的层级会被丢掉,需要时先 reset_index 再连。更省事的索引连接是 DataFrame.join(§36),参数更少、专门按索引办事。这些细节用到再说,先把列键连接玩熟。
小结
进阶四件套:多键连接用 on 列表、列名冲突用 suffixes 改名、行来源用 indicator 追踪、键唯一性用 validate 预检。非等值连接没有专门函数:cross 加过滤、merge_asof 做最近匹配、merge_ordered 做有序外连接,三种思路覆盖大多数场景。