7 Условные конструкции и циклы

7.1 Выражения if, else, else if

Стандратная часть практически любого языка программирования — условные конструкции. R не исключение. Однако и здесь есть свои особенности. Начнем с самого простого варианта с одним условием. Выглядеть условная конcтрукция будет вот так:

if (условие) выражение

Вот так это будет работать на практике:

number <- 1
if (number > 0) "Положительное число"
## [1] "Положительное число"

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

number <- 1
if (number > 0) {
  "Положительное число"
}
## [1] "Положительное число"

В рассмотренной нами конструкции происходит проверка на условие. Если условие верно14, то происходит то, что записано в последующем выражении. Если же условие неверно15, то ничего не происходит.

Оператор else позволяет задавать действие на все остальные случаи:

if (условие) выражение else выражение

Работает это так:

number <- -3
if (number > 0) {
  "Положительное число"
} else {
  "Отрицательное число или ноль"
}
## [1] "Отрицательное число или ноль"

Иногда нам нужна последовательная проверка на несколько условий. Для этого есть оператор else if. Вот как выглядит ее применение:

number <- 0
if (number > 0) {
  "Положительное число"
} else if (number < 0){
  "Отрицательное число"
} else {
  "Ноль"
}
## [1] "Ноль"

Как мы помним, R — язык, в котором векторизация играет большое значение. Но вот незадача — условные конструкции не векторизованы в R! Давайте попробуем применить эти конструкции для вектора значений и посмотрим, что получится.

number <- -2:2
if (number > 0) {
  "Положительное число"
} else if (number < 0){
  "Отрицательное число"
} else {
  "Ноль"
}
## Warning in if (number > 0) {: длина условия > 1, будет использован только первый
## элемент
## Warning in if (number < 0) {: длина условия > 1, будет использован только первый
## элемент
## [1] "Отрицательное число"

R выдает сообщение, что используется только первое значение логического вектора внутри условия. Остальные просто игнорируются. Как же посчитать для всего вектора сразу?

7.2 Циклы for

Во-первых, можно использовать for. Синтаксис у for похож на синтаксис условных конструкций.

for(переменная in последовательность) выражение

Теперь мы можем объединить условные конструкции и for. Немножко монструозно, но это работает:

for (i in number) {
  if (i > 0) {
    print("Положительное число")
  } else if (i < 0) {
    print("Отрицательное число")
  } else {
    print("Ноль")
  }
}
## [1] "Отрицательное число"
## [1] "Отрицательное число"
## [1] "Ноль"
## [1] "Положительное число"
## [1] "Положительное число"

Чтобы выводить в консоль результат вычислений внутри for, нужно использовать print().

Здесь стоит отметить, что for используется в R относительно редко. В подавляющем числе ситуаций использование for можно избежать. Обычно мы работаем в R с векторами или датафреймами, которые представляют собой множество относительно независимых наблюдений. Если мы хотим провести какие-нибудь операции с этими наблюдениями, то они обычно могут быть выполнены параллельно. Скажем, вы хотите для каждого испытуемого пересчитать его массу из фунтов в килограммы. Этот пересчет осуществляется по одинаковой формуле для каждого испытуемого. Эта формула не изменится из-за того, что какой-то испытуемый слишком большой или слишком маленький - для следующего испытуемого формула будет прежняя. Если Вы встречаете подобную задачу (где функцию можно применить независимо для всех значений), то без цикла for вполне можно обойтись.

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

cumsum(1:10)
##  [1]  1  3  6 10 15 21 28 36 45 55

Если же нет подходящей векторизованной функции, то можно воспользоваться семейством функций apply() (см. @ref(apply_f) ).

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

Зачем вообще избегать конструкций for? Некоторые говорят, что они слишком медленные, и частично это верно, если мы сравниваем с векторизованными функциями, которые написаны на более низкоуровневых языках. Но в большинстве случаев низкая скорость for связана с неправильным использованием этой конструкции. Например, стоит избегать ситуации, когда на каждой итерации for какой-то объект (вектор, список, что угодно) изменяется в размере. Лучше будет создать заранее объект нужного размера, который затем будет наполняться значениями:

