Поиск…


замечания

Для циклов - метод управления потоком для повторения задачи или набора задач над доменом. Основная структура цикла for

for ( [index] in [domain]){
  [body]
}

куда

  1. [index] - это имя принимает ровно одно значение [domain] за каждую итерацию цикла.
  2. [domain] - это вектор значений, для которого выполняется итерация.
  3. [body] - это набор инструкций для применения на каждой итерации.

В качестве тривиального примера рассмотрим использование цикла for для получения суммарной суммы вектора значений.

x <- 1:4
cumulative_sum <- 0
for (i in x){
  cumulative_sum <- cumulative_sum + x[i]
}
cumulative_sum

Оптимизация структуры для циклов

Для циклов может быть полезно для концептуализации и выполнения задач для повторения. Однако, если они не сконструированы тщательно, они могут быть очень медленными для выполнения по сравнению с предпочтительным apply семейства функций. Тем не менее, есть несколько элементов, которые вы можете включить в свою конструкцию цикла для оптимизации цикла. Во многих случаях хорошая конструкция цикла for обеспечит вычислительную эффективность, очень близкую к эффективности функции приложения.

Строка «правильно построенная» для цикла строится на основной структуре и включает оператор, объявляющий объект, который будет захватывать каждую итерацию цикла. Этот объект должен иметь как объявленный класс, так и длину.

[output] <- [vector_of_length]
for ([index] in [length_safe_domain]){
  [output][index] <- [body]
}

Чтобы проиллюстрировать это, напишем цикл для квадрата каждого значения в числовом векторе (это только тривиальный пример для иллюстрации. «Правильный» способ выполнения этой задачи - x_squared <- x^2 ).

x <- 1:100
x_squared <- vector("numeric", length = length(x))
for (i in seq_along(x)){
  x_squared[i] <- x[i]^2
}

Опять же, обратите внимание, что мы сначала объявили приемник для вывода x_squared и дали ему класс «числовой» с той же длиной, что и x . Кроме того, мы объявили «безопасный домен», используя функцию seq_along . seq_along генерирует вектор индексов для объекта, который подходит для использования в циклах for. Хотя кажется интуитивным для использования for (i in 1:length(x)) , если x имеет длину 0, цикл попытается выполнить итерацию по домену 1:0 , что приведет к ошибке (0-й индекс не определен в R ).

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

Векторизация для циклов

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

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

column_mean_loop <- vector("numeric", length(mtcars))
for (k in seq_along(mtcars)){
  column_mean_loop[k] <- mean(mtcars[[k]])
}

Цикл for может быть преобразован в функцию приложения, переписав тело цикла как функцию.

col_mean_fn <- function(x) mean(x)
column_mean_apply <- vapply(mtcars, col_mean_fn, numeric(1))

И для сравнения результатов:

identical(column_mean_loop, 
          unname(column_mean_apply)) #* vapply added names to the elements
                                     #* remove them for comparison

Преимущества векторизованной формы заключаются в том, что мы смогли устранить несколько строк кода. Механика определения длины и типа выходного объекта и итерации по длине безопасного домена обрабатывается нами с помощью функции apply. Кроме того, функция apply немного быстрее, чем цикл. Разница скорости часто незначительна в человеческих терминах в зависимости от количества итераций и сложности тела.

Основные для строительства петли

В этом примере мы вычислим квадрат отклонения для каждого столбца в кадре данных, в этом случае mtcars .

Вариант A: целочисленный индекс

squared_deviance <- vector("list", length(mtcars))
for (i in seq_along(mtcars)){
  squared_deviance[[i]] <- (mtcars[[i]] - mean(mtcars[[i]]))^2
}

squared_deviance - это список из 11 элементов, как и ожидалось.

class(squared_deviance)
length(squared_deviance)

Вариант B: индекс символа

squared_deviance <- vector("list", length(mtcars))
Squared_deviance <- setNames(squared_deviance, names(mtcars))
for (k in names(mtcars)){
  squared_deviance[[k]] <- (mtcars[[k]] - mean(mtcars[[k]]))^2
}

Что делать, если мы хотим получить data.frame ? Ну, есть много вариантов преобразования списка в другие объекты. Тем не менее, и , возможно , самый простой в данном случае, будет хранить for результатов в data.frame .

squared_deviance <- mtcars #copy the original
squared_deviance[TRUE]<-NA  #replace with NA or do squared_deviance[,]<-NA
for (i in seq_along(mtcars)){
  squared_deviance[[i]] <- (mtcars[[i]] - mean(mtcars[[i]]))^2
}
dim(squared_deviance)
[1] 32 11

Результат будет тем же самым событием, хотя мы используем опцию character (B).

Оптимальное построение цикла

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

  1. Использование плохо оптимизированного для цикла
  2. Использование хорошо оптимизированного для цикла
  3. Использование *apply семейство функций
  4. Использование функции colMeans

Каждый из этих параметров будет отображаться в коде; будет показано сравнение времени вычисления для выполнения каждой опции; и, наконец, будет дано обсуждение различий.

Плохо оптимизирован для цикла

column_mean_poor <- NULL
for (i in 1:length(mtcars)){
  column_mean_poor[i] <- mean(mtcars[[i]])
}

Хорошо оптимизирован для цикла

column_mean_optimal <- vector("numeric", length(mtcars))
for (i in seq_along(mtcars)){
  column_mean_optimal <- mean(mtcars[[i]])
}

