【问题标题】:Wrapper to FOR loops with progress bar带有进度条的 FOR 循环包装器
【发布时间】:2011-11-13 01:20:15
【问题描述】:

我喜欢在运行缓慢的for 循环时使用进度条。这可以通过几个助手轻松完成,但我确实喜欢 tcltk 包中的 tkProgressBar

一个小例子:

pb <- tkProgressBar(title = "Working hard:", min = 0, max = length(urls), width = 300)
for (i in 1:300) {
    # DO SOMETHING
    Sys.sleep(0.5)
    setTkProgressBar(pb, i, label=paste( round(i/length(urls)*100, 0), "% ready!"))
}
close(pb)

我想设置一个小函数来存储在我的 .Rprofile 中,命名为 forp(如:for loop with progressbar),像 for 一样调用,但使用 auto添加了进度条 - 但不幸的是不知道如何实现和获取循环函数的 expr 部分。我对do.call 进行了一些实验,但没有成功:(

虚构的工作示例(其作用类似于 for 循环,但会创建 TkProgressBar 并在每次迭代中自动更新它):

forp (i in 1:10) {
    #do something
}

UPDATE:我认为问题的核心是如何编写一个函数,它不仅在函数后面的括号中有参数(如:foo(bar)),而且可以处理@987654333 @ 在右括号后指定,例如:foo(bar) expr


BOUNTY OFFER:将转到任何可以修改 my suggested function 以像基本 for 循环的语法一样工作的答案。例如。而不是

> forp(1:1000, {
+   a<-i
+ })
> a
[1] 1000

可以这样称呼:

> forp(1:1000) {
+   a<-i
+ }
> a
[1] 1000

再次澄清任务:我们如何获取函数调用的{ expression } 部分?恐怕这是不可能的,但会为专业人士留下几天的赏金:)

【问题讨论】:

  • 我们能否在forp 和表达式之间添加一些内容,例如forp(1:1000) %do% { expression }?如果是这样,它应该类似于foreach 包的作用,甚至可以直接使用。我不认为你可以不添加它,但愿意被纠正。
  • 感谢@Aaron 的评论。我希望可以有某种解决方案,而无需对语法进行额外的调整。如果没有,那么下面的工作功能无需任何修改就可以了。
  • 我们会看看是否有人提出了一种无需修改的方法;与此同时,我确实编写了我的上述建议,至少只需要在循环顶部进行修改(即最后没有额外的))。
  • 这很酷@Aaron,谢谢!如果即使是很小的修改也没有解决方案,那么赏金将归您所有:)
  • 您正在尝试修改语言。我会非常小心...您可能会忘记很多事情(例如中断/继续语句等),并为未来的神秘错误做好准备。小心点。

标签: function r loops expression wrapper


【解决方案1】:

如果您使用plyr 系列命令而不是 for 循环(如果可能的话,这通常是一个好主意),您可以获得整个进度条系统的额外奖励。

R.utils也内置了一些进度条,还有instructions for using them in for loops

【讨论】:

  • 感谢您的回答:plyr 在大多数情况下都是一个非常棒的工具,但有时我肯定需要for 循环(具有复杂的结构,其中数据分布在多个数据集中)。不幸的是,链接的资源只是显示了一个例子,就像我在我的问题中输入的那样,所以只有几种手动向for 循环添加进度条的方法,但没有关于我所追求的自动进度条的想法(例如forp函数)。
【解决方案2】:

R 的语法不能让你做你想做的事,即:

forp (i in 1:10) {
    #do something
}

但你可以做的是创建某种迭代器对象并使用 while() 循环:

while(nextStep(m)){sleep.milli(20)}

现在你的问题是m 是什么,以及如何使nextStep(m)m 产生副作用,以使其在循环结束时返回FALSE。我编写了执行此操作的简单迭代器,以及允许您在循环中定义和测试老化和细化期的 MCMC 迭代器。

最近在 R 用户会议上,我看到有人定义了一个 'do' 函数,然后用作运算符,例如:

do(100) %*% foo()

但我不确定那是确切的语法,我不确定如何实现它或它是谁提出的......也许其他人可以记得!

【讨论】:

  • 后一个示例看起来类似于 foreach 包中的 foreach 语法。
  • 也谢谢@Spacedman!我现在不确定您的建议如何帮助我构建forp 函数,但会努力赶上:) 会报告。
【解决方案3】:

你所希望的,我想应该是这样的

body(for)<- as.call(c(as.name('{'),expression([your_updatebar], body(for))))

是的,问题在于“for”不是一个函数,或者至少不是一个“body”可访问的函数。我想,您可以创建一个“forp”函数,该函数将 1) 一个要转换为循环计数器的字符串作为参数,例如 " ( i in seq(1,101,5) )" 和 2) 预期循环的主体,例如 y[i]&lt;- foo[i]^2 ; points(foo[i],y[i],然后跳过一些 getcallparse 魔术来执行实际的 for 循环。 然后,在伪代码中(不接近实际的 R 代码,但我想你知道应该发生什么)

forp<-function(indexer,loopbody) { 

pseudoparse( c("for (", indexer, ") {" ,loopbody,"}") }

【讨论】:

    【解决方案4】:

    鉴于提供的其他答案,我怀疑完全按照您指定的方式进行操作是不可能

    但是,如果您创造性地使用plyr 包,我相信有一种方法可以非常接近。诀窍是使用l_ply,它将列表作为输入并且不创建输出。

    此解决方案与您的规范之间唯一真正的区别在于,在for 循环中,您可以直接修改同一环境中的变量。使用l_ply你需要发送一个函数,所以如果你想修改父环境中的东西,你将不得不更加小心。

    尝试以下方法:

    library(plyr)
    forp <- function(i, .fun){
      l_ply(i, .fun, .progress="tk")
    }
    
    a <- 0
    forp(1:100, function(i){
      Sys.sleep(0.01)
      a<<-a+i
      })
    print(a)
    [1] 5050
    

    这会创建一个进度条并修改全局环境中a 的值。


    编辑。

    为免生疑问:参数.fun 将始终是具有单个参数的函数,例如.fun=function(i){...}.

    例如:

    for(i in 1:10){expr} 等价于forp(1:10, function(i){expr})

    换句话说:

    • i是循环的循环参数
    • .fun 是一个带有单个参数的函数 i

    【讨论】:

    • 这看起来确实是我的伪代码答案的一个不错的版本。但是:如果你想运行一个包含多个变量的现有函数会发生什么?据我所知,lply(i, myfunc(x,y)) 不会起作用。
    • @CarlWitthoft 没关系,不是吗?因为在 for 循环中只能有一个变量。任何其他变量都在函数体内简单地引用...由于调用堆栈的作用域,它可以工作 - 与 for 循环完全相同。
    • 安德烈,我想我明白你的意思了。 i &lt;- c(1,3,5,6,7,8,9); forp(i,myfunc(x=i,y)) 是它的工作方式。
    • 非常感谢,这是一个巧妙的解决方案,但有一些妥协(+1)。不幸的是它离我所追求的有点远,但我的目标似乎无法实现。
    • @CarlWitthoft 我不确定这是否可行。我已经编辑了我的答案以提供更多细节。 for(i in seq(1, 9, by=2){expr} 的等价物是 forp(i=seq(1, 9, by=2), .fun=function(i){expr})。换句话说,.fun 将始终是一个只有一个参数的函数。
    【解决方案5】:

    我的解决方案与 Andrie 的解决方案非常相似,只是它使用基础 R,我支持他的 cmets 需要将您想要做的事情包装在一个函数中,并且随后需要使用 &lt;&lt;- 在更高的环境中修改内容.

    这是一个什么都不做的函数,而且做的很慢:

    myfun <- function(x, text) {
      Sys.sleep(0.2)
      cat("running ",x, " with text of '", text, "'\n", sep="")
      x
    }
    

    这是我的forp 函数。请注意,无论我们实际循环的是什么,它都会在序列1:n 上循环,并在循环中获得我们实际想要的正确术语。 plyr 会自动执行此操作。

    library(tcltk)
    forp <- function(x, FUN, ...) {
      n <- length(x)
      pb <- tkProgressBar(title = "Working hard:", min = 0, max = n, width = 300)
      out <- vector("list", n)
      for (i in seq_len(n)) {
        out[[i]] <- FUN(x[i], ...)
        setTkProgressBar(pb, i, label=paste( round(i/n*100, 0), "% ready!"))
      }
      close(pb)
      invisible(out)
    }
    

    下面是forforp 的用法,如果我们只想调用myfun

    x <- LETTERS[1:5]
    for(xi in x) myfun(xi, "hi")
    forp(x, myfun, text="hi")
    

    如果我们想在此过程中修改某些内容,它们可能会被使用。

    out <- "result:"
    for(xi in x) {
      out <- paste(out, myfun(xi, "hi"))
    }
    
    out <- "result:"
    forp(x, function(xi) {
        out <<- paste(out, myfun(xi, "hi"))
    })
    

    两个版本的结果都是

    > out
    [1] "result: A B C D E"
    

    编辑:在看到您的(daroczig 的)解决方案后,我有另一个想法可能不是那么笨拙,那就是评估父框架中的表达式。这使得允许除i(现在使用index 参数指定)以外的值变得更容易,尽管到目前为止,我认为它不会将函数作为表达式处理,尽管只是为了代替 for没关系的循环。

    forp2 <- function(index, x, expr) {
      expr <- substitute(expr)
      n <- length(x)
      pb <- tkProgressBar(title = "Working hard:", min = 0, max = n, width = 300)
      for (i in seq_len(n)) {
        assign(index, x[i], envir=parent.frame())
        eval(expr, envir=parent.frame())
        setTkProgressBar(pb, i, label=paste( round(i/n*100, 0), "% ready!"))
      }
      close(pb)
    }
    

    从上面运行我的示例的代码是

    out <- "result:"
    forp2("xi", LETTERS[1:5], {
        out <- paste(out, myfun(xi, "hi"))
    })
    

    结果是一样的。

    另一个编辑,基于您的赏金提供中的附加信息:

    语法forX(1:1000) %doX$ { expression } 是可能的;这就是foreach 包的作用。我现在太懒了,无法根据您的解决方案构建它,但是在我的解决方案基础上,它可能看起来像这样:

    `%doX%` <- function(index, expr) {
      x <- index[[1]]
      index <- names(index)
      expr <- substitute(expr)
      n <- length(x)
      pb <- tkProgressBar(title = "Working hard:", min = 0, max = n, width = 300)
      for (i in seq_len(n)) {
        assign(index, x[i], envir=parent.frame())
        eval(expr, envir=parent.frame())
        setTkProgressBar(pb, i, label=paste( round(i/n*100, 0), "% ready!"))
      }
      close(pb)
      invisible(out)
    }
    
    forX <- function(...) {
      a <- list(...)
      if(length(a)!=1) {
        stop("index must have only one element")
      }
      a
    }
    

    那么使用语法就是这样,结果和上面一样。

    out <- "result:"
    forX(xi=LETTERS[1:5]) %doX% {
      out <- paste(out, myfun(xi, "hi"))
    }
    out
    

    【讨论】:

    • 谢谢 Aaron,这也很棒 (+1)。不完全符合我的要求,但很接近:)
    • 再次感谢 Aaron,特别是更新的脚本。正如我之前写的,如果我们找不到“完美”的解决方案,那么应该将赏金奖励给你。谢谢!
    【解决方案6】:

    感谢大家的热心回答!由于这些都不符合我古怪的需求,我开始窃取一些给定的答案并制作了一个非常定制的版本:

    forp <- function(iis, .fun) {
        .fun <- paste(deparse(substitute(.fun)), collapse='\n')
        .fun <- gsub(' <- ', ' <<- ', .fun, fixed=TRUE)
        .fun <- paste(.fun, 'index.current <- 1 + index.current; setTkProgressBar(pb, index.current, label=paste( round(index.current/index.max*100, 0), "% ready!"))', sep='\n')
        ifelse(is.numeric(iis), index.max <- max(iis), index.max <- length(iis))
        index.current <- 1
        pb <- tkProgressBar(title = "Working hard:", min = 0, max = index.max, width = 300) 
        for (i in iis) eval(parse(text=paste(.fun)))
        close(pb)
    }
    

    对于这样一个简单的函数来说,这相当冗长,但仅依赖于 base(当然是 anf:tcltk)并且有一些不错的特性:

    • 可用于表达式,而不仅仅是函数,
    • 您不必在表达式中使用&lt;&lt;- 来更新全局环境,&lt;- 在给定的表达式中被替换为&lt;&lt;-。好吧,这对某人来说可能很烦人。
    • 可以与非数字索引一起使用(见下文)。这就是代码变得如此之长的原因:)

    用法类似于for,除了您不必指定i in 部分并且您必须使用i 作为循环中的索引。另一个缺点是我没有找到方法来获取函数后指定的{...} 部分,因此必须将其包含在参数中。

    示例#1:基本使用

    > forp(1:1000, {
    +   a<-i
    + })
    > a
    [1] 1000
    

    试试看你电脑上整洁的进度条! :)

    示例#2:循环一些字符

    > m <- 0
    > forp (names(mtcars), {
    +   m <- m + mean(mtcars[,i])
    + })
    > m
    [1] 435.69
    

    【讨论】:

    • 请注意,a &lt;&lt;- b 将被替换为 `a
    • 确实如此 :) 感谢您指出@Carl Witthoft!我已经根据这个问题更新了我的函数,认为由于这个修改,编写 forp 函数的表达式部分将要求用户使用正确格式化的语法(在 &lt;- 之前和之后留一个空格)。
    【解决方案7】:

    问题在于 R 中的 for 循环被特殊对待。不允许正常功能看起来像那样。一些小的调整可以使它循环非常接近。正如@Aaron 所提到的,foreach 包的%dopar% 范式似乎是最合适的。这是我的版本:

    `%doprogress%` <- function(forExpr, bodyExpr) {
       forExpr <- substitute(forExpr)
       bodyExpr <- substitute(bodyExpr)
    
       idxName <- names(forExpr)[[2]]
       vals <- eval(forExpr[[2]])
    
       e <- new.env(parent=parent.frame())
    
       pb <- tkProgressBar(title = "Working hard:", min = 0, max = length(vals), width = 300)
       for (i in seq_along(vals)) {
         e[[idxName]] <- vals[[i]]
         eval(bodyExpr, e)
         setTkProgressBar(pb, i, label=paste( round(i/length(vals)*100, 0), "% ready!"))
       }
    }
    
    
    # Example usage:
    
    foreach(x = runif(10)) %doprogress% { 
      # do something
      if (x < 0.5) cat("small\n") else cat("big")
    }
    

    如您所见,您必须输入x = 1:10 而不是x in 1:10,并且需要中缀运算符%&lt;whatever&gt;% 来获取循环结构和循环体。我目前不做任何错误检查(以避免混淆代码)。您应该检查函数的名称 ("foreach")、它的参数数量 (1) 以及您实际上得到了一个有效的循环变量 ("x") 而不是一个空字符串。

    【讨论】:

    • 如果你选择直接使用foreach,我建议也使用foreach包中的迭代函数iter;类似于foreach:::doSEQ
    • 谢谢汤米,这也很酷。因为@Aaron 更快,尤其是%doX%%doprogress% 短,所以赏金归他:) 我只能给你点赞。
    【解决方案8】:

    我在此提出两个使用标准 for 语法的解决方案,两者都使用来自 Gábor Csárdi 和 Rich FitzJohn 的出色包 progress

    • 1) 我们可以临时或本地覆盖for 函数以环绕base::for 并支持进度条。
    • 2) 我们可以定义未使用的for&lt;-,并使用语法pb -&gt; for(it in seq) {exp} 环绕base::for,其中pb 是使用progress::progress_bar$new() 构建的进度条。

    两种解决方案都作为标准调用:

    • 上一次迭代中更改的值可用
    • 发生错误时,修改后的变量将具有错误前的值

    我打包了我的解决方案,并将在下面进行演示,然后将通过代码


    用法

    #devtools::install_github("moodymudskipper/pbfor")
    library(pbfor)
    

    使用pb_for()

    默认情况下pb_for() 将覆盖for 函数仅运行一次。

    pb_for()
    for (i in 1:10) {
      # DO SOMETHING
      Sys.sleep(0.5)
    }
    

    使用来自progress::progress_bar$new()的参数:

    pb_for(format = "Working hard: [:bar] :percent :elapsed", 
           callback = function(x) message("Were'd done!"))
    for (i in 1:10) {
      # DO SOMETHING
      Sys.sleep(0.5)
    }
    

    使用for&lt;-

    与标准的for 调用相比,唯一的限制是第一个参数必须存在并且不能是NULL

    i <- NA 
    progress_bar$new() -> for (i in 1:10) {
      # DO SOMETHING
      Sys.sleep(0.5)
    }
    

    我们可以定义一个自定义进度条,并且可以方便地在初始化脚本或 R 配置文件中定义它。

    pb <- progress_bar$new(format = "Working hard: [:bar] :percent :elapsed", 
           callback = function(x) ("Were'd done!"))
    pb  -> for (i in 1:10) {
      # DO SOMETHING
      Sys.sleep(0.5)
    }
    

    对于嵌套进度条,我们可以使用以下技巧:

    pbi <- progress_bar$new(format = "i: [:bar] :percent\n\n")
    pbj <- progress_bar$new(format = "j: [:bar] :percent  ")
    i <- NA
    j <- NA
    pbi  -> for (i in 1:10) {
      pbj  -> for (j in 1:10) {
        # DO SOMETHING
        Sys.sleep(0.1)
      }
    }
    

    请注意,由于运算符优先级,调用 for&lt;- 并受益于 for 调用语法的唯一方法是使用从左到右的箭头 ´->´。


    它们的工作原理

    pb_for()

    pb_for() 在其父环境中创建一个for 函数对象,然后是新的for

    • 设置进度条
    • 修改循环内容
    • 在循环内容表达式的末尾添加`*pb*`$tick()
    • 在干净的环境中将其反馈给base::`for`
    • 在退出时将所有修改或创建的变量分配给父环境。
    • 如果onceTRUE(默认值)则删除自己

    覆盖操作符通常很敏感,但它会自行清理,如果在函数中使用不会影响全局环境,所以我认为使用起来足够安全。

    for&lt;-

    这种方法:

    • 不会覆盖for
    • 允许使用进度条模板
    • 有一个可以说更直观的 api

    但是它有一些缺点:

    • 它的第一个参数必须存在,这是所有赋值函数 (fun&lt;-) 的情况。
    • 它使用了一些记忆魔法来找到它的第一个参数的名称,因为它是 not easily done with assignment functions,这可能会降低性能,而且我不能 100% 确定稳健性
    • 我们需要包pryr

    它的作用:

    • 使用辅助函数查找第一个参数的名称
    • 克隆进度条输入
    • 编辑它以考虑循环的迭代次数(for&lt;- 的第二个参数的长度

    在此之后,它类似于上面部分中为 pb_for() 描述的内容。


    代码

    pb_for()

    pb_for <-
      function(
        # all args of progress::progress_bar$new() except `total` which needs to be
        # infered from the 2nd argument of the `for` call, and `stream` which is
        # deprecated
        format = "[:bar] :percent",
        width = options("width")[[1]] - 2,
        complete = "=",
        incomplete = "-",
        current =">",
        callback = invisible, # doc doesn't give default but this seems to work ok
        clear = TRUE,
        show_after = .2,
        force = FALSE,
        # The only arg not forwarded to progress::progress_bar$new()
        # By default `for` will self detruct after being called
        once = TRUE) {
    
        # create the function that will replace `for`
        f <- function(it, seq, expr){
          # to avoid notes at CMD check
          `*pb*` <- IT <- SEQ <- EXPR <- NULL
    
          # forward all arguments to progress::progress_bar$new() and add
          # a `total` argument computed from `seq` argument
          pb <- progress::progress_bar$new(
            format = format, width = width, complete = complete,
            incomplete = incomplete, current = current,
            callback = callback,
            clear = clear, show_after = show_after, force = force,
            total = length(seq))
    
          # using on.exit allows us to self destruct `for` if relevant even if
          # the call fails.
          # It also allows us to send to the local environment the changed/created
          # variables in their last state, even if the call fails (like standard for)
          on.exit({
            vars <- setdiff(ls(env), c("*pb*"))
            list2env(mget(vars,envir = env), envir = parent.frame())
            if(once) rm(`for`,envir = parent.frame())
          })
    
          # we build a regular `for` loop call with an updated loop code including
          # progress bar.
          # it is executed in a dedicated environment and the progress bar is given
          # a name unlikely to conflict
          env <- new.env(parent = parent.frame())
          env$`*pb*` <-  pb
          eval(substitute(
            env = list(IT = substitute(it), SEQ = substitute(seq), EXPR = substitute(expr)),
            base::`for`(IT, SEQ,{
              EXPR
              `*pb*`$tick()
            })), envir = env)
        }
        # override `for` in the parent frame
        assign("for", value = f,envir = parent.frame())
      }
    

    for&lt;-(和fetch_name()

    `for<-` <-
      function(it, seq, expr, value){
        # to avoid notes at CMD check
        `*pb*` <- IT <- SEQ <- EXPR <- NULL
        # the symbol fed to `it` is unknown, R uses `*tmp*` for assignment functions
        # so we go get it by inspecting the memory addresses
        it_chr <- fetch_name(it)
        it_sym <-as.symbol(it_chr)
    
        #  complete the progress bar with the `total` parameter
        # we need to clone it because progress bars are environments and updated
        # by reference
        pb <- value$clone()
        pb$.__enclos_env__$private$total <- length(seq)
    
        # when the script ends, even with a bug, the values that have been changed
        # are written to the parent frame
        on.exit({
          vars <- setdiff(ls(env), c("*pb*"))
          list2env(mget(vars, env),envir = parent.frame())
        })
    
        # computations are operated in a separate environment so we don't pollute it
        # with it, seq, expr, value, we need the progress bar so we name it `*pb*`
        # unlikely to conflict by accident
        env <- new.env(parent = parent.frame())
        env$`*pb*` <-  pb
        eval(substitute(
          env =  list(IT = it_sym, SEQ = substitute(seq), EXPR = substitute(expr)),
          base::`for`(IT, SEQ,{
            EXPR
            `*pb*`$tick()
          })), envir = env)
    
        # because of the `fun<-` syntax we need to return the modified first argument
        invisible(get(it_chr,envir = env))
      }
    

    帮手:

    fetch_name <- function(x,env = parent.frame(2)) {
      all_addresses       <- sapply(ls(env), address2, env)
      all_addresses       <- all_addresses[names(all_addresses) != "*tmp*"]
      all_addresses_short <- gsub("(^|<)[0x]*(.*?)(>|$)","\\2",all_addresses)
    
      x_address       <- tracemem(x)
      untracemem(x)
      x_address_short <- tolower(gsub("(^|<)[0x]*(.*?)(>|$)","\\2",x_address))
    
      ind    <- match(x_address_short, all_addresses_short)
      x_name <- names(all_addresses)[ind]
      x_name
    }
    
    address2 <- getFromNamespace("address2", "pryr")
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-06
      • 1970-01-01
      • 1970-01-01
      • 2014-08-30
      相关资源
      最近更新 更多