Циклы в r studio

Циклы в r studio thumbnail

автор: Samoedd
Ноябрь 29, 2017

Программирование на R

Создание скриптов и функций в R зачастую требует навыки программирования, а именно: использования логических операторов (например, >= “больше или равно”) и управляющих структур (if elsefor и while). Благодаря им мы можем задавать условия, при которых будет выполняться то или иное действие, а также определять порядок выполнения действий и их повторяемость.

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

Логические операторы в R

Операторы в R можно разделить на две категории: арифметические и логические. Арифметическими операторами являются знакомые нам со школы знаки сложения, вычитания, умножения и деления, а также знак возведения в степень и модульные операции (+, -, *, /, ^, %% и %/%, соответственно). Логические операторы обычно используются при проверке условий и выдают значения: TRUE или FALSE.

В языке R существует 9 логических операторов, без знания которых программирование не представляется возможным.

ОператорОписание
>Больше
>=Больше или равно
<Меньше
<=Меньше или равно
==Равно
!=Не равно
&Логическое И
|Логическое ИЛИ
!Логическое НЕ

Обратите внимание, что “=” и “==” – это два разных оператора: в то время как первый присваивает значение переменной, второй сравнивает их на предмет равенства и выдает результат в виде TRUE или FALSE. Давайте напишем несколько примеров используя логические операторы, чтобы понять как с ними работать.

# Верно ли утверждение, что 11 в третьей степени не равняется 1111?
11^3 != 1111
[1] TRUE

# 11 в третьей степени меньше 1111?
11^3 < 1111
[1] FALSE

# То есть 11 в третьей степени это 11*11*11
# и оба этих выражения равняются 1331?
a = 11^3
b = 11*11*11
a & b == 1331
[1] TRUE

В целом принцип простой: на левой стороне от логического оператора находится “значение/переменная 1”, на правой – “значение/переменная 2”, в то время, как сам оператор является критерием, по которому R “судит” о правильности утверждения. Если утверждение верно, то в командной строке будет выведено TRUE, если утверждение ложно – FALSE. Следует добавить, что логические операторы работают со всеми типами данных: от векторов до таблиц, что делает их незаменимым инструментом в стат. анализе.

a <- c(1,2,3,5,8,10)
a >= 5
[1] FALSE FALSE FALSE TRUE TRUE TRUE

b <- a[a >= 5]
b
[1] 5 8 10

Теперь мы знаем, что такое логические операторы и готовы к изучению второй части этой статьи – работе с управляющими структурами в R.

Программирование и управляющие структуры

Существует около десятка управляющих структур на которых базируется программирование в R. Среди них можно выделить три наиболее используемые: оператор условий if else и два типа циклов – for и while.

Оператор условий if else используется, когда есть два и более варианта развития сценария: если условие выполняется – “делай это”, если не выполняется – “делай то”. Суть же циклов в том, что они повторяют одно и то же действие несколько раз: в цикле while действие повторяется пока не выполнится условие цикла, а в цикле for – определенное пользователем количество раз.

Циклы while и for в R

На рисунке изображены три вида управляющих структур, где стрелки отображают поток данных. Если условие выполняется (TRUE), то поток данных движется вниз от условия, если нет (FALSE), то вправо и вниз. Как можно заметить в структурах типа while и for при выполнении условия, поток данных циркулирует по кругу: именно по этой причине их и называют циклами. Давайте разберем каждую из этих структур на практике!

Оператор условий if else в R

В языке программирования R оператор условий if else состоит из трех элементов:

  1. индикатор структуры: if, else или else if (в случае, когда условий больше одного)
  2. условие структуры, заключенного в круглые скобки (где находится условие для выполнения действия).
  3. тело структуры, заключенного в фигурные скобки (где находится код самого действия)

Пример 1: покупай больше, плати меньше – if без else

Давайте создадим простейший вариант структуры if else, когда есть только одно условие при соблюдении которого, требуется выполнить дополнительное действие в сценарии. Допустим, в магазине акция: при покупке на сумму от 100$, предоставляется 12.5% скидка. Сколько мы в итоге потратим если наша покупка (x) была на сумму 120$?

x = 120
if(x >= 100){
x = x – x*12.5/100
print(x)
}
[1] 105

Итак, в скобках находится условие, что общая стоимость покупок будет меняться только в случае, если x >= 100. Внутри фигурных скобок отображен код, иллюстрирующий механизм изменения финальной стоимости. Как Вы видите, индикатор else был не указан в конструкции. Мы его опустили, так как в случае, если x < 100, то никаких действий производиться не будет.

Следует также отметить, что для того, чтобы изменить показатель x, и проверить финальную цену, нам придется запускать весь код конструкции заново. Это непрактично, именно поэтому конструкцию if else чаще всего используют внутри функции. Давайте создадим и запустим функцию с оператором условий if else внутри.

shop <- function(x){
  if(x >= 100){
   x = x – x*12.5/100
   print(x)
  }
}

shop(120)
[1] 105
shop(50)
[1] 50

Пример 2: прогрессивная система скидок – индикатор else if

Добавим второе условие: если сумма покупок больше или равна 1000$, то магазин предоставит 25% скидку. Для этого условия мы будем использовать индикатор else if. В этом случае, нужно также изменить параметры первого условия, где x должно быть больше или равно 100, но меньше 1000. Если же ни первое, ни второе условие не соблюдается, то выведем на экран сообщение “No discounts” после финальной цены при помощи индикатора else.

shop <- function(x){
  if(x >= 100 && x < 1000){
    x = x – x*12.5/100
    print(x)
  }
  else if(x >= 1000){
    x = x – x*20/100
    print(x)
  }
  else{
    print(c(x, “No discounts”))
  }
}
shop(20)
[1] 20 “No discounts”
shop(200)
[1] 175
shop(2000)
[1] 1600

Читайте также:  Резко увеличился менструальный цикл

Также внутрь оператора условий if else можно вставить другой оператор if else, либо циклы while или for. Подобное свойство вложения управляющих структур позволяет реализовывать сложные многоуровневые сценарии (алгоритмы) на практике, создавая функции с несколькими аргументами, и множеством условий и циклов внутри.

Циклы while и for в R

Ранее мы упоминали, что при неоднократном повторении кода в скрипте следует использовать R функции, чтобы уменьшить размер кода и сделать его более читабельным. Однако, в большинстве ситуаций это будет сделать невозможно без использования циклов внутри функции. Если есть условие, при исполнении которого потребуется повторить действие, используйте цикл while (перевод с англ.: “до тех пор, пока”). Если условия нет, но надо выполнить действие определенное количество раз, воспользуйтесь циклом for.

Пример 3: уникальная методика бега – цикл for

Допустим у нас есть друг который решил заняться бегом. До этого он не бегал и находится в ужасной физической форме: максимум сколько он смог пробежать за первую тренировку – 100 метров. Друг пообещал, что через 100 дней он за тренировку будет пробегать больше 10 км, так как он разработал собственную методику: он будет заниматься ежедневно и прибавлять по 5% к дистанции от предыдущей нагрузки.

Проверим при помощи цикла for сработает ли его методика в теории. Для этого создадим функцию run.10km и переменную y, обозначающую дистанцию тренировки (в км). Внутри круглых скобок цикла for напишем что круг будет повторяться 100 раз, а внутри квадратных код вычислений дистанции для каждого дня. Дистанция последнего дня будет выделена на экран при использовании функции.

run.10km <- function(y){
for(i in 1:100){
y<-y+y*0.05
}
print(y)
}

run.10km(0.1)
[1] 13.15013

Оказалось, Ваш друг действительно прав: благодаря этой методике он сможет пробежать через 100 дней более 13 км за тренировку! Теоретически…

Пример 4: может тренироваться реже, но интенсивнее – цикл while

Однако, тренироваться ежедневно без выходных для начинающего – это неминуемый путь к физическому и психическому истощению. Чтобы у друга дни нагрузок чередовались с днями отдыха, давайте предложим ему альтернативную методику: тренироваться через день, но прибавляя к дистанции по 10% от предыдущей нагрузки (вместо 5%).

Рассчитаем, используя цикл while, через сколько дней друг начнет пробегать более 10 км за тренировку и выведем результат в виде таблицы каждая строчка которой отображает день тренировки и предполагаемую дистанцию.

alter.10km <- function(y){
i <- 1
Day <- i
Distance <- y
while(y <= 10){
i <- i + 2
y<-y+y*0.1
Day <- append(Day,i)
Distance <- append(Distance,y)
}
DF <- data.frame(Day, Distance)
return(DF)
}

results <- alter.10km(0.1)
tail(results, 3)
   Day  Distance
48  95  8.819749
49  97  9.701723
50  99 10.671896

Таким образом, наш друг с 99-го дня станет пробегать более 10 км за тренировку занимаясь реже, но интенсивнее! Выглядит, как более реалистичный вариант, но что скажет друг?

Заключение

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

Источник

Меньше недели назад в журнале Хакер вышла авторская версия материала, посвященного фичам при использовании циклов при разработке на R. По согласованию с Хакером, мы делимся полной версией первой статьи. Вы узнаете о том, как правильно писать циклы при обработке больших объемов данных.

Примечание: орфография и пунктуация автора сохранены.

Во многих языках программирования циклы служат базовыми строительными блоками, которые используются для любых повторяющихся задач. Однако в 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, 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.

lapply(X, FUN, …)

Первым параметром передается список или вектор, а вторым — функция, которую надо вызвать для каждого элемента. Функции sapply и vapply — это обертки вокруг lapply. Первая пытается привести результат к вектору, матрице или массиву. Вторая добавляет проверку типов возвращаемого значения.

Достаточно распространен такой способ применения sapply, как работа с колонками. Например, у нас есть таблица

data <- data.frame(co1_num = 1, col2_num = 2, col3_char = “a”, col4_char = “b”)

При передаче sapply таблицы она будет рассматриваться как список колонок (векторов). Поэтому, применив sapply к нашему data.frame и указав в качестве вызываемой функции is.numeric, мы проверим, какие столбцы являются числовыми.

sapply(data, is.numeric)
co1_num col2_num col3_char col4_char
TRUE TRUE FALSE FALSE

Выведем на экран только столбцы с числовыми значениями:

data[,sapply(data, is.numeric)]
co1_num col2_num
1 1 2

Циклы, основанные на apply, отличаются от классических тем, что возвращается результат работы цикла, состоящий из результатов каждой итерации.

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

Применим apply к первоначальной таблице, выбрав обработку по строчкам, и в качестве применяемой функции укажем базовую суммирующую функцию sum. В итоге apply вернет вектор, где для каждой строки будет указана сумма ее колонок. Добавим этот вектор в качестве нового столбца первоначальной таблице и получим искомый результат:

a_plus_b <- apply(testDF, 1,sum)
testDF$c <- a_plus_b

Замер времени исполнения показывает 0,248 секунды, что в сто раз быстрее первого варианта, но все еще в десять раз медленнее функций операций с таблицами.

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

install.packages(“foreach”) # Установка пакета на компьютер (один раз)
library(foreach) # Подключение пакета

Несмотря на то что foreach — сторонняя функция, на сегодняшний день это очень популярный подход к написанию циклов. foreach был разработан одной из самых уважаемых в мире R компанией — Revolution Analytics, создавшей свой коммерческий дистрибутив R. В 2015 году компания была куплена Microsoft, и сейчас все ее наработки входят в состав Microsoft SQL Server R Services. Впрочем, foreach представляет собой обычный open source проект под лицензией Apache License 2.0.

Основные причины популярности foreach:

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

Начнем c простого. Для чисел от 1 до 10 на каждой итерации число умножается на 2. Результаты всех итераций записываются в переменную result в виде списка:

result <- foreach(i = 1:10) %do%
(i*2)

Если мы хотим, чтобы результатом был не список, а вектор, то необходимо указать c в качестве функции для объединения результатов:

result <- foreach(i = 1:10, .combine = “c”) %do%
(i*2)

Можно даже просто сложить все результаты, объединив их с помощью оператора +, и тогда в переменную result будет просто записано число 110:

result <- foreach(i = 1:10, .combine = “+”) %do%
(i*2)

При этом в foreach можно указывать одновременно несколько переменных для обхода. Пусть переменная a растет от 1 до 10, а b уменьшается от 10 до 1. Тогда мы получим в result вектор из 10 чисел 11:

result <- foreach(a = 1:10, b = 10:1, .combine = “c”) %do%
(a+b)

Итерации циклов могут возвращать не только простые значения. Допустим, у нас есть функция, которая возвращает data.frame:

customFun <- function(param)
{
data.frame(param = param, result1 = sample(1:100, 1), result2 = sample(1:100, 1))
}

Если мы хотим вызвать эту функцию сто раз и объединить результаты в один data.frame, то в .combine для объединения можно использовать функцию rbind:

result <- foreach(param = 1:100,.combine = “rbind”) %do%
customFun(param)

В результате в переменной result у нас собрана единая таблица результатов.

В .combine возможно также использовать свою собственную функцию, причем с помощью дополнительных параметров можно оптимизировать производительность, если твоя функция умеет принимать больше чем два параметра сразу (в документации foreach есть описание параметров .multicombine и .maxcombine).

Одно из главных преимуществ foreach заключается в легкости перехода от последовательной обработки к параллельной. Фактически этот переход осуществляется заменой %do% на %dopar%, но при этом есть несколько нюансов:

  1. До вызова foreach у тебя уже должен быть зарегистрирован parallel backend. В R есть несколько популярных реализаций parallel backend doParallel, doSNOW, doMC, и у каждого есть свои особенности, но предлагаю ради простоты выбрать первый и написать несколько строчек кода для его подключения:

    library(doParallel) # Загружаем библиотеку в память
    cl <- makeCluster(8) # Создаем «кластер» на восемь потоков
    registerDoParallel(cl) # Регистрируем «кластер»

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

system.time({
foreach(i=1:8) %dopar% Sys.sleep(1) #
})

user system elapsed
0.008 0.005 1.014

После использования parallel backend можно остановить:

stopCluster(cl)

Нет никакой необходимости каждый раз перед foreach создавать, а затем удалять parallel backend. Как правило, он создается один раз в программе и используется всеми функциями, которые могут с ним работать.

  1. Тебе надо явно указать, какие пакеты необходимо загрузить в рабочие потоки с помощью параметра .packages.

Например, ты хочешь на каждой итерации создавать файл с помощью пакета readr, который загрузили в память перед вызовом foreach. В случае последовательного цикла (%do%) все отработает без ошибок:

library(readr)
foreach(i=1:8) %do%
write_csv(data.frame(id = 1), paste0(“file”, i, “.csv”))

При переходе на параллельную обработку (`%dopar%`) цикл закончится с ошибкой:

library(readr)
foreach(i=1:8) %do%
write_csv(data.frame(id = 1), paste0(“file”, i, “.csv”))

Error in write_csv(data.frame(id = 1), paste0(“file”, i, “.csv”)) :
task 1 failed – “could not find function “write_csv””

Ошибка возникает, поскольку внутри параллельного потока не загружен пакет readr. Исправим эту ошибку с помощью параметра .packages:

foreach(i=1:8, .packages = “readr”) %dopar%
write_csv(data.frame(id = 1), paste0(“file”, i, “.csv”))

  1. Вывод на консоль в параллельном потоке не отображается на экране. Иногда это может здорово усложнить отладку, поэтому обычно сложный код сначала пишут без параллельности, а потом заменяют %do% на %dopar% либо перенаправляют вывод каждой итерации в свой файл с помощью функции sink.
  • При работе с большим объемом данных циклы не всегда оказываются лучшим выбором. Использование специализированных функций для выборки, агрегации и трансформации данных всегда эффективнее циклов.

  • R предлагает множество вариантов реализации циклов. Основное отличие классических for, while и repeat от группы функций на основе apply заключается в том, что последние возвращают значение.

  • Использование циклов foreach из одноименного внешнего пакета позволяет упростить написание циклов, гибко оперировать возвращаемыми итерациями значениями, а за счет подключения многопоточной обработки еще и здорово увеличить производительность решения.
  • Официальная документация пакета foreach
  • Официальный обзор функциональности foreach
  • Анализ данных с использованием R. Часть 1
  • Изучаем R. Часть 2: векторизация и визуализация
  • Программируем на языке R: как правильно писать циклы для обработки больших объемов данных

Источник

Читайте также:  Вероятность забеременеть после месячных при регулярном цикле