在 mutate 中使用引号:一个替代 mutate_(.dots = ...) 的方法

9
我希望将不同的函数应用于tibble中的同一列。这些函数被存储在一个字符字符串中。我以前使用mutate_.dots参数来实现,像这样:
library(dplyr)

myfuns <- c(f1 = "a^2", f2 = "exp(a)", f3 = "sqrt(a)")
tibble(a = 1:3) %>% 
  mutate_(.dots = myfuns)

这种方法仍然可以正常工作,但 mutate_ 已经被弃用。我尝试使用 mutaterlang 包来实现相同的结果,但进展不顺。

在我的真实示例中,myfuns 包含约 200 个函数,因此逐个输入它们不是一个选项。

提前致谢。


2
“这些函数存储在字符串中。” — 这基本上是一个坏主意。这些函数来自哪里?如果是在代码中,将它们存储为未评估的表达式或实际函数。不要(滥用)字符串来表示代码。 - Konrad Rudolph
嗯,这是一个很长的故事。基本上每个组件都是一个冗长的公式,可以用某个公式“拼写”出来。这就是为什么它们被存储在字符字符串中的原因。 我没有(现在也没有)知道任何其他规避这种情况的方法。 - Cettt
如果它可以作为字符串拼写,那么肯定也可以作为代码拼写?只需删除周围的“…”并使用公式(~ …)或函数function (x) …)即可。 - Konrad Rudolph
谢谢Konrad,我会尝试的。 - Cettt
1
所以我刚意识到AntoniosK的答案本质上建议相同,只是你可以稍微简化代码,因为没有理由将纯函数调用包装成公式:myfuns = c(f1 = ~ . ^ 2, f2 = exp, f3 = sqrt)。但如果要在多个变量中执行相同操作,则需要使用quosures/rlang。 - Konrad Rudolph
6个回答

7

对于只需要单个输入的简单方程,仅提供函数本身即可,例如:

iris %>% mutate_at(vars(-Species), sqrt)

或者,当使用方程而不是简单函数时,通过公式实现:
iris %>% mutate_at(vars(-Species), ~ . ^ 2)

当使用涉及多个变量的方程式时,需要使用rlang quosures:

area = quo(Sepal.Length * Sepal.Width)
iris %>% mutate(Sepal.Area = !! area)

在这里,quo 创建了一个“quosure”,也就是你的方程的引用表示。与字符串不同的是,它是正确作用域的,可以直接通过 dplyr 使用,并且概念上更加清晰:它像其他 R 表达式一样,只是尚未被评估。区别如下:

  • 1 + 2 是一个值为 3 的表达式。
  • quo(1 + 2) 是一个未评估的表达式,其值为 1 + 2,它需要明确进行评估。那么我们如何评估一个未评估的表达式呢?嗯 …:

然后,!!(读作“bang bang”) 取消引用 先前引用的表达式,即在 mutate 的上下文中评估它。这很重要,因为 Sepal.LengthSepal.Width 仅在 mutate 调用内部已知,而在外部不可见。


在所有上述情况中,表达式也可以存在于列表中。唯一的区别是对于列表,需要使用 !!! 而不是 !!

funs = list(
    Sepal.Area = quo(Sepal.Length * Sepal.Width),
    Sepal.Ratio = quo(Sepal.Length / Sepal.Width)
)

iris %>% mutate(!!! funs)
!!! 操作被称为“反引号插入”,其思想是将其参数的列表元素“插入”到父调用中。也就是说,它似乎修改了调用,就好像它包含了列表元素作为参数一样(这只在支持该操作的函数中有效,例如mutate)。

谢谢,但如何在列表内使用表达式? 设置 area2 = list(f1 = quo(Sepal.Length * Sepal.Width), f2 = quo(Sepal.Length + Sepal.Width)) 不起作用,使用 mutate(!! area2) 也不行。 - Cettt
@Cettt 抱歉,我应该加上那段代码。请查看修改后的答案。 - Konrad Rudolph

6
将您的字符串转换为表达式
myexprs <- purrr::map( myfuns, rlang::parse_expr )

然后使用quasiquotation将这些表达式传递给常规的mutate函数:

tibble(a = 1:3) %>% mutate( !!!myexprs )
# # A tibble: 3 x 4
#       a    f1    f2    f3
#   <int> <dbl> <dbl> <dbl>
# 1     1     1  2.72  1   
# 2     2     4  7.39  1.41
# 3     3     9 20.1   1.73

请注意,这也适用于涉及多列的字符串/表达式。

你可以使用rlang::parse_exprs(parse_expr的复数形式)来代替使用purrr::map。甚至可以内联:mutate(!!! parse_exprs(myfuns)) - zeehio
2
@zeehio parse_exprs无法保留表达式名称(f1, f2, f3),这些名称将成为结果的列名称。我今天早些时候在GitHub问题上提出了这个问题。 - Artem Sokolov
抱歉误导了您,我没有考虑到那个问题 :-S 谢谢您提出的 Github 问题! - zeehio

4
一个基础的替代品:
myfuns <- c(f1 = "a^2", f2 = "exp(a)", f3 = "sqrt(a)")
df <- data.frame(a = 1:3)
df[names(myfuns)] <- lapply(myfuns , function(x) eval(parse(text= x), envir = df))
df
#>   a f1        f2       f3
#> 1 1  1  2.718282 1.000000
#> 2 2  4  7.389056 1.414214
#> 3 3  9 20.085537 1.732051

