news 2026/10/13 8:41:56

dplyr 采样函数剖析:sample_n()/sample_frac() 的加权抽样、分组行为与错误信息链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
dplyr 采样函数剖析:sample_n()/sample_frac() 的加权抽样、分组行为与错误信息链
  • 数据分析

【免费下载链接】dplyr

dplyr: A grammar of data manipulation

项目地址:https://gitcode.com/gh_mirrors/dp/dplyr
点击查看免费下载

本篇指南围绕 dplyr 中sample_n()/sample_frac()两个(已被slice_sample()取代的)采样动词展开,完整解析其参数语义、加权抽样的底层实现、分组场景下的逐组行为,并逐条还原其 8 个典型错误场景的消息链路。读完后,你将能够解释Can't compute indices.这类错误是如何从slice()的错误处理机制中生成的,并能将旧代码平滑迁移到slice_sample()。

1. 定位:两个被标记为 superseded 的采样动词

sample_n()与sample_frac()用于从数据框中随机抽取固定行数(sample_n())或固定比例(sample_frac())的行。二者的 roxygen 文档头部带有lifecycle::badge("superseded")标记(见 R/sample.R),官方立场是:

sample_n()andsample_frac()have been superseded in favour ofslice_sample(). While they will not be deprecated in the near future, retirement means that we will only perform critical bug fixes, so we recommend moving to the newer alternative.

文档同时给出了取代的具体动机(R/sample.R):

  • 与其用两个函数分别表达“按行数采样”和“按比例采样”,不如在一个函数里用两个互斥参数(n/prop)表达;
  • 旧函数与slice()的关联不明显;
  • 第一个参数名tbl与其他单表动词使用的.data不一致;
  • size参数使用了 tidy evaluation,行为意外且未被文档说明;
  • 已废弃的.env参数得以彻底移除;
  • ...的所在位置不理想。

NEWS.md也印证了这一状态:sample_n()andsample_frac()have been superseded byslice_sample()(见 NEWS.md)。每次调用这两个函数时,源码会主动发出 lifecycle 信号(lifecycle::signal_stage("superseded", "sample_n()"),见 R/sample.R 与 R/sample.R)。但作为 CRAN 历史代码中大量存在的 API,理解它们的行为与错误语义仍然重要——这正是本文主体,即快照测试文件 tests/testthat/_snaps/sample.md 所固化的内容。

2. 函数签名与参数

两个函数的完整签名(R/sample.R、R/sample.R):

sample_n <- function( tbl, size, replace = FALSE, weight = NULL, .env = NULL, ... ) sample_frac <- function( tbl, size = 1, replace = FALSE, weight = NULL, .env = NULL, ... )
参数说明
tbldata frame 输入。非 data frame 时落入默认方法并报错(见第 4.3 节)。
size对sample_n()是要抽取的行数;对sample_frac()是要抽取的比例(默认1)。若tbl已分组,size对每个分组生效。支持n()等数据掩码函数(如sample_n(df, n() - 2))。
replace是否放回抽样,默认FALSE。为TRUE时允许size超过数据/分组规模。
weight抽样权重,必须求值为与输入等长的非负数值向量;权重会被自动标准化为和为 1。注意:它被直接传入 base Rsample.int()的prob参数。
.env已废弃(DEPRECATED),不再有实际效果。传入时会打印提醒:`sample_n()` argument `.env` is deprecated and no longer has any effect.(见 R/sample.R)。
...被忽略(ignored)。

size与weight在数据框方法里通过enquo()捕获(R/sample.R),因此可以在表达式中引用数据列与n(),而不是普通的求值——这也是文档中提到的“sizeargument uses tidy evaluation”的历史设计。

3. 执行链路:从 S3 分发到 sample.int()

3.1 S3 分发与类型检查

