Содержание статьи
Когда‑то это был язык для работы со статистикой, но сейчас его вполне можно считать языком общего назначения (хотя основную свою направленность он сохранил). Кто не верит, может заглянуть на страничку проекта Shiny, с помощью которого любой может создавать полноценные веб‑приложения на R.
Ну да ладно, нас язык R интересует именно в той области, где он действительно хорош. В этой статье я расскажу о самых базовых объектах в языке и его особенностях. Кто‑то мудрый давно заметил, что самое хорошее в языке R — это то, что он был создан специалистами по статистике, а самое плохое — то, что он был создан специалистами по статистике :).
Опустив вопросы установки R, с которыми довольно легко справиться (тем более что про это я немного уже писал в прошлой статье), приступим к изучению языка.
Все, что вы хотели знать о функции, но боялись спросить
В языке R доступна очень большая инфраструктура пакетов и, как следствие, совершенно невероятное количество функций для повседневного использования. Что делать, если ты знаешь название, но не помнишь правильное употребление функции? Решением станет очень приличный help, правильным образом встроенный в систему. Для того чтобы получить справку об использовании той или иной функции, достаточно набрать ?имя_функции
.
Векторы
Начнем с базовых объектов, из которых состоит язык. В большинстве языков программирования, с которыми тебе приходилось иметь дело, примитивными объектами являются числа, объекты булева типа и прочие действительно примитивные вещи.
В языке R полно сюрпризов, и первый из них заключается в том, что примитивным объектом в R является вектор, то есть совокупность значений одной природы. К примеру, вектор вещественных чисел. Хочется спросить, а как быть с обычными числами? Скажем, с числом 10. Ответ на этот вопрос довольно прост — это вектор из одного элемента.
Векторы бывают следующих типов:
- целые;
- числовые (вещественные);
- символьные;
- комплексные;
- логические.
По умолчанию числа в R — это вещественные числа, то есть числа с плавающей запятой. Для того чтобы явно указать R, что число, с которым ты собираешься иметь дела, целое, нужно добавить суффикс L. Например, 10L. Это легко иллюстрирует следующий код:
> x <- 1> typeof(x)[1] "double"> y <- 1L> typeof(y)[1] "integer"
Здесь будет использоваться символ приглашения >
, чтобы отличать код от ответа системы. В работе с числами существует специальный символ Inf
для представления бесконечности.
Следует обратить внимание на оператор присваивания <
. Рассмотрим следующий фрагмент:
> x <- 10> x # печатаем x[1] 10> print(x) # еще раз печатаем[1] 10
В этом, казалось бы, очевидном фрагменте кода есть два важных момента: печатать можно, просто указывая имя переменной в строке или используя функцию, предназначенную для печати. Это довольно характерно для всех языков, в которых есть интерактивный интерпретатор REPL (Read — Evaluate — Print Loop). Функция print
скорее используется для печати внутри других функций для отладки или просто для вывода какой‑либо информации. Что более важно и менее очевидно, строка [
, выводимая в качестве результата в R, говорит, что это первый (и единственный) элемент вектора.
Число 1 в квадратных скобках выводится для удобства чтения. К примеру, если вектор не влезает в ширину экрана, то он разбивается на строки и числа перед каждой строкой — это индекс элемента вектора, с которого начинается данная строка.
Для создания обычного вектора используется функция c
:
> x <- c(1,2,3)> x[1] 1 2 3
Так же как и во многих других языках, можно создавать векторы, указывая интервал значений:
> x <- c(1:3)> x[1] 1 2 3
Казалось бы, результат тот же, однако все не совсем так. Так как это целочисленный интервал, содержимое второго вектора — это целые числа, а первого — вещественные. Что легко проверить с помощью функции typeof
. В последнем случае еще можно писать просто x <
. Значения булева типа в языке R выглядят как TRUE
и FALSE
или просто T
и F
. Для того чтобы создать пустой вектор нужного типа, необходимо использовать функцию vector
.
> x <- vector("numeric", length = 10)> x[1] 0 0 0 0 0 0 0 0 0 0> length(x)[1] 10
Неявное преобразование типов в R хорошо иллюстрируется следующим примером:
> x <- c("a", TRUE, 1.3)> x[1] "a" "TRUE" "1.3"> y <- c(2, TRUE, FALSE)> y[1] 2 1 0
Часто бывает необходимо воспользоваться явным преобразованием типов. Рассмотрим пример:
> as(TRUE, "character")[1] "TRUE"> as.character(TRUE)[1] "TRUE"
Два строчки делают в точности то же самое, однако с точки зрения читаемости кода
второй подход выглядит более предпочтительным. Вообще, функции создания, проверки и преобразования типов легко запомнить следующим образом. Для создания (пустого вектора строк) используется character(
, где length
— количество элементов, is.
используется для сравнения, а as.
для преобразования. Когда преобразование невозможно, то его результатом будет специальное значение NA
.
Элементы вектора могут быть заименованы, это можно сделать следующим образом:
> v <- c(x = 1.0, y = 2.5, z = -0.1)> v x y z 1.0 2.5 -0.1
или так:
> u <- c(1.0, -0.5, -0.5)> names(u) <- c("x", "y", "z")
Матрицы
С векторами все довольно просто, давай теперь попробуем разобраться с другой полезной структурой данных — матрицами. Для создания матрицы есть специальная функция matrix
:
> m <- matrix(nrow = 2, ncol = 3)> m [,1] [,2] [,3][1,] NA NA NA[2,] NA NA NA
Как видно, изначально создается пустая матрица. Для того чтобы получить размеры матрицы, существует специальный атрибут dim
:
> dim(m)[1] 2 3> attributes(m)$dim[1] 2 3
Следует отметить, что в смысле хранения двумерных объектов (массивов, матриц) все языки делятся на две группы: те, что хранят матрицы по строкам, такие как C и Java, и те, что хранят по столбцам, — это, к примеру, FORTRAN и R. В том, что это именно так, легко убедиться следующим образом:
> m <- matrix(1:6, nrow = 2, ncol = 3)> m [,1] [,2] [,3][1,] 1 3 5[2,] 2 4 6
Причем задавать двумерную структуру можно, просто добавляя атрибут dim
к вектору:
> v <- 1:6> dim(v) <- c(2, 3)> v [,1] [,2] [,3][1,] 1 3 5[2,] 2 4 6
Сверх того, строкам и колонкам матрицы можно давать имена:
> m <- matrix(1:4, nrow=2, ncol=2)> dimnames(m) <- list(c("a", "b"), c("c", "d"))> m c da 1 3b 2 4
В языке R существует также механизм создания двумерных структур из одномерных с помощью операций присоединения строки или столбца:
> x <- 1:3> y <- 11:13> cbind(x, y) x y[1,] 1 11[2,] 2 12[3,] 3 13> rbind(x, y) [,1] [,2] [,3]x 1 2 3y 11 12 13
О том, как работать с отдельными элементами структур данных, я напишу чуть позже, а пока продолжим разбираться с самими структурами.
Списки и факторы
В этом разделе рассмотрим еще две полезные структуры данных. Первая — это, конечно, список. Дело в том, что часто приходится хранить данные разного типа в одном месте.
Как мы знаем, вектор здесь не подходит, потому что его элементы должны быть одного типа, поэтому в R предусмотрены списки:
> lst <- list("hello", 1.5, TRUE, 1+2i)> lst[[1]][1] "hello"[[2]][1] 1.5[[3]][1] TRUE[[4]][1] 1+2i
Как видно, в списке содержится четыре элемента, и все они разного типа: строка, вещественное число, булево значение и комплексное число. Элементы списка можно именовать, как и элементы вектора:
> l <- list(a="test", b=3.14)> l$a[1] "test"$b[1] 3.14
Во многих языках программирования есть перечислимый тип данных. Он нужен для того, чтобы работать с данными, в качестве значения которых могут выступать элементы конечного множества. Когда такого типа в языке нет, этот вопрос решается с помощью констант для соответствующего набора значений. В языке R для обеспечения подобной функциональности есть специальный тип — фактор:
> x <- factor(c("yes", "no", "yes", "no", "no"))> x[1] yes no yes no noLevels: no yes
Здесь создается вектор факторов с двумя возможными значениями: yes и no. Можно, к примеру, подсчитать, сколько соответствующих значений есть в нашем векторе:
> table(x)x no yes 3 2
Фрейм данных (Data Frame)
Фрейм данных — один из самых полезных типов данных в R. Когда мы работаем с реальными табличными данными, именно этот тип представляет таблицы. В отличие от матриц, данный тип позволяет хранить различные типы данных в разных колонках. С точки зрения хранения этот тип данных можно представить как список специального вида, где элементами списка являются списки одинаковой длины (колонки). Для загрузки фрейма данных из CSV-файла существует функция read.
, которая уже встречалась нам в предыдущей статье этой серии.
Можно создать фрейм данных вручную, например так:
> x <- data.frame(a=c(F, F, T, T), b=c(F, T, F, T), or=c(F, T, T, T))> x a b or1 FALSE FALSE FALSE2 FALSE TRUE TRUE3 TRUE FALSE TRUE4 TRUE TRUE TRUE
Помимо атрибута names
, для фрейма данных также есть row.
:
> names(x)[1] "a" "b" "or"> row.names(x)[1] "1" "2" "3" "4"
Доступ к элементам
Как ты мог заметить, до сих пор я рассказывал лишь о том, какие бывают структуры данных, но ничего не сказал про то, как получать доступ к отдельным элементам или подмножествам. Посмотрим, как это работает, на простом примере с вектором:
> x <- c(11, 21, 31, 41, 11, 21, 31)> x[1][1] 11> x[2][1] 21> x[x > 21][1] 31 41 31> j <- x > 21> j[1] FALSE FALSE TRUE TRUE FALSE FALSE TRUE> x[j][1] 31 41 31> x[1:3][1] 11 21 31
Наверное, единственный комментарий, который требуется к данному примеру, — это то, что операция >
работает как векторная операция и результатом ее выполнения будет вектор булевых значений, этот вектор может быть использован для выборки данных из вектора.
При работе с матрицами также не возникает никаких сложностей, нужные строки и столбцы мы можем получить легко и непринужденно. К примеру, запись x[
— это элемент во второй строке и в третьем столбце, а x[
и z[,
— это первая строка и второй столбец соответственно. По умолчанию эти операции возвращают вектор, а не матрицу, в которой одна строка или столбец, и если мы хотим, чтобы результатом выполнения данной операции была все‑таки матрица, пусть и другого размера, то нужно использовать дополнительный параметр x[
.
С доступом к элементам списка все немного сложнее. Рассмотрим следующий пример:
> l <- list(a=0.5, b=1:3)> l$a[1] 0.5> l$b[1] 1 2 3> x <- l[2]> x$b[1] 1 2 3> typeof(x)[1] "list"> y <- l[[2]]> typeof(y)[1] "integer"> y[1] 1 2 3
На этом примере достаточно хорошо видны особенности доступа к элементам в R.
Как можно заметить, использование [[]
не гарантирует соответствие типа возвращаемого значения изначальному, а в случае одинарных скобок [
возвращаемое значение также является списком.
В этом смысле $
и [[]
работают очень похоже. Хотя есть некоторая особенность — значение в двойных квадратных скобках может быть вычислено, а имя после знака $
— нет.
Списки бывают вложенными, и доступ к их элементам осуществляется с помощью вложенных скобок, как и полагается: x[[
, однако можно сделать запись чуть более понятной, используя функцию c
. К примеру, последнее выражение можно записать как x[[
. Причем использовать селектор с одной скобкой не получится (подумай почему).
Управляющие структуры
Надо заметить, что в данной статье я рассматриваю R именно как язык программирования, но на самом деле можно было бы взглянуть на него как на систему для работы со статистикой с типичными функциями для решения повседневных задач. Однако я предпочту быть консервативным в изложении и, как полагается после описания базовых типов, перейду к разговору об условных блоках и циклах.
Начнем с условного оператора. Надо сказать, что здесь в R нет почти ничего особенного, но все‑таки:
if (x > 0) { y <- x } else { y <- -x }
Ничего нового тут нет, и else
может быть опущен. Хотя и тут не обошлось без несколько необычного поведения. В функциональных языках, таких как Haskell, конструкция if
является выражением, а не оператором, то есть возвращает значение, а не изменяет состояние. В языке R эта идея также нашла себе место в следующей конструкции:
y <- if (x > 0) { x } else { -x } # фигурные скобки можно опустить
Последняя конструкция делает то же самое, что и предыдущая, только в функциональном стиле. Однако в функциональных языках выражение должно быть определено и поэтому наличие ветви else
обязательно. Здесь же это не так, конструкция z <
вполне допустима, но значением этого выражения при x >
будет специальное значение NULL
. Для сравнения с этим значением можно использовать функцию is.
.
Теперь стоит сказать пару слов о циклах. Циклы в R работают медленно, но когда мы имеем сравнительно небольшой объем данных, то использование циклов может быть вполне допустимо и даже удобно. С этой точки зрения R также мало отличается от других языков программирования. Начнем с цикла for
, который реализует известную парадигму for-in.
x <- c("a", "b", "c", "d", "e")for(i in 1:5) { print(x[i])}for(ch in x) print(ch)
Как и полагается, цикл for-in реализует процесс итерации по вектору или последовательности, последний вариант более лаконичен, однако если нам каким‑либо образом нужны индексы, то хорошо бы иметь возможность создавать последовательность, соответствующую заданному вектору.
Для этого в R предусмотрена специальная функция seq_along
, принимающая в качестве аргумента вектор или список, для которых строится последовательность индексов. Таким образом, первый цикл можно было бы переписать в виде for(
. Для того чтобы сгенерировать последовательность заданной длины, можно воспользоваться функцией seq_len
.
Кстати, все эти вопросы легко решить известными средствами, используя лишь функцию вычисления длины length
. Цикл while
имеет вполне классическую форму while (
.
Логические связки в R также выглядят стандартным образом: &&
, ||
и !
. В дополнение к банальному while(
в R присутствует небанальный repeat { ...
, выход из которого обеспечивает, как обычно, комбинация if
и break
. Для перехода к следующей итерации предусмотрен оператор с несколько неожиданным названием next
.
Как можно заметить, разделителей вроде точки с запятой между операторами в R нет.
Функции
Как уж без функций в приличном языке программирования? В самом общем виде определение функции выглядит следующим образом:
f <- function(<args>) { ...}
Как и в функциональных языках, функции в R являются объектами класса. Это означает, что их можно передать в качестве аргумента в другую функцию и вернуть в качестве значения. Анонимные (лямбда) функции также присутствуют:
f <- function(g) { function(x) g(g(x))}y <- f(function(x) x * x)(5)
Здесь функция f
принимает в качестве аргумента функцию g
и возвращает функцию, которая имеет один формальный параметр x
и дважды применяет к нему функцию g
. Также в коде можно увидеть передачу анонимной функции в качестве аргумента, а полученный результат (композиция функций g
и самой себя) применяется к числу 5. Таким образом, число 5 будет дважды возведено в квадрат.
Порядок вычисления аргументов в R является отложенным (lazy), то есть аргумент не вычисляется, если он не нужен:
> f <- function(x, y) x * x> f(3)[1] 9> f(3, 5/0)[1] 9
Для того чтобы правильно работать с состоянием в случае замыканий, существует оператор <<
. Рассмотрим пример:
counter <- function() { i <- 0 function() { i <<- i + 1 i }}
Теперь мы можем создать счетчик или даже два и проверить, как все работает:
> counter_one <- counter()> counter_two <- counter()> counter_one()[1] 1> counter_one()[1] 2> counter_two()[1] 1
Как видно, все работает штатно, однако если заменить оператор <<
на обычный оператор присваивания, то ничего работать не будет и счетчик всегда будет выдавать число 1.
Язык R достаточно гибок при работе с формальными параметрами и аргументами. В нем допускается использование значения по умолчанию и вызов функции с произвольным порядком аргументов (по имени):
> f <- function(x, y=1) x + y> f(y=2, x=5)
Многие функции в R имеют довольно большой список формальных параметров и значений по умолчанию, и поэтому передавать в них аргументы удобнее по имени.
Продолжение следует
В следующей статье я продолжу рассказывать про программирование на R с использованием уже реальных примеров. Некоторые важные аспекты, такие как векторизация, визуализация и минимально необходимый список пакетов, также будут рассмотрены в следующей статье.