number_descriptions <- character(length(number)) #создаем строковый вектор с такой же длиной, как и исходный вектор
for (i in 1:length(number)) {
  if (number[i] > 0) {
    number_descriptions[i] <- "Положительное число"
  } else if (number[i] < 0) {
    number_descriptions[i] <- "Отрицательное число"
  } else {
    number_descriptions[i] <- "Ноль"
  }
}
number_descriptions
## [1] "Отрицательное число" "Отрицательное число" "Ноль"               
## [4] "Положительное число" "Положительное число"

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

7.3 Векторизованные условные конструкции: функции ifelse() и dplyr::case_when()

Альтернатива сочетанию условных конструкций и циклов for является использование встроенной функции ifelse(). Функция ifelse() принимает три аргумента - 1) условие (т.е. просто логический вектор, состоящий из TRUE и FALSE), 2) что выдавать в случае TRUE, 3) что выдавать в случае FALSE. На выходе получается вектор такой же длины, как и изначальный логический вектор (условие).

ifelse(number > 0, "Положительное число", "Отрицательное число или ноль")
## [1] "Отрицательное число или ноль" "Отрицательное число или ноль"
## [3] "Отрицательное число или ноль" "Положительное число"         
## [5] "Положительное число"

Периодически я встречаю у студентов строчку вроде такой: ifelse(условие, TRUE, FALSE). Эта конструкция избыточна, т.к. получается, что логический вектор из TRUE и FALSE превращается в абсолютно такой же вектор из TRUE и FALSE на тех же самых местах. Выходит, что ничего не меняется!

Пакеты {dplyr} и {data.table} предоставляют более быстрые и более строгие альтернативы для базовой функции ifelse() с аналогичным синтаксисом:

dplyr::if_else(number > 0, "Положительное число", "Отрицательное число или ноль")
## [1] "Отрицательное число или ноль" "Отрицательное число или ноль"
## [3] "Отрицательное число или ноль" "Положительное число"         
## [5] "Положительное число"
data.table::fifelse(number > 0, "Положительное число", "Отрицательное число или ноль")
## [1] "Отрицательное число или ноль" "Отрицательное число или ноль"
## [3] "Отрицательное число или ноль" "Положительное число"         
## [5] "Положительное число"

Если вы пользуетесь одним из этих пакетов (о них пойдет речь далее — см. @ref(tidy_intro)), то я советую пользоваться соотвествующей функцией вместо базового ifelse().

Обе функции будут избегать скрытого приведения типов (см. 3.2) и намеренно выдавать ошибку при использовании разных типов данных в параметрах yes = и no =. Помните, что NA по умолчанию — это логический тип данных, поэтому в этих функциях нужно использовать NA соответствующего типа NA_character_, NA_integer_, NA_real_, NA_complex_ (см. 3.7).

У ifelse() тоже есть недостаток: он не может включать в себя дополнительных условий по типу else if. В простых ситуациях можно вставлять ifelse() внутри ifelse():

ifelse(number > 0,
       "Положительное число",
       ifelse(number < 0, "Отрицательное число", "Ноль"))
## [1] "Отрицательное число" "Отрицательное число" "Ноль"               
## [4] "Положительное число" "Положительное число"

Достаточно симпатичное решение есть в пакете dplyr — функция case_when(), которая работает с использованием формулы:

dplyr::case_when(
  number > 0 ~ "Положительное число",
  number < 0 ~ "Отрицательное число",
  number == 0 ~ "Ноль")
## [1] "Отрицательное число" "Отрицательное число" "Ноль"               
## [4] "Положительное число" "Положительное число"

В data.table тоже есть свой (более быстрый) аналог case_when() — функция fcase(). Синтаксис отличается только тем, что вместо формул используются простые запятые:

data.table::fcase(
  number > 0, "Положительное число",
  number < 0, "Отрицательное число",
  number == 0, "Ноль")
## [1] "Отрицательное число" "Отрицательное число" "Ноль"               
## [4] "Положительное число" "Положительное число"

Задача создания вектора или колонки по множественным условиям из другой колонки плавно перетекает в задачу объединения двух датафреймов по единому ключу, и такое решение может оказаться наиболее быстрым (см. @ref(tidy_join)).


  1. В принципе, необязательно внутри должна быть проверка условий, достаточно просто значения TRUE.↩︎

  2. Аналогично, достаточно просто значения FALSE.↩︎