7 Условные конструкции и циклы
7.1 Выражения if
, else
, else if
Стандратная часть практически любого языка программирования — условные конструкции. R не исключение. Однако и здесь есть свои особенности. Начнем с самого простого варианта с одним условием. Выглядеть условная конcтрукция будет вот так:
if (условие) выражение
Вот так это будет работать на практике:
<- 1
number if (number > 0) "Положительное число"
## [1] "Положительное число"
Если выражение (expression) содержит больше одной строчки, то они объединяются фигурными скобками. Впрочем, использовать их можно, даже если строчка всего в выражении всего одна.
<- 1
number if (number > 0) {
"Положительное число"
}
## [1] "Положительное число"
В рассмотренной нами конструкции происходит проверка на условие. Если условие верно14, то происходит то, что записано в последующем выражении. Если же условие неверно15, то ничего не происходит.
Оператор else
позволяет задавать действие на все остальные случаи:
if (условие) выражение else выражение
Работает это так:
<- -3
number if (number > 0) {
"Положительное число"
else {
} "Отрицательное число или ноль"
}
## [1] "Отрицательное число или ноль"
Иногда нам нужна последовательная проверка на несколько условий. Для этого есть оператор else if
. Вот как выглядит ее применение:
<- 0
number if (number > 0) {
"Положительное число"
else if (number < 0){
} "Отрицательное число"
else {
} "Ноль"
}
## [1] "Ноль"
Как мы помним, R — язык, в котором векторизация играет большое значение. Но вот незадача — условные конструкции не векторизованы в R! Давайте попробуем применить эти конструкции для вектора значений и посмотрим, что получится.
<- -2:2
number 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
какой-то объект (вектор, список, что угодно) изменяется в размере. Лучше будет создать заранее объект нужного размера, который затем будет наполняться значениями:
<- character(length(number)) #создаем строковый вектор с такой же длиной, как и исходный вектор
number_descriptions 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
на тех же самых местах. Выходит, что ничего не меняется!
У ifelse()
тоже есть недостаток: он не может включать в себя дополнительных условий по типу else if
. В простых ситуациях можно вставлять ifelse()
внутри ifelse()
:
ifelse(number > 0,
"Положительное число",
ifelse(number < 0, "Отрицательное число", "Ноль"))
## [1] "Отрицательное число" "Отрицательное число" "Ноль"
## [4] "Положительное число" "Положительное число"
Достаточно симпатичное решение предлогает пакет dplyr
(основа tidyverse) — функция case_when()
, которая работает с использованием формулы:
::case_when(
dplyr> 0 ~ "Положительное число",
number < 0 ~ "Отрицательное число",
number == 0 ~ "Ноль") number
## [1] "Отрицательное число" "Отрицательное число" "Ноль"
## [4] "Положительное число" "Положительное число"