Обертка для циклов FOR с индикатором выполнения - PullRequest
19 голосов
/ 08 сентября 2011

Мне нравится использовать индикатор выполнения при медленных циклах for. Это можно легко сделать с помощью нескольких помощников, но мне нравится пакет tkProgressBar из tcltk .

Небольшой пример:

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, но с добавленным авто Индикатор выполнения - но, к сожалению, понятия не имею, как реализовать и захватить expr часть функции цикла. У меня было несколько экспериментов с do.call, но безуспешно: (

Мнимый рабочий пример (который действует как цикл for, но создает TkProgressBar и автоматически обновляет его в каждой итерации):

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

ОБНОВЛЕНИЕ : Я думаю, суть вопроса в том, как написать функцию, которая не только имеет параметры в скобках после функции (например: foo(bar)), но также может обрабатывать expr указывается после закрывающих скобок, например: foo(bar) expr.


BOUNTY OFFER : пойдет на любой ответ, который может изменить предложенную мной функцию , чтобы она работала как синтаксис базовых циклов for. Например. вместо

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

это можно назвать как:

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

Просто чтобы прояснить задачу еще раз : как мы можем получить часть { expression } вызова функции? Боюсь, что это невозможно, но оставлю на награду на несколько дней профи:)

Ответы [ 8 ]

6 голосов
/ 08 сентября 2011

Мое решение очень похоже на решение Андри, за исключением того, что оно использует базу R, и я комментирую его необходимость в том, чтобы обернуть то, что вы хотите сделать в функции, и последующую необходимость использовать <<- для изменения содержимого в более высокой среде..

Вот функция, которая ничего не делает и делает это медленно:

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)
}

И вот как можно использовать for и forp, если все, что мы хотим сделать, это вызвать 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"

РЕДАКТИРОВАТЬ: После просмотра вашего (ДарокцигаРешение, у меня есть еще одна идея, которая может быть не такой уж громоздкой, которая заключается в оценке выражения в родительском фрейме.Это облегчает учет значений, отличных от i (теперь указывается с помощью аргумента index), хотя на данный момент я не думаю, что он обрабатывает функцию в качестве выражения, хотя просто добавлю вместоцикл, который не должен иметь значения.

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
6 голосов
/ 08 сентября 2011

Учитывая другие предоставленные ответы, я подозреваю, что невозможно трудно сделать именно так, как вы указали.

Однако я считаю, что есть способ очень близко подойти, если вы используете пакет 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 в глобальной среде.


EDIT.

Во избежание сомнений: аргумент .fun всегда будет функцией с одним аргументом, например, .fun=function(i){...}.

Например:

for(i in 1:10){expr} эквивалентно forp(1:10, function(i){expr})

Другими словами:

  • i - параметр цикла цикла
  • .fun - это функция с одним аргументом i
3 голосов
/ 04 мая 2019

Предлагаю два решения, использующих стандартный синтаксис for, оба используют отличный пакет прогресс от Габора Чарди и Рича ФитцДжона

  • 1) мы можем временно или локально переопределить функцию for, чтобы обернуть вокруг base::for и поддерживать индикаторы выполнения.
  • 2) мы можем определить неиспользованную for<- и обернуть вокруг base::for, используя синтаксисpb -> for(it in seq) {exp}, где 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<-

Единственное ограничение по сравнению со стандартным вызовом 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<- и воспользоваться синтаксисом вызовов for - это использовать стрелку слева направо ´-> ´.


как они работают

pb_for()

pb_for() создает функциональный объект for в родительской среде, затем новый for:

  • устанавливает индикатор выполнения
  • изменяет содержимое цикла
  • добавляет `*pb*`$tick() в конце выражения содержимого цикла
  • возвращает его обратно к base::`for` в чистой среде
  • назначает назавершить все измененные или созданные переменные в родительскую среду.
  • удаляет себя, если once is TRUE (по умолчанию)

Обычно чувствителен к переопределению оператора, но он очищается после себя и не влияет на глобальную среду, если используется в функции, поэтому я думаю, что это безопаснодостаточно для использования.

for<-

