嵌套时聚合函数的作用域 apply(within())

Scope of Aggregation Functions when nesting apply(within())

编辑原文 post 以澄清问题

背景
我正在学习 R 并看到了这种情况,但不了解 R 如何处理(我称之为)隐含的上下文转换。我试图理解的脚本只是遍历矩阵的每一行并打印该行中包含该行最小值的列的索引。我不明白的是 R 如何处理 上下文转换 因为不同的函数应用于因变量 x:

  1. x(当定义为 function(x) 的参数时)是一个 原子向量 因为 apply() 函数带有 MARGIN = 1 参数
  2. 然后 which() 函数迭代原子向量 x 中的单个 元素 以查看哪些 == min(x)
  3. 这是真正让我困惑的部分: 尽管 which() 正在迭代原子向量 元素 x,您可以在 which() 函数中调用 min(x) 并且 R 以某种方式切换 x 再次定义为整个 原子向量 计算向量中的 min() 与单个 元素范围内的

示例数据矩阵

a <- matrix (c(5, 2, 7, 1, 2, 8, 4, 5, 6), 3, 3)
         [,1] [,2] [,3]
[1,]    5    1    4
[2,]    2    2    5
[3,]    7    8    6

这是 return 我难以理解的列索引的脚本

apply (a, 1, function(x) which(x == min(x)))

我的问题:

which() 函数中,为什么 min(x) return 原子向量的最小值 (如所期望的那样)而不是该向量中单个 元素 范围内的最小值,因为 which() 正在迭代 原子向量 x?

编辑:关于whichx的讨论:

  • 你问题中的 不正确:

    x is anonymous function, lambda

    x 只是一个变量,没什么特别的。 function(x) 将其声明为匿名函数的第一个(也是唯一一个)参数,然后对 x 的每个引用都引用传递给该匿名函数的内容;

  • 代码使用了一个匿名函数;通常,您在 R 中所做的几乎所有事情都使用命名函数(例如,meanmin)。在某些情况下(例如,在 apply 和相关函数中),将整个函数定义为参数而不是命名它是有意义的,如

    ## anonymous (unnamed) function
    apply(m, 1, function(x) which(x == min(x)))
    
    ## equivalently, with a named function
    myfunc <- function(x) which(x == min(x))
    apply(m, 1, myfunc)
    

    第一种情况,function(x) which(x == min(x)))没有命名,所以是“匿名”。两次 apply 调用的结果相同。

  • 鉴于该上下文,x 是函数的第一个参数(myfunc 或您的情况下的匿名函数)。下面 apply/MARGIN 讨论的其余部分,

    • x(在本例中)包含整行(当 MARGIN=1 时);

    • min(x) return是x内最小值的,长度始终为1 );和

    • which(x == min(x)) return 是 x 内最低值的 索引 ;在这种情况下,它的长度将始终为 1 或更大,因为我们确信总有一个元素等于该向量的最小值……但是,不能保证 which 会找到任何匹配项,因此 which(...) 的 return 值的长度可以介于 0 和输入的长度之间。示例:

      which(11:15 == 13)
      # [1] 3
      which(11:15 == 1:5)
      # integer(0)
      which(11:15 == 11:15)
      # [1] 1 2 3 4 5
      which(11:15 %in% c(12, 14))
      # [1] 2 4
      

apply 一次处理一个或多个维度。现在,我将坚持使用二维矩阵,在这种情况下 MARGIN= 选择行或列。 (有一个警告,见下文。)

我将使用逐步详细功能来尝试显示每个步骤。我将其命名为 anonfunc,但在您的脑海中稍后将 apply(a, 1, anonfunc) 转换为 apply(a, 1, function(x) { ... }),您就会明白我打算做什么。另外,我有一个 dematrix 函数来帮助 显示 anonfunc.

中使用的内容
dematrix <- function(m, label = "") {
  if (!is.matrix(m)) m <- matrix(m, nrow = 1)
  out <- capture.output(print(m))[-1]
  out <- gsub("^[][,0-9]+", "", out)
  paste(paste0(c(label, rep(strrep(" ", nchar(label)), length(out) - 1)), out),
        collapse = "\n")
}
anonfunc <- function(x) {
  message(dematrix(x, "Input: "))
  step1 <- x == min(x)
  message(dematrix(step1, "Step1: "))
  step2 <- which(step1)
  message("Step2: ", paste(step2, collapse = ","), "\n#\n")
  step2
}

