R Language
Структуры управляющих потоков
Поиск…
замечания
Для циклов - метод управления потоком для повторения задачи или набора задач над доменом. Основная структура цикла for
for ( [index] in [domain]){
[body]
}
куда
-
[index]
- это имя принимает ровно одно значение[domain]
за каждую итерацию цикла. -
[domain]
- это вектор значений, для которого выполняется итерация. -
[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).
Оптимальное построение цикла
Чтобы проиллюстрировать эффект хорошей конструкции цикла, мы вычислим среднее значение для каждого столбца четырьмя различными способами:
- Использование плохо оптимизированного для цикла
- Использование хорошо оптимизированного для цикла
- Использование
*apply
семейство функций - Использование функции
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
}