R 中的 xtab 和聚合之间 na.action 的不一致

Inconsistency of na.action between xtabs and aggregate in R

我有以下 data.frame:

x <- data.frame(A = c("Y", "Y", "Z", NA),
                B = c(NA, TRUE, FALSE, TRUE),
                C = c(TRUE, TRUE, NA, FALSE))

我需要用 xtabs 计算以下 table:

A      B C
  Y    1 2
  Z    0 0
  <NA> 1 0

,确实 returns table 我需要:

xtabs(formula = cbind(B, C) ~ A,
      data = x,
      addNA = TRUE,
      na.action = NULL)

A      B C
  Y    1 2
  Z    0 0
  <NA> 1 0

但是,na.action = na.passreturns一个不同的table:

xtabs(formula = cbind(B, C) ~ A,
      data = x,
      addNA = TRUE,
      na.action = na.pass)

A       B  C
  Y        2
  Z     0   
  <NA>  1  0

但是 xtabs 的文档说:

na.action
When it is na.pass and formula has a left hand side (with counts), sum(*, na.rm = TRUE) is used instead of sum(*) for the counts.

aggregatena.action = na.pass returns 预期结果(以及 na.action = NULL):

aggregate(formula = cbind(B, C) ~ addNA(A),
          data = x,
          FUN = sum,
          na.rm = TRUE,
          na.action = na.pass) # same result with na.action = NULL

  addNA(A) B C
1            Y 1 2
2            Z 0 0
3         <NA> 1 0

虽然我通过 xtabs 获得了我需要的 table,但我无法从文档中理解 xtabsna.action 的行为。所以我的问题是:

如果不描述 xtabs 的工作原理,就很难给出规范的答案。如果我们逐步查看其源代码的要点,我们将清楚地看到发生了什么。

经过一些基本的类型检查后,对 xtabs 的调用在内部工作,方法是首先使用 stats::model.frame 创建公式中包含的所有变量的数据框,正是为了这个 na.action 传递参数.

它的做法相当巧妙。 xtabs 首先复制你通过 match.call 对它的调用,像这样:

m <- match.call(expand.dots = FALSE)

然后它去掉不需要传递给stats::model.frame的参数,像这样:

m$... <- m$exclude <- m$drop.unused.levels <- m$sparse <- m$addNA <- NULL

正如帮助文件中所承诺的那样,如果 addNATRUE 而缺少 na.action,则现在默认为 na.pass:

    if (addNA && missing(na.action)) 
        m$na.action <- quote(na.pass)

然后它将要调用的函数从xtabs更改为stats::model.frame,如下所示:

m[[1L]] <- quote(stats::model.frame)

所以对象 m 是一个调用(也是一个独立的 reprex),在你的例子中看起来像这样:

stats::model.frame(formula = cbind(B, C) ~ A, data = list(A = structure(c(1L, 
1L, 2L, NA), .Label = c("Y", "Z"), class = "factor"), B = c(NA, TRUE, FALSE, TRUE), 
C = c(TRUE, TRUE, NA, FALSE)), na.action = NULL)

请注意,您的 na.action = NULL 已传递给此呼叫。这具有将所有 NA 值保留在框架中的效果。当上面的调用被评估时,它给出了这个数据框:

eval(m)
#>   cbind(B, C).B cbind(B, C).C    A
#> 1            NA          TRUE    Y
#> 2          TRUE          TRUE    Y
#> 3         FALSE            NA    Z
#> 4          TRUE         FALSE <NA>

请注意,这与您通过 na.action = na.pass:

时得到的结果相同
stats::model.frame(formula = cbind(B, C) ~ A, data = list(A = structure(c(1L, 
1L, 2L, NA), .Label = c("Y", "Z"), class = "factor"), B = c(NA, TRUE, FALSE, TRUE), 
C = c(TRUE, TRUE, NA, FALSE)), na.action = na.pass)
#>   cbind(B, C).B cbind(B, C).C    A
#> 1            NA          TRUE    Y
#> 2          TRUE          TRUE    Y
#> 3         FALSE            NA    Z
#> 4          TRUE         FALSE <NA>

但是,如果您通过 na.action = na.omit,您将只剩下一行,因为只有第 2 行没有 NA 个值。

在任何情况下,"model frame" 结果都存储在变量 mf 中。然后将其拆分为自变量 - 在您的情况下为 A 列和响应变量 - 在您的情况下 cbind(B, C).

响应存储在y中,变量存储在by中:

        i <- attr(attr(mf, "terms"), "response")
        by <- mf[-i]
        y <- mf[[i]]

现在,处理 by 以确保每个自变量都是一个因子,并且如果您指定 addNA = TRUE:[=81,任何 NA 值都将转换为因子水平=]

    by <- lapply(by, function(u) {
        if (!is.factor(u)) 
            u <- factor(u, exclude = exclude)
        else if (has.exclude) 
            u <- factor(as.character(u), levels = setdiff(levels(u), 
                exclude), exclude = NULL)
        if (addNA) 
            u <- addNA(u, ifany = TRUE)
        u[, drop = drop.unused.levels]
    })

现在我们来到了关键点。 na.action 再次用于确定如何计算响应变量中的 NA 值。在您的情况下,由于您传递了 na.action = NULL,您将看到 naAct 将获得存储在 getOption("na.action") 中的值,如果您从未更改它,则应设置为 na.omit.这反过来又会导致变量 na.rm, 的值为 TRUE:

    naAct <- if (!is.null(m$na.action)) {
        m$na.action
    }else {getOption("na.action", default = quote(na.omit))}
    na.rm <- identical(naAct, quote(na.omit)) || identical(naAct, 
        na.omit) || identical(naAct, "na.omit")

请注意,如果您通过了 na.action = na.pass,那么如果您跟踪这段代码,na.rm 将是 FALSE

最后,我们来到 xtabs table 是使用 tapply 中的 sum 构建的部分,而 tapply 本身也在 lapply 中.

lapply(as.data.frame(y), tapply, by, sum, na.rm = na.rm, default = 0L)

您可以看到 na.rm 变量用于确定是否在尝试对列求和之前从列中删除 NA。这个 lapply 的结果然后被强制到最终的交叉表中。


那么这如何回答您的问题?

当文档说如果您不传递 na.action,它将默认为 na.pass,这是真的。但是,na.action 在两个地方使用:一次是在调用 model.frame 时,一次是确定 na.rm 的值。从源代码中可以清楚地看出,如果 na.actionna.pass,那么 na.rm 将是 FALSE,因此您将错过任何包含 NA 值。这和帮助文件里写的相反

绕过这个的唯一方法是传递 na.action = NULL,因为这将允许 model.frame 保留 NA 值,但也会导致 sum 函数默认至 na.rm.


TL;DR xtabs 的文档在这一点上是错误的。

对不起,我现在才加入。确实,xtabs() 的最后六个修改都是我做的,所以我也必须在这里承担责任。

深入研究 xtabs() 的所有变体和分支总是需要一些时间,而我还没有花时间(这次;当然在当时..)。

但你终于得到了答案:

  • 是的,R 代码或文档中存在错误(而且 "or" 包含在内......;-)

  • 我目前的直觉是指向帮助(文件)而不是实现中的错误

  • R 的 bugzilla 是我们应该详细了解这一点的地方,尤其是因为那是 "wired" 到 R Core 团队的频道。

  • --> 跟进:https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17770.