在函数的每次迭代中动态更新输入数据帧,无需全局分配

Posted

技术标签:

【中文标题】在函数的每次迭代中动态更新输入数据帧,无需全局分配【英文标题】:Dynamically update input dataframe at each iteration of function without global assignment 【发布时间】:2018-11-28 13:58:43 【问题描述】:

我有 (1) 一个评分参考表,以及 (2) 一个函数,它根据这些评分随机生成结果并根据生成的结果更新评分。

虽然下面的可重现示例有更简单的解决方案,但预期的应用是根据对手的 Elo 评分来模拟对手之间的结果,每轮比赛后都会更新评分以运行模拟“热”。

在这里,我有一个评级参考表ref,并使用函数genResult 生成随机结果并使用全局分配更新参考表。

set.seed(123)
ref <- data.frame(id = LETTERS[1:5],
                  rating = round(runif(5, 100, 200)))

genResult <- function(ref) 

  id_i <- LETTERS[floor(runif(1, 1, 5))]

  score_i <- round(rnorm(1, 0, 20))

  ref[ref$id == id_i,]$rating <- ref[ref$id == id_i,]$rating + score_i

  result_i <- data.frame(id = id_i, score = score_i)

  # assign('ref', ref, envir=.GlobalEnv)
  ref <<- ref

  return(list(result_i, ref))

复制这个函数两次,我们可以看到ref按预期更新了。

replicate(2, genResult(ref), simplify = F)

返回这个,我们可以看到参考表在两次迭代中的每一次都更新了。

[[1]]
[[1]][[1]]
id score
1  A     1

[[1]][[2]]
id rating
1  A    130
2  B    179
3  C    141
4  D    188
5  E    194


[[2]]
[[2]][[1]]
id score
1  C    -2

[[2]][[2]]
id rating
1  A    130
2  B    179
3  C    139
4  D    188
5  E    194

现在假设我要复制上述(复制的)函数;使用动态更新的评级模拟 5 个结果的 3 个单独实例并仅输出结果。我再次创建引用表ref 并定义了一个使用全局赋值的类似函数:

set.seed(123)
ref <- data.frame(id = LETTERS[1:5],
                  rating = round(runif(5, 100, 200)))

genResult2 <- function(ref) 

  id_i <- LETTERS[floor(runif(1, 1, 5))]

  score_i <- round(rnorm(1, 0, 20))

  ref[ref$id == id_i,]$rating <- ref[ref$id == id_i,]$rating + score_i

  result_i <- data.frame(id = id_i, score = score_i)

  ref <<- ref

  return(result_i)

然后使用apply 循环并将结果列表折叠到数据框:

lapply(1:3, function(i) 

  ref_i <- ref

  replicate(5, genResult2(ref_i), simplify = F) %>% 
    plyr::rbind.fill() %>% 
    mutate(i)

) %>% 
  plyr::rbind.fill()

返回:

id score i
1   A     1 1
2   C    -2 1
3   B     9 1
4   A    26 1
5   A    -9 1
6   D    10 2
7   D     8 2
8   C     5 2
9   A    36 2
10  C    17 2
11  B    14 3
12  B   -15 3
13  B    -4 3
14  A   -22 3
15  B   -13 3

现在这似乎按预期工作,但是 (i) 感觉真的很难看,并且 (ii) 我已经读过无数次了,全局分配可能而且将会造成意想不到的伤害。

谁能提出更好的解决方案?

【问题讨论】:

【参考方案1】:

您可以使用new.env() 创建一个新环境并在那里进行计算:

将这个想法应用到您的第一个函数中:

set.seed(123)
ref1 <- data.frame(id = LETTERS[1:5],
                  rating = round(runif(5, 100, 200)))
ref1

refEnv <- new.env()
refEnv$ref = ref1

genResult <- function(ref) 

  id_i <- LETTERS[floor(runif(1, 1, 5))]

  score_i <- round(rnorm(1, 0, 20))

  ref[ref$id == id_i,]$rating <- ref[ref$id == id_i,]$rating + score_i

  result_i <- data.frame(id = id_i, score = score_i)

  assign('ref', ref, envir=refEnv)

  return(list(result_i, ref))

replicate(2, genResult(refEnv$ref), simplify = F)

ref1
refEnv$ref

