【问题标题】:dplyr tidyr – How to generate case_when with dynamic conditons?dplyr tidyr – 如何在动态条件下生成 case_when?
【发布时间】:2021-09-30 00:31:28
【问题描述】:

有没有办法在 dplyr 中以不同的列名和/或不同数量的条件动态/以编程方式生成 case_when 条件?我有一个交互式脚本,我正在尝试将其转换为函数。 case_when 语句中有很多重复的代码,我想知道它是否可以以某种方式自动化,而无需我一次又一次地从头开始编写所有内容。

这是一个虚拟数据集:

test_df = tibble(low_A=c(5, 15, NA),
                 low_TOT=c(NA, 10, NA),
                 low_B=c(20, 25, 30),
                 high_A=c(NA, NA, 10),
                 high_TOT=c(NA, 40, NA),
                 high_B=c(60, 20, NA))

expected_df = tibble(low_A=c(5, 15, NA),
                     low_TOT=c(NA, 10, NA),
                     low_B=c(20, 25, 30),
                     ans_low=c(5, 10, 30),
                     high_A=c(NA, NA, 10),
                     high_TOT=c(NA, 40, NA),
                     high_B=c(60, 20, NA),
                     ans_high=c(60, 40, 10))

> expected_df
# A tibble: 3 x 8
  low_A low_TOT low_B ans_low high_A high_TOT high_B ans_high
  <dbl>   <dbl> <dbl>   <dbl>  <dbl>    <dbl>  <dbl>    <dbl>
1     5      NA    20       5     NA       NA     60       60
2    15      10    25      10     NA       40     20       40
3    NA      NA    30      30     10       NA     NA       10

我想要的逻辑是,如果._TOT 列有值,请使用它。如果不是,则尝试列._A,如果不是,则尝试列._B。请注意,我故意没有将._TOT 作为组的第一列。在这种情况下,我可以只使用 coalesce(),但我想要一个通用的解决方案,而不考虑列顺序。

