【问题标题】:Avoid for loop when loop has an increment当循环有增量时避免 for 循环
【发布时间】:2020-02-07 08:51:59
【问题描述】:

在 R 中,我系统地尝试避免“for”循环并改用lapply() family。
但是当迭代包含增量步时怎么做呢?

例如:使用lapply 方法是否可以获得与以下相同的结果?

a <- c()
b <- c()
set.seed(1L) # required for reproducible data
for (i in 1:10){
  a <- c(a, sample(c(0,1), 1))
  b <- c(b, (paste(a, collapse = "-")))
}
data.frame(a, b)


> data.frame(a, b)
>    a                   b
> 1  0                   0
> 2  1                 0-1
> 3  0               0-1-0
> 4  0             0-1-0-0
> 5  1           0-1-0-0-1
> 6  0         0-1-0-0-1-0
> 7  0       0-1-0-0-1-0-0
> 8  0     0-1-0-0-1-0-0-0
> 9  1   0-1-0-0-1-0-0-0-1
> 10 1 0-1-0-0-1-0-0-0-1-1

编辑 我的问题被编辑得很糟糕。下面的新示例更具说明性:如果每次迭代都是从前一个迭代计算出来的,是否仍然使用lapplyfamily?

a <- c()
b <- c()
for (i in 1:10){
  a <- c(a, sample(c(0,1), 1))
  b <- c(b, (paste(a, collapse = "-")))
}
data.frame(a, b)

> data.frame(a, b)
   a                   b
1  0                   0
2  1                 0-1
3  0               0-1-0
4  1             0-1-0-1
5  1           0-1-0-1-1
6  1         0-1-0-1-1-1
7  1       0-1-0-1-1-1-1
8  0     0-1-0-1-1-1-1-0
9  1   0-1-0-1-1-1-1-0-1
10 1 0-1-0-1-1-1-1-0-1-1

