R 数据框连接:left_join、inner_join 等六种连接完全指南
实验数据和分析数据很少长在同一张表里:受试者信息在 subjects,每次测量的结果在 measures,中间靠一个 subject_id 串起来。把两张表按这个共同的列拼起来,就是连接(join)。连接是整个数据整理环节里最容易出错的步骤——不报错,只是结果行数悄悄变了,后面所有统计量跟着错。
键与三种对应关系
Section titled “键与三种对应关系”连接依据的列叫键(key)。左表里能唯一标识一行的叫主键(primary key),右表里引用它的叫外键(foreign key)。按两边的唯一性,连接分成三种:
| 关系 | 含义 | 例子 |
|---|---|---|
| 一对一 | 两边键都唯一 | 学号 ↔ 身份证号 |
| 一对多 | 左表键唯一,右表可重复 | 受试者 ↔ 多次测量记录 |
| 多对多 | 两边键都可重复 | 会笛卡尔积,通常是你出错了 |
六种连接的差别只在「保留哪些行」:
| 函数 | 保留的行 | 常见用途 |
|---|---|---|
inner_join() |
两边都匹配上的 | 只要完整观测 |
left_join() |
左表全部 | 给主表补充信息(最常用) |
right_join() |
右表全部 | 少见,交换参数即可 |
full_join() |
两边全部 | 核对两份名单的差异 |
semi_join() |
左表中能匹配上的行 | 筛选,不加列 |
anti_join() |
左表中匹配不上的行 | 查缺失、查孤儿记录 |
连接前的三项检查
Section titled “连接前的三项检查”连接出错基本都出在键上,动手写 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() 就能做完,比事后核对结果行数便宜得多。
一对一:inner_join 与 left_join
Section titled “一对一:inner_join 与 left_join”dplyr 自带两个小数据集正好用来演示,不用手工造数据:
library(dplyr)
# 乐队成员与各自会的乐器,name 是共同的键band_membersband_instruments# A tibble: 3 × 2 name band <chr> <chr>1 Mick Stones2 John Beatles3 Paul Beatles
# A tibble: 3 × 2 name plays <chr> <chr>1 John guitar2 Paul bass3 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 guitar2 Paul Beatles bassleft_join() 换成「以左表为准」,Mick 保留下来,缺少的乐器填 NA:
band_members |> left_join(band_instruments, by = "name")# A tibble: 3 × 3 name band plays <chr> <chr> <chr>1 Mick Stones NA2 John Beatles guitar3 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 NA2 John Beatles guitar3 Paul Beatles bass4 Keith NA guitar四行结果里,Mick 和 Keith 各缺一半——这类「两边都有人缺」的行,正是 full_join() 存在的理由。实际场景是核对两份名单:伦理审查批件上的受试者名单和数据库里的入组记录,用 full_join() 接一次,NA 出现在哪一侧就说明谁多了谁少了。比起写两个 setdiff() 再手动比对,一次连接能同时看到缺失和多余两个方向,也不用担心中文姓名排序的差异。
不过 full_join() 的行数最容易失控:两边键都有重复时,它同样会做笛卡尔积。核对名单时如果只关心「谁在谁不在」,用 anti_join() 跑两次比 full_join() 更安全。
行数为什么会变
Section titled “行数为什么会变”这是连接最需要盯的地方。用 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] 32distinct(cyl, .keep_all = TRUE) 保留每个 cyl 第一次出现的行。更稳妥的做法是先想清楚「每个 cyl 对应的那个值」到底该怎么算——取第一辆车的马力没有统计学意义,通常应该先 summarise(mean_hp = mean(hp)) 再连接。
如果 inner_join() 之后行数比左表还少,说明左表里有匹配不上的键,用 anti_join() 一查就知道是哪些:
a |> anti_join(b_unique, by = "cyl") |> nrow()by = join_by() 的几种写法
Section titled “by = join_by() 的几种写法”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 12 0.5 23 1 2dose_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.82 S02 20 22 13.1S01 有两次测量(第 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 A2 S01 2 11.8 A3 S02 1 13.1 B4 <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 Beatles2 Paul Beatles
# A tibble: 1 × 2 name band <chr> <chr>1 Mick Stonessemi_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() 支持多列组合键,也能和管道接在一起,实际写起来短得多。代价是它对整行做比较,某一列有任何改动都会让这行同时出现在两个方向的差异里。
连接时的常见问题
Section titled “连接时的常见问题”键的类型不一致。一边是字符、一边是数值(或一边是因子、一边是字符),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.x 和 date.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.table 的 setkey() + 连接语法,或者 arrow 包对 Parquet 文件做连接,两者都支持按键预排序,速度差一个数量级。真要处理这种数据,通常也意味着不该把全表读进 R 的工作内存。
连接是数据整理里最重的一步,接完通常就要进汇总或建模。如果你也用 Python,pandas 的 merge()、join() 是同一族操作,可以对照 /python/pandas/merging 看写法差异;merge() 的 how 参数对应 dplyr 的六种连接函数。