sample_n()是泛型(UseMethod("sample_n")),对 data frame 分派到sample_n.data.frame;其他一切输入(如空列表)落入sample_n.default。默认方法只做一件事——中止并报类型错误(R/sample.R):

sample_n.default <- function(tbl, size, replace = FALSE, weight = NULL, .env = parent.frame(), ...) { msg <- glue("`tbl` must be a data frame, not {obj_type_friendly(tbl)}.") abort(msg) }

sample_frac.default(R/sample.R)同理。这正是第 4.3 节快照中`tbl` must be a data frame, not an empty list.的直接来源。

3.2 data.frame 方法:slice() + local() 数据掩码

sample_n.data.frame的核心实现只有几行(R/sample.R):

sample_n.data.frame <- function(tbl, size, replace = FALSE, weight = NULL, .env = NULL, ...) { if (!is_null(.env)) { inform("`sample_n()` argument `.env` is deprecated and no longer has any effect.") } size <- enquo(size) weight <- enquo(weight) dplyr_local_error_call() slice( tbl, local({ size <- check_size(!!size, n(), replace = replace) sample.int(n(), size, replace = replace, prob = !!weight) }) ) }

要点:

  1. 采样被实现为slice()的特例:索引表达式放在local()中,由slice()的 data mask 逐组求值。因此采样天然具备分组语义——对 grouped data frame,表达式在每个分组内独立执行,n()返回当前分组的行数。
  2. check_size()先做规模校验:无放回且size > n()时中止;
  3. 真正的随机性来自 base Rsample.int(n(), size, replace, prob = !!weight):权重向量被展开为prob。base R 对概率向量的合法性校验(如非零权重数量不足)产生的错误会沿调用链上抛,成为第 4 节快照里Caused by error in sample.int(): ! too few positive probabilities的源头。

sample_frac.data.frame(R/sample.R)结构完全一致,只是规模换算方式不同:

size <- round(n() * check_frac(!!size, replace = replace)) sample.int(n(), size, replace = replace, prob = !!weight)

即先把比例通过check_frac()校验,再乘以当前(组)行数并四舍五入取整。

3.3 check_size() 与 check_frac() 的校验规则

两个辅助函数定义了“无放回时规模上限”的报错逻辑(R/sample.R):

check_size <- function(size, n, replace = FALSE) { if (size <= n || replace) { return(invisible(size)) } bullets <- c( glue("`size` must be less than or equal to {n} (size of data)."), i = "set `replace = TRUE` to use sampling with replacement." ) abort(bullets, call = NULL) } check_frac <- function(size, replace = FALSE) { if (size <= 1 || replace) { return(invisible(size)) } bullets <- c( "`size` of sampled fraction must be less or equal to one.", i = "set `replace = TRUE` to use sampling with replacement." ) abort(bullets, call = NULL) }

注意两条消息中都附带了i提示:setreplace = TRUEto use sampling with replacement.——这与第 4 节快照输出完全对应。replace = TRUE时两个校验直接放行,因为放回抽样不受池子大小的限制。

4. 快照测试:8 个错误场景逐条解析

测试入口是 tests/testthat/test-sample.R 中名为"sample_*() gives meaningful error messages"的expect_snapshot()块,其固化输出即 tests/testthat/_snaps/sample.md。以下按测试文件中的注释分组,完整还原快照内容。

4.1 base R 错误消息(分组 + 权重)

构造的数据是x = rep(1:2, 100)、y = rep(c(0, 1), 100)、g = rep(1:2, each = 100),即两个分组各 100 行,每组内y恰好一半为 0、一半为 1:

df2 <- tibble(x = rep(1:2, 100), y = rep(c(0, 1), 100), g = rep(1:2, each = 100)) grp <- group_by(df2, g) (expect_error(sample_n(grp, nrow(df2) / 2, weight = y)))

输出:

<error/rlang_error> Error in `sample_n()`: ! Can't compute indices. i In group 1: `g = 1`. Caused by error in `sample.int()`: ! too few positive probabilities

