【问题标题】:Passing data-variables to R formulas将数据变量传递给 R 公式
【发布时间】:2023-02-24 11:46:33
【问题描述】:

假设我想写anscombe %>% lm_tidy("x1", "y1")(实际上,我想写anscombe %>% lm_tidy(x1, y1),其中x1和y1是数据框的一部分).因此,由于以下功能似乎有效:

plot_gg <- function(df, x, y) {
  x <- enquo(x)
  y <- enquo(y)
  ggplot(df, aes(x = !!x, y = !!y)) + geom_point() +
    geom_smooth(formula = y ~ x, method="lm", se = FALSE)
}

我开始编写以下函数:

lm_tidy_1 <- function(df, x, y) {
  x <- enquo(x)
  y <- enquo(y)
  fm <- y ~ x            ##### I tried many stuff here!
  lm(fm, data=df)
}
## Error in model.frame.default(formula = fm, data = df, drop.unused.levels = TRUE) : 
##   object is not a matrix

passing in column name as argument 中的一条评论指出,embrace {{...}} 是引号-反引号模式的简写符号。不幸的是,两种情况下的错误消息都不同:

lm_tidy_2 <- function(df, x, y) {
  fm <- !!enquo(y) ~ !!enquo(x) # alternative: {{y}} ~ {{x}} with different errors!!
  lm(fm, data=df)
}
## Error:
## ! Quosures can only be unquoted within a quasiquotation context.

这似乎有效(基于@jubas's answer,但我们坚持使用字符串处理和paste):

lm_tidy_str <- function(df, x, y) {
  fm <- formula(paste({{y}}, "~", {{x}}))
  lm(fm, data=df)
}

再一次,{{y}} != !!enquo(y)。但更糟糕的是:以下函数出现与之前相同的 Quosure 错误:

lm_tidy_str_1 <- function(df, x, y) {
  x <- enquo(x)
  y <- enquo(y)
  fm <- formula(paste(!!y, "~", !!x))
  lm(fm, data=df)
}
  1. 是{{y}} != !!enquo(y)吗?
  2. 如何将数据变量传递给lm?

    编辑:对不起,我的许多试验都有遗留问题。我想直接将数据变量(比如x1和y1)传递给将它们用作公式组件的函数(例如lm)而不是它们的字符串版本("x1"和@987654342 @): 我尽量避免使用字符串,从用户的角度来看,它更加精简。

【问题讨论】:

  • 首先,您是传递带引号的变量还是不带引号的变量?即字符串与符号?另外,如果您要编写这样的函数,为什么不直接使用lm.fit?
  • 举例说明您希望如何使用它,以及为什么需要它
  • 你知道包rlang - 它有元编程的功能。 - 首先 - 请向我们展示您想要抽象的代码 - 哪些代码 - 以及您想要抽象的代码的哪些部分?
  • 您可以使用 x &lt;- if (is.character(substitute(x))) x else deparse(substitute(x)) 将带引号或不带引号的变量转换为字符串。然后lm(reformulate(x, y), data = data)一行代码不需要添加依赖
  • @rawr ino 需要if else。只需 as.charcter(substitute(x)) 即可。还要检查提供的答案

标签: r function lazy-evaluation lm


【解决方案1】:

考虑:

lm_tidy_1 <- function(df, x, y) {
  fm <- reformulate(as.character(substitute(x)), substitute(y))
  lm(fm, data=df)
}

lm_tidy_1(iris, Species, Sepal.Length)
lm_tidy_1(iris, 'Species', Sepal.Length)
lm_tidy_1(iris, Species, 'Sepal.Length')
lm_tidy_1(iris, 'Species', 'Sepal.Length')

编辑:

如果您需要公式出现,请更改调用对象:

lm_tidy_1 <- function(df, x, y) { 
   fm <- reformulate(as.character(substitute(x)), substitute(y)) 
   res<-lm(fm, data=df) 
   res$call[[2]]<- fm
   res
}

lm_tidy_1(iris, Species, Sepal.Length) 

Call:
lm(formula = Sepal.Length ~ Species, data = df)

Coefficients:
      (Intercept)  Speciesversicolor   Speciesvirginica  
            5.006              0.930              1.582  

【讨论】:

  • 您的解决方案有效。一个小问题是我们丢失了实际的公式(只显示 fm)。为什么 substitute 所以我们坚持使用 reformulate 转换回字符串?我一直在寻找元编程的另一种方式。
  • @greendiod 请注意,任何作为变量传递给lm 的公式都将丢失,除非您更改对象的调用。其次,任何地方都不会更改为字符串。 reformulate 创建公式而不是字符串。
  • 我知道 reformulate 返回一个公式。但它确实将字符串作为输入。
  • @greendiod 有问题吗?你想让它在传递字符串时抛出错误吗?
  • @onyambu 调用 [[2]] 很好!最后,R 的语法看起来很乱——无论使用哪种方法——你的或我的方法 ;)。
【解决方案2】:

@BiranSzydek 的回答很好。 但是它有3个缺点:

Call:
lm(formula = fm, data = .)
  1. 看不到实际使用的公式或数据。
  2. 必须将符号作为字符串输入。
  3. 来自rlang 的依赖 - 虽然它是一个很棒的包。

    你确实可以用纯 base R 解决这个问题!

    纯碱基 R 的解决方案

    R 实际上是 Lisp 的底层。它适用于此类元编程任务。 R 的唯一缺点是它可怕的语法。 特别是在面对元编程时,它不像 Lisp 语言那样漂亮和优雅。语法确实会造成很多混淆 - 正如您在尝试解决此问题时亲身经历的那样。

    解决方案是使用substitute(),您可以通过它以引用的方式替换代码片段:

    lm_tidy <- function(df, x, y) {
      # take the arguments as code pieces instead to evaluate them:
      .x <- substitute(x)
      .y <- substitute(y)
      .df <- substitute(df)
      # take the code piece `y ~ x` and substitute using list lookup table
      .fm <- substitute(y ~ x, list(y=.y, x=.x))
      # take the code `lm(fm, data=df)` and substitute with the code pieceses defined by the lookup table
      # by replacing them by the code pieces stored in `.fm` and `.df`
      # and finally: evaluate the substituted code in the parent environment (the environment where the function was called!)
      eval.parent(substitute(lm(fm, data=df), list(fm=.fm, df=.df)))
    }
    

    诀窍是使用eval.parent(substitute( &lt;your expression&gt;, &lt;a list which determines the evaluation lookup-table for the variables in your expression&gt;))。

    小心范围界定!只要 &lt;your expression&gt; 仅使用在函数内部或在给定 substitute() 的查找列表中定义的变量构造,就不会有任何范围界定问题!但避免引用&lt;your expression&gt; 内的任何其他变量! - 因此,这是您在这种情况下安全使用eval()/eval.parent()必须遵守的唯一规则! 但即使 eval.parent() 小心,替换的代码 在调用此函数的环境中执行。

    现在,你可以这样做:

    lm_tidy(mtcars, cyl, mpg)
    

    输出现在符合要求:

    Call:
    lm(formula = mpg ~ cyl, data = mtcars)
    
    Coefficients:
    (Intercept)          cyl  
         37.885       -2.876  
    

    我们用纯碱基 R 做到了这一点!

    安全使用 eval() 的诀窍实际上是 substitute() 表达式中的每个变量都在 substitute() 或函数参数的查找表中定义/给出。换句话说:没有一个被替换的变量引用函数定义之外的任何悬空变量。

    plot_gg函数

    因此,按照这些规则,您的 plot_gg 函数将定义为:

    plot_gg <- function(df, x, y) {
      .x <- substitute(x)
      .y <- substitute(y)
      .df <- substitute(df)
      .fm <- substitute( y ~ x, list(x=.x, y=.y))
      eval.parent(substitute(
        ggplot(df, aes(x=x, y=y)) + geom_point() +
          geom_smooth(formula = fm, method="lm", se=FALSE),
        list(fm=.fm, x=.x, y=.y, df=.df)
      ))
    }
    

    当您想输入 x 和 y 作为字符串时

    
    lm_tidy_str <- function(df, x, y) {
      .x <- as.name(x)
      .y <- as.name(y)
      .df <- substitute(df)
      .fm <- substitute(y ~ x, list(y=.y, x=.x))
      eval.parent(substitute(lm(fm, data=df), list(fm=.fm, df=.df)))
    }
    
    plot_gg_str <- function(df, x, y) {
      .x <- as.name(x)
      .y <- as.name(y)
      .df <- substitute(df)
      .fm <- substitute( y ~ x, list(x=.x, y=.y))
      eval.parent(substitute(
        ggplot(df, aes(x=x, y=y)) + geom_point() +
          geom_smooth(formula = fm, method="lm", se=FALSE),
        list(fm=.fm, x=.x, y=.y, df=.df)
      ))
    }
    
    lm_tidy_str(mtcars, "cyl", "mpg")
    
    # Call:
    # lm(formula = mpg ~ cyl, data = mtcars)
    # 
    # Coefficients:
    # (Intercept)          cyl  
    #      37.885       -2.876  
    # 
    
    require(ggplot2)
    plot_gg_str(mtcars, "cyl", "mpg")
    
    
    
    

