Как сделать цикл в r studio

Обновлено: 04.07.2024

Команда ifelse() воспринимает первый аргумент как условие, второй аргумент возвращается если условие верно, а третий аргумент - если нет. Таким образом условие может быть вектором. Например, мы генерируем последовательность от 1 до 10 и хотим вывести значения меньше чем 5 и больше чем 8.

R позволяет реализовать циклы тремя путями:

  • циклы "for";
  • циклы с предусловием ("while");
  • "бесконечные" циклы ("repeat");

Для использования оператора for требуется указать индекс (в примере - k ) и вектор (в примере - 1:5 ), а также указать выполняемую конструкцию и указать повторяемые операторы в фигурных скобках. Механизм работы следующий:

  1. интерпретатор последовательно выбирает значения из вектора и присваивает их переменной k ;
  2. с каждым значением k выполняется список функций в фигурных скобках.

Синтаксис цикла while вполне стандартен:

  1. Ключевое слово while .
  2. Условие выполнимости в скобках.
  3. Список функций для итерированного выполнения в фигурных скобках.

Нужно быть осторожным при использовании этого типа циклов, так как неправильное использование прерываний цикла (оператора break) может привести к бесконечному выполнению (зависанию) цикла. Синтаксис следующий:

  1. Ключевое слово repeat .
  2. Список функций для итерированного выполнения в фигурных скобках.
  3. Наличие условия прерывания выполнения внутри цикла.

Циклы обычно медленны и лучше избегать их по возможности.

  • Функция apply() может применить функцию к элементу матрицы или массиву. Чтобы применить к строке, нужно вторым параметром отдать цифру 1 ; чтобы применить к столбцу - 2 .
  • Функция lapply() применяет функцию к каждому столбцу структуры и возвращает список.
  • Функция sapply() похожа, но не возвращает ничего на экран. Могут существовать векторы или матрицы зависящие от этой функции.
  • Функция tapply() применяет функцию к каждому уровню factor-а.

Циклы в R обычно медленны. Итераторы могут быть более эффективны, чем циклы. Для более подробного изучения посетите Revolution Computing Blogs (англ.) .

Определение функции - это присвоение блока операторов переменной. Синтаксис:


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

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

О циклах

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

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

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

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

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

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

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

  • for — самый распространенный тип циклов. Синтаксис очень прост и знаком разработчикам на различных языках программирования. Мы уже пробовали им воспользоваться в самом начале статьи. for выполняет переданную ему функцию для каждого элемента.
  • Чуть менее распространенные while и repeat , которые тоже часто встречаются в других языках программирования. В while перед каждой итерацией проверяется логическое условие, и если оно соблюдается, то выполняется итерация цикла, если нет — цикл завершается:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Несмотря на то что 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 в виде списка:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Циклы while и for в R

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

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

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

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

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

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

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

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

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

Заключение

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

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

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

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

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

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

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

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

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

7.2 Циклы for

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

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

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

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

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

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

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

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

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

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

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

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

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

Если вы пользуетесь одним из этих пакетов (о них пойдет речь далее — см. @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() :

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

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

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

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

Читайте также: