【发布时间】:2021-09-11 06:50:05
【问题描述】:
动机
我正在创建一个自定义实用函数streak_over(),它旨在在语法上模仿dplyr 动词group_by()。虽然streak_over() 本质上封装了group_by() 的分组功能,但这种分组是进一步操作的前奏。
在给定数据集的上下文中,streak_over() 的目的是索引共享一个组的连续观察的每个“条纹”,其中分组是预先建立的(通过先前的 group_by() ) 或在streak_over() 本身中指定(通过tidy evaluation)。
这是一个插图,其中分组变量分别为x和y:
x y group_id streak_index
<dbl> <chr> <int> <int>
1 1 a 1 1
2 1 a 1 1
3 2 b 4 2
4 2 b 4 2
5 1 a 1 3
6 2 a 3 4
7 1 b 2 5
8 2 b 4 6
详情
除了一个小问题之外,我的一切都完全按预期工作。这是streak_over 的一般形式,它在... 中接受group_by() 的所有参数,然后返回带有条纹索引的integer 向量(如streak_index)。 注意: .start 和 .min 是我自己定义“连续”标准的参数;除了它们作为函数头中的命名参数存在之外,它们与我的问题无关。
streak_over <- function(..., .start = 1, .min = 2) {
return(
dplyr::group_by(...) # %>%
# ... %>% Further Operations %>% ...
)
}
一般来说,可用性是理想的。给定一个data.frame 就像x |上图y
df <- data.frame(x = c(1, 1, 2, 2, 1, 2, 1, 2), y = c("a", "a", "b", "b", "a", "a", "b", "b"))
我们可以通过符合人体工程学的工作流程生成条纹指数向量:
df %>% group_by(x, y) %>% streak_over(.add = TRUE)
# [1] 1 1 2 2 3 4 5 6
df %>% group_by(x) %>% streak_over(y, .add = TRUE)
# [1] 1 1 2 2 3 4 5 6
df %>% streak_over(x, y)
# [1] 1 1 2 2 3 4 5 6
我们也可以像连续使用group_by()一样更改分组:
df %>% group_by(x) %>% streak_over(y, .add = FALSE)
# [1] 1 1 2 2 3 3 4 4
df %>% group_by(x) %>% streak_over(y) # .add = FALSE by default
# [1] 1 1 2 2 3 3 4 4
df %>% streak_over(y)
# [1] 1 1 2 2 3 3 4 4
最后,我们可以生成没有任何分组的索引:
df %>% streak_over() # no grouping given
# [1] 1 1 1 1 1 1 1 1
但是,我想更改一种默认行为。
问题
根据它所包装的group_by() 函数的当前默认值(.add = FALSE),streak_over() 当前会在未另外指定.add 时覆盖现有分组。虽然我通常喜欢这种行为,但有一种情况是违反直觉且不方便:
df %>% group_by(x, y) %>% streak_over()
# [1] 1 1 1 1 1 1 1 1
在这里,一个分组已经存在并且可能有用。此外,streak_over() 不包含进一步的分组变量,否则需要通过.add 消除歧义。 在这种特殊情况下,简单地使用streak_over()保留现有分组会非常方便,而所有其他默认设置都遵循group_by()。
df %>% group_by(x, y) %>% streak_over()
# [1] 1 1 2 2 3 4 5 6
这里列出了我想要的行为:
.add |
Grouping Variables Specified in streak_over() Call |
Grouping Variables Unspecified |
|---|---|---|
| (missing) | Current Default Behavior for group_by()
|
Always Add to Existing Grouping |
TRUE |
Add to Existing Grouping | Add to Existing Grouping |
FALSE |
Override Existing Grouping | Override Existing Grouping |
我也希望这种“延迟”是动态的:如果 R 团队使用新的默认设置(如 mutate(.add = TRUE,)更新 dplyr::group_by(),我希望 streak_over() 也能效仿使用dplyr 工作流程,而不是使用可能会过时的硬编码默认值(如streak_over(.add = TRUE,)。
最后,为了美观和专业,我希望将streak_over()的函数头保留为大致规范的形式:
streak_over <- function(..., .start = 1, .min = 2) {
# ...
}
# Or...
streak_over <- function(.data, ..., .add, .drop, .start = 1, .min = 2) {
# ...
}
尝试
都没有成功,我探索了很多方法。我当前的迭代
streak_over <- function(.data, ..., .add, .drop,
.start = 1, .min = 2) {
if(rlang::is_missing(.add) && !length(list(...))) {
.add <- TRUE
if(!rlang::is_empty(dplyr::group_vars(.data))) {
message("Existing groups will be kept. Discard with '.add = FALSE'.")
}
}
return(
dplyr::group_by(.data, ...,
.add = rlang::maybe_missing(.add),
.drop = rlang::maybe_missing(.drop)) # %>%
# ... %>% Further Operations %>% ...
)
}
和许多其他人一样失败了,似乎有以下原因:
- 尽管this answer 支持我最初的直觉,
length(list(...))无法有效测试掩码变量 的存在(例如df中df中的ystreak_over(y))和@ 987654386@ 似乎同样不可行。确实,前一种方法给了我以下错误;它似乎将y解释为自身的对象,而不仅仅是.data中变量的符号:
df %>% group_by(x) %>% streak_over(y)
# Error in streak_over(., y) : object 'y' not found
- 似乎
dplyr与rlang::maybe_missing()配合不佳,rlang::maybe_missing()的值应该模拟缺少的参数。由于group_by()无条件地将.add强制转换为logical,我收到以下错误:
streak_over <- function(.data, ..., .add, .drop,
.start = 1, .min = 2) {
# Condition REMOVED to progress beyond error above.
return(
dplyr::group_by(.data, ...,
.add = rlang::maybe_missing(.add),
.drop = rlang::maybe_missing(.drop)) # %>%
# ... %>% Further Operations %>% ...
)
}
df %>% group_by(x) %>% streak_over(y)
# Error in if (.add) { : argument is not interpretable as logical
- 即使我试图折叠函数头 (
streak_over(..., .start = 1, .min = 2)),并让.add“潜伏在阴影中”直到它被明确指定 (streak_over(.add = TRUE,),我还是不得不以某种方式改变它的值 :有条件的new_add <- TRUE,后跟group_by(..., .add = new_add)。不幸的是,如果用户确实明确指定了.add,R 会将它包含在...中,紧邻.add = new_add,并且通过在适当的地方分配new_add <- NULL无法避免由此产生的冲突。结果是(不可预测的)错误:
streak_over <- function(..., .start = 1, .min = 2) {
# Check if '.add' is present in '...'; and if no masked variables are present therein.
if(is.null(list(...)$.add) && all(names(list(...)) %in% c(".data", ".add", ".drop"))) {
new_add <- TRUE
} else {
new_add <- NULL
}
return(
dplyr::group_by(..., .add = new_add) # %>%
# ... %>% Further Operations %>% ...
)
}
df %>% group_by(x, y) %>% streak_over(.add = FALSE)
# Error in dplyr::group_by(..., .add = add) :
# formal argument ".add" matched by multiple actual arguments
结论
我觉得 必须 是一种有条件地将形式参数的默认值覆盖到包装函数的方法,即使它是神秘的 ... 包含屏蔽或整齐评估的变量。但是,我怀疑这接近于symbol 领域,这是我绝对没有经验的 R 领域。
一如既往,感谢您的考虑,以及您可能提供的任何帮助。
更新
感谢来自ktiu 的hint,在 Stack Overflow 上与 further research 合成,我拼凑出一个有点“hacky”的解决方案,这似乎满足了我最初的标准:
streak_over <- function(.data, ..., .add, .drop,
.start = 1, .min = 2) {
# Store the defaults for 'group_by()', in case they are needed.
gb_formals <- formals(dplyr::group_by)
# If neither '.add' nor masked variables in '...' were supplied to
# 'streak_over()', yet a grouping already exists in '.data', override the
# 'group_by()' default to intuitively preserve the grouping.
if(rlang::is_missing(.add) && !length(rlang::enquos(...)) &&
!rlang::is_empty(dplyr::group_vars(.data))) {
.add <- TRUE
message("Existing groups will be kept. Discard with '.add = FALSE'.")
}
return(
dplyr::group_by(.data, ...,
.add = rlang::maybe_missing(.add, gb_formals$.add),
.drop = rlang::maybe_missing(.drop, gb_formals$.drop)) # %>%
# ... %>% Further Operations %>% ...
)
}
我欢迎任何进一步的帮助:
- 提供更优雅的解决方案;可能(比如说)建议 here 和
default包,如果安全实施,这似乎理想的外科手术......虽然我确实想知道掩蔽是否会正确翻译. 不幸的是,我的第一次尝试失败了:虽然一切看起来都很干净
streak_over <- function(..., .start = 1, .min = 2) {
# Condition always TRUE here to illustrate the point.
if(TRUE) {
default::default(group_by) <- list(.add = TRUE)
}
# Print out default, to check if correctly updated.
default::default(group_by)
return(
group_by(...) # %>%
# ... %>% Further Operations %>% ...
)
}
并且打印输出表明.add 现在默认为本地TRUE 的group_by(),输出仍然像.add = FALSE:
df %>% group_by(x, y) %>% streak_over()
# - .data = [none]
# - ... = [none]
# * - .add = TRUE
# - .drop = group_by_drop_default(.data)
# [1] 1 1 1 1 1 1 1 1
- 改进了我现有解决方案的技术(工具、结构、语法等);可能(比如说)使用
rlang::fn_fmls()而不是base::formals()。 - 稳定此解决方案的功能;尤其是对
dplyr::group_by()的结构变化更加稳健,包括但不限于:-
group_by()中参数的现有默认值的更改 - 为
group_by()中以前没有默认值的参数添加默认值 -
group_by()中现有参数的重命名 - 向
group_by()添加新参数。
-
除了满足原始标准的答案外,任何成功提供进一步帮助的答案(同时满足原始标准)都将获得我的支持并考虑接受。
奖金
我也很好奇以下哪个streak_over() 标头更规范:
-
streak_over(..., .start = 1, .min = 2):典型的包装器。 -
streak_over(..., .add, .start = 1, .min = 2): suggested by ktiu。 -
streak_over(.data, ..., .add, .drop, .start = 1, .min = 2):类似于dplyr。
请注意,此功能旨在模仿 dplyr 语法,模仿 group_by(.data, ..., .add, .drop)。然而在执行进一步操作之前,streak_over() 仍然包装另一个函数:在这种情况下,R 以压倒性多数将传递给包装函数的所有参数表示为包装头中的...。再说一次,出于纯粹的功能目的,.add 是传递给group_by() 的唯一形式参数,需要在streak_over() 标头中显式存在于... 之外。
权威参考资料支持这一点的明确性,可能会成为接受答案的“决胜局”。
再次感谢! — 格雷格
【问题讨论】:
-
如果您提供一些可以运行的测试用例来验证可能的解决方案,这将更容易提供帮助。当您收到错误时,不清楚您正在运行什么命令。到目前为止您提供的代码不会返回问题中显示的结果。我们不需要您的实际功能,只需一个简单的可重现示例,可用于测试您尝试解决的特定问题。
-
嗨@MrFlick,感谢您的阅读!关于这个问题的问题在于它本质上是概念性的:“我如何用
streak_by(..., .start, .min)(或streak_by(.data, ..., .add, .drop, .start, .min))形式的函数包装group_by(),这样我就可以覆盖.add的默认值时并且仅当...(可能包含整齐评估和掩码变量)和.add都丢失时,否则保留group_by()的当前默认值?”。我在分组之后所做的细节无关紧要;它们只是动力。 -
因此,函数
streak_over <- function(..., .start = 1, .min = 2){return(dplyr::group_by(...))}就足够了,或者streak_over <- function(.data, ..., .add, .drop, .start = 1, .min = 2){return(dplyr::group_by(...))}。我真正需要的只是有条件地覆盖group_by()的当前默认值,同时保留它们,以获得表中定义并在示例输出中说明的分组行为。 -
@MrFlick 我用可重现的错误更新了我的问题。需要明确的是,这些是我已经诊断出的错误示例,它们的原因并不神秘。但是,由于它们具体说明了解决方案的概念障碍,所以我认为它们确实服务于概念目的。
标签: r dplyr parameter-passing wrapper tidyeval