在R中使用lapply或with函数的用户定义函数中进行非标准评估

3

我编写了一个ftable的包装器,因为我需要计算多个变量的频率和百分比的平面表格。由于类“formula”的ftable方法使用非标准评估,所以该包装器依赖于do.callmatch.call来允许使用ftablesubset参数(更多细节请参见我的先前问题)。

mytable <- function(...) {
    do.call(what = ftable,
            args = as.list(x = match.call()[-1]))
    # etc
}

然而,我不能在 lapplywith 中使用这个包装器:

# example 1: error with "lapply"
lapply(X = warpbreaks[c("breaks",
                        "wool",
                        "tension")],
       FUN = mytable,
       row.vars = 1)

Error in (function (x, ...)  : object 'X' not found

# example 2: error with "with"
with(data = warpbreaks[warpbreaks$tension == "L", ],
     expr = mytable(wool))

Error in (function (x, ...)  : object 'wool' not found

这些错误似乎是由于未在正确的环境中评估match.call引起的。
由于这个问题与我的上一个问题密切相关,这里总结一下我的问题:
- 使用do.callmatch.call的包装器无法与lapplywith一起使用。 - 不使用do.callmatch.call的包装器无法使用ftablesubset参数。
以下是我的问题总结:
我该如何编写一个包装器,既可以使用ftablesubset参数,又可以与lapplywith一起使用?我有避免使用lapplywith的想法,但我希望理解并纠正这些错误,以提高我对R的了解。 lapply的错误是否与?lapply中的以下注释相关?
“由于历史原因,由lapply创建的调用未评估,并且已编写代码(例如,bquote)依赖于此。这意味着记录的调用始终采用FUN(X [ [i]],...),其中i被当前(整数或双精度)索引替换。这通常不是问题,但如果FUN使用sys.call或match.call或者它是使用调用的原始函数,则可能会出现问题。这意味着通常更安全地使用包装器调用原始函数,以便例如需要确保正确发生is.numeric的方法分派的lapply(ll,function(x)is.numeric(x))。”

@RLave 谢谢您的评论。我已经对我的问题进行了大量编辑。希望它有所帮助! - Thomas
@Swolf 谢谢,但我还是得到了同样的错误。你呢? - Thomas
我理解的是,您想将传递给 mytable 的所有参数传递到 ftable 中,这正确吗? - divibisan
@divibisan 是的,完全正确! - Thomas
3个回答

2
使用match.calllapply的问题在于,match.call返回传递给它的字面调用,而没有任何解释。为了看清楚发生了什么,请创建一个更简单的函数,该函数显示您的函数如何解释传递给它的参数:

最初的回答

match_call_fun <- function(...) {
    call = as.list(match.call()[-1])
    print(call)
}

当我们直接调用时,match.call会正确获取参数并将它们放入一个列表中,我们可以使用do.call来使用这个列表:

最初的回答

match_call_fun(iris['Species'], 9)

[[1]]
iris["Species"]

[[2]]
[1] 9

但是当我们使用lapply时,请观察会发生什么(我只包含了内部print语句的输出):

最初的回答:

lapply('Species', function(x) match_call_fun(iris[x], 9))

[[1]]
iris[x]

[[2]]
[1] 9

由于match.call获取的是传递给它的字面量参数,因此它接收到的是iris[x],而不是我们想要的正确解释的iris['Species']。当我们使用do.call将这些参数传递给ftable时,它会在当前环境中查找一个名为x的对象,当找不到时返回错误。我们需要对其进行解释。
正如您所见,添加envir = parent.frame()可以解决问题。这是因为添加该参数告诉do.call在父框架中评估iris[x],父框架是lapply中匿名函数的位置,其中x具有其正确的含义。为了看到这个过程,让我们再创建一个简单的函数,使用do.call从3个不同的环境级别打印ls
z <- function(...) {
    print(do.call(ls, list()))
    print(do.call(ls, list(), envir = parent.frame()))
    print(do.call(ls, list(), envir = parent.frame(2)))
}

当我们从全局环境调用z()时,我们会看到函数内部的空环境,然后是全局环境:

最初的回答

z()

character(0)                                  # Interior function environment
[1] "match_call_fun" "y"              "z"     # GlobalEnv
[1] "match_call_fun" "y"              "z"     # GlobalEnv

但是当我们在lapply内部调用时,我们可以看到parent.frame上升了一级,指向lapply中的匿名函数:


lapply(1, z)

character(0)                                  # Interior function environment
[1] "FUN" "i"   "X"                           # lapply
[1] "match_call_fun" "y"              "z"     # GlobalEnv

因此,通过添加envir = parent.frame()do.call知道要在lapply环境中评估iris[x],在该环境中,它知道x实际上是'Species',并且会正确评估。"Original Answer"翻译成“最初的回答”。
mytable_envir <- function(...) {
    tab <- do.call(what = ftable,
                   args = as.list(match.call()[-1]),
                   envir = parent.frame())
    prop <- prop.table(x = tab,
                       margin = 2) * 100
    bind <- cbind(as.matrix(x = tab),
                  as.matrix(x = prop))
    margin <- addmargins(A = bind,
                         margin = 1)
    round(x = margin,
          digits = 1)
}



# This works!
lapply(X = c("breaks","wool","tension"),
       FUN = function(x) mytable_envir(warpbreaks[x],row.vars = 1))

关于为什么添加envir = parent.frame()会有所不同,因为它似乎是默认选项。我并不完全确定,但我的猜测是当使用默认参数时,parent.framedo.call函数中被评估,在其中返回do.call运行的环境。然而,我们正在调用parent.framedo.call之外,这意味着它返回比默认版本高一个级别的环境。
这是一个测试函数,以parent.frame()作为默认值:
fun <- function(y=parent.frame()) {
    print(y)
    print(parent.frame())
    print(parent.frame(2))
    print(parent.frame(3))
}

现在看看我们在 lapply 中调用它时会发生什么,无论是否传入 parent.frame() 作为参数:


lapply(1, function(y) fun())
<environment: 0x12c5bc1b0>     # y argument
<environment: 0x12c5bc1b0>     # parent.frame called inside
<environment: 0x12c5bc760>     # 1 level up = lapply
<environment: R_GlobalEnv>     # 2 levels up = globalEnv

lapply(1, function(y) fun(y = parent.frame()))
<environment: 0x104931358>     # y argument
<environment: 0x104930da8>     # parent.frame called inside
<environment: 0x104931358>     # 1 level up = lapply
<environment: R_GlobalEnv>     # 2 levels up = globalEnv

在第一个例子中,y的值与您在函数内调用parent.frame()时得到的值相同。在第二个例子中,y的值与上一级环境(在lapply内部)相同。因此,尽管它们看起来相同,但实际上它们正在执行不同的操作:在第一个例子中,当它看到没有y=参数时,parent.frame在函数内被评估,在第二个例子中,parent.frame首先在lapply匿名函数中被评估,然后再调用fun并传递给它。"最初的回答"

非常感谢您提供这么详细的答案,它帮助我更深入地理解了问题。然而,关于为什么需要使用 parent.frame(),即使它是默认参数,我不明白为什么默认参数的行为会与手动指定相同的参数有所不同... - Thomas
非常感谢您这一次的编辑!我现在明白了为什么添加envir = parent.frame()即使是do.call的默认参数也有区别。您提供的帮助值得超过+1!请注意:with(data = warpbreaks, expr = z())with(warpbreaks, fun())with(warpbreaks, fun(y = parent.frame()))也表明问题与with相同。 - Thomas
没问题,找出解决方法很有趣!在我的工作中,我通常尽可能避免使用环境,因为我并不完全理解它们。所以花时间深入了解它们的实际工作方式对我来说是很好的。 - divibisan
我终于意识到封装器在使用mapply时失败,尽管在使用lapply时它是有效的。你能否请看一下我的新问题:https://dev59.com/TLTna4cB1Zd3GeqPAM3C? - Thomas
1
就记录而言,关于为什么添加envir = parent.frame()即使它是do.call默认参数也会产生差异的原因:“了解函数参数评估的最重要的事情之一是,提供的参数和默认参数被不同地处理。函数的提供的参数在调用函数的评估框架中进行评估。函数的默认参数在函数的评估框架中进行评估。”(来自https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Argument-evaluation) - Thomas

0

由于您只想将传递给ftable的所有参数传递,因此不需要使用do.call()。

mytable <- function(...) {
  tab <- ftable(...)
  prop <- prop.table(x = tab,
                     margin = 2) * 100
  bind <- cbind(as.matrix(x = tab),
                as.matrix(x = prop))
  margin <- addmargins(A = bind,
                       margin = 1)
  return(round(x = margin,
               digits = 1))
}

以下的lapply会为每个变量单独创建一个表格,我不确定这是否符合您的要求。
lapply(X = c("breaks",
             "wool",
             "tension"),
       FUN = function(x) mytable(warpbreaks[x],
                                 row.vars = 1))

如果您想要将所有3个变量放在一个表格中。
warpbreaks$newVar <- LETTERS[3:4]

lapply(X = cbind("c(\"breaks\", \"wool\", \"tension\")",
             "c(\"newVar\", \"tension\",\"wool\")"),
       FUN = function(X)
        eval(parse(text=paste("mytable(warpbreaks[,",X,"],
                                 row.vars = 1)")))
)

谢谢您的回答。然而,正如我在问题中所解释的那样,我需要使用do.call函数来使用formula类别的ftable方法的子集参数,因为它使用非标准评估(更多细节请参见我的先前问题)。 - Thomas

0

由于这个问题,包装器变成了:

# function 1
mytable <- function(...) {
    do.call(what = ftable,
            args = as.list(x = match.call()[-1]),
            envir = parent.frame())
    # etc
}

或者:

# function 2
mytable <- function(...) {
    mc <- match.call()
    mc[[1]] <- quote(expr = ftable)
    eval.parent(expr = mc)
    # etc
}

我现在可以使用ftablesubset参数,并在lapply中使用包装器:

lapply(X = warpbreaks[c("wool",
                        "tension")],
       FUN = function(x) mytable(formula = x ~ breaks,
                                 data = warpbreaks,
                                 subset = breaks < 15))

然而,我不明白为什么我必须提供envir = parent.frame()do.call,因为它是一个默认参数。

更重要的是,这些方法无法解决另一个问题:我无法使用mapplysubset参数与ftable一起使用


我已经发布了一个答案,希望能更深入地解释这里发生的事情,尽管你已经基本上自己找到了答案。对于你的“奖励问题”,你应该为它们提出新的问题 - StackOverflow 的格式是针对每个问题一个问题的。 - divibisan

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