当然,这一切都可以通过几个case_when 语句轻松完成。我的问题是:

  1. 我正在尝试制作一个通用函数,因此不想要交互式/整洁的评估。
  2. 我有一大堆这样的专栏。都以_TOT, _A, _B 之一结尾,但前缀不同(例如low_TOT, low_A, low_B, high_TOT, high_A, high_B,.....,我不想一次又一次地重写一堆case_when 函数。

我现在拥有的看起来像这样(我为每个前缀写了一个case_when):

def my_function = function(df) { 
    df %>% mutate(
          # If a total low doesn't exist, use A (if exists) or B (if exists)
          "ans_low" := case_when(
            !is.na(.data[["low_TOT"]]) ~ .data[["low_TOT"]],
            !is.na(.data[["low_A"]]) ~ .data[["low_A"]],
            !is.na(.data[["low_B"]]) ~ .data[["low_B"]],
          ),

          # If a total high doesn't exist, use A (if exists) or B (if exists)
          "ans_high" := case_when(
            !is.na(.data[["high_TOT"]]) ~ .data[["high_TOT"]],
            !is.na(.data[["high_A"]]) ~ .data[["high_R"]],
            !is.na(.data[["high_B"]]) ~ .data[["high_B"]],
              
         # Plus a whole bunch of similar case_when functions...
}

我想要的是理想地获得一种方法来动态生成具有不同条件的case_when 函数,这样我就不会每次都通过利用以下事实编写新的case_when

  1. 所有三个条件都具有相同的通用形式和相同的变量名称结构,但前缀不同(high_low_ 等)。
  2. 它们具有!is.na( .data[[ . ]]) ~ .data[[ . ]] 形式的相同公式,其中(.) 是动态生成的列名称。

我想要的是这样的:

def my_function = function(df) { 
    df %>% mutate(
          "ans_low" := some_func(prefix="Low"),
          "ans_high" := some_func(prefix="High")
}

我尝试创建自己的case_when 生成器来替换标准case_when,如下所示,但出现错误。我猜那是因为 .data 在 tidyverse 函数之外并不能真正工作?

some_func = function(prefix) {
  case_when(
    !is.na(.data[[ sprintf("%s_TOT", prefix) ]]) ~ .data[[ sprintf("%s_TOT", prefix) ]],
    !is.na(.data[[ sprintf("%s_A", prefix) ]]) ~ .data[[ sprintf("%s_A", prefix) ]],
    !is.na(.data[[ sprintf("%s_B", prefix) ]]) ~ .data[[ sprintf("%s_B", prefix) ]]
  )
}

我很好奇的另一件事是制作一个更通用的case_when 生成器。在到目前为止的示例中,只有列的名称(前缀)发生了变化。如果我想怎么办

  1. 更改后缀的数量和名称(例如,high_W, high_X, high_Y, high_Z, low_W, low_X, low_Y, low_Z, .......),然后将后缀的字符向量作为some_func 的参数
  2. 更改公式的形式。现在,它的所有条件都是!is.na(.data[[ . ]]) ~ .data[[ . ]] 的形式,但是如果我想让它成为some_func 的参数呢?例如,!is.na(.data[[ . ]]) ~ sprintf("%s is missing", .)

我很乐意让它与不同的前缀一起工作,但了解我如何使用任意(但常见的)后缀和任意公式实现更通用的东西会非常酷,这样我就可以做到@ 987654353@.

【问题讨论】:

  • 请展示一个可重现的小例子
  • 如果您包含一个简单的reproducible example,其中包含可用于测试和验证可能解决方案的示例输入和所需输出,则更容易为您提供帮助。如果您只是想获取第一个非 NA 值,则像 coalesce() 这样的函数可能更合适。
  • 立即查看。我添加了一个简单的数据集作为示例,并将问题重写为更清晰、更简短。这仍然有点长,因为,真的,我要问 3 个问题,关于我想要的越来越普遍的水平(并看看这是否可能,首先)。
  • A coalesce() 可能是一个潜在的答案,但我对动态生成条件更感兴趣(is.na 只是这里的特定示例,coalesce 还需要特定的列顺序)。我真的很想了解如何更好地使用 dplyr 编程并实现更高级别的抽象/通用性。
  • 我也刚刚尝试了 coalese() 对列进行了先前的重新排序,但它给出了同样的主要问题:我现在必须编写一大堆 coalesce 语句。我想利用列组的公共前缀,这样我就不必编写 10 个不同的 case_whencoalese 语句。

标签: r dplyr tidyr rlang


【解决方案1】:

这是一个自定义的 case_when 函数,您可以使用 purrr::reduce 和变量名的字符串部分来调用它(在示例中为 c("low", "high")

library(dplyr)
library(purrr)

my_case_when <- function(df, x) {
  
  mutate(df,
         "ans_{x}" := case_when(
           !is.na(!! sym(paste0(x, "_TOT"))) ~ !! sym(paste0(x, "_TOT")),
           !is.na(!! sym(paste0(x, "_A"))) ~ !! sym(paste0(x, "_A")),
           !is.na(!! sym(paste0(x, "_B"))) ~ !! sym(paste0(x, "_B"))
           )
  )
}

test_df %>% 
  reduce(c("low", "high"), my_case_when, .init = .)

#> # A tibble: 3 x 8
#>   low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
#>   <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
#> 1     5      NA    20     NA       NA     60       5       60
#> 2    15      10    25     NA       40     20      10       40
#> 3    NA      NA    30     10       NA     NA      30       10

reprex package (v0.3.0) 于 2021-07-22 创建

我在 Github {dplyover} 上也有一个包,它是为这种情况制作的。对于具有两个以上变量的示例,我将使用 dplyover::over 和特殊语法将字符串作为变量名进行评估。我们可以进一步使用dplyover::cut_names("_TOT") 来提取"_TOT" 之前或之后的变量名的字符串部分(在示例中为"low""high")。

我们可以使用case_when:

library(dplyr)
library(dplyover) # https://github.com/TimTeaFan/dplyover

test_df %>% 
  mutate(over(cut_names("_TOT"),
              list(ans = ~ case_when(
                  !is.na(.("{.x}_TOT")) ~ .("{.x}_TOT"),
                  !is.na(.("{.x}_A")) ~ .("{.x}_A"),
                  !is.na(.("{.x}_B")) ~ .("{.x}_B")
                  )),
              .names = "{fn}_{x}")
         )

#> # A tibble: 3 x 8
#>   low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
#>   <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
#> 1     5      NA    20     NA       NA     60       5       60
#> 2    15      10    25     NA       40     20      10       40
#> 3    NA      NA    30     10       NA     NA      30       10

或者更简单的coalesce:

test_df %>% 
  mutate(over(cut_names("_TOT"),
              list(ans = ~ coalesce(.("{.x}_TOT"),
                                    .("{.x}_A"),
                                    .("{.x}_B"))),
              .names = "{fn}_{x}")
  )

#> # A tibble: 3 x 8
#>   low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
#>   <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
#> 1     5      NA    20     NA       NA     60       5       60
#> 2    15      10    25     NA       40     20      10       40
#> 3    NA      NA    30     10       NA     NA      30       10

reprex package (v0.3.0) 于 2021 年 7 月 22 日创建

【讨论】:

  • 让我对dplyover 感兴趣!对于任意多个后缀,如何做到这一点?考虑:_TOT_A_B、...、_Z_AA_AB、...等等;由正则表达式_(TOT|[A-Z]+)$定义。
  • @Greg:我们可以在选择函数cut_namesextract_names 中使用正则表达式。然而,在上面的case_when 函数中,我们需要对所有后缀进行硬编码,至少在使用over 时是这样。还有over2over2x 也允许.y 参数,但最终取决于case_when 函数的外观。
  • @Greg,我刚刚找到了一个解决方案(我自己进行了一些编辑),它允许使用任意后缀。在下面查找我的解决方案。
【解决方案2】:

冒着不回答问题的风险,我认为解决这个问题的最简单方法是重塑并使用coalesce()。无论哪种方式,您的数据结构都需要两个枢轴(我认为),但这不需要仔细考虑存在哪些前缀。

library(tidyverse)

test_df <- tibble(
  low_A = c(5, 15, NA),
  low_TOT = c(NA, 10, NA),
  low_B = c(20, 25, 30),
  high_A = c(NA, NA, 10),
  high_TOT = c(NA, 40, NA),
  high_B = c(60, 20, NA)
)

test_df %>%
  rowid_to_column() %>%
  pivot_longer(cols = -rowid, names_to = c("prefix", "suffix"), names_sep = "_") %>%
  pivot_wider(names_from = suffix, values_from = value) %>%
  mutate(ans = coalesce(TOT, A, B)) %>%
  pivot_longer(cols = c(-rowid, -prefix), names_to = "suffix") %>%
  pivot_wider(names_from = c(prefix, suffix), names_sep = "_", values_from = value)
#> # A tibble: 3 x 9
#>   rowid low_A low_TOT low_B low_ans high_A high_TOT high_B high_ans
#>   <int> <dbl>   <dbl> <dbl>   <dbl>  <dbl>    <dbl>  <dbl>    <dbl>
#> 1     1     5      NA    20       5     NA       NA     60       60
#> 2     2    15      10    25      10     NA       40     20       40
#> 3     3    NA      NA    30      30     10       NA     NA       10

还要注意case_when 没有整洁的评估,因此不使用mutate 会大大简化您的some_func。您已经在 mutate 中使用 !!sym 得到了答案,所以这里有一个版本说明了一种更简单的方法。除非必要,否则我不喜欢使用 tidyeval,因为我想使用 mutate 链,而这里并不是真的需要。

some_func <- function(df, prefix) {
  ans <- str_c(prefix, "_ans")
  TOT <- df[[str_c(prefix, "_TOT")]]
  A <- df[[str_c(prefix, "_A")]]
  B <- df[[str_c(prefix, "_B")]]
  
  df[[ans]] <- case_when(
    !is.na(TOT) ~ TOT,
    !is.na(A) ~ A,
    !is.na(B) ~ B
  )
  df
}

reduce(c("low", "high"), some_func, .init = test_df)
#> # A tibble: 3 x 8
#>   low_A low_TOT low_B high_A high_TOT high_B low_ans high_ans
#>   <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
#> 1     5      NA    20     NA       NA     60       5       60
#> 2    15      10    25     NA       40     20      10       40
#> 3    NA      NA    30     10       NA     NA      30       10

【讨论】:

  • My instinct 也只是“重塑和使用coalesce()。不过我很好奇:我们如何将任意多个字母后缀概括为_A_B、...、_Z_AA_AB 等等?对于本身可能包含_ 的前缀,例如another_prefix_A?也许通过将列名转换为 name 列,然后将每个 name 拆分为 (1) 匹配正则表达式 _(TOT|[A-Z]+)$ 的子字符串,以及 (2) 之前所有内容的子字符串。
  • 要处理任意后缀,我可能想订购后缀列表并拼接到coalesce。对于复杂的前缀,pivot_longer 支持names_pattern,因此您可以使用正则表达式来选择您想要的组(例如(^.*)_([^_]+$) 将(我认为)使后缀成为字符串结尾之前的最后一个_,以及下划线之前的前缀。
  • 我真的很喜欢您的基本 R 解决方案(远胜于多个枢轴)。不幸的是,你的并没有真正解决动态生成case_when 条件的问题,所以我不得不选择 TimeTeaFan 作为接受的答案。但如果我不得不再次这样做,我肯定会使用你的基础 R 解决方案,它更容易掌握。
【解决方案3】:

更新的解决方案 我认为这个仅基于基础 R 的解决方案可能会对您有所帮助。

fn <- function(data) {
  
  do.call(cbind, lapply(unique(gsub("([[:alpha:]]+)_.*", "\\1", names(test_df))), function(x) {
    tmp <- test_df[paste0(x, c("_TOT", "_A", "_B"))]
    tmp[[paste(x, "ans", sep = "_")]] <- Reduce(function(a, b) {
      i <- which(is.na(a))
      a[i] <- b[i]
      a
    }, tmp)
    tmp
  }))
}

fn(test_df)

fn(test_df)

   high_TOT high_A high_B high_ans low_TOT low_A low_B low_ans
1       NA     NA     60       60      NA     5    20       5
2       40     NA     20       40      10    15    25      10
3       NA     10     NA       10      NA    NA    30      30

【讨论】:

    【解决方案4】:

    这不会生成任何case_when,但您可以按如下方式创建两个新列。当然,这也可以是一个以test_dfans_orderand_groups 作为参数的函数。

    ans_order <- c('TOT', 'A', 'B')
    ans_groups <- c('low', 'high')
    
    test_df[paste0('ans_', ans_groups)] <- 
      apply(outer(ans_groups, ans_order, paste, sep = '_'), 1, 
            function(x) do.call(dplyr::coalesce, test_df[x]))
    
    test_df
    #>   low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
    #> 1     5      NA    20     NA       NA     60       5       60
    #> 2    15      10    25     NA       40     20      10       40
    #> 3    NA      NA    30     10       NA     NA      30       10
    

    如果您不想使用任何软件包,另一种选择是

    test_df[paste0('ans_', ans_groups)] <- 
      apply(outer(ans_groups, ans_order, paste, sep = '_'), 1, 
            function(x) Reduce(function(x, y) ifelse(is.na(x), y, x), test_df[x]))
    

    【讨论】:

      【解决方案5】:

      感谢大家的回答! Calum You 的回答特别让我意识到一直坚持 Tidyverse 不一定是最好的,有时 base R 有更好、更简单、更优雅的解决方案。

      感谢 noahm 在 RStduio 社区上进行的大量搜索和this excellent post,我还能够提出自己的解决方案来满足我的需求:

      library(tidyverse)
      library(rlang)
      library(glue)
      
      make_expr = function(prefix, suffix) {
        rlang::parse_expr(glue::glue('!is.na(.data[[\"{prefix}_{suffix}\"]]) ~ .data[[\"{prefix}_{suffix}\"]]'))
      }
      
      make_conds = function(prefixes, suffixes){
        map2(prefixes, suffixes, make_expr)
      }
      
      ans_df = test_df %>%  
          mutate(
              "ans_low" := case_when(
                  !!! make_conds( prefixes=c("low"), suffixes=c("TOT", "A", "B") ) 
              ),
              "ans_high" := case_when(
                  !!! make_conds( prefixes=c("high"), suffixes=c("TOT", "A", "B") ) 
              )
          )
      
      # The ans is the same as the expected solution
      > all_equal(ans_df, expected_df)
      [1] TRUE
      

      我还检查了这在函数内部是否有效(这对我来说是另一个重要的考虑因素)。

      这个解决方案的一个好处是后缀不是硬编码的,并且至少达到了我所寻找的第一级通用性。

      我想一些带有替换的字符串操作也可能允许公式结构的通用性。最终,通用公式将需要某种字符串模板解决方案,因为使用这种结构,您可以将其保留在胶水中。

      【讨论】:

        【解决方案6】:

        虽然答案已被接受,但我觉得这可以在dplyr 中完成(即使对于任意数量的列集),而无需提前编写自定义函数。

        test_df %>%
          mutate(across(ends_with('_TOT'), ~ coalesce(., 
                                                      get(gsub('_TOT', '_A', cur_column())), 
                                                      get(gsub('_TOT', '_B', cur_column()))
                                                      ),
                        .names = "ans_{gsub('_TOT', '', .col)}"))
        
        # A tibble: 3 x 8
          low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
          <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
        1     5      NA    20     NA       NA     60       5       60
        2    15      10    25     NA       40     20      10       40
        3    NA      NA    30     10       NA     NA      30       10
        

        一个完整的基础 R 方法

        Reduce(function(.x, .y) {
          xx <- .x[paste0(.y, c('_TOT', '_A', '_B'))]
          .x[[paste0('ans_',.y)]] <- apply(xx, 1, \(.z) head(na.omit(.z), 1))
          .x
        }, unique(gsub('([_]*)_.*', '\\1', names(test_df))),
        init = test_df)
        
        # A tibble: 3 x 8
          low_A low_TOT low_B high_A high_TOT high_B ans_low ans_high
          <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl>   <dbl>    <dbl>
        1     5      NA    20     NA       NA     60       5       60
        2    15      10    25     NA       40     20      10       40
        3    NA      NA    30     10       NA     NA      30       10
        

        【讨论】:

          猜你喜欢
          • 2021-09-17
          • 2020-08-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-02-07
          • 2020-09-16
          • 1970-01-01
          相关资源
          最近更新 更多