【讨论】:

  • plot_gg 函数确实按原样工作,比如 anscombe %&gt;% plot_gg(x2, y2)。问题是:当我尝试对lm_tidy_1(我的第一次尝试)中的公式组件使用相同的机制时,它不再起作用。
  • 顺便说一句,感谢纯基础 R 解决方案,但我不介意对 dplyr/rlang 的额外依赖,因为无论如何我都会在其他地方使用他们的设施。另一句话:您的解决方案适用于我的预期用例anscombe %&gt;% lm_tidy(x1, y1),而anscombe %&gt;% lm_tidy("x1", "y1")导致错误。而且,它似乎接近@onyambu 的解决方案,它在两种情况下都适用。
  • 是的,如果您故意将 x1 和 y1 作为字符串提供,则它不起作用。您保存了 4 个击键 - 或者您希望它们可以作为字符串进行修改?
  • @greendiod 你用lm_tidy_1 试过什么?
  • @greendiod 你还需要使用字符串吗?您似乎说您只需要符号,但您的 cmets 建议您还需要字符串。
【解决方案3】:

将公式包裹在“expr”中,然后对其求值。

library(dplyr)
lm_tidy <- function(df, x, y) {
  x <- sym(x)
  y <- sym(y)
  fm <- expr(!!y ~ !!x)
  lm(fm, data = df)
}

这个函数是等价的:

lm_tidy <- function(df, x, y) {
  fm <- expr(!!sym(y) ~ !!sym(x))
  lm(fm, data = df)
}

然后

lm_tidy(mtcars, "cyl", "mpg")

给

Call:
lm(formula = fm, data = .)

Coefficients:
(Intercept)          cyl  
     37.885       -2.876  

根据以下评论进行编辑:

library(rlang)
lm_tidy_quo <- function(df, x, y){
    y <- enquo(y)
    x <- enquo(x)
    fm <- paste(quo_text(y), "~", quo_text(x))
    lm(fm, data = df)
}

然后您可以将符号作为参数传递

lm_tidy_quo(mtcars, cyl, mpg)

【讨论】:

  • 您的解决方案适用于我最初的 anscombe %&gt;% lm_tidy("x1", "y1") 用例,但不适用于我预期的用例 anscombe %&gt;% lm_tidy(x1, y1)。 sym 和 enquo 有什么区别?
  • @greendiod sym是rlang/dplyr用字符串制作符号的方法。在 base R 中,您使用 as.name() 代替。 enquo() 等同于 substitute() 接受没有评估的参数。它来自'enquote'。
  • 当我尝试将 anscombe %&gt;% lm_tidy_enquo("x1", "y1") 与所有 sym 调用替换为 enquo 时,我得到了一个无效的模型公式错误。那么如果enquo 等同于substitute,那么dplyr/rlang 等同于eval 是什么?
  • 此处详细讨论了取消引用:adv-r.hadley.nz/evaluation.html,包括使用 eval。还讨论了我们如何使用 expr_print(fm) 在函数调用中显示指定的模型,因此我们可以通过以下方式解决@Gwang-jin Kim 的反对意见 #1:lm_tidy <- function(df, x, y) { fm < - expr(!!sym(y) ~ !!sym(x)) rlang::expr_print(fm) lm(fm, data = df)}
  • @BrianSyzdek 你介意使用数据变量/符号而不是列名作为字符串来编辑你的答案吗?
猜你喜欢
  • 1970-01-01
  • 2013-01-18
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 2017-10-25
  • 2018-01-02
相关资源
最近更新 更多