Этот подход:

  • не отменяет for
  • позволяет использовать индикатор выполненияШаблоны
  • имеют, пожалуй, более интуитивно понятный API

Однако у него есть несколько недостатков:

  • его первый аргумент должен существовать, что имеет место для всехфункции присваивания (fun<-).
  • он использует магию памяти, чтобы найти имя своего первого аргумента, так как это нелегко сделать с помощью функций присваивания , это может привести к снижению производительности, иЯ не уверен на 100% в надежности
  • нам нужен пакет pryr

Что он делает:

  • найтиимя первого аргумента, используя вспомогательную функцию
  • , клонировать вход индикатора выполнения
  • отредактируйте его, чтобы учесть количество итераций цикла (длина второго аргумента for<-

После этого это похоже на то, что описано для 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<-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")
3 голосов
/ 20 сентября 2011

Проблема в том, что цикл for в R обрабатывается специальным образом. Нормальная функция не может выглядеть так. Некоторые небольшие изменения могут сделать это довольно близко. И, как заметил @Aaron, парадигма %dopar% пакета foreach кажется наиболее подходящей. Вот моя версия того, как это может работать:

`%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, а инфиксный оператор %<whatever>% необходим, чтобы получить зацикливающую конструкцию и тело цикла. В настоящее время я не делаю никакой проверки ошибок (чтобы не запутать код). Вам следует проверить имя функции ("foreach"), количество аргументов для нее (1) и наличие действительной переменной цикла ("x"), а не пустой строки.

3 голосов
/ 08 сентября 2011

То, на что вы надеетесь, я думаю, будет похоже на

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

И да, проблема в том, что "for" - это не функция, или, по крайней мере, не та, чье "тело""доступно.Вы могли бы, я полагаю, создать функцию «forp», которая принимает в качестве аргументов 1) строку для преобразования в счетчик цикла, например, " ( i in seq(1,101,5) )", и 2) тело вашего предполагаемого цикла, например, y[i]<- foo[i]^2 ; points(foo[i],y[i],а затем прыгните через некоторую магию getcallparse для выполнения фактического цикла for.Затем в псевдокоде (не близко к реальному R-коду, но я думаю, вы видите, что должно произойти)

forp<-function(indexer,loopbody) { 

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

3 голосов
/ 08 сентября 2011
Синтаксис

R не позволяет вам делать именно то, что вы хотите, то есть:

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

Но вы можете создать некий объект итератора и цикл с помощью while ():

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

Теперь у вас есть проблема с тем, что m и как вы заставляете nextStep(m) иметь побочные эффекты на m, чтобы он возвращал FALSE в конце вашего цикла.Я написал простые итераторы, которые делают это, а также итераторы MCMC, которые позволяют вам определять и тестировать период обжига и прореживания в вашем цикле.

Недавно на конференции R User я видел, как кто-то определил 'do'функция, которая тогда работала как оператор, что-то вроде:

do(100) %*% foo()

, но я не уверен, что это был точный синтаксис, и я не уверен, как его реализовать или кто это выдвинул... Возможно, кто-то еще может вспомнить!

3 голосов
/ 08 сентября 2011

Если вы используете семейство команд plyr вместо цикла for (как правило, хорошая идея, если это возможно), вы получаете в качестве дополнительного бонуса целую систему индикаторов выполнения.

R.utils также имеет некоторые встроенные индикаторы выполнения, и существуют инструкции по их использованию в циклах .

0 голосов
/ 09 сентября 2011

Спасибо всем за ваши добрые ответы!Так как ни один из них не соответствует моим дурацким потребностям, я начал красть некоторые части данных ответов и составил совершенно индивидуальную версию:

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)
}

Это довольно долго для такой простой функции, как эта, но зависит только отна базе (и конечно: tcltk) и имеет несколько приятных особенностей:

  • может использоваться в выражениях, а не только в функциях,
  • вам не нужно использовать <<- вВаши выражения для обновления глобальной среды, <- заменяются на <<- в данном выражении.Что ж, это может кого-то раздражать.
  • можно использовать с нечисловыми индексами (см. Ниже).Вот почему код становится таким длинным:)

Использование похоже на 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
...