二维数组

我将通过添加一列来稍微修改您的示例数据。这有助于可视化有多少函数调用以及函数的输入有多大。

apply(a, 1, anonfunc)
# Input:     5    1    4   11
# Step1:  FALSE TRUE FALSE FALSE
# Step2: 2
# #
# Input:     2    2    5   12
# Step1:  TRUE TRUE FALSE FALSE
# Step2: 1,2
# #
# Input:     7    8    6   13
# Step1:  FALSE FALSE TRUE FALSE
# Step2: 3
# #
# [[1]]
# [1] 2
# [[2]]
# [1] 1 2
# [[3]]
# [1] 3

我们的匿名函数被调用了三次,每行一次。在每次调用中,都会传递一个长度为 4 的向量,这是矩阵中一行的大小。

请注意,我们在 return 中得到一个 list。通常 apply return 是一个向量或矩阵。 return 值实际上是 MARGIN= 轴的维度,加上 return 值的长度维度。也就是说,a 的亮度为 3x4;如果每次调用 anon-func 的 return 值的长度为 1,则 return 值“有点像”3x1,但 R 将其简化为长度为 3 的向量(这可能被解释为由于数学上不一致,我不同意)。如果来自每个 anon-func 调用的 return 值的长度为 10,则输出将是一个 3x10 的矩阵。

但是,当任何匿名函数 return 与其他 length/size/class 不同时,apply 将 return 一个 list. (这与 sapply 的行为相同,如果它在您不期望的情况下发生变化,可能会令人沮丧。据称 R-devel 中有一个补丁允许我们强制使用 apply(..., simplify=FALSE) 的列表.)

如果我们改为使用 MARGIN=2,我们将对列进行操作:

apply(a, 2, anonfunc)
# Input:     5    2    7
# Step1:  FALSE TRUE FALSE
# Step2: 2
# #
# Input:     1    2    8
# Step1:  TRUE FALSE FALSE
# Step2: 1
# #
# Input:     4    5    6
# Step1:  TRUE FALSE FALSE
# Step2: 1
# #
# Input:    11   12   13
# Step1:  TRUE FALSE FALSE
# Step2: 1
# #
# [1] 2 1 1 1

现在,每列调用一次(4 次调用),x 是一个长度为 3(源矩阵中的行数)的向量。

可以一次操作多个轴;虽然使用 matrix(二维数组)似乎毫无意义,但使用更大尺寸的数组更有意义。

apply(a, 1:2, anonfunc)
# Input:     5
# Step1:  TRUE
# Step2: 1
# #
# Input:     2
# Step1:  TRUE
# Step2: 1
# #
# Input:     7
# Step1:  TRUE
# Step2: 1
# #
# ...truncated... total of 12 calls to `anonfunc`
# #
#      [,1] [,2] [,3] [,4]
# [1,]    1    1    1    1
# [2,]    1    1    1    1
# [3,]    1    1    1    1

从输出维度的讨论来看,MARGIN=1:2 意味着输出维度将是边距的维度——3x4——与输出的 dimension/length。由于此处的输出始终是长度 1,因此技术上是 3x4x1,在 R 语言中是暗淡的 3x4 矩阵。

每个边距使用矩阵的图片:

3d 数组

让我们稍微大一点,看看一些“平面”操作。

a3 <- array(1:24, dim = c(3,4,2))
a3
# , , 1
#      [,1] [,2] [,3] [,4]
# [1,]    1    4    7   10
# [2,]    2    5    8   11
# [3,]    3    6    9   12
# , , 2
#      [,1] [,2] [,3] [,4]
# [1,]   13   16   19   22
# [2,]   14   17   20   23
# [3,]   15   18   21   24

MARGIN=1 开始。当两个数组都可见时,查看第一个 Input: 并查看正在使用原始 a3 数组中的哪个“平面”。它似乎转置了,当然...

