Основные типы и структуры данных
Векторы
Атомарные типы
Вектор — простой набор элементов одного типа данных. В R для создания векторов используется шесть базовых (их иногда ещё называют атомарными) типов. Подробнее о базовых типах см. в главе ООП:
-
character
— используется для текстовых значений. Любой набор знаков, заключённый в кавычки, в R воспринимается как текстовое значение. Два нюанса — знак#
, заключённый в кавычки, перестаёт быть символом, обозначающим комментарий. А обратный слэш\
всё так же остается знаком экранирования, что позволяет использовать в текстовых строках такие специальные символы, как, например,\n
. -
complex
— используется при работе с комплексными числами. -
numeric
(илиdouble
) — используется для числовых значений (с плавающей точкой). -
integer
— целочисленные значения (по форме хранения, а не математическое множество целых). -
logical
— логические значения TRUE/FALSE. -
raw
— достаточно редко используемый тип данных, побайтовая запись в шестнадцатеричном формате.
Особенности числовых типов
Тип numeric
в R хранится в виде чисел с плавающей запятой и двойной точностью. Числа с одинарной точностью можно симулировать с помощью функции single()
или as.single()
, приписав numeric
-объекту атрибут Csingle
. Однако это не настоящий хранимый формат, и его использование имеет смысл только при обращении к некоторым функциям, написанным на C или Fortran. Хранение чисел в формате с плавающей запятой может приводить к некоторым казусам, когда требуется каким-то образом обрабатывать числовые значения, которые не умещаются в 32 бита. Максимально возможное целочисленное значение можно определить с помощью .Machine$integer.max
, всё, что больше него, будет преобразовано в double
с плавающей запятой.
Значения, превышающие .Machine$integer.max
, достаточно редко используются как числовые. Чаще всего это просто длинные идентификаторы (пользователей, например), поэтому есть смысл их импортировать и хранить в виде строковых значений, иначе можно натолкнуться на ошибки округлений при работе с числами с плавающей точкой. Пример некорректной обработки целочисленных значений, превышающих .Machine$integer.max
:
15280137481015627194 - 15280137481015627776
## [1] 0
Создание векторов
Векторы создаются с помощью функции c()
(c
— от combine
). Например:
## [1] 1 3 5
В редких случаях используются векторы с именованными элементами (аналоги словарей в некоторых других языках программирования). В повседневной практике встречается достаточно редко. Как правило, именованные векторы можно получить в результате функции sapply()
и некоторых подобных:
## var1 var2 var3
## 12 23 34
Простую неименованную последовательность атомарного типа можно задать также другими командами, наиболее часто используемые из них:
-
seq()
— создание последовательности значений в указанном интервале с заданным шагом (seq
отsequence
). Например:
## [1] 10 8 6 4 2
Первые два аргумента задают первое и последнее значения последовательности, а аргумент by
— шаг последовательности (по умолчанию равен единице). Знак -
для значения аргумента by
маркирует, что последовательность убывающая.
Выражение
:
— аналог функцииseq()
, используется для создания последовательности целых чисел, где последующее отличается от предыдущего на1
. Так, выражение5:1
тождественно выражениюseq(5, 1, -1)
.rep()
— повтор какого-либо элемента заданное число раз. В качестве элемента может выступать практически любой объект R (в зависимости от осмысленности в целом подобного выражения). Аргументыtimes
илиeach
задают, какое количество раз повторять весь объект или каждый элемент объекта соответственно:
## [1] "a" "b" "c" "a" "b" "c"
rep(x, each = 2)
## [1] "a" "a" "b" "b" "c" "c"
Если значения аргументов задавать позиционно, то первый аргумент — это итерируемый объект, а второй аргумент — аргумент times
:
rep(x, 2)
## [1] "a" "b" "c" "a" "b" "c"
-
sample()
— создание последовательности случайно выбранных значений из какого-то заданного вектора значений. Первый аргумент задаёт выборку, из которой необходимо извлечь подвыборку, второй аргумент (size
) определяет объём извлекаемой выборки. Аргументreplace
определяет, выборка извлекается с возвращением элементов или нет:
sample(x = 1:5, size = 5, replace = FALSE)
## [1] 1 2 5 4 3
sample(x = 1:5, size = 5, replace = TRUE)
## [1] 3 5 1 3 3
-
vector()
— функция создания вектора значений определённого типа (обычно атомарного, но можно создавать и более сложные объекты, например, списки), где первый аргумент задаёт тип, а второй — длину вектора (количество элементов в последовательности). По умолчанию созданная последовательность заполнена значениямиFALSE
, если вектор логический,0
и''
, если вектор числовой или текстовый соответственно. Одно из самых простых применений подобной конструкции — предварительное выделение памяти:
## [1] "" "second" "" "" ""
-
raw()
,logical()
,integer()
,numeric()
,complex()
,character()
— функции создания векторов, аналогичныеvector()
, для каждого из атомарных типов отдельно. В качестве аргумента эти функции принимают требуемую длину создаваемого вектора и создают вектор значений по умолчанию (00
для raw-векторов,FALSE
для логических векторов,0
— для числовых и""
— для текстовых):
character(length = 5)
## [1] "" "" "" "" ""
Вектор единичной длины
Простое присвоение x <- 'alpha'
также будет созданием вектора. В R в принципе отсутствуют скалярные значения и любое единичное значение/константа — это всё тот же вектор, просто единичной длины.
x <- 'alpha'
length(x)
## [1] 1
Неявные преобразования
В R используется достаточно простая система автоматического преобразования атомарных типов по логике от самого строгого к самому гибкому, то есть по цепочке logical
-> integer
-> numeric
-> complex
-> character
(тип raw
не включается в эту систему). Если попытаться создать вектор из последовательности элементов разных атомарных типов, то все элементы будут преобразованы к самому общему типу в заданной последовательности:
## [1] 1 1 2 0 0
## [1] 1.0 3.6 2.0 5.1
## [1] "a" "3.6" "b" "5.1"
## [1] "TRUE" "3.6" "b" "2"
Второе следствие неявных преобразований — возможность использовать конструкции вида sum(5 == 5)
, потому что логический результат сравнения (TRUE
) будет преобразован в целое числовое значение (1
, так как TRUE
преобразовывается в 1
, а FALSE
— в 0
). Следовательно, можно вычислить количество верных утверждений. Подобные процедуры крайне часто используются в ситуациях, когда надо посчитать количество определённых значений в векторе или в колонке таблицы. Вообще во избежание неожиданных результатов рекомендуется остерегаться неявных преобразований и контролировать типы самостоятельно.
Массивы
В R, помимо подобных одномерных массивов (векторов), также можно создавать и многомерные массивы — двумерные массивы называются матрицами, многомерные соответственно многомерными массивами или многомерными матрицами. Все элементы массивов также должны быть элементами одного типа.
Матрицы создаются с помощью функции matrix()
, где первым аргументом задаётся набор значений, а вторым и третьим аргументами — количество строк и столбцов в матрице. Заполнение ячеек массива по умолчанию происходит по колонкам: сначала заполняется построчно первая колонка, потом вторая (если есть) и т. д. Название строк и колонок при желании можно задать с помощью аргумента dimnames
в виде списка векторов названий:
## c1 c2
## r1 1 3
## r2 2 4
Многомерные массивы задаются аналогичным образом, только вместо аргументов количества строк и колонок используется аргумент dim
(от dimension
), задающий размерность массива. При dim = c(2, 2)
будет создан двумерный массив 2*2, матрица, как и в примере выше.
Если создать и вывести на печать трёхмерный массив, который можно назвать стопкой двумерных массивов, то будут выведены матрицы для каждого элемента третьего измерения, для n-мерных массивов аналогично. Например, ниже мы создаём и выводим трёхмерный массив размером 2*2*2. В результате мы получаем две матрицы — [, , v1]
и [, , v2]
:
## , , v1
##
## c1 c2
## r1 1 3
## r2 2 4
##
## , , v2
##
## c1 c2
## r1 5 7
## r2 6 8
Следует помнить, что массивы — это всё те же векторы (наборы элементов атомарных типов), просто с атрибутом размерности:
## [,1] [,2] [,3] [,4] [,5]
## [1,] 1 3 5 7 9
## [2,] 2 4 6 8 10
Списки
Списки — это коллекции объектов любых типов (в том числе и другие списки) любого уровня вложенности. По сути, списки представляют собой наборы ссылок на другие объекты. Для создания списка используется команда list()
, где в аргументах через запятую перечисляются все элементы, которые необходимо включить в список. Например:
x <- seq(from = 13, to = 0, by = -3)
y <- rep(x = 'c', times = 3)
z <- TRUE
my_list <- list(x, y, z)
print(my_list)
## [[1]]
## [1] 13 10 7 4 1
##
## [[2]]
## [1] "c" "c" "c"
##
## [[3]]
## [1] TRUE
Помимо простого объединения объектов в списки с последующим вызовом элементов по номеру в списке, можно создавать именованные списки, где каждый элемент будет иметь заданное название. Например:
x <- seq(from = 13, to = 0, by = -3)
y <- rep(x = 'c', times = 3)
z <- TRUE
my_list <- list(seq_example = x,
rep_example = y,
atomic_example = z)
print(my_list)
## $seq_example
## [1] 13 10 7 4 1
##
## $rep_example
## [1] "c" "c" "c"
##
## $atomic_example
## [1] TRUE
Таблицы (датафреймы)
data.frame
— базовый для R формат, который используется для представления таблиц. В R поддерживается классическая концепция таблиц, характерная для социальных наук и data science, когда по строкам расположены наблюдения, а колонки образуют пространство признаков этих наблюдений. На уровне структуры data.frame
— это всё те же списки, в которых могут храниться разные по типу объекты, однако с требованием равенства длины объектов. Важно: все значения одной колонки могут быть только одного типа, а не как в Excel, OpenOffice или другом табличном процессоре.
Таблицы можно создать с помощью функции data.frame()
, где аналогично созданию списка через запятую перечисляются векторы, значения которых будут представлены в виде колонок (использование списков для формирования колонок возможно, но встречается реже и несколько сложнее в последующем использовании). Так как все колонки должны быть одной длины, то и длины векторов соответственно должны быть одинаковы. Возможны и другие варианты создания data.frame
-объектов путем преобразования из других объектов с помощью функции as.data.frame()
.
# создаем data.frame-объект
x <- seq(from = 10, to = 0, by = -2)
y <- rep(x = 'a', times = 6)
z <- sample(x = 1:20, size = 6, replace = FALSE)
dataset <- data.frame(column1 = x,
column2 = y,
column3 = z)
str(dataset)
## 'data.frame': 6 obs. of 3 variables:
## $ column1: num 10 8 6 4 2 0
## $ column2: chr "a" "a" "a" "a" ...
## $ column3: int 12 14 5 19 3 11
data.frame
— это базовый класс для таблиц в R, однако в настоящее время используется, как правило, один из двух других подходов — tibble
(из экосистемы tidyverse
) или data.table
. Оба эти класса наследуются от data.frame
, поэтому в целом поддерживают базовые принципы и идеи data.frame
.
Факторы
Факторы (factor
) — специфичный для R формат, в других языках аналогом будет тип enum
(enumerated type
). Это упорядоченный вектор номинальных или интервальных значений, аналог категориальных шкал. При этом объект класса factor
содержит как собственно значения, так и уровни, по которым эти значения упорядочены. Обычно факторы используют для упорядочивания номинальных, категориальных или текстовых данных, а также в различных статистических моделях и некоторых графиках.
Создадим вектор значений low
, medium
, high
и преобразуем его в фактор, где high
— значение с самым низким рангом, а low
— с самым высоким:
quality <- c("low", "high", "medium", "high", "low", "medium", "high")
quality_f <- factor(quality, levels = c("high", "medium", "low"))
print(quality_f)
## [1] low high medium high low medium high
## Levels: high medium low
При выводе на печать, помимо собственно значений фактора, выводятся также его уровни в порядке возрастания (если не задавать уровни при конвертации в фактор, то уровни задаются в соответствии с алфавитным порядком, лексикографически). Следовательно, если отсортировать фактор, то значения будут выведены в соответствии с уровнями, в то время как простой вектор значений, на основе которых был создан фактор, будет отсортирован в алфавитном порядке:
sort(quality)
## [1] "high" "high" "high" "low" "low" "medium" "medium"
sort(quality_f)
## [1] high high high medium medium low low
## Levels: high medium low
Факторы можно конвертировать в числа, в результате будет получен вектор значений, в котором значения фактора преобразованы в номер его уровня (ранг категориальной переменной):
print(quality_f)
## [1] low high medium high low medium high
## Levels: high medium low
as.numeric(quality_f)
## [1] 3 1 2 1 3 2 1
Есть два нюанса при работе с факторами, которые могут существенно затруднить работу. Первый — уровни фактора хранятся в атрибутах объекта. А второй — до недавнего времени (до релиза R 4.0.0) большинство базовых функций импорта автоматически, если не указано обратное, конвертировали текстовые значения в факторы. В результате легко можно было попасть в ситуацию, когда из базы данных импортируется несколько миллионов уникальных строк, но при импорте они конвертируются в факторы. А так как уровни фактора хранятся в атрибутах, то хранение таблицы удорожается в два раза, а при выборке из датасета десятка строк объект с этими десятью строками будет содержать ещё и все значения уровней из исходного массива.
# удалим все значения medium и law, но в уровнях они останутся
quality_f <- quality_f[quality_f == 'high']
print(quality_f)
## [1] high high high
## Levels: high medium low
Для предотвращения подобных ситуаций, если вдруг возникла необходимость работать с версией R ниже 4.0.0, рекомендуется либо в аргументах функций импорта явно указывать stringsAsFactors = TRUE
(если аргумент stringsAsFactors
есть, конечно же), либо в опциях сессии или в .RProfile
проверять и указывать options(stringsAsFactors = FALSE)
.
Дата и время
Работа с датами и временем традиционно вызывает много сложностей, в частности, из-за формата представления данных или из-за необходимости учитывать временную зону. В R можно работать как просто с датами, так и с более сложными сущностями в виде даты и времени. Самое базовое правило при работе с датами и временем — стараться выбирать наиболее простой и общий формат. Как правило, это формат даты.
Date
Класс Date
используется при работе с датами. Date
-объект не содержит информацию о времени или временной зоне и представляет собой количество дней с 1 января 1970 года (1970-01-01), представленное в человекочитаемом формате дат. В базовом R получить объект класса Date
можно преобразованием других объектов с помощью функции as.Date()
:
## [1] "Date"
## [1] "Date"
# преобразование POSIXct-значения
x <- as.POSIXct(1522540800, origin = '1970-01-01')
x <- as.Date(x)
class(x)
## [1] "Date"
Origin
— это дата и время, от которых отсчитывается количество дней или секунд до настоящего момента. По умолчанию это таймстамп 1970-01-01 00:00:00
, который служит точкой отсчета в Unix-системе (и прочих POSIX-совместимых операционных системах) описания моментов времени:
## [1] 17622
# вычитаем полученное количество из исходной даты
as.Date('2018-04-01') - 17622
## [1] "1970-01-01"
Наиболее часто используемый формат представления дат — это формат yyyy-mm-dd
, однако нередко встречаются иные варианты, особенно если импортируются данные из табличных процессоров типа Excel или Google Spreadsheet, например, mm/dd/yy
или dd.mm.yyyy
. В таких случаях в функции as.Date()
необходимо указывать используемый формат представления дат (подробнее в манипуляциях с датами):
## Date[1:1], format: "2018-04-01"
Так как формат Date
— это в неявном виде количество дней, то при неаккуратном использовании можно столкнуться с ситуацией, когда объект даты конвертируется и используется в числовом виде. Например, если добавить объект даты к числовому вектору, то он будет представлен в виде числового значения:
## [1] 1 4 9 17622
POSIXct, POSIXlt
Для работы с данными даты и времени используют функции as.POSIXct()
и as.POSIXlt()
(cl
от calendar time
, lt
от local time
). Обе функции работают с данными в Unix-формате (количество секунд с 1970-01-01 00:00:00
) и в качестве дополнительного аргумента требуют указать временную зону:
# создаём as.POSIXct-объект
x_ct <- as.POSIXct(1522571987, origin = '1970-01-01', tz = 'UTC')
str(x_ct)
## POSIXct[1:1], format: "2018-04-01 08:39:47"
# создаём as.POSIXlt-объект
x_lt <- as.POSIXlt(1522571987, origin = '1970-01-01', tz = 'UTC')
str(x_lt)
## POSIXlt[1:1], format: "2018-04-01 08:39:47"
Основное различие между функциями as.POSIXct()
и as.POSIXlt()
— первая аналогично Date
принимает информацию о временной метке в числовом виде и представляет результат в текстовом виде, а также содержит метку временной зоны в атрибуте tz
. Вторая возвращает именованный список параметров даты (день, месяц, год, час и т. д.), который также представляет собой таймстамп в указанной временной зоне:
# снимаем атрибут POSIXct-класса с объекта
unclass(x_ct)
## [1] 1522571987
## attr(,"tzone")
## [1] "UTC"
## sec min hour mday mon year wday yday isdst
## 47 39 8 1 3 118 0 90 0
Следует учитывать, что время в Unix-формате — это всего лишь длина временного интервала в секундах и информацию о временной зоне она не содержит. Функции as.POSIXct()
и as.POSIXlt()
по умолчанию используют временную зону локали, если не указано обратное. Как следствие, могут быть ситуации, когда в базе данных хранятся данные времени в Unix-формате (как правило, это так) и время на сервере выставлено по Гринвичу (GMT/UTC), а при импорте на локальный компьютер дата корректируется с учётом локальной временной зоны (несмотря на то, что используется одно и то же числовое представление даты в Unix-формате):
psql_server=> select to_timestamp(1522540800) as server_timestamp;
server_timestamp
------------------------
2018-04-01 00:00:00+00
# этот же таймстамп без указания временной зоны локали
as.POSIXct(1522540800, origin = '1970-01-01')
## [1] "2018-04-01 03:00:00 MSK"
Формулы
На данный момент формулы — специальный объект, который описывает схему взаимодействия зависимой переменной (таргета) и независимых переменных (предикторов), а также взаимодействие предикторов между собой. Формульная нотация для записи регрессионной модели предложена Роджерсом и Вилкинсоном в 1973 году (G. Wilkinson и Rogers 1973), позднее её включили в язык S
(Chambers и Hastie 1991) и, следовательно, она попала в R
. Простой пример формулы, описывающий одну зависимую переменную и две независимые:
y ~ x + z
Слева от знака тильды находится зависимая переменная (LHS, left-hand side). В стандартном использовании слева только одна переменная, если же необходимо указать несколько зависимых и/или их взаимодействие, то следует воспользоваться соответствующими пакетами. Справа от тильды, right-hand side (RHS), — две независимые переменные. Знак +
в формулах означает независимое влияние переменных. По сути, формульная нотация — это своего рода DSL внутри R, так как в нотации привычные знаки и операторы имеют собственное значение:
+
используется для того, чтобы перечислить переменные-предикторы;-
используется, чтобы указать, влияние какого фактора необходимо исключить из модели;:
передаёт в качестве предиктора модели только взаимодействие факторов (аналогичноx1 * x2 - x1 - x2
);*
задаёт как изолированное влияние каждого фактора, так и их взаимодействие (аналогичноx1 + x2 + x1:x2
);|
используется в тех ситуациях, когда необходимо ввести в модель случайные эффекты;-1
или0
в формуле указывает, что из модели следует исключить случайный член (intercept);.
позволяет указывать в качестве предикторов все колонки в таблице, помимо зависимой переменной.
Легче всего показать разницу в формулах с помощью простейшей линейной регрессии. Создадим несколько переменных, из которых разными методами сконструируем зависимую переменную:
Создадим зависимую исходя из возможных вариантов взаимодействия предикторов. Следует помнить, что знак *
в обычном выражении и в формуле интерпретируется по-разному.
y <- 3 * x1 + 2 * x2 + 6 * (x2 * x3) + 7
Зададим и применим модель (:
означает взаимодействие факторов). Модель должна вычислить заданные нами коэффициенты, которые мы использовали при создании вектора y
:
lm(y ~ x1 + x2 + x2:x3)
##
## Call:
## lm(formula = y ~ x1 + x2 + x2:x3)
##
## Coefficients:
## (Intercept) x1 x2 x2:x3
## 7 3 2 6
Аналогичный результат можно получить, если задать модель не через :
, а через *
и -
:
lm(y ~ x1 + x2 + x2*x3 - x3)
##
## Call:
## lm(formula = y ~ x1 + x2 + x2 * x3 - x3)
##
## Coefficients:
## (Intercept) x1 x2 x2:x3
## 7 3 2 6
Так как поведение операторов в формулах иное, чем в обычных выражениях, то при необходимости использовать в формуле собственно сложение или другие операции, следует воспользоваться функцией I()
(от Inhibit
):
y <- 4 * (x1 * x2)
# неправильная формула, так как * задаёт взаимодействие и независимое влияние, а не перемножение
lm(y ~ x1 * x2)
##
## Call:
## lm(formula = y ~ x1 * x2)
##
## Coefficients:
## (Intercept) x1 x2 x1:x2
## 7.177e-17 1.863e-15 1.140e-17 4.000e+00
##
## Call:
## lm(formula = y ~ I(x1 * x2))
##
## Coefficients:
## (Intercept) I(x1 * x2)
## 2.247e-16 4.000e+00
Формулу можно создать несколькими путями: просто создать текстовую переменную либо же воспользоваться функцией formula()
, в конце концов, можно просто написать my_formula = y ~ x
. Результаты потом можно использовать в соответствующих функциях. Текстовое описание модели в формуле позволяет создавать генераторы моделей (например, при переборе комбинаций предикторов в машинном обучении). Пример случайной генерации моделей, сочетающих два независимых предиктора и взаимодействие ещё двух предикторов:
# создаём вектор текстовых значений имён предикторов
predictors <- c('x1', 'x2', 'x3', 'x4')
for (i in 1:5) {
tmp <- paste(
# выбираем независимые факторы
paste(sample(predictors, 2), collapse = ' + '),
# выбираем взаимодействующие факторы
paste(sample(predictors, 2), collapse = ':'),
sep = ' + ')
tmp <- paste('y ~', tmp)
print(tmp)
}
## [1] "y ~ x2 + x3 + x1:x2"
## [1] "y ~ x4 + x3 + x2:x1"
## [1] "y ~ x2 + x4 + x4:x3"
## [1] "y ~ x4 + x3 + x1:x2"
## [1] "y ~ x3 + x4 + x4:x2"
Формулы обычно используются в линейных моделях, случайных лесах и многих других функциях, в которых требуется описание модели взаимосвязи нескольких переменных. Так как тильда в формулах обозначает, что ничего не надо вычислять и надо просто запомнить выражение и его окружение, возможны и другие варианты использования формул. Например, помимо соотношения таргета и предикторов, тильда и формульная нотация может использоваться для определения формы взаимодействия колонок при решейпинге таблиц. Подобное использование ~
встречается и в других пакетах:
пакеты
lattice
,plotly
используют тильду для указания переменных, по которым строится график;в пакете
ggplot2
тильда используется для создания отдельных панелей графиков (фасет) и их взаимного расположения по осям;пакет
dplyr
и ряд других пакетовtidyverse
используют~
в некоторых функциях, где важно работать с окружением объектов, например,dplyr::case_when()
.
Выражения и вызовы функции
Выражения в узком смысле объект класса expression
, который содержит запись операции (операций) на R, чтобы можно было выполнить записанную операцию не сразу, а в необходимом месте (так называемое unevaluated expression, подробнее см. Выполнение выражений). В базовом R выражения можно создать с помощью функции expression()
по аналогии с созданием вектора. При использовании таких выражений необходимо их явно выполнять с помощью функции eval()
:
# создаём выражение из двух операций
my_exp <- expression(x <- 6, x ^ 2)
print(my_exp)
## expression(x <- 6, x^2)
# выполняем выражение
eval(my_exp)
## [1] 36
Второй вариант создания выражения — это парсинг текстовой строки, в котором также записана операция на языке R. Для подобного парсинга используется функция parse(),
где строковая запись передаётся в аргумент text
(подробнее см. Парсинг выражений из строки). Также в функции parse()
можно указывать кодировку записи, что может быть полезно, когда скрипт был написан и сохранен в одной кодировке, а вызывается в рабочей среде с другой кодировкой. В примере ниже мы хотим записать две операции в одну строку, поэтому их необходимо разделить ;
:
# записываем операции в строковый объект
my_exp <- "x <- 6; x ^ 2"
class(my_exp)
## [1] "character"
## [1] "expression"
# выполняем результат парсинга
eval(my_exp)
## [1] 36
Функция call()
создаёт объект, несколько похожий на объект класса expression, только вместо записи какой-либо операции содержит запись вызова функции. Первым аргументом в call()
передаётся название функции, последующими аргументами — значения, которые должны быть переданы в аргументы этой функции:
## [1] "call"
# выполняем объект вызова функции
eval(my_call)
## [1] 9
На практике нередко встречается другой вариант работы с вызовами функции, когда с помощью do.call()
вызывают функцию со списком аргументов и сразу получают результат. Самый наглядный пример — это вызов функции merge()
:
df1 <- data.frame(var1 = letters[1:3], df1_col = sample(5, 3))
df2 <- data.frame(var1 = letters[1:3], df2_col = sample(month.abb, 3))
df3 <- data.frame(var1 = letters[1:3], df3_col = rnorm(3))
list_df <- list(df1, df2, df3)
do.call('merge', c(list_df[1:2], list(by = 'var1', all.x = TRUE)))
## var1 df1_col df2_col
## 1 a 3 May
## 2 b 1 Dec
## 3 c 2 Sep
NULL, NA, Inf, NaN
Ряд специальных значений или объектов используется в ситуациях, когда значения или объекта нет, значение недоступно или некорректно:
-
NA
—not available
. Ситуация, когда значение пропущено или не определено. Например, для какой-то строки в колонке таблицы нет значения или текстовое значение пытаются преобразовать в числовое или логическое. Следует учитывать, чтоNA
— это в первую очередь логическое значение, маркирующее, что значение пропущено и будет ошибкой приравнять его к нулю, например, или кNULL
. Когда известно, какого типа значения вектора, можно использовать специализированные вариацииNA
—NA_character_
,NA_integer_
,NA_real_
илиNA_complex_
. -
NULL
— пустой (null) объект с собственным одноимённым типом. ОбъектNULL
не может быть использован одновременно с другими объектами, например, при создании последовательности, а присвоение какому-то элементу объекта значенияNULL
удаляет этот элемент. Чаще всего это встречается при работе со списками или таблицами, так как присвоение элементу значенияNULL
удаляет из списка ссылку на заданный объект. -
NaN
,Inf
—not a number
иinfinite
соответственно. Используются при работе с числовыми значениями и появляются при некорректных с математической точки зрения операциях —0/0
и1/0
соответственно. ЗначениеInf
может быть как положительным, так и отрицательным.
Окружения
Окружения — это хеш-таблицы, то есть пары ключ-значение. Значениями обычно являются объявленные функции, данные, другие окружения. Окружения похожи на именованные списки, но сходство только внешнее, так как окружения, в отличие от списков, не имеют порядок объектов (соответственно, объекты окружения нельзя вызвать по их номеру, как в листе) и ансамбль окружений может быть представлен в виде дерева — практически у любого окружения есть родительское окружение.
За пределами функций использование окружений достаточно ограничено. Как правило, это либо ситуативные модификации функций пакетов, либо эзотерические практики организации объектов в интерактивном рабочем пространстве. Однако у окружений есть одна нечасто используемая характеристика: можно использовать окружения именно как хеш-таблицы, в которых операции поиска, вставки или удаления по ключам значений (названию) осуществляются в среднем быстрее, чем по обычным таблицам.
Например, создадим два объекта — таблицу, в которой в одной колонке будет вектор имён
— текстовых значений, а во второй — числовое значение (0L
). Второй объект — хешированное окружение с таким же количеством объектов, как строк в таблице. Каждый объект этого окружения имеет название, соответствующее значениям в первой колонке таблицы, а значения объектов — значениям из второй колонки таблицы (0L
для всех объектов, как и для всех строк таблицы).
# создаём вектор уникальных "имён"
vector_letters <- paste(
sample(letters, 10e4, replace = TRUE),
sample(10e4, replace = FALSE),
sep = '_')
# создаём таблицу
simple_df <- data.frame(var1 = vector_letters, var2 = 0L)
# создаём хешированное окружение
hash_env <- new.env(hash = TRUE)
for (i in vector_letters) hash_env[[i]] <- 0L
ls(hash_env)[1:3]
## [1] "a_10012" "a_10031" "a_1004"
Как можно увидеть по бенчмаркам, поиск значения определённого объекта в хешированном окружении существенно быстрее поиска соответствующих значений в таблице:
item <- vector_letters[999]
microbenchmark::microbenchmark(
df_search = simple_df[simple_df$var1 == item, 'var2'],
hash_env_search = hash_env[[item]],
times = 1000,
unit = 'ms'
)
## Unit: milliseconds
## expr min lq mean median uq max
## df_search 1.137430 1.319790 1.664632644 1.4144280 1.578067 13.486095
## hash_env_search 0.000156 0.000264 0.001897494 0.0017455 0.003175 0.013404
## neval cld
## 1000 b
## 1000 a
Функции
Функции — такие же объекты R, как и списки, окружения и т. д. Функции состоят из трёх основных элементов: списка аргументов, тела функции и её окружения. Объявим простейшую функцию:
new_fun <- function(x) {
# комментарий в функции
x <- sqrt(x)
x
}
new_fun(3)
## [1] 1.732051
Список аргументов является одной из форм списков, унаследованных от S, pairlist
, и может быть вызван с помощью функции formals()
или в более удобной для чтения форме — функции args()
. Так как список аргументов — это отдельный объект, то при желании можно извлечь названия аргументов и/или модифицировать список аргументов, например, добавить аргументу значение по умолчанию. Стоит отметить, что изменение аргументов подобным образом — крайне редкая практика, притом потенциально опасная, так как снижает прозрачность и понятность кода.
# смотрим аргументы функции
args(new_fun)
## function (x)
## NULL
formals(new_fun)
## $x
## [1] "pairlist"
# добавляем аргументу значение по умолчанию
formals(new_fun) <- list(x = 9)
# вызываем функцию с обновленным списком аргументов
new_fun()
## [1] 3
Тело функции — код, который находится в фигурных скобках и описывает все действия, что выполняются при вызове функции. Код тела функции можно вызвать с помощью функции body()
, однако возвращён будет только исполняемый код, т. е. комментарии будут опущены. В том случае, когда хочется посмотреть весь код функции, можно просто вывести на печать объект функции. Впрочем, оба метода не работают для примитивных функций (см. ниже про .Primitive
). Как и список аргументов, тело функции можно модифицировать, передав ему новое значение в виде выражения (см. также NSE), и точно так же по возможности следует избегать подобных практик:
# смотрим тело функции
body(new_fun)
## {
## x <- sqrt(x)
## x
## }
# выводим на печать всю функцию
print(new_fun)
## function (x = 9)
## {
## x <- sqrt(x)
## x
## }
# модифицируем тело функции и вызываем обновленную функцию
body(new_fun) <- expression(x ^ 3)
new_fun()
## [1] 729
Третий элемент функций — это рабочее окружение функции (другими словами, область видимости функции, подробнее см. Окружения функций). В окружениях функций возможно как локальное, так и глобальное присвоение, есть механизмы поиска используемых объектов по дереву родительских окружений, если объект не найден в текущем окружении. Функция environment()
возвращает, в зависимости от того, как и в какой момент вызвана, либо окружение, в котором объявлена функция, либо адрес объекта окружения функции в памяти. В последнем случае объект окружения можно сохранить и использовать, в том числе и в родительском окружении:
# объявляем функцию, в которой есть вывод на печать списка объектов и адрес окружения функции
new_fun2 <- function(x) {
print(environment())
print(ls())
x <- sqrt(x)
return(x)
}
# смотрим, в каком окружении объявлена функция
environment(new_fun2)
## <environment: R_GlobalEnv>
# смотрим, как работает вызов environment() в теле функции
new_fun2(3)
## <environment: 0x560a4c0668e0>
## [1] "x"
## [1] 1.732051
Стоит учесть, что ряд функций напрямую обращается к коду на C и не имеет кода на R. Такие функции называют примитивными, их относительно немного, и все они принадлежат базовому пакету (base
). В силу того, что примитивные функции обращаются к C-коду напрямую, их поведение может иногда отличаться от прочих функций, написанных на R. В том числе эти функции возвращают NULL
при попытке увидеть их pairlist-список аргументов (через formals()
), тело и окружение функции. Пример примитивной функции:
print(sqrt)
## function (x) .Primitive("sqrt")
args(sqrt)
## function (x)
## NULL
formals(sqrt)
## NULL
body(sqrt)
## NULL
environment(sqrt)
## NULL