【问题标题】:Conditionally Override Default Arguments When Passing Masked Variables (in '...') to group_by()将掩码变量(在“...”中)传递给 group_by() 时有条件地覆盖默认参数
【发布时间】:2021-09-11 06:50:05
【问题描述】:

动机

我正在创建一个自定义实用函数streak_over(),它旨在在语法上模仿dplyr 动词group_by()。虽然streak_over() 本质上封装了group_by() 的分组功能,但这种分组是进一步操作的前奏。

在给定数据集的上下文中,streak_over() 的目的是索引共享一个组的连续观察的每个“条纹”,其中分组是预先建立的(通过先前的 group_by() ) 或在streak_over() 本身中指定(通过tidy evaluation)。

这是一个插图,其中分组变量分别为xy

      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 %>% ...
  )
}

和许多其他人一样失败了,似乎有以下原因:

  1. 尽管this answer 支持我最初的直觉,length(list(...)) 无法有效测试掩码变量 的存在(例如dfdf 中的y streak_over(y))和@ 987654386@ 似乎同样不可行。确实,前一种方法给了我以下错误;它似乎将y 解释为自身的对象,而不仅仅是.data 中变量的符号
df %>% group_by(x) %>% streak_over(y)
# Error in streak_over(., y) : object 'y' not found
  1. 似乎dplyrrlang::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 
  1. 即使我试图折叠函数头 (streak_over(..., .start = 1, .min = 2)),并让 .add“潜伏在阴影中”直到它被明确指定 (streak_over(.add = TRUE,),我还是不得不以某种方式改变它的值 :有条件的new_add &lt;- TRUE,后跟group_by(..., .add = new_add)。不幸的是,如果用户确实明确指定了.add,R 会将它包含在... 中,紧邻.add = new_add,并且通过在适当的地方分配new_add &lt;- 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 领域。

一如既往,感谢您的考虑,以及您可能提供的任何帮助。


更新

感谢来自ktiuhint,在 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 %>% ...
  )
}

我欢迎任何进一步的帮助:

  • 提供更优雅的解决方案;可能(比如说)建议 heredefault 包,如果安全实施,这似乎理想的外科手术......虽然我确实想知道掩蔽是否会正确翻译. 不幸的是,我的第一次尝试失败了:虽然一切看起来都很干净
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 现在默认为本地TRUEgroup_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 &lt;- function(..., .start = 1, .min = 2){return(dplyr::group_by(...))} 就足够了,或者streak_over &lt;- function(.data, ..., .add, .drop, .start = 1, .min = 2){return(dplyr::group_by(...))}。我真正需要的只是有条件地覆盖group_by() 的当前默认值,同时保留它们,以获得表中定义并在示例输出中说明的分组行为。
  • @MrFlick 我用可重现的错误更新了我的问题。需要明确的是,这些是我已经诊断出的错误示例,它们的原因并不神秘。但是,由于它们具体说明了解决方案的概念障碍,所以我认为它们确实服务于概念目的。

标签: r dplyr parameter-passing wrapper tidyeval


【解决方案1】:

这是一种方法

  1. 使用rlang::enquos()来化解...中的函数参数,
  2. 在以下情况下提供.add = TRUE
    • .add 未明确传递 AND
    • ... 仅包含一个“非特殊点”参数(数据的名称,或管道中的 .
  3. 使用这些变量为group_by() 调用自定义包装器:
streak_over <- function(..., .start = 1, .min = 2) {
  defused <- rlang::enquos(...)
  if (any(".add" %in% names(defused),
          sum(! grepl("\\..+", names(defused))) > 1))
    custom_wrapper(...)
  else
    custom_wrapper(..., .add = TRUE)
}

custom_wrapper <- function(...) {
  # add custom logic here
  dplyr::group_by(...)
}

请注意,我并没有竭尽全力匹配您指定的所有案例,但这可能是一个概念证明,您可以将其引入更成熟的解决方案。

试一试:

示例 1

library(dplyr)

df %>%
   group_by(x) %>%
   streak_over()

保持分组:

# A tibble: 8 x 2
# Groups:   x [2]
      x y    
  <dbl> <chr>
1     1 a    
2     1 a    
3     2 b    
4     2 b    
5     1 a    
6     2 a    
7     1 b    
8     2 b    

示例 2

df %>%
  group_by(x) %>%
  streak_over(y)

覆盖分组:

# A tibble: 8 x 2
# Groups:   y [2]
      x y    
  <dbl> <chr>
1     1 a    
2     1 a    
3     2 b    
4     2 b    
5     1 a    
6     2 a    
7     1 b    
8     2 b    

示例 3

df %>%
   group_by(x) %>%
   streak_over(.add = F)

删除分组:

# A tibble: 8 x 2
      x y    
  <dbl> <chr>
1     1 a    
2     1 a    
3     2 b    
4     2 b    
5     1 a    
6     2 a    
7     1 b    
8     2 b    

示例 4

df %>%
  group_by(x) %>%
  streak_over(y, .add = T)

添加分组:

# A tibble: 8 x 2
# Groups:   x, y [4]
      x y    
  <dbl> <chr>
1     1 a    
2     1 a    
3     2 b    
4     2 b    
5     1 a    
6     2 a    
7     1 b    
8     2 b    

【讨论】:

  • 谢谢,@ktiu,我正在探讨这个问题!
  • 仅供参考,我确实希望在创建函数头streak_over &lt;- function(..., .add时避免硬编码group_by()参数的任何默认值= F, .start = 1, .min = 2)。原因是 R 团队将来可能会更改 group_by() 的默认值,我希望 streak_over() 自动与这些更改保持一致。因此,我想实现rlang::maybe_missing() 应该做的事情,而不是在我的示例列表中抛出错误(#2)。
  • 我更新了我的答案,看看新方法是否满足你的限制。
  • 谢谢@ktiu,我去看看!
  • 由于正则表达式,我认为您的新 streak_over() 中存在边缘情况:对于 streak_over(.data = df %&gt;% group_by(x), y),它无法覆盖现有组。作为命名参数,.datanames(defused) 中显示为".data" 而不是"",这将与正则表达式"\\..+" 的不匹配计数减少1。因此,当在.data 旁边仅指定一个掩码变量时,不匹配的计数仅为1。因此,当.add 也被忽略时,if 条件为FALSEelse 语句被执行,它将.add = TRUE 传递给group_by()
猜你喜欢
  • 2011-06-07
  • 1970-01-01
  • 2014-05-01
  • 1970-01-01
  • 2015-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多