为了简洁起见(为时已晚!),我将缩写 anonfunc 的第三次和后续迭代以仅显示详细输出的第一行(内部矩阵行)。

apply(a3, 1, anonfunc)
# Input:     1   13
#            4   16
#            7   19
#           10   22
# Step1:   TRUE FALSE
#         FALSE FALSE
#         FALSE FALSE
#         FALSE FALSE
# Step2: 1
# #
# Input:     2   14
#            5   17
#            8   20
#           11   23
# Step1:   TRUE FALSE
#         FALSE FALSE
#         FALSE FALSE
#         FALSE FALSE
# Step2: 1
# #
# Input:     3   15 ...
# #
# [1] 1 1 1

同样,MARGIN=2。我将再次显示 a3,这样您就可以看到正在使用哪个“平面”:

a3
# , , 1
#      [,1] [,2] [,3] [,4]
# [1,]    1    4    7   10
# [2,]    2    5    8   11
# [3,]    3    6    9   12
# , , 2
#      [,1] [,2] [,3] [,4]
# [1,]   13   16   19   22
# [2,]   14   17   20   23
# [3,]   15   18   21   24

apply(a3, 2, anonfunc)
# Input:     1   13
#            2   14
#            3   15
# Step1:   TRUE FALSE
#         FALSE FALSE
#         FALSE FALSE
# Step2: 1
# #
# Input:     4   16
#            5   17
#            6   18
# Step1:   TRUE FALSE
#         FALSE FALSE
#         FALSE FALSE
# Step2: 1
# #
# Input:     7   19 ...
# Input:    10   22 ...
# #
# [1] 1 1 1 1

MARGIN=3 不是很令人兴奋:anonfunc 只被调用两次,每个面向前面的“平面”调用一次(此处不需要缩写):

apply(a3, 3, anonfunc)
# Input:     1    4    7   10
#            2    5    8   11
#            3    6    9   12
# Step1:   TRUE FALSE FALSE FALSE
#         FALSE FALSE FALSE FALSE
#         FALSE FALSE FALSE FALSE
# Step2: 1
# #
# Input:    13   16   19   22
#           14   17   20   23
#           15   18   21   24
# Step1:   TRUE FALSE FALSE FALSE
#         FALSE FALSE FALSE FALSE
#         FALSE FALSE FALSE FALSE
# Step2: 1
# #
# [1] 1 1

一个也可以在这里使用多个维度,这就是我认为Input:字符串变得有点清晰的地方:

a3
# , , 1
#      [,1] [,2] [,3] [,4]
# [1,]    1    4    7   10
# [2,]    2    5    8   11
# [3,]    3    6    9   12
# , , 2
#      [,1] [,2] [,3] [,4]
# [1,]   13   16   19   22
# [2,]   14   17   20   23
# [3,]   15   18   21   24

apply(a3, 2:3, anonfunc)
# Input:     1    2    3
# Step1:  TRUE FALSE FALSE
# Step2: 1
# #
# Input:     4    5    6
# Step1:  TRUE FALSE FALSE
# Step2: 1
# #
# Input:     7    8    9 ...
# Input:    10   11   12 ...
# Input:    13   14   15 ...
# Input:    16   17   18 ...
# Input:    19   20   21 ...
# Input:    22   23   24 ...
# #
#      [,1] [,2]
# [1,]    1    1
# [2,]    1    1
# [3,]    1    1
# [4,]    1    1

并且由于 a3 的维度是 3,42,我们正在查看边距 2:3,并且每次调用 anonfunc returns 长度 1,我们的 returned 矩阵是 4x2x1(其中 x1 被 R 悄悄丢弃)。

为了可视化 MARGIN= 的每次调用实际使用的内容,请参见下图:

“词法范围根据函数在创建时的嵌套方式查找符号值,而不是在调用[=17]时它们的嵌套方式=]。使用词法作用域,你不需要知道函数是如何被调用来确定变量的值将在哪里查找。你只需要查看函数的定义。”**

**来源:http://adv-r.had.co.nz/Functions.html#lexical-scoping