dplyrtidyr–如何使用动态条件生成case_when?

有没有办法来动态/编程产生case_when的条件dplyr有不同的列名和/或不同数量的条件是什么?我有一个交互式脚本,我正在尝试将其转换为函数。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列有一个值,就使用它。如果没有,则尝试 column ._A,如果没有,则尝试column ._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. 它们具有与 form 相同的公式!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", .)

我很高兴让它与不同的前缀一起工作,但是理解我如何使用任意(但常见)后缀和任意公式实现更通用的东西会很酷,这样我就可以做到some_func(prefix, suffixes, formula)

回答

这是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 包(v0.3.0)于 2021 年 7 月 22 日创建

我也在 Github {dplyover} 上有一个包,它是为这种情况制作的。对于具有两个以上变量的示例,我将dplyover::over与特殊语法一起使用来将字符串评估为变量名。我们可以进一步使用dplyover::cut_names("_TOT")来提取变量名称之前或之后的字符串部分"_TOT"(在示例中是"low"and "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 包(v0.3.0)于 2021 年 7 月 22 日创建


回答

更新的解决方案
我认为这个完全基于 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


回答

冒着不回答这个问题的风险,我认为解决这个问题的最简单方法就是重塑和使用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很多。您已经使用!!syminside mutate得到了答案,所以这里有一个版本说明了一种更简单的方法。除非必要,否则我不想使用 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


以上是dplyrtidyr–如何使用动态条件生成case_when?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>