机制:size = 100对每个分组生效;组内 100 行中只有 50 行权重非零。无放回抽样要求“被抽行数 ≤ 非零权重行数”,100 > 50,于是 base Rsample.int()报too few positive probabilities。该错误在slice()的索引求值阶段被捕获、重新包装(见第 5 节),并附加了出错分组的定位信息In group 1:g = 1``。

同类地:

(expect_error(sample_frac(grp, 1, weight = y)))

输出:

<error/rlang_error> Error in `sample_frac()`: ! Can't compute indices. i In group 1: `g = 1`. Caused by error in `sample.int()`: ! too few positive probabilities

sample_frac(grp, 1)在每组内换算为round(100 * 1) = 100行,同样撞上非零权重不足的问题。

4.2 size 超过分组规模(无放回)

(expect_error(sample_n(group_by(mtcars, cyl), 10)))

输出:

<error/rlang_error> Error in `sample_n()`: ! Can't compute indices. i In group 2: `cyl = 6`. Caused by error: ! `size` must be less than or equal to 7 (size of data). i set `replace = TRUE` to use sampling with replacement.

mtcars按cyl分组后,cyl = 6的分组只有 7 行(cyl = 4有 14 行、cyl = 8有 11 行)。check_size()在第一个不满足size <= n的分组(这里是第 2 组cyl = 6)处中止,消息中{n}被插值为 7。这条错误由 dplyr 自身抛出(Caused by error:下没有具体函数名),而非 base R。

4.3 非 data frame 输入

(expect_error(sample_n(list()))) (expect_error(sample_frac(list())))

两者输出完全对称:

<error/rlang_error> Error in `sample_n()`: ! `tbl` must be a data frame, not an empty list.
<error/rlang_error> Error in `sample_frac()`: ! `tbl` must be a data frame, not an empty list.

如第 3.1 节所述,list()无法分派到.data.frame方法,走.default方法直接abort()。注意错误顶部标注的是Error insample_n()``——这是dplyr_local_error_call()把报错调用定位到泛型入口的结果,而不是定位到内部默认方法。

4.4 未分组场景下的三类错误

# respects weight df <- data.frame(x = 1:2, y = c(0, 1)) (expect_error(sample_n(df, 2, weight = y)))

输出:

<error/rlang_error> Error in `sample_n()`: ! Can't compute indices. Caused by error in `sample.int()`: ! too few positive probabilities

df只有 2 行且未分组,size = 2但y = c(0, 1)只有一个非零权重,故sample.int()报错。注意未分组场景下没有In group ...一行——分组定位信息只在有活跃分组上下文时出现(实现见 R/conditions.R 的cnd_bullet_cur_group_label(),无分组时返回空)。

(expect_error(sample_frac(df, 2)))

输出:

<error/rlang_error> Error in `sample_frac()`: ! Can't compute indices. Caused by error: ! `size` of sampled fraction must be less or equal to one. i set `replace = TRUE` to use sampling with replacement.

比例2 > 1且replace = FALSE,被check_frac()拦下。

(expect_error(sample_frac(group_by(df, y), 2)))

输出:

<error/rlang_error> Error in `sample_frac()`: ! Can't compute indices. i In group 1: `y = 0`. Caused by error: ! `size` of sampled fraction must be less or equal to one. i set `replace = TRUE` to use sampling with replacement.

同一check_frac()错误发生在分组上下文中,因此多了In group 1:y = 0`` 的定位行。对比上一个未分组用例,可以清晰看出分组标签是错误链的可选组成部分。

(expect_error(sample_frac(df, 1, weight = y)))

输出:

<error/rlang_error> Error in `sample_frac()`: ! Can't compute indices. Caused by error in `sample.int()`: ! too few positive probabilities

size = 1比例合法(check_frac通过),换算为round(2 * 1) = 2行,但 2 行里只有 1 个非零权重,仍由sample.int()拒绝。

