遍历数据框,其中每次迭代都有效地依赖于 R 中的前一项
Posted
技术标签:
【中文标题】遍历数据框,其中每次迭代都有效地依赖于 R 中的前一项【英文标题】:iterate through data frame where each iteration is dependent on the previous item in R efficiently 【发布时间】:2018-07-19 15:04:01 【问题描述】:我有一个包含两个长度为 5 和变量的向量的数据框:
x <- seq(1:5)
y <- rep(0,5)
df <- data.frame(x, y)
z <- 10
我需要遍历数据框并根据与 x 相关的条件使用 z 更新 y,并且我需要在每次迭代时更新 z。使用 for 循环,我会这样做:
for (i in seq(2,nrow(df)))
if(df$x[i] %% 2 == 0)
df$y[i] <- df$y[i-1] + z
z <- z - df$x[i]
else
df$y[i] <- df$y[i-1]
使用数据帧很慢,并且必须使用 df$x[i] 访问第 i 个项目效率不高,但我不确定如何对其进行矢量化,因为 y 和 z 都会根据每次迭代而改变。
有没有人推荐最好的迭代方法?我希望完全避免使用数据帧,只使用向量来简化查找,或者使用 tidyverse 中的一些东西,使用 tibbles 和 purrr 包,但似乎没有什么容易实现的。谢谢!
【问题讨论】:
这将有助于查看您的预期输出:y 和 z 的最终值。 @neilfws 在循环之后调用df
。
@jaySf 您假设显示的代码没有错误:) 它有助于了解提问者认为输出应该是什么
@neil 这是高度简化的。 y 从 0 开始,到 18 结束。它仅在 i = 2 或 4 时增加,因此首先添加 10,然后添加 8。在真实的 df 中,z 是取决于 x 的几个函数之一,它需要一个在每次迭代中变化的起始量。这篇文章的目标是简化这种类型的循环或提高它的效率。我还是 r 和函数式编程的新手。
【参考方案1】:
你可以使用sapply
函数:
y=0
z=10
sapply(df$x,function(x)ifelse(x%%2==0,y<<-y+z;z<<-z-x;y,y<<-y))
[1] 0 10 10 18 18
【讨论】:
谢谢,这是非常紧凑的,似乎工作正常。我会对此进行更多研究,这可能效果最好 希望它确实适合您的目的。也可以看Reduce
【参考方案2】:
这是一个矢量化的版本
vec_fun <- function(x, z)
L <- length(x)
vec_z <- rep(0, L)
I <- seq(2, L, by=2)
vec_z[I] <- head(z-c(0, cumsum(I)), length(I))
cumsum(vec_z)
替代版本 - sapply
& tidyverse
sapply_fun <- function(x, z)
y=0
sapply(df$x,function(x)ifelse(x%%2==0,y<<-y+z;z<<-z-x;y,y<<-y))
library(tidyverse)
library(tidyverse)
tidy_fun <- function(df)
df %>%
filter(x %% 2 != 0) %>%
mutate(z = accumulate(c(z, x[-1] - 1), `-`)) %>%
right_join(df, by = c("x", "y")) %>%
mutate(z = lag(z), z = ifelse(is.na(z), 0, z)) %>%
mutate(y = cumsum(z)) %>%
select(-z) %>%
pluck("y")
您的数据
df <- data.frame(x=1:5, y=0)
z <- 10
让我们确保它们都返回相同的结果
identical(vec_fun(df$x, z), sapply_fun(df$x, z), tidy_fun(df))
# TRUE
基准测试与小数据集 - sapply_fun
似乎稍快
library(microbenchmark)
microbenchmark(vec_fun(df$x, z), sapply_fun(df$x, z), tidy_fun(df), times=100L, unit="relative")
# Unit: relative
# expr min lq mean median uq max neval
# vec_fun(df$x, z) 1.349053 1.316664 1.256691 1.359864 1.348181 1.146733 100
# sapply_fun(df$x, z) 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 100
# tidy_fun(df) 411.409355 378.459005 168.689084 301.029545 270.519170 4.244833 100
现在有了更大的 data.frame
df <- data.frame(x=1:1000, y=0)
z <- 10000
结果相同 - 是的
identical(vec_fun(df$x, z), sapply_fun(df$x, z), tidy_fun(df))
# TRUE
基准测试具有更大的数据集 - 现在很明显 vec_fun
更快
library(microbenchmark)
microbenchmark(vec_fun(df$x, z), sapply_fun(df$x, z), tidy_fun(df), times=100L, unit="relative")
# Unit: relative
# expr min lq mean median uq max neval
# vec_fun(df$x, z) 1.00000 1.00000 1.00000 1.00000 1.00000 1.000 100
# sapply_fun(df$x, z) 42.69696 37.00708 32.19552 35.19225 27.82914 27.285 100
# tidy_fun(df) 259.87893 228.06417 201.43230 218.92552 172.45386 380.484 100
【讨论】:
看来tidy_fun
来自我的帖子。我建议在管道末尾执行pluck("y")
,而不是将整个内容分配给df2
,然后如果您希望函数只返回一个向量,请访问y
列。
但是很好的解决方案。我的建议只是想确保您以正确的方式进行基准测试。你的vec_fun
仍然是最快的。
@www 我不会对您的评论提出任何批评。我会用pluck
更新我的帖子
但是,您的vec_fun
仍然是最快的。感谢您提供如此好的解决方案。【参考方案3】:
由于您的数据仅包含数字,因此您可以使用矩阵而不是数据框,这会稍微快一些。
mx <- matrix(c(x, y), ncol = 2, dimnames = list(1:length(x), c("x", "y")))
for (i in seq(2, nrow(mx)))
if(mx[i, 1] %% 2 == 0)
mx[i, 2] <- mx[i-1, 2] + z
z <- z - mx[i, 1]
else
mx[i, 2] <- mx[i-1, 2]
mx
# x y
# 1 1 0
# 2 2 10
# 3 3 10
# 4 4 18
# 5 5 18
microbenchmark()
结果:
# Unit: milliseconds
# expr min lq mean median uq max neval
# mx 8.675346 9.542153 10.71271 9.925953 11.02796 89.35088 1000
# df 10.363204 11.249255 12.85973 11.785933 13.59802 106.99920 1000
【讨论】:
你的矩阵赋值应该是nrow(df)
吗?
@KevinArseneau 我不这么认为,df 不再需要了,是吗?
不是在您创建矩阵后,但如果您在新的会话中尝试您的代码,您会发现您没有对象mx
可在nrow
中使用。或者,将length
用于x
或y
,则df
完全无关
@KevinArseneau 实际上,在这种情况下,一个生成向量的长度应该足够了。
谢谢,我希望简化数据类型,因为我认为矩阵会更快。这确实有帮助,但仍然使用相同的基本循环策略【参考方案4】:
如果我们可以对数据框进行矢量化操作,那就太好了。我的策略是计算每一行的z
值,然后使用cumsum
计算y 值。 purrr 包中的 accumulate
函数用于计算 z
值。来自dplyr 函数的right_join
函数和来自tidyr 包的fill
函数是为了进一步处理格式。
library(tidyverse)
df2 <- df %>%
filter(x %% 2 != 0) %>%
mutate(z = accumulate(c(z, x[-1] - 1), `-`)) %>%
right_join(df, by = c("x", "y")) %>%
mutate(z = lag(z), z = ifelse(is.na(z), 0, z)) %>%
mutate(y = cumsum(z)) %>%
select(-z)
df2
# x y
# 1 1 0
# 2 2 10
# 3 3 10
# 4 4 18
# 5 5 18
【讨论】:
我遇到的一个问题是 df 非常大,所以复制它感觉会很慢。我正在研究 purrr 包,并将进一步探索。以上是关于遍历数据框,其中每次迭代都有效地依赖于 R 中的前一项的主要内容,如果未能解决你的问题,请参考以下文章