遍历数据框,其中每次迭代都有效地依赖于 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 用于xy,则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 中的前一项的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地迭代 Java Map 中的每个条目?

Python - 如何有效地遍历字典的子集?

如何有效地迭代 Pandas 数据帧的连续块

如何将行附加到 R 数据框

迭代地子集数据帧并使用 R 应用于绘图函数

R:根据一天中的时间有效地对数据框进行子集化