4.5 快照揭示的错误消息结构

把 8 条输出放在一起,可以归纳出sample_*()错误链的三段式结构:

  1. 顶层调用行:Error insample_n()/Error in `sample_frac()`` —— 始终指向用户直接调用的泛型入口;
  2. slice() 包装层:! Can't compute indices.(索引计算失败的统一描述),有分组上下文时追加i In group N:key = value.;
  3. 根因层(Caused by):三类来源——
    • Caused by error insample.int(): ! too few positive probabilities(base R 权重校验);
    • Caused by error: !sizemust be less than or equal to 7 (size of data).(dplyr 的check_size());
    • Caused by error: !sizeof sampled fraction must be less or equal to one.(dplyr 的check_frac());
    • 以及不走包装、直接abort()的`tbl` must be a data frame, not an empty list.(类型检查发生在进入slice()之前)。

每一类都附带可操作的修复提示(i setreplace = TRUE` ... 或分组定位),这正是该快照测试守护的“meaningful error messages”承诺。

5. 错误链的源码出处:slice() 的索引求值与分组标签

第 4 节中Can't compute indices.与In group ...并非sample_*()自己生成的,而是来自其底层的slice()错误处理。调用链为:sample_n()→slice()→slice_rows()→slice_eval()/slice_combine()(R/slice.R)。

slice_eval()用withCallingHandlers()包裹索引表达式的求值(R/slice.R):

withCallingHandlers( mask$eval_all(quo(impl(!!!dots))), error = function(cnd) { if (inherits(cnd, "vctrs_error_subscript")) { action <- "process" } else { action <- "compute" } if (index && is_slice_call(error_call)) { local_error_context(dots, index, mask = mask) header <- cnd_bullet_header(action) } else { header <- glue("Can't {action} indices.") } bullets <- c(header, i = cnd_bullet_cur_group_label()) abort(bullets, call = error_call, parent = cnd) } )
  • 非下标错误(如sample.int()的概率错误)归入"compute",生成头语Can't compute indices.;
  • abort(bullets, call = error_call, parent = cnd)把原始条件设为parent——这就是快照里Caused by error insample.int(): ...这种“父错误链”的由来;
  • cnd_bullet_cur_group_label()(R/conditions.R)从当前分组掩码读出分组标签,仅当存在分组时返回In {label}.,否则为空,解释了 4.4 节中分组/未分组输出的差异。

另一处兜底在slice_combine()(R/slice.R):合并各分组索引时若num_as_location()等定位转换出错,同样以"Can't compute indices."为头语、mask$set_current_group(group)还原分组上下文后中止。

