跳到正文

R 数据框连接:left_join、inner_join 等六种连接完全指南

实验数据和分析数据很少长在同一张表里:受试者信息在 subjects,每次测量的结果在 measures,中间靠一个 subject_id 串起来。把两张表按这个共同的列拼起来,就是连接(join)。连接是整个数据整理环节里最容易出错的步骤——不报错,只是结果行数悄悄变了,后面所有统计量跟着错。

连接依据的列叫键(key)。左表里能唯一标识一行的叫主键(primary key),右表里引用它的叫外键(foreign key)。按两边的唯一性,连接分成三种:

关系 含义 例子
一对一 两边键都唯一 学号 ↔ 身份证号
一对多 左表键唯一,右表可重复 受试者 ↔ 多次测量记录
多对多 两边键都可重复 会笛卡尔积,通常是你出错了

六种连接的差别只在「保留哪些行」:

函数 保留的行 常见用途
inner_join() 两边都匹配上的 只要完整观测
left_join() 左表全部 给主表补充信息(最常用)
right_join() 右表全部 少见,交换参数即可
full_join() 两边全部 核对两份名单的差异
semi_join() 左表中能匹配上的行 筛选,不加列
anti_join() 左表中匹配不上的行 查缺失、查孤儿记录

连接出错基本都出在键上,动手写 join 之前花三十秒看三件事:

library(dplyr)
# 受试者表:一行一个受试者,subject_id 应当唯一
subjects <- tibble(
subject_id = c("S01", "S02", "S03"),
group = c("control", "treat", "treat"),
)
# 测量记录表:同一人多次测量,键会重复;另外混进一个缺失值
measures <- tibble(
subject_id = c("S01", "S01", "S02", NA, "S04"),
visit = c(1, 2, 1, 1, 1),
value = c(12.4, 11.8, 13.1, 9.7, 15.2),
)
measures |> count(subject_id) |> filter(n > 1) # 1. 左表有没有重复键
subjects |> count(subject_id) |> filter(n > 1) # 2. 右表有没有重复键
sum(is.na(measures$subject_id)) # 3. 键里有没有缺失值
class(measures$subject_id)
class(subjects$subject_id) # 4. 两边的键类型是否一致
# A tibble: 1 × 2
subject_id n
<chr> <int>
1 S01 2
# A tibble: 0 × 2
# ℹ 2 variables: subject_id <chr>, n <int>
[1] 1
[1] "character"
[1] "character"

第 2 条返回零行,说明右表的键是唯一的,可以当主表用。第 1 条返回一行 S01,说明左表有重复键——这未必是错误,纵向数据里同一人多条记录本来就正常,但它意味着 left_join(subjects, measures) 会把 S01 放大成两行。第 3 条返回 1,键里有缺失值,NA 匹配不上任何行,连接后这些记录会静默消失。第 4 条两边都是 character,类型一致。

四项里最要紧的是重复键出在哪一侧:右表键重复会让 left_join() 把行数放大;左表键重复则说明这份「主表」本身就不是一行一个观测,要么先去重,要么说明这个键不够用,需要再加一列凑成组合键(见下文 join_by() 一节)。缺失值和类型问题放在后面「常见问题」里展开。这些检查用 count()is.na()class() 就能做完,比事后核对结果行数便宜得多。

dplyr 自带两个小数据集正好用来演示,不用手工造数据:

library(dplyr)
# 乐队成员与各自会的乐器,name 是共同的键
band_members
band_instruments
# A tibble: 3 × 2
name band
<chr> <chr>
1 Mick Stones
2 John Beatles
3 Paul Beatles
# A tibble: 3 × 2
name plays
<chr> <chr>
1 John guitar
2 Paul bass
3 Keith guitar

两张表都只有三行,name 是键。inner_join() 只保留两边都出现的 name(John、Paul),Mick 没有乐器记录、Keith 不在成员表里,都被丢掉:

band_members |>
inner_join(band_instruments, by = "name")
# A tibble: 2 × 3
name band plays
<chr> <chr> <chr>
1 John Beatles guitar
2 Paul Beatles bass

left_join() 换成「以左表为准」,Mick 保留下来,缺少的乐器填 NA

band_members |>
left_join(band_instruments, by = "name")
# A tibble: 3 × 3
name band plays
<chr> <chr> <chr>
1 Mick Stones NA
2 John Beatles guitar
3 Paul Beatles bass

实际工作中 left_join() 用得最多:以手头的主表为基准,把人口学信息、分组标签、地区编码这些「字典表」贴上去。它的行数下限是左表行数,只有右表键重复时才会变多——这正是需要警惕的地方。

right_join() 保留右表全部行,效果等于把两个参数调换后做 left_join(),所以实践中很少单独使用。full_join() 保留两边所有行,适合核对名单:

band_members |>
full_join(band_instruments, by = "name")
# A tibble: 4 × 3
name band plays
<chr> <chr> <chr>
1 Mick Stones NA
2 John Beatles guitar
3 Paul Beatles bass
4 Keith NA guitar

