Requiring a match using dplyr::case_when()

Viewed 393

I am using dplyr::case_when() to match a complicated series of conditions based on several variables in a data frame. Being a defensive programmer I want to check that I have accounted for all possible conditions, but I can't find an elegant way of doing this.

Here is a motivating example:

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"
    ))

The return value has a mixture of NA: the "good ones" where I am just propagating input NA, and the "bad ones" where I forgot the possible condition x==y.

Conceptually what I want is something like:

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")
    ))

But (as clearly documented in dplyr) this won't work as every RHS expression is always evaluated. The following does work:

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))

But this (a) feels a bit hackish and (b) is harder to implement with non-character return types.

Does anybody have a (more elegant) solution to ensure that case_when() matches >0 of the specified conditions?


Side note:

It would actually be quite simple to add this functionality directly to dplyr e.g.:

@ -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
}

So that we can do:

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

I will submit a pull request for this patch (plus some roxygen stuff) unless of course my question gets shot down in flames :)

0 Answers
Related