而“分组逐组求值”的行为本身也有测试保障:tests/testthat/test-sample.R 验证mtcars |> group_by(cyl) |> sample_n(2)输出仍是grouped_df、每组恰好 2 行;tests/testthat/test-sample.R(issue #3413)验证size中可以使用n(),如sample_n(gdf, n() - 2L)在 3 组场景下返回总行数减 4;tests/testthat/test-sample.R(issue #1438)则保证filter()出空分组后再sample_n()不会在无行的组上求值表达式。

6. 迁移到 slice_sample():参数映射与行为差异

sample_n()/sample_frac()的 roxygen 文档自带官方迁移对照(R/sample.R):

df <- tibble(x = 1:5, w = c(0.1, 0.1, 0.1, 2, 2)) # sample_n() -> slice_sample() # Was: sample_n(df, 3) sample_n(df, 10, replace = TRUE) sample_n(df, 3, weight = w) # Now: slice_sample(df, n = 3) slice_sample(df, n = 10, replace = TRUE) slice_sample(df, n = 3, weight_by = w) # sample_frac() -> slice_sample() # Was: sample_frac(df, 0.25) sample_frac(df, 2, replace = TRUE) # Now: slice_sample(df, prop = 0.25) slice_sample(df, prop = 2, replace = TRUE)
sample_n()/sample_frac()slice_sample()说明
第一参数tbl.data命名与其他单表动词一致
size(行数)n互斥参数二选一
size(比例)prop与n互斥
weightweight_by同样传入sample.int()的prob,自动标准化
replacereplace语义一致

实现上,slice_sample.data.frame(R/slice.R)复用同一套slice()机制,仅规模计算换成get_slice_size(n, prop, allow_outsize = replace),采样委托给sample_int()(R/slice.R):

sample_int <- function(n, size, replace = FALSE, wt = NULL) { if (size == 0L) { integer(0) } else { sample.int(n, size, prob = wt, replace = replace) } }

一个关键行为差异值得注意:sample_n()在n超过(分)组规模时报错,而slice_sample()会静默截断到组内可用行数,与slice_head()等其它 slice 辅助函数保持一致(get_slice_size()在无放回时用clamp()把规模压到[0, n],见 R/slice.R;文档注释见 R/sample.R 与 R/slice.R)。即:

try(sample_n(df, 10)) # 报错 slice_sample(df, n = 10) # 返回全部 5 行,不报错

因此迁移时如果依赖“超采样会报错”的语义,需要自行加校验。另注意slice_sample()的weight_by与sample_*()的weight一样只是抽样概率权重,不能用于从样本重建总体统计量(文档 Details 部分有说明,见 R/slice.R)。

7. 测试方法论:为什么用 expect_snapshot() 固化错误消息

上述错误消息的精确文案由 tests/testthat/test-sample.R 的expect_snapshot()守护,快照存于 tests/testthat/_snaps/sample.md。这种写法适合错误消息回归测试的原因在于:

  • 测试体只写“这段代码应当报错”(expect_error(...)),报错文案本身作为快照输出被逐字固化,包括!/i项目符号、分组定位行与Caused by链;
  • 一旦上游改动(例如check_size()措辞调整、slice()包装层重构、base R 升级导致sample.int()消息变化)导致输出漂移,test_sample.R会失败并提示更新快照,从而强制开发者审查错误消息的每次变更;
  • 测试中用(expect_error(...))加括号的写法,是expect_snapshot()惯用技巧——把每个expect_error()包裹成独立表达式,使快照为每次报错单独生成一个Code/Output段落,而不是混在同一个输出块里。

该快照文件与R/sample.R中的check_size()/check_frac()报错文案、R/slice.R的错误包装逻辑、R/conditions.R的分组标签函数共同构成了一条完整的“实现—文档—测试”证据链:修改任何一环,另一环都会以测试失败的形式暴露不一致。

8. 实践要点小结

  • 新代码优先用slice_sample():参数为n/prop(互斥)、weight_by、replace;对超组规模请求采取截断而非报错;支持负值n/prop(表示从组大小中减去)。
  • 维护旧代码时,sample_n()/sample_frac()仍可用且仅接受关键 bug 修复;出现supersededlifecycle 信号属正常现象,可按第 6 节映射表批量改写。
  • 调试too few positive probabilities:检查权重向量在(每个)分组内非零值的数量是否足以支撑无放回抽样规模;要么提高非零权重覆盖,要么设置replace = TRUE。
  • 调试size must be less than or equal to N:size在分组数据中按组生效,报错的N是出错组的行数;i In group ...行可直接定位是哪个键值组合触发了限制。
  • 理解错误链:顶层Error insample_n()`` →Can't compute indices.(slice 包装层,可含分组定位)→Caused by(根因),三层信息分别由dplyr_local_error_call()、slice_eval()/slice_combine()的withCallingHandlers()与parent = cnd链式机制生成,源码位置见 R/slice.R 与 R/slice.R。

关键文件索引

文件内容
R/sample.Rsample_n()/sample_frac()泛型、默认方法、data frame 方法、check_size()/check_frac()
R/slice.Rslice()错误处理(slice_eval/slice_combine)、slice_sample()及其实现
R/conditions.R分组错误标签cnd_bullet_cur_group_label()、cnd_bullet_header()
tests/testthat/test-sample.R采样行为与错误消息快照测试
tests/testthat/_snaps/sample.md本文剖析的 8 条错误消息快照
NEWS.mdslice_sample()取代sample_n()/sample_frac()的变更记录
  • 数据分析

【免费下载链接】dplyr

dplyr: A grammar of data manipulation

项目地址:https://gitcode.com/gh_mirrors/dp/dplyr
点击查看免费下载

相关推荐

上一篇:Unison Transcript 信息字符串(info string)解析机制与错误诊断:从 `info-string-parse-error` 出发的源码级解读
下一篇:下载的视频打不开?res-downloader 视频解密操作与排错手册

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 8:41:33

AI编程核心基本功:超越Prompt的工程实践指南

1. 被误解的“基本功”&#xff1a;为什么 Prompt 不是终点 这两年跟不少同行聊 AI 编程&#xff0c;发现一个挺普遍的现象&#xff1a;很多人把“会写 Prompt”当成了核心竞争力&#xff0c;甚至觉得只要提示词写得够花哨&#xff0c;AI 就能自动把活干完。我一开始也这么想过…

作者头像 李华
网站建设 2026/10/13 8:41:24

脊柱手术机器人核心拆解:神经规避与紧急制动设计

简介&#xff1a;这是一份面向脊柱手术机器人研发与安全设计领域的系统技术文档&#xff0c;聚焦术中神经规避算法与机械臂紧急制动两大核心安全策略&#xff0c;适合机器人算法工程师、医疗器械研发人员及手术机器人方向研究者阅读。资源以单个PDF文件提供&#xff0c;压缩包大…

作者头像 李华
网站建设 2026/10/13 8:41:20

SQL Server+Quartz集群持久化实战:解决任务重复/漏执行

1. 项目概述&#xff1a;为什么SQL Server Quartz集群不是“加个库”那么简单Quartz 是 Java 生态中事实标准的作业调度框架&#xff0c;但默认使用 RAMJobStore 时&#xff0c;所有任务信息都存在内存里——服务一重启&#xff0c;任务全丢&#xff1b;单机部署&#xff0c;扛…

作者头像 李华
网站建设 2026/10/13 8:41:04

LLM接入开发工作流实战:从会聊天到能干活

1. 从“会聊天”到“能干活”&#xff1a;LLM 接入开发工作流的整体设计思路很多人第一次接触大语言模型&#xff0c;停留在网页对话框里问几句、答几句&#xff0c;觉得新鲜&#xff0c;但关掉页面之后&#xff0c;它跟自己的日常工作没有任何关系。这个项目要解决的核心问题&…

作者头像 李华
网站建设 2026/10/13 8:41:03

拆解六款开源RAG项目,自研检索增强生成蓝图全还原

聊RAG聊了这么长时间&#xff0c;我一直有个很深的体会&#xff1a;与其自己从零拍脑袋设计一套检索增强生成方案&#xff0c;不如先把手头那些成熟的开源RAG项目里里外外读懂。它们能走到今天&#xff0c;每一处设计都是真实场景里撞过墙之后留下的答案。最近我把六款有代表性…

作者头像 李华
网站建设 2026/10/13 8:40:50

基于DDQN的基坑开挖顺序优化:从MDP建模到MATLAB实现

简介&#xff1a;这是一份基于MATLAB与DDQN算法的深浅基坑优化开挖顺序研究资料&#xff0c;面向土木工程、地下施工及深度强化学习应用方向的工程师、研究人员与学生。资源围绕27米深基坑分八次开挖与12米浅基坑分四次开挖的实际工况&#xff0c;通过构建环境模拟、训练智能体…

作者头像 李华