【问题标题】:Using quotations inside mutate: an alternative to mutate_(.dots = ...)在 mutate 中使用引号:mutate_(.dots = ...) 的替代方法
【发布时间】:2019-11-17 21:00:37
【问题描述】:

我想对 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 个函数,因此不能一个一个地键入它们。

提前致谢。

【问题讨论】:

  • “这些函数存储在一个字符串中。” ——这基本上是个坏主意。这些功能从何而来?如果在代码中,将它们存储为未计算的表达式或实际函数。不要(ab)使用字符串来表示代码。
  • 嗯,说来话长。基本上每个组件都是一个冗长的公式,可以用某个公式“拼写”出来。这就是它们存储在字符串中的原因。我不知道(现在仍然不)有任何其他方法可以规避这一点。
  • 如果它可以拼写为字符串,那么它肯定也可以拼写为代码吗?只需删除周围的 "…" 并使用公式 (~ …) 或 函数 (function (x) …)。
  • 谢谢康拉德,我会试试的。
  • 所以我刚刚意识到 AntoniosK 的回答基本上是相同的,除了您可以稍微简化代码,因为没有理由将纯函数调用包装到公式中:myfuns = c(f1 = ~ . ^ 2, f2 = exp, f3 = sqrt)。不过,要对多个变量执行相同操作,您需要使用 quosures/rlang。

标签: r dplyr rlang


【解决方案1】:

对于采用单个输入的简单方程,提供函数本身就足够了,例如

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 creates a “quosure” — 即您的方程式的引用表示,与您对字符串的使用相同,除了与字符串不同的是,此表达式具有适当的范围,可由 dplyr 直接使用,并且在概念上更清晰:它就像任何其他 R 表达式,除非尚未评估。区别如下:

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

然后!! (pronounced “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)

!!! 操作称为“unquote-splice”。这个想法是将其参数的列表元素“拼接”到父调用中。也就是说,它似乎修改了调用,就好像它包含作为参数的列表元素一样(但这仅适用于支持它的函数,例如 mutate)。

【讨论】:

  • 谢谢,但是如何处理列表中的表达式呢?设置area2 = list(f1 = quo(Sepal.Length * Sepal.Width), f2 = quo(Sepal.Length + Sepal.Width)) 不起作用,使用mutate(!! area2) 不起作用
  • @Cettt 抱歉,我应该为此添加代码。查看修改后的答案。
【解决方案2】:

将字符串转换为表达式

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 parse_exprs 不保留表达式名称(在这种情况下为f1f2f3),它们将成为结果的列名称。我今天早些时候打开了GitHub issue
  • 对误导性评论表示歉意,我没有考虑过这个问题:-S 感谢 github 问题!
【解决方案3】:

您只有一列,因此以下两种方法都会给您相同的结果。

你只需要修改你的函数列表。

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

【讨论】:

  • 太好了,如果所有“突变”都涉及同一个变量,这将非常有效,就像我的问题中的情况一样。但是,我更感兴趣的解决方案是多个变量可以通过可能的不同函数进行变异。
  • 您的意思是每个变量都将使用其自己的特定函数集进行变异?
  • 是的,比如myfuns = c(f1 = "a^2", f2 = "a+b", f3 = "sqrt(b)")
  • 这是不同的,因为这些函数中的每一个都可以使用任何列或列的组合。我认为最好用一个简单的例子发布另一个问题。那是你以前用mutate_(.dots = …) 做的事情吗?
  • 是的,我知道,我没有意识到这会产生很大的不同,因为我已经确定了基于 rlang 的解决方案。如有必要,我会再等一会儿,然后再问一个问题。 mutate_(.dots = ...) 可以用多个函数处理多个变量。
【解决方案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 package (v0.3.0) 于 2019 年 7 月 8 日创建

【讨论】:

    【解决方案5】:

    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
    

    【讨论】:

      【解决方案6】:

      您也可以尝试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 表达式的原因。绕道在概念上很复杂,这种复杂性在您的代码中体现出来。 没有理由通过字符串绕道。
      • @KonradRudolph 没问题。我知道这不是一个优雅的解决方案。你知道提取函数项的更好方法吗?
      • 看我的回答。如果你有简单的功能,你可以直接使用它们(… %&gt;% mutate_at(vars(-Species), list(f1, f2, f3)).
      • @KonradRudolph 但这对于示例数据和函数是不可能的:df %&gt;% mutate_at(vars(a, b), list(f1, f2, f3)),对吧?请参阅 Artem Sokolov 的答案。太棒了。我只知道invoke_map 并试图找到具有此功能的解决方案,抱歉看起来很糟糕。您可以事先自行添加所需的列名;)
      • @KonradRudolph 你是对的。尽管 OP 使用术语“函数”,但我有点假设 myfuns 实际上是在某些给定数据框的上下文中定义的一组表达式,而不是具有自己范围的适当函数。郑重声明,我也不赞成用字符串编写代码,但问题是“我如何在 dplyr 动词中使用代码文本?”似乎不时出现。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-09
      • 2015-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多