资讯动态

dplyr `join_by()` 连接规范 DSL 深度解析:等值、不等式、滚动与重叠连接的完整指南

发布时间:2026/10/14 19:48:37 来源:尧图企业网站定制
数据分析【免费下载链接】dplyrdplyr: A grammar of data manipulation项目地址https://gitcode.com/gh_mirrors/dp/dplyr点击查看免费下载join_by()是 dplyr 提供的一门用于描述表连接方式的迷你领域特定语言domain specific language它以表达式的方式声明左右两张表如何匹配既支持最常见的等值连接也支持不等式连接、、、、滚动连接closest()与重叠连接between()、within()、overlaps()并可直接作为任意*_join()函数的by参数使用。读完本文你将掌握join_by()的全部语法要素、列引用规则、底层解析与执行流水线并理解其打印输出与错误消息的完整行为契约以仓库中的快照测试为证能够在销售时间序列、基因组区段等真实场景中正确构造复杂连接。一、join_by()是什么连接规范的语法核心在 dplyr 中left_join()、right_join()、inner_join()、full_join()、semi_join()、anti_join()等连接函数都通过by参数来决定拿哪些列、按什么规则去匹配。早期by只能接受字符向量或命名向量仅支持等值匹配dplyr 在连接机制全面重构时见 NEWS.md 中关于 joins overhaul 的条目灵感来自 data.table 的 join 语法涉及 issue #5914、#5661、#5413、#2240引入了join_by()把by升级为可编程的规范对象。从源码看join_by()的核心实现位于 R/join-by.Rjoin_by - function(...) { exprs - enquos(..., .named NULL) exprs - map(exprs, quo_squash) ... for (i in seq_len(n)) { bys[[i]] - parse_join_by_expr(exprs[[i]], i, error_call error_call) } new_join_by( exprs exprs, condition condition, filter filter, x x, y y ) }它做了三件关键的事捕获原始表达式而不求值使用enquos()捕获...再用quo_squash()将 quosure 递归压平为纯表达式从而既能在用户环境中编程使用又不依赖运行时求值逐个解析每个表达式交给parse_join_by_expr()分类处理等值、不等式、closest()、between()、within()、overlaps()产出规范对象new_join_by()构造一个 class 为dplyr_join_by的列表包含exprs原始表达式、condition每个匹配条件对应的运算符、filter是否滚动及滚动方向、x与y两侧列名四组平行向量。参见 R/join-by.R。join_by()支持的连接类型?join_by见 man/join_by.Rd包括类型语法要素典型用途等值连接 Equality joins或直接写单列名最常见按键相等匹配不等式连接 Inequality joins、、、时间序列、基因组学滚动连接 Rolling joinsclosest(expr)无精确匹配时滚动取最近值重叠连接 Overlap joinsbetween()、within()、overlaps()区段落在区间内/包含/相交交叉连接 Cross joinscross_join()全组合由独立函数实现其中等值、不等式、滚动、重叠连接由join_by()表达式直接描述交叉连接通过cross_join()单独实现——这一点也体现在快照测试中join_by()不带任何表达式时会直接报错并提示改用cross_join()见 tests/testthat/_snaps/join-by.md 首条快照。二、等值连接与单列名简写等值连接要求左右两表在若干对列上取值相等。用join_by()表达等值连接有两种方式# 显式x 表的 a 列 与 y 表的 b 列相等 join_by(a b) # 简写左右两侧列名相同时只写一个名字即可 # join_by(x) 等价于 join_by(x x) join_by(x)多列同时匹配时在...中并列多个表达式即可例如join_by(a b, c d)表示x$a匹配y$b且x$c匹配y$d同名列可直接简写为join_by(a, c)。这一简写在源码中由parse_join_by_expr()处理当表达式是单个符号或字符串时自动改写为expr(!!expr !!expr)见 R/join-by.R。测试 tests/testthat/test-join-by.R 验证了等值条件的内部结构by - join_by(x y, a b) # by$x - c(x, a) # by$y - c(y, b) # by$condition- c(, ) # by$filter - c(none, none)列名既可以不带引号写成符号也可以写成字符串join_by(a, b c)两者解析结果完全一致。三、不等式连接、、、不等式连接按非等值条件匹配常用于某事件发生在另一个事件之前/之后这类时间序列问题以及基因组学的区间查询。用法与等值连接类似把换成不等式即可# 对每一行 sale_date找出所有早于该销售的 promo_date join_by(id, sale_date promo_date)join_by()按位置约定表归属条件左侧的列默认来自左表x右侧的列默认来自右表y。所以sale_date promo_date意为x$sale_date y$promo_date。⚠️ 需要注意不等式连接会把x中的一行匹配到y中潜在的大量行产生行数膨胀。官方文档man/join_by.Rd明确提醒Be extra careful when constructing inequality join specifications!。同时连接重构后keep默认值变为NULL对等值条件等价于keep FALSE对不等式条件等价于keep TRUE保留两侧键详见 NEWS.md。此外keep FALSE在存在非等值条件时是不允许的join_cols()会直接报错见 R/join-cols.R。四、滚动连接closest()滚动连接是不等式连接的变体它同样基于不等式但把结果限制为最近的匹配在精确匹配不存在时向前/向后滚动。用法是把一个不等式整体包进closest()# 对每个 x 值在 y 中找到小于等于该 x 值的最近值 join_by(closest(x y))关于closest()有三条关键语义内部不等式类型closest()的参数必须是一个使用、、、之一的不等式不允许等值无滚动意义源码在parse_join_by_closest()中专门拦截closest(a b)见 R/join-by.R主表固定为x无论不等式怎么书写closest()总是以左表x为主表、在右表y中寻找最近匹配。例如closest(y$a x$b)恒被解释为closest(x$b y$a)滚动方向由运算符决定/表示向前滚动取最大值/表示向后滚动取最小值。这在内部结构上体现为filter字段测试 test-join-by.R 断言closest(a b)的filter为maxclosest(a b)的filter为min对应源码 R/join-by.R 中的switch映射。closest()也可以与其他条件并列组合例如同时要求滚动匹配与一个下界不等式来自官方文档示例sales - mutate(sales, sale_date_lower sale_date - 1) by - join_by(id, closest(sale_date promo_date), sale_date_lower promo_date) full_join(sales, promos, by)五、重叠连接between()、within()、overlaps()重叠连接是不等式连接的特例涉及左表的一到两列区间与右表两列定义的区间之间的包含/相交关系。join_by()识别三个 helper它们在底层都会展开为更简单的不等式组合。5.1between(x, y_lower, y_upper, ..., bounds [])对左表的每个值x找出所有落在右表区间[y_lower, y_upper]内的行。默认等价于x y_lower, x y_upper。bounds参数控制上下界的开闭是否包含端点取值与展开结果对应如下源码parse_join_by_between()中的switch见 R/join-by.R测试见 test-join-by.Rbounds展开的不等式含义[]默认x y_lower, x y_upper闭区间两端都包含[)x y_lower, x y_upper左闭右开(]x y_lower, x y_upper左开右闭()x y_lower, x y_upper开区间两端都不包含5.2within(x_lower, x_upper, y_lower, y_upper)对左表的每个区间[x_lower, x_upper]找出所有完全包含它的右表区间[y_lower, y_upper]。恒等价于x_lower y_lower, x_upper y_upperwithin()没有bounds参数因为不管区间开闭展开结果的不等式都相同见 R/join-by.R。5.3overlaps(x_lower, x_upper, y_lower, y_upper, ..., bounds [])对左表的每个区间找出所有以任意方式与之相交的右表区间。默认等价于x_lower y_upper, x_upper y_lower。overlaps()的bounds语义与between()略有不同[]使用与其余三种[)、(]、()都使用与且生成的不等式完全相同。源码在parse_join_by_overlaps()中用一个closed布尔量统一处理见 R/join-by.R并在注释中说明这样设计是为了与between(bounds )的参数名保持一致、便于记忆issue #6504。这三个 helper 都要求区间良构且非空当按[]处理时需满足x_lower x_upper否则需满足x_lower x_upper见 man/join_by.Rd。六、列引用规则x$与y$显式指定表归属默认情况下join_by()按左侧列来自x、右侧列来自y的约定解析列名。但有时把条件两边的列写反更清晰例如希望左表引用右表的列这时可以用x$或y$前缀显式指定归属# 等价于默认写法x 的 a 列 与 y 的 b 列相等 join_by(a b) # 显式引用x 的 a 列 与 y 的 b 列相等语义相同写法更明确 join_by(x$a y$b) # 把右表列写在左边y 的 a 列 与 x 的 b 列相等 join_by(y$a x$b)显式引用会触发条件翻转当左侧是y$列而右侧是x$列时join_by()会把x/y列归位并反转运算符。源码中维护了一张翻转查找表见 R/join-by.Rlookup - c( , , , , )测试断言join_by(y$a x$a, y$a x$a, y$a x$a, y$a x$a, y$a x$a)的条件依次被归一化为c(, , , , )见 test-join-by.R。closest()、between()、within()、overlaps()内同样支持x$/y$引用且这些 helper 也接受命名参数写法如between(a, y_upper b, y_lower c)、overlaps(y_lower c, y_upper d, x_lower a, x_upper b)。显式引用必须在同一表达式内引用不同表二元表达式的左右两侧、between()的上下界对、within()/overlaps()的左右两侧上下界都不能来自同一张表否则会在解析阶段报错详见第八节。七、打印输出与内部结构快照测试确立的行为契约dplyr_join_by对象有专属的打印方法见 R/join-by.R以Join By:为标题每个表达式一行、以-前缀列出。快照测试 tests/testthat/_snaps/join-by.md 固定了这些输出例如join_by(a, b) # Join By: # - a # - b join_by(a a, b c) # Join By: # - a a # - b c join_by(a a, closest(b c), closest(d e)) # Join By: # - a a # - closest(b c) # - closest(d e)两点值得注意字符串列名保留引号打印join_by(a, b)打印为- a、- bjoin_by(a a, b c)打印为- b c与符号写法在输出上有区分命名空间前缀原样保留join_by(dplyr::between(x, left, right))打印为- dplyr::between(x, left, right)即dplyr::前缀会在打印时保留该能力在 #6838 引入见 NEWS.md但其内部展开的condition、filter、x、y与不带前缀的写法完全一致测试allows for namespaced helpers验证了这一点。八、错误消息体系几乎覆盖所有非法输入join_by()的解析器对非法输入非常敏感快照文件集中记录了全部错误契约。按类别梳理如下每类均可在源码中找到对应抛出点8.1 顶层调用错误输入错误消息要点源码位置join_by()空Must supply at least one expression.并提示改用cross_join()R/join-by.Rjoin_by(a b)Cant name join expressions.提示是否误用而非R/join-by.Rjoin_by(NULL)Expressions cant be empty.第 1 个表达式为空R/join-by.Rjoin_by(1)Each element of ... must be a single column name or a join by expression.R/join-by.Rjoin_by(foo(x y))、join_by(x y, x^y)、join_by(1())Expressions must use one of: , , , , , closest(), between(), overlaps(), or within().stop_invalid_top_expression()见 R/join-by.Rjoin_by(x 1 y)、join_by(x y 1)Expressions cant contain computed columns只能按名字或x$col/y$col引用列parse_join_by_name()见 R/join-by.R最后一条对应官方文档的明确指引join_by()不允许计算列例如join_by(sales_date - 40 promo_date)是非法的——必须先mutate()生成新列再引用见 man/join_by.Rd 与 NEWS.md。8.2$引用错误输入错误消息要点join_by(x$a)Cant use $ when specifying a single column name.join_by(z$a y$b)、join_by(x$a z$b)The left-hand side of a $ expression must be either x$ or y$.join_by((x 1)$y b)The left-hand side of a $ expression must be a symbol or string.join_by($(x) y)缺失 name 参数Expressions using $ cant contain missing arguments.8.3 同一表引用错误输入错误消息要点join_by(x$a x$b)、join_by(y$a b)、join_by(closest(x$a x$b))The left and right-hand sides of a binary expression must reference different tables.join_by(between(x$a, x$a, x$b))等Expressions containing between()/within()/overlaps() cant all reference the same table.join_by(between(a, x$a, y$b))Expressions containing between() must reference the same table for the lower and upper bounds.join_by(within(x$a, y$b, y$a, y$b))... must reference the same table for the left-hand side lower and upper bounds.join_by(overlaps(x$a, x$b, y$a, x$b))... must reference the same table for the right-hand side lower and upper bounds.8.4 缺失参数错误这类错误对被包装的join_by()尤其有用快照中的nicely catches required missing arguments when wrapped案例当fn - function(x, y) join_by({{x}} {{y}})只传入一个参数时能精确报出缺失的是哪一侧的哪个参数Error: ! Expressions using cant contain missing arguments. x Argument y is missing.同样的机制覆盖每个 helperbetween(x)报Argument y_lower is missingwithin(x)报Argument x_upper is missingoverlaps(x)报Argument x_upper is missingclosest()报Argument expr is missing。实现上由check_missing_arg()统一处理见 R/join-by.R。8.5closest()专属错误join_by(closest(a b, 1))多余参数 →unused argument (1)closest()只接受一个参数join_by(closest(a b))The expression used in closest() cant use .join_by(closest(x))、join_by(closest(1))The first argument of closest() must be an expression.join_by(closest(x y))The expression used in closest() must use one of: , , , or .见stop_join_by_closest_invalid_expression()R/join-by.R。8.6bounds与多余...错误between()与overlaps()的bounds必须由check_bounds()校验R/join-by.R取值范围仅[]、[)、(]、()四种join_by(between(x, lower, upper, bounds 1)) # Error: # ! bounds must be a string or character vector. join_by(between(x, lower, upper, bounds a)) # Error: # ! bounds must be one of [], [), (], or (), not a.两个 helper 的...是为未来扩展预留的当前必须为空check_join_by_dots_empty()R/join-by.Rjoin_by(between(x, lower, upper, foo 1)) # Error: # ! ... must be empty. # i Non-empty dots were detected inside between().8.7 命名空间与by类型错误join_by(dplyrr::between(x, left, right))Expressions can only be namespace prefixed with dplyr::.只允许dplyr::前缀见stop_invalid_namespaced_expression()R/join-by.Ras_join_by(FALSE)by must be a (named) character vector, list, join_by() result, or NULL, not FALSE.as_join_by.default()R/join-by.R。九、编程友好性quosure、{{ }}拥抱与命名空间写法尽管join_by()基于纯表达式、不评估但它专门支持整洁求值tidy evaluation的编程手法。源码注释说明R/join-by.R先enquos()捕获 quosure再quo_squash()递归压平从而允许{{ }}拥抱注入。测试 test-join-by.R 验证了多种场景fn - function(x) { join_by({{ x }} y) } fn(foo) # - join_by(foo y) fn(a) # 表达式替换而非 quosure 求值仍是 join_by(a y) fn(!!a) # 可用 !! 内联求值得到 join_by(foo y) # 也支持在 between() 中拥抱注入 fn - function(x, top) { join_by(between({{ x }}, lower, {{ top }})) } fn(x, y) # - join_by(between(x, lower, y))因此可以放心地把join_by()封装进自定义函数、用...透传并通过{{ }}注入列名或整个表达式。从 #6838 起helper 还可以写成dplyr::between(...)、dplyr::within(...)、dplyr::overlaps(...)、dplyr::closest(...)的命名空间形式便于在调用环境中避免名称冲突。十、by参数的其他形态与join_by_common()join_by()结果只是by的合法形态之一。从as_join_by()泛型R/join-by.R可以看到by支持四种输入并被统一归一化为dplyr_join_by对象by形态说明join_by()结果直接透传字符向量未命名元素按两表同名处理by c(a, b)命名元素按x名 y名处理by c(x_a y_a)list含x、y两个字符向量编程式指定两侧列名NULL自然连接自动取两表公共列并打印Joining with by join_by(...)消息NULL时由join_by_common()R/join-by.R自动求两表列名交集并构造等值规范快照记录了其消息输出by - join_by_common(c(x, y), c(x, y)) # Message # Joining with by join_by(x, y)对于需要反引号包裹的特殊列名如_x、foo bar消息会自动加反引号Joining with by join_by(_x, foo bar)。若两表没有任何公共列则报错并要求显式提供by或使用cross_join()快照末尾的join_by_common()错误案例。十一、连接执行流水线从规范到行匹配一条join_by()规范从构造到真正执行匹配会依次经过以下环节结合源码调用链构造join_by()解析表达式产出dplyr_join_byexprs/condition/filter/x/y归一化*_join()内部把任意形态的by交给as_join_by()NULL时先走join_by_common()统一为dplyr_join_by列定位join_cols()R/join-cols.R校验列存在性、唯一性根据condition区分等值键与非等值键决定输出列与keep/后缀处理非等值条件下keep FALSE被禁止行匹配join_rows()R/join-rows.R把x、y键列与condition、filter一起交给vctrs::vec_locate_matches()由底层 C 实现完成匹配、滚动过滤与multiple/unmatched/relationship策略处理重组输出按匹配到的行索引从两表取数并拼接。从源码注释可见condition与filter正是驱动底层匹配的两组参数condition取、、、、之一filter取none普通匹配、max向前滚动取最大、min向后滚动取最小。compute_join_relationship()还根据condition决定是否检查 many-to-many 关系只要存在任何非等值条件就不做 many-to-many 检查因为不等式与重叠连接天然容易产生多对多见 R/join-rows.R——这也是官方文档提醒构造不等式连接要格外小心的底层原因。十二、实战示例滚动连接与重叠连接12.1 销售与促销从等值到滚动官方文档man/join_by.Rd用销售/促销数据完整演示了三层递进sales - tibble( id c(1L, 1L, 1L, 2L, 2L), sale_date as.Date(c(2018-12-31, 2019-01-02, 2019-01-05, 2019-01-04, 2019-01-01)) ) promos - tibble( id c(1L, 1L, 2L), promo_date as.Date(c(2019-01-01, 2019-01-05, 2019-01-02)) ) # 第 1 层等值连接——id 匹配 id销售日期匹配促销日期 by - join_by(id, sale_date promo_date) left_join(sales, promos, by) # 第 2 层不等式连接——每个销售找所有早于它的促销 by - join_by(id, sale_date promo_date) left_join(sales, promos, by) # 第 3 层滚动连接——每个销售只找最近一次早于它的促销 by - join_by(id, closest(sale_date promo_date)) left_join(sales, promos, by) # 若不允许精确匹配日期的促销同日不算改用 by - join_by(id, closest(sale_date promo_date)) left_join(sales, promos, by)注意第四步由于促销日期2019-01-05与销售日期同一天改用后它不再被视为最近匹配。这正是closest()中与差异的直接体现。12.2 基因组区段between()、within()、overlaps()与bounds用区段数据演示三种重叠语义的区别segments - tibble( segment_id 1:4, chromosome c(chr1, chr2, chr2, chr1), start c(140, 210, 380, 230), end c(150, 240, 415, 280) ) reference - tibble( reference_id 1:4, chromosome c(chr1, chr1, chr2, chr2), start c(100, 200, 300, 415), end c(150, 250, 399, 450) ) # 点的落入segment 的 start 是否落在 reference 的 [start, end] 内 by - join_by(chromosome, between(start, start, end)) full_join(segments, reference, by) # 完整包含segment 区间完全落在 reference 区间内 by - join_by(chromosome, within(x$start, x$end, y$start, y$end)) inner_join(segments, reference, by) # 任意相交segment 区间与 reference 区间有任何重叠即匹配 by - join_by(chromosome, overlaps(x$start, x$end, y$start, y$end)) full_join(segments, reference, by) # 右开区间 [)415 的端值不再与 415 的起始值重叠 by - join_by(chromosome, overlaps(x$start, x$end, y$start, y$end, bounds [))) full_join(segments, reference, by)这里特别展示了x$/y$显式引用的价值在within()与overlaps()中显式写出x$start、y$start等可以让表归属一目了然官方文档称之为makes your intentions clearerbounds [)则模拟了常见的半开区间右端不包含的生物学区间约定。十三、注意事项与最佳实践不要使用计算列join_by()只接受列名 运算符 列名的结构join_by(sales_date - 40 promo_date)会报Expressions cant contain computed columns。正确做法是先用mutate()生成sales_date_lower这类列再参与连接见 man/join_by.Rd 与 NEWS.md警惕不等式连接的行数膨胀一个x行可能匹配y的大量行构造条件前先想清楚基数关系非等值条件下keep FALSE不可用且不会触发 many-to-many 警告需要自行把握closest()恒以x为主表滚动语义始终是在y中找x的最近匹配需要换主表时把对应的表放在x位置bounds只接受四种取值[]、[)、(]、()并假设区间良构闭区间要求lower upper开区间要求lower upper生产中显式提供by省略by会触发自动公共列检测并打印消息显式by join_by(...)是官方推荐的规范写法见 R/join.R 中by参数文档复用与封装join_by()支持{{ }}拥抱与...透传适合封装进自定义分析函数helper 可加dplyr::前缀避免名称冲突#6838。十四、延伸阅读R/join-by.Rjoin_by()完整实现包括所有解析函数与错误抛出点man/join_by.Rd?join_by官方文档与全部示例tests/testthat/_snaps/join-by.md打印输出与错误消息的行为契约快照tests/testthat/test-join-by.R结构断言、条件展开与编程用法测试R/join-rows.R 与 R/join-cols.R行匹配与列组织的底层执行R/join-common-by.Rby NULL时自动公共列检测NEWS.mdjoin_by()引入与连接重构的历史背景#5914、#5661、#5413、#2240、#6838 等赞分享数据分析【免费下载链接】dplyrdplyr: A grammar of data manipulation项目地址https://gitcode.com/gh_mirrors/dp/dplyr点击查看免费下载相关推荐Polars 连接Join完全指南等值连接、连接策略、非等值连接与 Asof 连接的实操详解Polars 连接Join完全指南等值连接、连接策略、非等值连接与 Asof 连接的实操详解 在 Polars 中连接join是把一个或多个 Dat数据分析大数据cuDF libcudf Column Join API 完全指南从等值连接到条件连接的 GPU 连接算子cuDF libcudf Column Join API 完全指南从等值连接到条件连接的 GPU 连接算子 导读 本文围绕 cuDFGPU DataFram数据分析数据工程机器学习Apache Beam Schema 连接Join模式实战基于 Row 类型系统的等值连接指南Apache Beam Schema 连接Join模式实战基于 Row 类型系统的等值连接指南 本文围绕 Apache Beam 官方文档中的 Schem大数据批处理流处理数据工程上一篇微信聊天记录永久保存终极指南3步打造你的个人数字记忆库下一篇压测数据秒变可视化大屏wrkGrafana打造性能监控仪表盘创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