- 数据分析
【免费下载链接】dplyr
dplyr: A grammar of data manipulation
本篇指南围绕 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, ... )| 参数 | 说明 |
|---|---|
tbl | data 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) }) ) }要点:
- 采样被实现为
slice()的特例:索引表达式放在local()中,由slice()的 data mask 逐组求值。因此采样天然具备分组语义——对 grouped data frame,表达式在每个分组内独立执行,n()返回当前分组的行数。 check_size()先做规模校验:无放回且size > n()时中止;- 真正的随机性来自 base R
sample.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 probabilitiessample_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 probabilitiesdf只有 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 probabilitiessize = 1比例合法(check_frac通过),换算为round(2 * 1) = 2行,但 2 行里只有 1 个非零权重,仍由sample.int()拒绝。
4.5 快照揭示的错误消息结构
把 8 条输出放在一起,可以归纳出sample_*()错误链的三段式结构:
- 顶层调用行:
Error insample_n()/Error in `sample_frac()`` —— 始终指向用户直接调用的泛型入口; - slice() 包装层:
! Can't compute indices.(索引计算失败的统一描述),有分组上下文时追加i In group N:key = value.; - 根因层(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互斥 |
weight | weight_by | 同样传入sample.int()的prob,自动标准化 |
replace | replace | 语义一致 |
实现上,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.R | sample_n()/sample_frac()泛型、默认方法、data frame 方法、check_size()/check_frac() |
| R/slice.R | slice()错误处理(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.md | slice_sample()取代sample_n()/sample_frac()的变更记录 |
- 数据分析
【免费下载链接】dplyr
dplyr: A grammar of data manipulation
相关推荐
数据采样终极指南:PythonDataScienceHandbook中的随机抽样与分层抽样技巧
数据采样终极指南:PythonDataScienceHandbook中的随机抽样与分层抽样技巧 数据采样是数据分析和机器学习中的关键步骤,它直接影响模型的准确性
教程数据科学数据分析数据可视化机器学习DeepSeek-V2模型下载指南:HuggingFace仓库使用教程
DeepSeek V2模型下载指南:HuggingFace仓库使用教程 你还在为大模型下载流程复杂而烦恼?本文将带你快速掌握DeepSeek V2模型的下载与本
文档教程数据分析掌握ClickHouse数据采样:10倍速提升大数据分析效率的终极指南
掌握ClickHouse数据采样:10倍速提升大数据分析效率的终极指南 ClickHouse® 作为免费的大数据分析型数据库管理系统,其数据采样功能是处理海量数
数据库OLAP列式数据库大数据实时分析数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考