您会看到原始的ref1 没有被触及并保持不变,而refEnv$ref 包含上次迭代的结果。

并使用lapply 将其实现到您的第二个函数:

set.seed(123)
ref1 <- data.frame(id = LETTERS[1:5],
                   rating = round(runif(5, 100, 200)))
ref1

refEnv <- new.env()
refEnv$ref = ref1


genResult2 <- function(ref) 

  id_i <- LETTERS[floor(runif(1, 1, 5))]

  score_i <- round(rnorm(1, 0, 20))

  ref[ref$id == id_i,]$rating <- ref[ref$id == id_i,]$rating + score_i

  result_i <- data.frame(id = id_i, score = score_i)

  assign('ref', ref, envir=refEnv)

  return(result_i)


# Replicating this function twice, we can see `ref` is updated as expected.    
lapply(1:3, function(i) 

  replicate(5, genResult2(refEnv$ref), simplify = F) %>% 
    plyr::rbind.fill() %>% 
    mutate(i)

) %>% 
  plyr::rbind.fill()

ref1

【讨论】:

感谢您的回答@SeGa,我从不考虑使用new.env(),这对未来很有用:) 但是,我在这里接受了另一个答案,因为它可以让我避免在任何环境中分配我认为当包含在一个包中时,用户更容易看到发生了什么。【参考方案2】:

如果您正在迭代并且下一次迭代依赖于最后一次迭代,这通常是一个好兆头,您应该使用老式的 for 循环而不是 replicateapply 函数(另一种可能是使用 @ 987654324@ accumulate 参数设置为TRUE)。

这给出了与您发布的代码相同的输出,我使用了 for 循环并使您的函数也返回 ref:

genResult3 <- function(ref) 

  id_i <- LETTERS[floor(runif(1, 1, 5))]

  score_i <- round(rnorm(1, 0, 20))

  ref[ref$id == id_i,]$rating <- ref[ref$id == id_i,]$rating + score_i

  result_i <- data.frame(id = id_i, score = score_i)

  #ref <<- ref

  return(list(result_i,ref)) # added ref to output


lapply(1:3, function(i) 
  res <- list(5)
  for (k in 1:5)
    gr <- genResult3(ref)
    res[[k]] <- gr[[1]] # update rating
    ref      <- gr[[2]] # get result
    res[[k]] <- left_join(res[[k]], ref, by = "id") # combine for output
  
    plyr::rbind.fill(res) %>% 
    mutate(i)

) %>% 
  plyr::rbind.fill()

返回:

   id score rating i
1   A     1    130 1
2   C    -2    139 1
3   B     9    188 1
4   A    26    156 1
5   A    -9    147 1
6   D    10    198 2
7   D     8    206 2
8   C     5    146 2
9   A    36    165 2
10  C    17    163 2
11  B    14    193 3
12  B   -15    178 3
13  B    -4    174 3
14  A   -22    107 3
15  B   -13    161 3

【讨论】:

非常感谢——我想我几年前一定对 R 中的循环产生了反感,认为 *apply 在任何情况下都更胜一筹!是的,这最终不是最好的例子,但我已将ref 添加到您的答案的输出中,以查看rating 在每次迭代时更新和重置。 循环恐惧症很常见,但可以治愈:)。 For 循环的可读性通常较低(对于通过 *apply 练习的人来说),但是当它们更具可读性或避免像 &lt;&lt;- 这样的不良做法时,您应该使用它们,如果操作正确,它们的速度差不多。看到这个帖子:***.com/questions/48793273/why-not-use-a-for-loop/… 很好的答案!我认为理解 R 中的向量化函数通常更快,因为它们在 C 中进行了优化,这解释了*apply 总是比 for 循环更好的误解。 是的,有些人经常认为 for 循环可以更快,尽管在我上面链接的答案中,我仍在等待有人提出这种情况的示例。无论如何,它是相同的数量级。

以上是关于在函数的每次迭代中动态更新输入数据帧,无需全局分配的主要内容,如果未能解决你的问题,请参考以下文章

pyspark 从数据帧迭代 N 行到每次执行

c语言 动态数组

如何多次重复代码并将每次迭代的输出存储在同一个数据帧中?

js全局变量优点和缺点

如何将多处理用于函数而不是循环?

iOS程序中的内存分配 栈区堆区全局区(转)