这段内容由reprex软件包(版本v0.3.0)于2019年07月08日创建


4

您只有一列,因此以下两种方法将得到相同的结果。

您只需要修改函数列表即可。

library(dplyr)

myfuns <- c(f1 = ~.^2, f2 = ~exp(.), f3 = ~sqrt(.))

tibble(a = 1:3) %>% mutate_at(vars(a), myfuns)

tibble(a = 1:3) %>% mutate_all(myfuns)


# # A tibble: 3 x 4
#       a    f1    f2    f3
#   <int> <dbl> <dbl> <dbl>
# 1     1     1  2.72  1   
# 2     2     4  7.39  1.41
# 3     3     9 20.1   1.73

很好,如果所有的“变异”都涉及相同的变量,那么这个方法完美地奏效,就像我在问题中所描述的那样。 然而,我更感兴趣的是一种解决方案,可以使用可能不同的函数对多个变量进行变异。 - Cettt
您的意思是每个变量都将使用自己特定的函数集进行变异吗? - AntoniosK
是的,类似这样 myfuns = c(f1 = "a^2", f2 = "a+b", f3 = "sqrt(b)") - Cettt
1
这是不同的,因为每个函数都可以使用任何列或列的组合。我认为最好您发布另一个带有简单示例的问题。这是您以前使用mutate_(.dots = ...)做过的事情吗? - AntoniosK
是的,我知道,我没有意识到这会有很大的区别,因为我已经想好了基于rlang的解决方案。如果需要的话,我会再等一段时间并提出另一个问题。mutate_(.dots = ...)可以处理多个变量和多个函数。 - Cettt

1
一种方法是使用来自 rlang 的 parse_expr。
library(tidyverse)
library(rlang)

tibble(a = 1:3) %>% 
   mutate(ans =  map(myfuns, ~eval(parse_expr(.)))) %>%
   #OR mutate(ans =  map(myfuns, ~eval(parse(text  = .)))) %>%
   unnest() %>%
   group_by(a) %>%
   mutate(temp = row_number()) %>%
   spread(a, ans) %>%
   select(-temp) %>%
   rename_all(~names(myfuns))

# A tibble: 3 x 3
#    f1    f2    f3
#  <dbl> <dbl> <dbl>
#1     1  2.72  1   
#2     4  7.39  1.41
#3     9  20.1  1.73

1
你也可以尝试使用 purrr 方法。
# define the functions
f1 <- function(a) a^2
f2 <- function(a, b) a + b
f3 <- function(b) sqrt(b)

# put all functions in one list
tibble(funs=list(f1, f2, f3)) %>%
  # give each function a name 
  mutate(fun_id=paste0("f", row_number())) %>% 
  # add to each row/function the matching column profile
  # first extract the column names you specified in each function 
  #mutate(columns=funs %>% 
  #         toString() %>% 
  #         str_extract_all(., "function \\(.*?\\)", simplify = T) %>% 
  #         str_extract_all(., "(?<=\\().+?(?=\\))", simplify = T) %>%
  #         gsub(" ", "", .) %>% 
  #         str_split(., ",")) %>%
  # with the help of Konrad we can use fn_fmls_names
  mutate(columns=map(funs, ~ rlang::fn_fmls_names(.)))  %>% 
  # select the columns and add to our tibble/data.frame  
  mutate(params=map(columns, ~select(df, .))) %>% 
  # invoke the functions
  mutate(results = invoke_map(.f = funs, .x = params)) %>% 
  # transform  to desired output
  unnest(results) %>% 
  group_by(fun_id) %>% 
  mutate(n=row_number()) %>% 
  spread(fun_id, results) %>% 
  left_join(mutate(df, n=row_number()), .) %>% 
  select(-n)
Joining, by = "n"
# A tibble: 5 x 5
      a     b    f1    f2    f3
  <dbl> <dbl> <dbl> <dbl> <dbl>
1     2     1     4     3  1   
2     4     1    16     5  1   
3     5     2    25     7  1.41
4     7     2    49     9  1.41
5     8     2    64    10  1.41

一些数据

df <- data_frame(
  a = c(2, 4, 5, 7, 8),
  b = c(1, 1, 2, 2, 2))

很抱歉直言,但这是糟糕的代码,这正是为什么不应该使用字符串来处理R表达式的原因。这种绕路在概念上非常复杂,而这种复杂性体现在你的代码中。没有理由通过字符串走弯路。 - Konrad Rudolph
@KonradRudolph 没问题。我知道这不是一种优雅的解决方案。你知道提取函数术语的更好方法吗? - Roman
看我的答案。如果你有简单的函数,你可以直接使用它们 (… %>% mutate_at(vars(-Species), list(f1, f2, f3)) - Konrad Rudolph
@KonradRudolph 但是使用示例数据和函数 df %>% mutate_at(vars(a, b), list(f1, f2, f3)) 是不可能的,对吧?请看 Artem Sokolov 的答案。这很完美。我只知道 invoke_map 并试图找到一个解决方案,对于看起来很糟糕的问题感到抱歉。您可以自行添加所需的列名 ;) - Roman
2
@KonradRudolph 你说得对。尽管OP使用了“function”这个术语,但我有点假设myfuns实际上是在某个给定数据框的上下文中定义的一组表达式,而不是具有自己作用域的适当函数。为了记录,我也不赞成使用字符串编写代码,但“如何在dplyr动词中使用代码文本?”这个问题似乎时不时会出现。 - Artem Sokolov
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接