使用R数据表中的非标准评估方法理解作用域

3
如何确保我使用data.table进行非标准评估时,可以从父框架继承所需的变量?
根据我对动态作用域的理解,我下面的代码应该可以工作,但它没有。我做错了什么?
细节: 我有一个函数列表,我想将其应用于单个data.table,返回布尔检查和消息(当检查为TRUE时)。例如,假设我正在审核一个帐户表。
library(data.table)
#----- Example data -----------------------------------------------------------
n <- 100
set.seed(123)
df <- data.table( acct_id      = paste0('ID',seq(n)),
                  acct_balance = round(pmax(rnorm(n,1000,5000),0)),
                  days_overdue = round(pmax(rnorm(n,20,20),0))
                  )
#----- Example list of rules to check (real case has more elements)------------
AuditRules <- list(
  list(
    msg_id = 1,
    msg_cat = 'Balance',
    cond_fn = function(d) d[, acct_balance > balance_limit ],
    msg_txt = 
      function(d) d[, paste('Account',acct_id,'balance is',
                            acct_balance - balance_limit, 
                            'over the limit.')]
  ),
  list(
    msg_id = 2,
    msg_cat = 'Overdue',
    cond_fn = function(d) d[, days_overdue > grace_period ],
    msg_txt = 
      function(d) d[, paste('Account',acct_id,'is overdue',
                            days_overdue-grace_period,
                            'days beyond grace period.')]
  )
)

我正在循环遍历规则列表,并在每个规则上检查数据集。

期望的输出

在全局环境中,这样做是有效的。

balance_limit <- 1e4
grace_period  <-  14
audit <- rbindlist(
              lapply(AuditRules, function(item){
                with( item,
                      df[ cond_fn(df),
                         .(msg_id, 
                           msg_cat,
                           msg_txt = msg_txt(.SD) )
                         ]
                      )
                } )
            )
print(head(audit), row.names=FALSE)
#-----------------   Result   --------------------------------------
# msg_id msg_cat                                             msg_txt
#      1 Balance        Account ID44 balance is 1845 over the limit.
#      1 Balance        Account ID70 balance is 1250 over the limit.
#      1 Balance        Account ID97 balance is 1937 over the limit.
#      2 Overdue Account ID2 is overdue 11 days beyond grace period.
#      2 Overdue  Account ID3 is overdue 1 days beyond grace period.
#      2 Overdue  Account ID6 is overdue 5 days beyond grace period.

有哪些问题(需要解决)


rm(balance_limit, grace_period) # see "aside"

auditTheData <- function(d, balance_limit = 1e4, grace_period=14){
  rbindlist(
    lapply(AuditRules, function(item){
        with( item,
              d[ cond_fn(d),
                  .(msg_id, 
                    msg_cat,
                    msg_txt = msg_txt(.SD) )
                  ]
        )
    } )
  )
}
auditTheData(df)

导致错误的原因是:

Error in eval(jsub, SDenv, parent.frame()) : 
  object 'balance_limit' not found

虽然我读过一些关于编程时应该避免使用 with() 的文章,但这并不是与 with() 有关的问题。以下代码也无法正常工作:

auditTheData2 <- function(d, balance_limit = 1e4, grace_period=14){
  rbindlist(
    lapply(AuditRules, function(item){
          d[ item[['cond_fn']](d),
             .(msg_id, 
               msg_cat,
               msg_txt = item[['msg_txt']](.SD) )
             ]
    } )
  )
}
auditTheData2(df) # Same error
提示: 如果在“不起作用”的函数之前没有执行rm(balance_limit, grace_period) -- 也就是说,把它们留在全局环境中 -- 你会得到期望的结果。因此,似乎被lapply应用的function(item)可以“看到”全局环境,但不能看到父环境(AuditTheData)。


1这里我在非科学意义上使用“非标准”一词,表示“不寻常”。我不知道什么算是非标准,但这是另一个(而且太广泛?)的问题。

1个回答

3
这似乎可行:
ar <- list(
  list(
    cat = 'Balance',
    cond_expr = quote(acct_balance > balance_limit),
    msg_expr = quote(sprintf('Account %s balance is %s over the limit.',
      acct_id, 
      acct_balance - balance_limit))
  ),
  list(
    cat = 'Overdue',
    cond_expr = quote(days_overdue > grace_period),
    msg_expr = quote(sprintf('Account %s is overdue %s days beyond grace period.', 
      acct_id, 
      days_overdue-grace_period))
  )
)

audDT = rbindlist(rapply(ar, list, "call", how = "replace"), id="msg_id")

auditem = function(d, a, balance_limit = 1e4, grace_period = 14){
    a[, {
        cond    = cond_expr[[1]]
        msg     = msg_expr[[1]]
        .(txt = d[eval(cond), eval(msg)])
    }, by=.(msg_id, cat)]
}

For example ...

> head(auditem(df, audDT))
   msg_id     cat                                                 txt
1:      1 Balance        Account ID44 balance is 1845 over the limit.
2:      1 Balance        Account ID70 balance is 1250 over the limit.
3:      1 Balance        Account ID97 balance is 1937 over the limit.
4:      2 Overdue Account ID2 is overdue 11 days beyond grace period.
5:      2 Overdue  Account ID3 is overdue 1 days beyond grace period.
6:      2 Overdue  Account ID6 is overdue 5 days beyond grace period.

我不确定这些更改中哪一个起了作用:
  • 使用eval预定义表达式,而不是在函数内部组合它们到 j
  • 使用规则表格,并享有以下几个好处:
    • 由于每个条目应具有相同的结构,因此可以验证每个条目是否形成良好(没有缺失的组件)
    • msg_id 可以使用 rbindlist 自动编号,无需手动输入
    • 可以使用 by= 而不是 lapply ,因为后者具有一些奇怪的评估行为

我还将 paste 切换为sprintf,但确信那并不重要。

rapply 是必要的,因为 data.table 不支持调用/表达式作为列类型(显然),但支持列表列。


1
“rapply”对我来说是新的。我花了一段时间才让它工作。结果发现我的子列表排序不一致(尽管名称一致),这意外地导致了一个错误,并产生了一个关于“bmerge”的误导性错误消息。再次感谢Frank。 - C8H10N4O2

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