在函数的每次迭代中动态更新输入数据帧,无需全局分配
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 循环而不是 replicate
或 apply
函数(另一种可能是使用 @ 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 练习的人来说),但是当它们更具可读性或避免像 <<-
这样的不良做法时,您应该使用它们,如果操作正确,它们的速度差不多。看到这个帖子:***.com/questions/48793273/why-not-use-a-for-loop/…
很好的答案!我认为理解 R 中的向量化函数通常更快,因为它们在 C 中进行了优化,这解释了*apply
总是比 for 循环更好的误解。
是的,有些人经常认为 for 循环可以更快,尽管在我上面链接的答案中,我仍在等待有人提出这种情况的示例。无论如何,它是相同的数量级。以上是关于在函数的每次迭代中动态更新输入数据帧,无需全局分配的主要内容,如果未能解决你的问题,请参考以下文章