【问题讨论】:

    标签: r for-loop lapply


    【解决方案1】:

    为了完整起见,还有来自purrr 包的accumulate() 函数。

    所以,以SotosThomasIsCoding 的答案为基础:

    df <- data.frame(a = 1:10)
    df$b <- purrr::accumulate(df$a, paste, sep = "-")
    df
    
        a                    b
    1   1                    1
    2   2                  1-2
    3   3                1-2-3
    4   4              1-2-3-4
    5   5            1-2-3-4-5
    6   6          1-2-3-4-5-6
    7   7        1-2-3-4-5-6-7
    8   8      1-2-3-4-5-6-7-8
    9   9    1-2-3-4-5-6-7-8-9
    10 10 1-2-3-4-5-6-7-8-9-10
    

    Reduce() 的区别是

    • accumulate() 本身就是一个函数动词(不需要额外的参数accumulate = TRUE
    • 并且像 sep = "-" 这样的附加参数可以传递给映射函数,这可能有助于避免创建匿名函数。

    编辑

    如果我正确理解 OP 对问题的编辑,OP 会询问是否可以将迭代计算结果的 for 循环替换为 lapply()

    这对我来说很难回答。以下是一些想法和观察:

    首先accumulate() 仍然可以工作:

    set.seed(1L) # required for reproducible data
    df <- data.frame(a = sample(0:1, 10L, TRUE))
    df$b <- purrr::accumulate(df$a, paste, sep = "-")
    df                 
    
       a                   b
    1  0                   0
    2  1                 0-1
    3  0               0-1-0
    4  0             0-1-0-0
    5  1           0-1-0-0-1
    6  0         0-1-0-0-1-0
    7  0       0-1-0-0-1-0-0
    8  0     0-1-0-0-1-0-0-0
    9  1   0-1-0-0-1-0-0-0-1
    10 1 0-1-0-0-1-0-0-0-1-1
    

    这是可能的,因为a 的计算可以脱离循环,因为它不依赖于b

    恕我直言,accumulate()Reduce() 执行 OP 正在寻找但未被称为 lapply() 的操作:例如,它们获取上一次迭代的结果并将其与实际值相结合

    Reduce(`+`, 1:3)
    

    通过迭代计算 (((0 + 1) + 2) + 3) 返回 1、2 和 3 的总和。这可以通过使用accumulate 参数来可视化

    Reduce(`+`, 1:3, accumulate = TRUE)
    
    [1] 1 3 6
    

    第二for 循环和lapply() 系列的函数之间有一个主要区别:lapply(X, FUN, ...) 需要在@987654352 的每个元素上调用函数FUN @。因此,适用函数的范围规则。

    当我们将循环体移植到lapply()内的匿名函数中时

    a <- c()
    b <- c()
    set.seed(1L) # required for reproducible data
    lapply(1:10, function(i) {
      a <- c(a, sample(c(0,1), 1))
      b <- c(b, (paste(a, collapse = "-")))
    })
    

    我们得到

    [[1]]
    [1] "0"
    
    [[2]]
    [1] "1"
    
    [[3]]
    [1] "0"
    
    [[4]]
    [1] "0"
    
    [[5]]
    [1] "1"
    
    [[6]]
    [1] "0"
    
    [[7]]
    [1] "0"
    
    [[8]]
    [1] "0"
    
    [[9]]
    [1] "1"
    
    [[10]]
    [1] "1"
    
    data.frame(a, b)
    
    data frame with 0 columns and 0 rows    data.frame(a, b)
    

    由于范围规则,ab inside 函数被认为是函数的本地函数。未提及在函数外部定义的ab

    这可以通过全局赋值使用全局赋值运算符来修复&lt;&lt;-:

    a <- c()
    b <- c()
    set.seed(1L) # required for reproducible data
    lapply(1:10, function(i) {
      a <<- c(a, sample(c(0,1), 1))
      b <<- c(b, (paste(a, collapse = "-")))
    })
    data.frame(a, b)
    
       a                   b
    1  0                   0
    2  1                 0-1
    3  0               0-1-0
    4  0             0-1-0-0
    5  1           0-1-0-0-1
    6  0         0-1-0-0-1-0
    7  0       0-1-0-0-1-0-0
    8  0     0-1-0-0-1-0-0-0
    9  1   0-1-0-0-1-0-0-0-1
    10 1 0-1-0-0-1-0-0-0-1-1
    

    但是,global assignment 被认为是不好的编程习惯,应该避免,例如,请参阅 Patrick Burns' The R Inferno 的第 6 圈 以及许多关于 SO 的问题。

    第三,循环的编写方式在循环中增长了向量。这也被认为是不好的做法,因为它需要一遍又一遍地复制数据,这可能会随着大小的增加而大大减慢。参见,例如,Patrick Burns' The R Inferno第二个圈子

    但是原来的代码

    a <- c()
    b <- c()
    set.seed(1L) # required for reproducible data
    for (i in 1:10) {
      a <- c(a, sample(c(0,1), 1))
      b <- c(b, (paste(a, collapse = "-")))
    }
    data.frame(a, b)
    

    可以改写为

    a <- integer(10)
    b <- character(10)
    set.seed(1L) # required for reproducible data
    for (i in seq_along(a)) {
      a[i] <- sample(c(0,1), 1)
      b[i] <- if (i == 1L) a[1] else paste(b[i-1], a[i], sep = "-")
    }
    data.frame(a, b)
    

    在这里,向量被预先分配了所需的大小来保存结果。要更新的元素由下标标识。

    b[i] 的计算仍然仅取决于 OP 要求的前一次迭代 b[i-1] 的值和实际值 a[i]

    【讨论】:

    • 确实很优雅。我不知道这个。在性能方面,是否可以与lapply 媲美?我会测试一下。
    【解决方案2】:

    另一种方法是使用Reduceaccumulate = TRUE,即

    df$new <- do.call(rbind, Reduce(paste, split(df, seq(nrow(df))), accumulate = TRUE))
    

    给出,

        a                  new
    1   1                    1
    2   2                  1 2
    3   3                1 2 3
    4   4              1 2 3 4
    5   5            1 2 3 4 5
    6   6          1 2 3 4 5 6
    7   7        1 2 3 4 5 6 7
    8   8      1 2 3 4 5 6 7 8
    9   9    1 2 3 4 5 6 7 8 9
    10 10 1 2 3 4 5 6 7 8 9 10
    

    【讨论】:

      【解决方案3】:

      您可以使用sapplylapply 也可以,但它会返回一个列表)并遍历dfa 的每个值,并创建一个序列和paste 一起的值。

      df <- data.frame(a = 1:10)
      df$b <- sapply(df$a, function(x) paste(seq(x), collapse = "-"))
      df
      
      #    a                    b
      #1   1                    1
      #2   2                  1-2
      #3   3                1-2-3
      #4   4              1-2-3-4
      #5   5            1-2-3-4-5
      #6   6          1-2-3-4-5-6
      #7   7        1-2-3-4-5-6-7
      #8   8      1-2-3-4-5-6-7-8
      #9   9    1-2-3-4-5-6-7-8-9
      #10 10 1-2-3-4-5-6-7-8-9-10
      

      如果数据中可能存在我们无法使用seq 之类的非数值

      df <- data.frame(a =letters[1:10])
      

      在这种情况下,我们可以使用

      df$b <- sapply(seq_along(df$a), function(x) paste(df$a[seq_len(x)], collapse = "-"))
      df
      
      #   a                   b
      #1  a                   a
      #2  b                 a-b
      #3  c               a-b-c
      #4  d             a-b-c-d
      #5  e           a-b-c-d-e
      #6  f         a-b-c-d-e-f
      #7  g       a-b-c-d-e-f-g
      #8  h     a-b-c-d-e-f-g-h
      #9  i   a-b-c-d-e-f-g-h-i
      #10 j a-b-c-d-e-f-g-h-i-j
      

      【讨论】:

      • 正式用我的用例,这是一个很好的答案。但是,您的代码是可能的,因为您提前知道 b 的值是多少。你知道如果 df[4,"a"] = 4,那么 df[4, "b"] 将是 1-2-3-4。但是如果你必须知道 df[3, "b"] 的值来计算 df[4, "b"] 怎么办?这就是我提问的目的。
      • @Jrm_FRL 如果每个值都依赖于前一个值,我认为lapply 在这种情况下不会起作用。在这种情况下,您可以使用Reduceaccumulate 解决方案。
      • 我认为你是对的。无论如何,我已经编辑了我的问题。
      【解决方案4】:

      Reduce的另一种使用方式,与@Sotos的方式不同

      df$b <- Reduce(function(...) paste(...,sep = "-"), df$a, accumulate = T)
      

      这样

      > df
          a                    b
      1   1                    1
      2   2                  1-2
      3   3                1-2-3
      4   4              1-2-3-4
      5   5            1-2-3-4-5
      6   6          1-2-3-4-5-6
      7   7        1-2-3-4-5-6-7
      8   8      1-2-3-4-5-6-7-8
      9   9    1-2-3-4-5-6-7-8-9
      10 10 1-2-3-4-5-6-7-8-9-10
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-05-21
        • 2023-03-27
        • 2012-06-25
        • 2020-03-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多