Функция vapply

column_mean_vapply <- vapply(mtcars, mean, numeric(1))

Функция colMeans

column_mean_colMeans <- colMeans(mtcars)

Сравнение эффективности

Результаты сравнительного анализа этих четырех подходов показаны ниже (код не отображается)

Unit: microseconds
     expr     min       lq     mean   median       uq     max neval  cld
     poor 240.986 262.0820 287.1125 275.8160 307.2485 442.609   100    d
  optimal 220.313 237.4455 258.8426 247.0735 280.9130 362.469   100   c 
   vapply 107.042 109.7320 124.4715 113.4130 132.6695 202.473   100 a   
 colMeans 155.183 161.6955 180.2067 175.0045 194.2605 259.958   100  b

Обратите внимание, что оптимизированный for цикла выход из плохо сконструированного цикла. Плохо построенный для цикла постоянно увеличивает длину выходного объекта, и при каждом изменении длины R переоценивает класс объекта.

Часть этой накладной нагрузки удаляется оптимизированной для цикла, объявляя тип выходного объекта и его длину перед запуском цикла.

Однако в этом примере использование функции vapply удваивает вычислительную эффективность, в основном потому, что мы сказали R, что результат должен быть числовым (если какой-либо результат не был числовым, ошибка была бы возвращена).

Использование функции colMeans на ощупь медленнее, чем функция vapply . Эта разница объясняется некоторыми проверками ошибок, выполненными в colMeans и главным образом преобразованием as.matrix (поскольку mtcars - это data.frame ), которые не выполнялись в функции vapply .

Другие контуры Looping: while и repeat

R обеспечивает две дополнительных конструкции зацикливания, while и repeat , которые обычно используются в ситуациях , когда число итераций требуется неопределенное.


В while цикл

Общий вид в while цикла выглядит следующим образом ,

while (condition) {
    ## do something
    ## in loop body
}

где condition оценивается до ввода тела цикла. Если condition принимает значение TRUE , выполняется код внутри тела цикла, и этот процесс повторяется до тех пор, пока condition примет значение FALSE (или будет достигнута инструкция break , см. Ниже). В отличие от for цикла, если во while цикла используется переменная для выполнения дополнительных итераций, переменная должна быть объявлена и инициализирована раньше времени, и должен быть обновлен в теле цикла. Например, следующие циклы выполняют одну и ту же задачу:

for (i in 0:4) {
    cat(i, "\n")
}
# 0 
# 1 
# 2 
# 3 
# 4 

i <- 0
while (i < 5) {
    cat(i, "\n")
    i <- i + 1
}
# 0 
# 1 
# 2 
# 3 
# 4 

В while петли выше, линия i <- i + 1 необходимо , чтобы предотвратить бесконечный цикл.


Кроме того, можно завершить while цикл с вызовом break внутри тела цикла:

iter <- 0
while (TRUE) {
    if (runif(1) < 0.25) {
        break
    } else {
        iter <- iter + 1
    }
}
iter
#[1] 4

В этом примере condition всегда имеет значение TRUE , поэтому единственный способ завершить цикл - это вызов break внутри тела. Обратите внимание, что конечное значение iter будет зависеть от состояния вашего PRNG при запуске этого примера и должно производить разные результаты (по существу) каждый раз, когда выполняется код.


Цикл repeat

Конструкция repeat по существу такая же, как while (TRUE) { ## something } , и имеет следующий вид:

repeat ({
    ## do something
    ## in loop body
})

Дополнительные {} не требуются, но () . Переписывая предыдущий пример, используя repeat ,

iter <- 0
repeat ({
    if (runif(1) < 0.25) {
        break
    } else {
        iter <- iter + 1
    }
})
iter
#[1] 2 

Подробнее о break

Важно отметить, что break прерывает только замкнутый цикл . То есть, это бесконечный цикл:

while (TRUE) {
    while (TRUE) {
        cat("inner loop\n")
        break
    }
    cat("outer loop\n")
}

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

while (TRUE) {
    cat("outer loop body\n")
    while (TRUE) {
        cat("inner loop body\n")
        x <- runif(1)
        if (x < .3) {
            break
        } else {
            cat(sprintf("x is %.5f\n", x))
        }
    }
}

Одна возможность состоит в том, чтобы признать , что, в отличие от break , то return выражение не имеет возможности вернуть контроль на нескольких уровнях вмещающих петель. Однако, поскольку return действителен только при использовании внутри функции, мы не можем просто заменить break с return() выше, но также необходимо обернуть все выражение как анонимную функцию:

(function() {
    while (TRUE) {
        cat("outer loop body\n")
        while (TRUE) {
            cat("inner loop body\n")
            x <- runif(1)
            if (x < .3) {
                return()
            } else {
                cat(sprintf("x is %.5f\n", x))
            }
        }
    }
})()

В качестве альтернативы мы можем создать фиктивную переменную ( exit ) перед выражением и активировать ее через <<- из внутреннего цикла, когда мы готовы к завершению:

exit <- FALSE
while (TRUE) {
    cat("outer loop body\n")
    while (TRUE) {
        cat("inner loop body\n")
        x <- runif(1)
        if (x < .3) {
            exit <<- TRUE
            break
        } else {
            cat(sprintf("x is %.5f\n", x))
        }
    }
    if (exit) break
}


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow