<- 1
number if (number > 0) "Положительное число"
[1] "Положительное число"
if
, else
, else if
Стандартная часть практически любого языка программирования — условные конструкции. R не исключение. Однако и здесь есть свои особенности. Начнем с самого простого варианта с одним условием. Выглядеть условная конcтрукция будет вот так:
if (условие) выражение
Вот так это будет работать на практике:
<- 1
number if (number > 0) "Положительное число"
[1] "Положительное число"
Если выражение (expression) содержит больше одной строчки, то они объединяются фигурными скобками. Впрочем, использовать их можно, даже если строчка всего в выражении всего одна.
<- 1
number if (number > 0) {
"Положительное число"
}
[1] "Положительное число"
В рассмотренной нами конструкции происходит проверка на условие. Если условие верно1, то происходит то, что записано в последующем выражении. Если же условие неверно2, то ничего не происходит.
Оператор 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
numbers if (numbers > 0) {
"Положительное число"
else if (numbers < 0){
} "Отрицательное число"
else {
} "Ноль"
}
Error in if (numbers > 0) {: the condition has length > 1
Ошибка! Однако если у вас более старая версия R (до 4.2.0, апрель 2022), то вместо ошибки будет учитываться только первое значение вектора условий: остальные будут игнорироваться, при этом будет выводиться предупреждение. Как же посчитать для всего вектора сразу?
for
Во-первых, можно использовать for
. Синтаксис у for
похож на синтаксис условных конструкций.
for(переменная in последовательность) выражение
Теперь мы можем объединить условные конструкции и for
. Немножко монструозно, но это работает:
for (i in numbers) {
if (i > 0) {
print("Положительное число")
else if (i < 0) {
} print("Отрицательное число")
else {
} print("Ноль")
} }
[1] "Отрицательное число"
[1] "Отрицательное число"
[1] "Ноль"
[1] "Положительное число"
[1] "Положительное число"
Здесь стоит отметить, что for
используется в R относительно редко. В подавляющем числе ситуаций использование for
можно избежать. Обычно мы работаем в R с векторами или датафреймами, которые представляют собой множество относительно независимых наблюдений. Если мы хотим провести какие-нибудь операции с этими наблюдениями, то они обычно могут быть выполнены параллельно. Скажем, вы хотите для каждого испытуемого пересчитать его массу из фунтов в килограммы. Этот пересчет осуществляется по одинаковой формуле для каждого испытуемого. Эта формула не изменится из-за того, что какой-то испытуемый слишком большой или слишком маленький - для следующего испытуемого формула будет прежняя. Если Вы встречаете подобную задачу (где функцию можно применить независимо для всех значений), то без цикла for
вполне можно обойтись.
Даже во многих случаях, где расчеты для одной строчки зависят от расчетов предыдущих строчек, можно обойтись без for
векторизованными функциями, например, cumsum()
для подсчета кумулятивной суммы.
cumsum(1:10)
[1] 1 3 6 10 15 21 28 36 45 55
Если же нет подходящей векторизованной функции, то можно воспользоваться семейством функций apply()
(см. Глава 8.5).
Зачем вообще избегать конструкций for
? Некоторые говорят, что они слишком медленные, и частично это верно, если мы сравниваем с векторизованными функциями, которые написаны на более низкоуровневых языках. Но в большинстве случаев низкая скорость for
связана с неправильным использованием этой конструкции. Например, стоит избегать ситуации, когда на каждой итерации for
какой-то объект (вектор, список, что угодно) изменяется в размере. Лучше будет создать заранее объект нужного размера, который затем будет наполняться значениями:
<- character(length(numbers)) #создаем строковый вектор с такой же длиной, как и исходный вектор
numbers_descriptions for (i in 1:length(numbers)) {
if (numbers[i] > 0) {
<- "Положительное число"
numbers_descriptions[i] else if (numbers[i] < 0) {
} <- "Отрицательное число"
numbers_descriptions[i] else {
} <- "Ноль"
numbers_descriptions[i]
}
} numbers_descriptions
[1] "Отрицательное число" "Отрицательное число" "Ноль"
[4] "Положительное число" "Положительное число"
В общем, при правильном обращении с for
особых проблем со скоростью не будет, хотя векторизованные функции и будут быстрее. Но все равно это будет громоздкая конструкция, в которой легко ошибиться, и которую, скорее всего, можно заменить одной короткой строчкой. Кроме того, без конструкции for
код обычно легко превратить в набор функций, последовательно применяющихся к данным, что мы будем по максимуму использовать, работая в tidyverse и применяя пайпы (см. Глава 10.4).
ifelse()
и dplyr::case_when()
Из-за того, что конструкция if/else/else if не векторизованная, она редко используется непосредственно в операциях с данными, обычно она используется при написании функций (Глава 8.1) и разработке пакетов.
Альтернативой сочетанию условных конструкций и циклов for
является использование встроенной функции ifelse()
. Функция ifelse()
принимает три аргумента:
test =
– условие (т.е. просто логический вектор, состоящий из TRUE
и FALSE
),
yes =
– что выдавать в случае TRUE
,
no =
– что выдавать в случае FALSE
.
На выходе получается вектор такой же длины, как и изначальный логический вектор (условие). Это очень похоже на ЕСЛИ()
в Microsoft Excel.
ifelse(numbers > 0, "Положительное число", "Отрицательное число или ноль")
[1] "Отрицательное число или ноль" "Отрицательное число или ноль"
[3] "Отрицательное число или ноль" "Положительное число"
[5] "Положительное число"
Пакеты {dplyr}
и {data.table}
предоставляют более быстрые и более строгие альтернативы для базовой функции ifelse()
с аналогичным синтаксисом:
::if_else(numbers > 0, "Положительное число", "Отрицательное число или ноль") dplyr
[1] "Отрицательное число или ноль" "Отрицательное число или ноль"
[3] "Отрицательное число или ноль" "Положительное число"
[5] "Положительное число"
::fifelse(numbers > 0, "Положительное число", "Отрицательное число или ноль") data.table
[1] "Отрицательное число или ноль" "Отрицательное число или ноль"
[3] "Отрицательное число или ноль" "Положительное число"
[5] "Положительное число"
Если вы пользуетесь одним из этих пакетов (о них пойдет речь далее — см. Глава 9, то я советую пользоваться соотвествующей функцией вместо базового ifelse()
.
У ifelse()
тоже есть недостаток: он не может включать в себя дополнительных условий по типу else if
. В простых ситуациях можно вставлять ifelse()
внутри ifelse()
:
ifelse(numbers > 0,
"Положительное число",
ifelse(numbers < 0, "Отрицательное число", "Ноль"))
[1] "Отрицательное число" "Отрицательное число" "Ноль"
[4] "Положительное число" "Положительное число"
Достаточно симпатичное решение есть в пакете dplyr
— функция case_when()
, которая работает с использованием формулы:
::case_when(
dplyr> 0 ~ "Положительное число",
numbers < 0 ~ "Отрицательное число",
numbers == 0 ~ "Ноль") numbers
[1] "Отрицательное число" "Отрицательное число" "Ноль"
[4] "Положительное число" "Положительное число"
Функция case_when()
работает по той же логике, что и if/else/else if конструкция: сначала идет проверка на первое условие (как первое if в конструкции if/else/else if ). Если проверка проходит (то есть в условии получается TRUE
), то соответствующее значение возвращается, а остальные условия не проверяются. Если же первое условие не выполняется, то идет проверка на следующее условие (аналог else if). Если же и оно не выполняется, то идет проверка на следующее (следующее else if), пока проверка не пройдет до последнего условия. Можно поставить значение по умолчанию с помощью параметра .default =
, которое будет возвращаться, если все проверки выдали FALSE
.
<- read.csv("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv", na.strings = c("NA", "-", "-99"))
heroes
$weight_group <- dplyr::case_when(
heroes$Weight > 200 ~ "overweight", # "if"
heroes$Weight > 120 ~ "somewhat overweight", # "else if"
heroes$Weight < 50 ~ "underweight", # next "else if"
heroes.default = "typical weight" # final "else"
)
Важный момент: если ifelse()
возвращает NA
на NA
в условии, что обычно нас устраивает (у нас нет данных, что у нас на входе, следовательно, не знаем, что на выходе), то case_when()
такого не делает. NA
в условии считается как FALSE
, поэтому нужно дополнительно обрабатывать условие для него. Чтобы на место NA
поставить NA, нужно записать вот так:
$weight_group <- dplyr::case_when(
heroes$Weight > 200 ~ "overweight", # "if"
heroes$Weight > 120 ~ "somewhat overweight", # "else if"
heroes$Weight < 50 ~ "underweight", # next "else if"
heroesis.na(heroes$Weight) ~ NA, # one more "else if", maps NA to NA
.default = "typical weight" # final "else"
# final "else" )
В {data.table}
тоже есть свой (более быстрый) аналог case_when()
— функция fcase()
. Синтаксис отличается только тем, что вместо формул используются простые запятые. То есть первый аргумент – условие, второй – значение, которое возвращается при верности первого аргумента, третий аргумент – условие, четвертый – возвращаемое значение при верности третьего аргумента и т.д.
::fcase(
data.table> 0, "Положительное число",
numbers < 0, "Отрицательное число",
numbers == 0, "Ноль") numbers
[1] "Отрицательное число" "Отрицательное число" "Ноль"
[4] "Положительное число" "Положительное число"
Задача создания вектора или колонки по множественным условиям из другой колонки плавно перетекает в задачу объединения двух датафреймов по единому ключу, и такое решение может оказаться наиболее быстрым (см. Глава 11.1.2).
В принципе, необязательно внутри должна быть проверка условий, достаточно просто значения TRUE
.↩︎
Аналогично, достаточно просто значения FALSE
.↩︎
В более свежих версиях пакета {dplyr}
разработчики отказались от этой “строгости”, поэтому NA
все-таки будут приводиться к нужному типу. Однако data.table::fifelse()
остался строгим.↩︎