使用 dplyr::case_when() 要求匹配

Mat*_*ood 6 r dplyr

我正在使用dplyr::case_when()基于数据框中的几个变量来匹配一系列复杂的条件。作为一名防御性程序员,我想检查我是否已经考虑了所有可能的条件,但我找不到一种优雅的方法来做到这一点。

这是一个励志的例子:

library("tidyverse")

df <- expand_grid(x=c(NA_integer_, 1L, 2L), y=1L:2L)

df %>%
    mutate(Case = case_when(
        is.na(x) | is.na(y) ~ NA_character_,
        x > y ~ "x>y",
        x < y ~ "x<y"
    ))
Run Code Online (Sandbox Code Playgroud)

返回值混合了 NA:我只是传播输入 NA 的“好”和我忘记了可能条件 x==y 的“坏”。

从概念上讲,我想要的是:

df %>%
    mutate(Case = case_when(
        is.na(x) | is.na(y) ~ NA_character_,
        x > y ~ "x>y",
        x < y ~ "x<y",
        TRUE ~ stop("Unmatched condition")
    ))
Run Code Online (Sandbox Code Playgroud)

但是(如 dplyr 中明确记录的那样)这将不起作用,因为每个 RHS 表达式总是被评估。以下确实有效:

library("magrittr")
df %>%
    mutate(Case = case_when(
        is.na(x) | is.na(y) ~ NA_character_,
        x > y ~ "x>y",
        x < y ~ "x<y",
        TRUE ~ "UNMATCHED"
    )) %T>%
    with(., stopifnot(! "UNMATCHED" %in% Case))
Run Code Online (Sandbox Code Playgroud)

但这(a)感觉有点hackish,(b)用非字符返回类型更难实现。

有没有人有(更优雅的)解决方案来确保case_when()匹配> 0 的指定条件?


边注:

将此功能直接添加到 dplyr 实际上非常简单,例如:

@ -139,7 +139,7 @@
#' starwars %>%
#'   mutate(type = case_character_type(height, mass, species, robots = FALSE)) %>%
#'   pull(type)
case_when <- function(...) {
case_when <- function(..., require_match = FALSE) {
  fs <- compact_null(list2(...))
  n <- length(fs)

@ -173,6 +173,10 @@ case_when <- function(...) {
    replaced <- replaced | (query[[i]] & !is.na(query[[i]]))
  }

  if (isTRUE(require_match) && !all(replaced)) {
    abort(paste0("Unmatched cases for ", sum(!replaced), " element", if(sum(!replaced)>1) "s"))
  }

  out
}
Run Code Online (Sandbox Code Playgroud)

这样我们就可以做到:

df %>%
    mutate(Case = case_when(
        is.na(x) | is.na(y) ~ NA_character_,
        x > y ~ "x>y",
        x < y ~ "x<y",
        require_match = TRUE
    ))
Run Code Online (Sandbox Code Playgroud)

我将提交此补丁的拉取请求(加上一些 roxygen 的东西),当然除非我的问题在火焰中被击落:)