使用数据掩码在R中评估最大似然表达式

4
我正在尝试使用数据掩码评估最大似然表达式。 这个想法是允许在函数内按名称调用参数和变量,同时避免多次调用attach()和detach()。这是一个非常简化的小例子,真实函数相当大且复杂。
set.seed(1)

# Data
db <- data.frame(
  x = runif(10),
  y = runif(10),
  z = sample(c(0, 1), 10, replace = TRUE)
)

# Log likelihood function
ll_lik <- function(param) {
  pr_1 <- 1 / (1 + exp(-(param[1]*x - param[2]*y)))
  pr_2 <- 1 - pr_1
  lik <- z * pr_1 + (1 - z) * pr_2
  log(lik)
}

# Parameters
param <- c(p1 = 0.1, p2 = 0.2)

# Run the model with attach()/detach()
attach(db)
model <- maxLik::maxLik(ll_lik, start = param)
detach(db)
summary(model)


这很好,但是我必须要调用attach()detach()来访问参数。为了按照名称访问参数,我需要在对数似然函数中将param转换成列表,然后再调用attach()/detach()。这不仅很混乱,而且对于大型函数和数据会产生不必要的开销。一个可能性是使用rlang包和主要用于表达式整洁评估的包装器函数。现在,仅创建数据掩码并尝试计算对数似然函数是行不通的。
mask <- as_data_mask(db)
eval_tidy(quo(maxLik::maxLik(ll_lik, start = param)), mask)

它无法访问datamask中的对象(Error in fnOrig(theta, ...) : object 'x' not found)。也许问题出在maxLik上,但是我甚至无法评估ll_lik(),并且这会给出相同的错误:

eval_tidy(quo(ll_lik(param)), mask)

但这是有效的:

eval_tidy(quo(x*3), mask)

因此,我开始怀疑ll_lik()的"错误"父项,这就是为什么我的数据掩码可能不在函数的搜索路径中,因此它无法找到变量。现在,as_data_mask()的帮助部分提供了一些如何通过创建顶层、中间层和底层环境来"嵌套"环境的示例。好的,那么让我们看看是否可以将我的函数作为数据掩码结构的一部分创建:
call_stack <- function() {lobstr::cst()}

# Create a new environment (child of empty) that takes a list of objects to populate it
top <- new_environment(list(ll_lik = ll_lik, call_stack = call_stack))

# Create a child of the "top" environment"
middle <- env(top)

# Create a child of the "middle environment and add the data object to it
bottom <- env(middle, db=db)

# Create a data_mask where the bottom contains the masking elements and the top
# the last element of the data_mask.
new_mask <- new_data_mask(bottom, top = top)

很遗憾,我仍然无法访问x。我甚至没有尝试使用maxLik函数。为了尝试深入了解,我开始调整函数调用堆栈。

eval_tidy(call_stack(), data = new_mask)

实际上,如果我理解正确的话,该函数的父级环境是全局环境。

1. ├─rlang::eval_tidy(call_stack(), data = new_mask)
 2. └─global::call_stack()
 3.   └─lobstr::cst()

然而,我不知道如何使它起作用。非常感激任何帮助。

BONUS: 如果我能在maxLik内部通过名称调用参数而无需调用attach()/detach(),那将是很棒的。


嗨!我不是datamasks的专家,所以无法评论如何使用该方法访问db中的对象,但我只是想问一个(也许很愚蠢的)问题。我在maxLik函数的帮助页面上读到了以下句子:“...:[...]优化器未使用的参数将转发给logLikgradhess”。我认为你可以使用参数xyz定义ll_lik函数,然后使用db$xwith函数传递相应的值。如果您想要,我可以用这种方法提供更详细的答案。 - agila
是的,使用attach()的目的是避免在所有变量前面都加上db$(在某些情况下有相当多的变量)。我想我之前尝试过使用with()函数,但遇到了类似的问题。如果我正确理解with(),它的功能类似于数据掩码,但数据掩码可以包含您可能想要在对数似然函数内访问的数据、函数和其他对象。我也可能误解了这一点。 - edsandorf
1个回答

3
一种选择是创建一个包装器,将ll_lik的主体作为表达式进行评估,并使用db作为上下文:
llwrap <- function(param) {
  eval( body(ll_lik), db )
}

model <- maxLik::maxLik(llwrap, start=param)      # Works

编辑以回答你的问题:是的,body()返回一个表达式,因此您可以在该表达式中使用任何名称,只要您在评估时提供适当的上下文即可。但是,如果您完全将函数体与其参数列表分离,为什么不从一开始就定义它为表达式呢?

ll_expr <- rlang::expr({                       # An expression, not a function
  pr_1 <- 1 / (1 + exp(-(p1*x - p2*y)))        # <-- now using p1, p2
  pr_2 <- 1 - pr_1
  lik <- z * pr_1 + (1 - z) * pr_2
  log(lik)
})

llwrap2 <- function(param) {
  ctx <- c( as.list(db), as.list(param) )      # Combine param and db into one context
  eval( ll_expr, ctx )                         # No longer need body()
}

model <- maxLik::maxLik(llwrap2, start=param)  # Works

有趣。我不知道 body()。这种方法是否也可扩展,以允许按名称调用 param 的元素,而不是 param[1]param[2] 等等? - edsandorf
非常感谢您提供的更新答案。这绝对是朝着正确方向迈出的一步。我会在更大的代码环境中尝试使用您的解决方案,看看它的表现如何。谢谢。 - edsandorf
我对这个问题和答案很感兴趣,你所做的事情有什么更广泛的用途,还是主要用于在数据框中工作时使用?我喜欢不创建环境的想法,你有其他类似问题的链接吗,以便更全面地了解?谢谢。 - user63230
1
@user63230:从技术上讲,它确实创建了一个环境。eval()将把作为第二个参数提供的列表转换为一个环境,在该环境中将评估第一个参数(表达式)。这个答案中的eval模式基本上只是一个复杂的with()。考虑with(mtcars, mpg*cyl)。通过将表达式存储在变量e <- rlang::expr(mpg*cyl)中,我们可以延迟其评估直到mtcars准备好:eval(e, mtcars)。这就是这里发生的事情。数据框是最典型的情况,但任何具有变量屏蔽的东西都是公平的游戏。 - Artem Sokolov
1
谢谢,所以这只是变量掩码,类似于mutate。我所说的关于创建环境的意思是你不必显式地创建一个,比如envr <- new.env(),然后填充它! - user63230
1
在表达式求值的上下文中,您可以将列表、数据框和环境视为可互换的。因此,是的,这是真的:我们没有明确调用 new.env()。但是,我们使用 c(as.list(db), as.list(param)) 显式地创建了一个列表,然后通过 eval() 隐式地将其转换为环境。 - Artem Sokolov

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