四行结果里,MickKeith 各缺一半——这类「两边都有人缺」的行,正是 full_join() 存在的理由。实际场景是核对两份名单:伦理审查批件上的受试者名单和数据库里的入组记录,用 full_join() 接一次,NA 出现在哪一侧就说明谁多了谁少了。比起写两个 setdiff() 再手动比对,一次连接能同时看到缺失和多余两个方向,也不用担心中文姓名排序的差异。

不过 full_join() 的行数最容易失控:两边键都有重复时,它同样会做笛卡尔积。核对名单时如果只关心「谁在谁不在」,用 anti_join() 跑两次比 full_join() 更安全。

这是连接最需要盯的地方。用 mtcars 演示多对多的后果:

a <- mtcars |> select(cyl, mpg)
b <- mtcars |> select(cyl, hp)
nrow(a)
nrow(b)
inner_join(a, b, by = "cyl") |> nrow()
[1] 32
[1] 32
[1] 366

两张表都只有 32 行,连接后变成 366 行。原因在于 cyl 在两边都不唯一:四缸车有 11 辆,11 × 11 = 121;六缸 7 × 7 = 49;八缸 14 × 14 = 196,合计 366。多对多连接会做笛卡尔积,两边的每个组合都会生成一行,结果行数等于「各组行数乘积之和」。

366 这个数听起来还算可控,但换成两张各有 5 万行的表,一个不唯一的键就能瞬间生成上亿行,R 会直接卡死。所以连接前养个习惯:先看键的唯一性。

# 修正:把右表压成每个 cyl 一行,多对多退化成多对一
b_unique <- b |> distinct(cyl, .keep_all = TRUE)
inner_join(a, b_unique, by = "cyl") |> nrow()
[1] 32

distinct(cyl, .keep_all = TRUE) 保留每个 cyl 第一次出现的行。更稳妥的做法是先想清楚「每个 cyl 对应的那个值」到底该怎么算——取第一辆车的马力没有统计学意义,通常应该先 summarise(mean_hp = mean(hp)) 再连接。

如果 inner_join() 之后行数比左表还少,说明左表里有匹配不上的键,用 anti_join() 一查就知道是哪些:

a |> anti_join(b_unique, by = "cyl") |> nrow()

by = "name" 是最简单的形式:两边键同名、同类型。dplyr 1.1 起提供了 join_by(),把键的对应关系写得更明确:

# 与 by = "name" 等价
band_members |> inner_join(band_instruments, by = join_by(name))

