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

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

 

О циклах

Начнем с того, что часто оказывается неприятным сюрпризом для тех, кто переходит на R с классических языков программирования: если мы хотим написать цикл, то стоит перед этим на секунду задуматься. Дело в том, что в языках для работы с большим объемом данных циклы, как правило, уступают по эффективности специализированным функциям запросов, фильтрации, агрегации и трансформации данных. Это легко запомнить на примере баз данных, где большинство операций производится с помощью языка запросов SQL, а не с помощью циклов.

Чтобы понять, насколько важно это правило, давай обратимся к цифрам. Допустим, у нас есть очень простая таблица из двух столбцов a и b. Первый растет от 1 до 100 000, второй уменьшается со 100 000 до 1:

testDF <- data.frame(a = 1:100000, b = 100000:1)

Если мы хотим посчитать третий столбец, который будет суммой первых двух, то ты удивишься, как много начинающих R-разработчиков могут написать код такого вида:

for(row in 1:nrow(testDF))
  testDF[row, 3] <- testDF[row, 1] + testDF[row, 2] # Ужас!

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

testDF <- testDF %>% mutate(c = a + b)

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

 

Классические циклы

R поддерживает основные классические способы написания циклов:

  • for — самый распространенный тип циклов. Синтаксис очень прост и знаком разработчикам на различных языках программирования. Мы уже пробовали им воспользоваться в самом начале статьи. for выполняет переданную ему функцию для каждого элемента.
      # Напечатаем номера от 1 до 10
      for(i in 1:10)
        print(i)
    
      # Напечатаем все строки из вектора strings
      strings <- c("Один", "Два", "Три")
      for(str in strings)
        print(str)
  • Чуть менее распространенные while и repeat, которые тоже часто встречаются в других языках программирования. В while перед каждой итерацией проверяется логическое условие, и если оно соблюдается, то выполняется итерация цикла, если нет — цикл завершается:
      while(cond) expr

    В repeat цикл повторяется до тех пор, пока в явном виде не будет вызван оператор break:

    repeat expr

Стоить отметить, что for, while и repeat всегда возвращают NULL, — и в этом их отличие от следующей группы циклов.

 

Циклы на основе apply

apply, eapply, lapply, mapply, rapply, sapply, tapply, vapply — достаточно большой список функций-циклов, объединенных одной идеей. Отличаются они тем, к чему цикл применяется и что возвращает. Начнем с базового apply, который применяется к матрицам:

apply(X, MARGIN, FUN, ...)

В первом параметре (X) указываем исходную матрицу, во втором параметре (MARGIN) уточняем способ обхода матрицы (1 — по строкам, 2 — по столбцам, с(1,2) — по строкам и столбцам), третьим параметром указываем функцию FUN, которая будет вызвана для каждого элемента. Результаты всех вызовов будут объединены в один вектор или матрицу, которую функция apply и вернет в качестве результирующего значения.

Например, создадим матрицу m размером 3 х 3.

m <- matrix(1:9, nrow = 3, ncol = 3)

print(m)
         [,1] [,2] [,3]
 [1,]       1    4    7
 [2,]       2    5    8
 [3,]       3    6    9

Попробуем функцию apply в действии.

apply(m, MARGIN = 1, FUN = sum) # Сумма ячеек для каждой строчки
[1] 12 15 18

apply(m, MARGIN = 2, FUN = sum) # Сумма ячеек для каждого столбца
[1]  6 15 24

Для простоты я передал в apply существующую функцию sum, но ты можешь использовать свои функции — собственно, поэтому apply и является полноценной реализацией цикла. Например, заменим сумму нашей функцией, которая сначала производит суммирование и, если сумма равна 15, заменяет возвращаемое значение на 100.

apply(m, MARGIN = 1, # Вызов нашей функции для каждой строчки
  FUN = function(x)  # Определяем нашу функцию прямо в вызове apply
  {
    s <- sum(x)   # Считаем сумму
    if (s == 15)  # Если сумма равна 15, то поменяем ее на 100
      s <- 100
    (s)
  }
)
[1]  12 100  18

Другая распространенная функция из этого семейства — lapply.

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для статей, опубликованных более двух месяцев назад.


Комментарии

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Как Apple обходит стандарты, заставляя тебя платить. Колонка Олега Афонина

Иногда сложные вещи начинаются с простых: планшет iPad Pro 10.5 вдруг перестал заряжаться …