首页 / Pandas 入门教程 / 连接进阶:多键、冲突与条件连接

Pandas 入门教程

连接进阶:多键、冲突与条件连接

本教程共 54 篇 · 第 35 篇 · 更新于 2026-08-11 · 约 7 分钟阅读

PandasPandas 入门教程mergesuffixesindicatorvalidate

本节目标:掌握多键连接、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 没有通用的非等值连接函数,但有几种思路:

  1. cross join 加过滤:how=“cross” 生成笛卡尔积,再用布尔条件筛掉不合格的行,适合小数据;
  2. merge_asof:按有序键做”最近匹配”,对左表每一行取右表中键不大于它的最后一行,两表都要先按键排序;
  3. 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。

Note

merge_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 做有序外连接,三种思路覆盖大多数场景。