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语句轻松完成。我的问题是:
- 我正在尝试制作一个通用功能,因此不想要交互式/整洁的评估。
- 我有一大堆这样的专栏。都以其中一个结尾,
_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通过利用以下事实来每次都编写一个新函数:
- 所有这三个条件都相同的一般形式,并为变量名相同的结构,但使用不同的前缀(
high_,low_,等)。 - 它们具有与 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生成器。在到目前为止的示例中,只有列的名称(前缀)发生了变化。如果我想怎么办
- 更改后缀的数量和名称(例如,
high_W, high_X, high_Y, high_Z, low_W, low_X, low_Y, low_Z, .......),因此将后缀的字符向量作为参数some_func - 改变公式的形式。现在,它是
!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