键名不同时,用 by = c("左表列" = "右表列")join_by(左表列 == 右表列),两种写法等价,输出保留左表的列名。如果两个参数都不写,dplyr 会打印 Joining with by = join_by(name)` 提示它替你选了哪些键——看到这行提示要停下来核对,别当成噪音。

join_by() 真正不可替代的场景是不等值连接(non-equi join)。比如要在剂量之间两两配对:

ToothGrowth |>
distinct(dose) |>
rename(dose_low = dose) |>
inner_join(
ToothGrowth |> distinct(dose) |> rename(dose_high = dose),
by = join_by(dose_low < dose_high)
)
# A tibble: 3 × 2
dose_low dose_high
<dbl> <dbl>
1 0.5 1
2 0.5 2
3 1 2

dose_low < dose_high 里的左边取左表的列,右边取右表的列。这个例子只是展示语法,真实用途是「每辆车配对到比它更省油的车」这类区间匹配,还包括按时间找最近一次随访的滚动连接(rolling join)。closest() 需要 dplyr 1.1.0 以上,把不等式包起来就是滚动连接:

# 随访计划表:每个人应访一次;样本表:实际测量记录,时间参差不齐
followup <- tibble(id = c("S01", "S02"), visit_time = c(10, 20))
samples <- tibble(id = c("S01", "S01", "S02"), measure_time = c(8, 15, 22), value = c(12.4, 11.8, 13.1))
# 对左表每一行,在右表里找最近的一次测量(测量时间不晚于计划随访时间)
followup |> left_join(samples, by = join_by(id, closest(visit_time <= measure_time)))
# A tibble: 2 × 4
id visit_time measure_time value
<chr> <dbl> <dbl> <dbl>
1 S01 10 15 11.8
2 S02 20 22 13.1

S01 有两次测量(第 8 天和第 15 天),计划随访排在第 10 天。条件 visit_time <= measure_time 要求测量时间不早于随访时间,第 8 天那条不满足,第 15 天那条满足且距离最近,于是被选中。

closest() 永远以左表为主表、在右表里找最近值,跟不等式两边怎么写无关。参数里的 >= 允许精确匹配,换成 > 则要求严格小于。功能上它等价于一个按组排序再取最近行的循环,但写起来是声明式的。

一个键不够用时可以传多个,组合键的意思是「这几列拼起来才算一个标识」:

subject_visits <- tibble(
subject_id = c("S01", "S01", "S02"),
visit = c(1, 2, 1),
arm = c("A", "A", "B"),
)
measures |> left_join(subject_visits, by = c("subject_id", "visit"))
# A tibble: 5 × 4
subject_id visit value arm
<chr> <dbl> <dbl> <chr>
1 S01 1 12.4 A
2 S01 2 11.8 A
3 S02 1 13.1 B
4 <NA> 1 9.7 <NA>
5 S04 1 15.2 <NA>

只在 subject_id 上匹配会让 S01 的两条记录都撞上右表的同一行;加上 visit 之后 S01 的两次随访各归各的。第 4 行键为 NA,第 5 行的 S04 在右表里没有对应记录,left_join() 保留这两行并把 arm 填成缺失值。连接本身不报错,这两条记录要自己回头查。

纵向研究里 subject_id 单独看会重复(每个人有多条记录),subject_id + visit 才唯一。组合键的坑在于:每一个单独的列看起来都很正常,只有合起来才会暴露重复,所以检查唯一性时要把 count() 的参数写成 count(subject_id, visit)

semi_join 与 anti_join:只筛行,不加列

Section titled “semi_join 与 anti_join:只筛行,不加列”

这两个函数的输出列数和左表完全一致,它们只做筛选,不做合并。这一点经常被误解成「保留匹配的行并加列」,其实不是:

band_members |> semi_join(band_instruments, by = "name")
band_members |> anti_join(band_instruments, by = "name")
# A tibble: 2 × 2
name band
<chr> <chr>
1 John Beatles
2 Paul Beatles
# A tibble: 1 × 2
name band
<chr> <chr>
1 Mick Stones

semi_join() 还有一个容易被忽略的性质:即使右表里一个键对应多行,左表也只会留下一次,不会复制行。查多选题「至少选过 A 的人」时,用它不会出现重复受试者;换成 inner_join() 就会把人按选项数量复制好几遍。

anti_join() 是数据质量检查的常用工具:measures |> anti_join(subjects, by = "subject_id") 找出没有对应受试者的孤儿记录;反过来 subjects |> anti_join(measures, by = "subject_id") 找出一次都没来的人。做纵向研究时,这两条命令应该写在正式分析之前。

它还有一个日常用途是比较两版数据:收到导师发来的修订版表格后,新版 |> anti_join(旧版, by = c("id", "value")) 能列出新增的行,把两个 anti_join() 的结果并起来就是完整的差异清单。这种检查和 setdiff() 的效果接近,但 anti_join() 支持多列组合键,也能和管道接在一起,实际写起来短得多。代价是它对整行做比较,某一列有任何改动都会让这行同时出现在两个方向的差异里。

键的类型不一致。一边是字符、一边是数值(或一边是因子、一边是字符),dplyr 会直接报类型不兼容的错误,这时用 mutate(across(键, as.character)) 把两边对齐。读取数据时 read.csv() 默认把字符列读成因子,是这类问题的高发来源,建议加 stringsAsFactors = FALSE 或者改用 readr::read_csv()

NA 键会互相匹配。在 SQL 里 NULL 从不等于 NULL,所以键为空的记录连接时必然落空;R 的连接相反——两边的 NA 键默认被视为相等,会彼此配上。这类「幽灵行」在含缺失的 ID 列上很容易出现:某批样本的 subject_id 缺失,连接后它们会自己凑成一组。想按 SQL 的语义来,写 na_matches = "never";更稳妥的做法是连接前先决定这些行该不该参与分析。

非键的同名列会被加后缀。左右表都有一个叫 date 的列(但都不是键),输出里会变成 date.xdate.y,用 suffix = c("_左", "_右") 可以改成看得懂的名字。更省事的做法是在连接前先 select() 掉不需要的列。

连接后的行数要写进脚本。dplyr 1.1 起给连接函数加了 relationship 参数,把预期写进代码:left_join(x, y, by = "id", relationship = "many-to-one") 表示「右表的 id 必须唯一」,一旦不满足就直接报错,而不是安静地把行数放大。relationship 可取 "one-to-one""one-to-many""many-to-one""many-to-many";另一个参数 unmatched = "error" 要求两边必须完全匹配,用来防「字典表没覆盖全」这类问题。不用这两个参数的场合,退一步写 stopifnot(nrow(result) == nrow(left)) 也能挡住大部分事故。

分组状态会跟着传下去。如果左表是 group_by() 过的,连接结果仍然是分组数据框,分组变量是原左表的分组列。这种「隐式分组」在接完表立刻 summarise() 时会按组汇总,看着合理但常常不是你要的。接表之前先 ungroup() 更省心。

性能。dplyr 的连接会把整列复制一遍,几十万行的表感觉不到,上千万行时内存会明显吃紧。这种量级下的替代方案是 data.tablesetkey() + 连接语法,或者 arrow 包对 Parquet 文件做连接,两者都支持按键预排序,速度差一个数量级。真要处理这种数据,通常也意味着不该把全表读进 R 的工作内存。

连接是数据整理里最重的一步,接完通常就要进汇总或建模。如果你也用 Python,pandas 的 merge()join() 是同一族操作,可以对照 /python/pandas/merging 看写法差异;merge()how 参数对应 dplyr 的六种连接函数。