Манипуляции с данными
Создание и вызов объектов
Привязки значений, assign()
При создании объекта происходит связывание (биндинг) значения и символьной записи названия объекта. Например, в выражении x <- 5
мы связываем значение 5
с именем x
. Если потом мы сделаем x <- 9
, то таким образом мы получим новый объект с тем же именем x
и новым значением.
Самый простой способ создания связки имя-значение — это операторы <-
, <<-
, =
, ->
, ->>
. Обычно эти операторы (как и процесс биндинга) называют операторами присваивания, что несколько некорректно, но более понятно для начинающих пользователей.
# простое создание объекта
x <- 5
print(x)
## [1] 5
Помимо операторов присваивания, можно воспользоваться функцией assign()
, которая явным образом связывает значение и его название. Первым аргументом мы указываем строку-название объекта, в аргументе value
— значение.
## [1] 13
Также у assign()
есть аргументы pos
и envir
, которые позволяют указывать, в каком окружении создаётся объект (происходит привязка). Как правило, использовать assign()
с аргументами окружения — намного более безопасный и контролируемый способ создавать объекты в ином окружении, чем окружение вызова (например, в глобальном окружении при выполнении функции). В этом смысле assign()
предпочтительнее операторов <<-
и ->>
.
# создаём окружение и в нём объект x
my_env <- new.env()
assign(x = 'new_x', value = 21, envir = my_env)
# выводим на печать объекты new_x из обоих окружений
print(new_x)
## [1] 13
print(my_env$new_x)
## [1] 21
Активные привязки
Помимо обычных привязок значений, в R можно использовать так называемые активные привязки. Это особая форма связи названия и значения, когда значение отсутствует в постоянном виде и формируется только при обращении к объекту. Достигается это за счёт создания привязки к какой-то определённой функции, можно сказать, что создаётся алиас функции.
В базовом R активные привязки можно сделать с помощью makeActiveBinding()
, в R6
-классах для этого есть отдельный аргумент active
функции R6Class()
(см. в разделе по R6). Для создания активной привязки в makeActiveBinding()
в аргументе sym
указывается, какое должно быть имя объекта, в fun
— функция, которая вызывается по этому названию, тут можно использовать анонимные функции. Аргумент env
нужен, чтобы указать, в каком окружении создаётся привязка.
# создаём привязку
makeActiveBinding(sym = 'rnd', fun = function(x) rnorm(1), env = .GlobalEnv)
# пробуем
rnd
## [1] -0.9058725
# пробуем ещё раз
rnd
## [1] -0.4197879
Активная привязка — это объект того класса, который получается при вызове функции (в нашем случае numeric
):
class(rnd)
## [1] "numeric"
Доступ к объектам, get()
Обычно в выражениях и разных конструкциях объекты вызываются просто по названию объекта. Например, при выводе содержимого объекта или при возведении в степень:
x <- 5
print(x)
## [1] 5
x ^ 2
## [1] 25
Иногда возникают случаи, когда надо получить доступ к значению не по названию объекта, а по его строковой записи (или даже с использованием другого объекта). Например, когда надо перебрать определённые колонки в датасете, пройтись по списку моделей и тому подобное. В таких случаях необходимо использовать функцию get()
:
x <- 9
new_obj <- 'x'
# вызываем с помощью строки
get('x')
## [1] 9
# вызываем с помощью отдельного объекта
get(new_obj)
## [1] 9
# используем в возведении в степень
get(new_obj) ^ 2
## [1] 81
Так как get()
ищет объект по его названию, то в процессе работы просматривает связанные окружения (в первую очередь окружение вызова и родительские окружения). Однако при желании или необходимости можно явно указать, в каком окружении необходимо вести поиск объекта — для этого есть аргументы pos
(номер окружения в иерархическом списке всех подключенных окружений, его можно просмотреть с помощью search()
) и envir
(прямое указание окружения).
# создаём новое окружение
my_env <- new.env()
# создаём x в новом окружении
my_env$x <- 17
# вызываем x с указанием окружения
get('x', envir = my_env)
## [1] 17
# вызываем x без указания окружения (в глобальном)
get('x')
## [1] 9
Выбор элемента объекта
Выбор элемента вектора
Выбор элементов векторов указывается с помощью оператора [
по следующей схеме: x[<condition>]
. Это читается как "элементы вектора x, которые удовлетворяют условию <condition>"
. В качестве условия может быть указан как номер элемента в последовательности (индекс), так и какое-то логическое условие. При использовании оператора [
в <condition>
может быть передан вектор значений (или логическое выражение, возвращающее вектор значений), а оператор [[
, как мы помним, принимает только одно значение (вектор единичной длины).
Выбор по номеру позиции
Простой вариант выбора определённых элементов последовательности — по номеру в последовательности (по индексу, индекс начинается с 1). Например, в последовательности 4, 3, 2
на третьем месте находится значение 2
. В коде выбор третьего элемента этой последовательности выглядит вот так:
x <- c(4, 3, 2)
x[3]
## [1] 2
В список номеров последовательности также можно передавать не только единичное значение, но и вектор значений номеров последовательности, который мы хотим извлечь. Например:
## [1] 3 1
0.0.0.1 Выбор по значению
Помимо простого выбора по индексу, возможен выбор элементов вектора, которые удовлетворяют условию. Например, те, которые больше 10, или все равные трём, или все чётные. Логика такого выделения следующая: каждый элемент вектора сравнивается с условием и полученный вектор логических значений используется для выбора элементов. Так, если сравнение верное (5 == 5
, TRUE
), то этот элемент возвращается как подходящий под условие. Фактически метка TRUE
здесь является указателем элемента, который надо вернуть, аналогично номеру позиции при выделении по индексу.
Например:
## int [1:8] 10 6 5 4 1 8 2 7
# сравниваем каждое значение с 5
x_cond <- x > 5
str(x_cond)
## logi [1:8] TRUE TRUE FALSE FALSE FALSE TRUE ...
# делаем выбор по условию с указанием вектора, удовлетворяет элемент условию или нет
x[x_cond]
## [1] 10 6 8 7
# аналогично, но без создания отдельного вектора
x[x > 5]
## [1] 10 6 8 7
Выбор по наличию в другом векторе
Нередко встречаются ситуации, когда необходимо выбрать значения вектора, которые присутствуют в другом векторе. Например, из списка группы студентов выбрать тех, кто указан в списке недопущенных к сессии. Для этого используется выражение x %in% y
. Оператор %in%
проверяет, встречается ли каждый элемент вектора х
в векторе y
. Как и в сравнении по условию, в результате получается логический вектор, который можно использовать для выделения элементов. Выделенные элементы можно записать в отдельный объект.
Например:
x <- c('aaa', 'abc', 'bgs', 'gtr', 'ant', 'cer')
y <- c('tue', 'bgs', 'mtw', 'cer', 'lka')
# сравниваем элементы списков
x %in% y
## [1] FALSE FALSE TRUE FALSE FALSE TRUE
# выделяем те элементы списка х, которые есть в у
x[x %in% y]
## [1] "bgs" "cer"
# записываем результат в отдельный объект
z <- x[x %in% y]
str(z)
## chr [1:2] "bgs" "cer"
match
## function (x, table, nomatch = NA_integer_, incomparables = NULL)
## .Internal(match(x, table, nomatch, incomparables))
## <bytecode: 0x5567d478b260>
## <environment: namespace:base>
Выбор элемента списка
Выбор элементов списка также использует выбор по номеру элемента (по индексу). Следует учитывать, что для списков несколько различается поведение операторов [
и [[
. Так, оператор [
позволяет выделить элемент списка в виде отдельного списка:
# создадим список
my_list <- list(seq_example = seq(from = 13, to = 0, by = -3),
rep_example = rep(x = 'c', times = 3),
atomic_example = TRUE)
str(my_list)
## List of 3
## $ seq_example : num [1:5] 13 10 7 4 1
## $ rep_example : chr [1:3] "c" "c" "c"
## $ atomic_example: logi TRUE
# выберем первый элемент списка
first_element <- my_list[1]
str(first_element)
## List of 1
## $ seq_example: num [1:5] 13 10 7 4 1
Оператор [[
позволяет вызвать значения вызываемых элементов списка. Так, my_list[[1]]
вызовет не первый элемент в виде списка, а значения первого элемента в виде вектора (как они и были заданы):
# выберем значения первого элемента списка
first_element_values <- my_list[[1]]
str(first_element_values)
## num [1:5] 13 10 7 4 1
Также для именованных списков можно использовать выделение по имени элемента. Для указания элемента списка используется оператор $
и конструкция вида list_name$element_name
. Например:
# смотрим на список
my_list
## $seq_example
## [1] 13 10 7 4 1
##
## $rep_example
## [1] "c" "c" "c"
##
## $atomic_example
## [1] TRUE
# выбираем элемент seq_example
str(my_list$seq_example)
## num [1:5] 13 10 7 4 1
Выбор строк или столбцов в data.frame
Выбор строк в data.frame
Выбор строк в data.frame
осуществляется аналогично выбору элементов в векторе — по номеру строки или по какому-то условию. При выборе по номеру строки также можно указать вектор номеров строк, которые необходимо вернуть. При выборке строки по условию проверяется, удовлетворяет ли условию каждый элемент строки в определённой колонке, и если удовлетворяет, выделяется вся строка.
Так как data.frame
— это в какой-то степени двумерный массив, то логика указания и выделения строк и столбцов аналогичная, и имеет общий вид dataset[выбор строк, операции над колонками]
.
# создаём датасет
df_dataset <- data.frame(var1 = sample(1:12, 8, replace = T),
var2 = sample(letters[1:15], 8, replace = T),
var3 = seq(8, 1, -1),
var4 = rep(c('abc', 'xyz'), 2))
str(df_dataset)
## 'data.frame': 8 obs. of 4 variables:
## $ var1: int 7 6 10 6 4 8 4 4
## $ var2: chr "e" "h" "d" "h" ...
## $ var3: num 8 7 6 5 4 3 2 1
## $ var4: chr "abc" "xyz" "abc" "xyz" ...
Выбор строки или нескольких строк полностью аналогичен операциям над векторами: можно указать номер желаемой строки или вектор номеров строк. Если указывать вектор строк, то необходимо помнить, что вектор всего лишь указывает, какой элемент вектора необходимо извлечь, соответственно, какой-нибудь элемент можно извлечь несколько раз, можно задать свой порядок номеров извлекаемых строк и так далее.
# выбор по одному номеру строки
df_dataset[5, ]
## var1 var2 var3 var4
## 5 4 c 4 abc
# выбор по нескольким номерам строк
# сначала создаём вектор номеров строк, пятую строку извлекаем дважды, после восьмой строки извлекаем ещё и первую
my_rows <- c(2, 5, 5, 8, 1)
# выводим строки, которые мы указали в векторе
df_dataset[my_rows, ]
## var1 var2 var3 var4
## 2 6 h 7 xyz
## 5 4 c 4 abc
## 5.1 4 c 4 abc
## 8 4 o 1 xyz
## 1 7 e 8 abc
# или аналогично сразу указываем, какие строки хотим выделить
df_dataset[c(2, 5, 8), ]
## var1 var2 var3 var4
## 2 6 h 7 xyz
## 5 4 c 4 abc
## 8 4 o 1 xyz
В том случае, когда необходимо выбрать какие-то строки по условию относительно той или иной колонки, нужно указать требуемую колонку с помощью оператора $
(или любым другим образом, ниже подробнее про указание и выделение колонок). Точно так же, по сути, сначала производится логическое сравнение вектора значений указанной колонки, и результат в виде вектора логических значений TRUE/FALSE
уже используется для указания целевых строк (для которых результат сравнения будет TRUE
):
# выбор по условию
# выводим все строки, в которых в колонке var4 есть значение 'xyz'
df_dataset[df_dataset$var4 == 'xyz', ]
## var1 var2 var3 var4
## 2 6 h 7 xyz
## 4 6 h 5 xyz
## 6 8 d 3 xyz
## 8 4 o 1 xyz
# выводим все строки, в которых в колонке var3 значения больше 5
df_dataset[df_dataset$var3 > 5, ]
## var1 var2 var3 var4
## 1 7 e 8 abc
## 2 6 h 7 xyz
## 3 10 d 6 abc
# выводим все строки, в которых значения в колонке var1 чётные
df_dataset[(df_dataset$var1 %% 2) == 0, ]
## var1 var2 var3 var4
## 2 6 h 7 xyz
## 3 10 d 6 abc
## 4 6 h 5 xyz
## 5 4 c 4 abc
## 6 8 d 3 xyz
## 7 4 o 2 abc
## 8 4 o 1 xyz
# вывод сочетания условий — только те строки, где в var4 значения 'xyz'
# и где в var3 значения больше 5
df_dataset[df_dataset$var4 == 'xyz' & df_dataset$var3 > 5, ]
## var1 var2 var3 var4
## 2 6 h 7 xyz
Выбор колонки в data.frame
Синтаксис data.frame
позволяет несколькими путями выбрать одну колонку. Первый вариант предполагает использование порядкового номера колонки в датасете. Номер колонки задаётся с помощью оператора [
(чаще всего), и при этом надо помнить, что в двумерных массивах сначала, до запятой, задаются строки, а потом — колонки. В редких случаях можно использовать оператор [[
, и тогда переданный номер сразу интерпретируется как номер колонки в датасете. В результате подобной операции мы получаем вектор значений:
df_dataset[, 3]
## [1] 8 7 6 5 4 3 2 1
class(df_dataset[, 3])
## [1] "numeric"
Несмотря на то, что это очень простой и очевидный вариант, его использовать не рекомендуется, особенно в ситуации большого проекта, совместной работы над кодом или каких-то исследовательских задачах — во всех ситуациях, когда возможно случайное или слабо контролируемое изменение порядка колонок в датасете или добавление/удаление колонок, или что-то подобное. Общая рекомендация в данном случае — использовать названия колонок вместо числовых индексов или же обращаться к целевым колонкам с помощью операторов $
или [[
. Например:
# выделяем колонку по имени
df_dataset[, 'var3']
## [1] 8 7 6 5 4 3 2 1
# выделяем по имени и с помощью оператора [[
df_dataset[['var3']]
## [1] 8 7 6 5 4 3 2 1
# выделяем колонку по имени и с помощью оператора $
df_dataset$var3
## [1] 8 7 6 5 4 3 2 1
При всём удобстве оператора $
не следует забывать, что его использование за счёт постоянных повторов названия таблицы делает код визуально тяжелым и несколько избыточным. Это, в свою очередь, затрудняет чтение кода. Использование оператора [[
в целом достаточно редкая практика, хотя тоже возможная. С точки зрения читабельности хорошим вариантом может быть вызов колонок в виде data.frame[, 'your_col_name']
при указании колонки или колонок и data.frame['your_rows', ]
для указания строк(и). С другой стороны, это самый медленный способ, [[
немного быстрее, а $
быстрее почти в пять раз.
Также следует иметь в виду разное поведение операторов [
и [[
при указании колонок: если использовать синтаксис списков, а не массивов (то есть не использовать запятую для указания, что берутся колонки таблицы), то эти операторы воспринимают таблицу как список. Соответственно, результат использования оператора [
будет таблицей, а [[
— вектором:
# смотрим содержание таблицы
str(df_dataset)
## 'data.frame': 8 obs. of 4 variables:
## $ var1: int 7 6 10 6 4 8 4 4
## $ var2: chr "e" "h" "d" "h" ...
## $ var3: num 8 7 6 5 4 3 2 1
## $ var4: chr "abc" "xyz" "abc" "xyz" ...
# смотрим класс второй колонки при вызове через синтаксис матриц
df_dataset[, 2]
## [1] "e" "h" "d" "h" "c" "d" "o" "o"
class(df_dataset[, 2])
## [1] "character"
# смотрим класс второй колонки при вызове через `[`
df_dataset[2]
## var2
## 1 e
## 2 h
## 3 d
## 4 h
## 5 c
## 6 d
## 7 o
## 8 o
class(df_dataset[2])
## [1] "data.frame"
# смотрим класс второй колонки при вызове через `[[`
df_dataset[[2]]
## [1] "e" "h" "d" "h" "c" "d" "o" "o"
class(df_dataset[[2]])
## [1] "character"
Временами может встречаться выражение вида df_dataset[, 'var1', drop = FALSE]
. Здесь используется аргумент drop
, который по умолчанию имеет значение TRUE
. При drop = FALSE
результатом выражения будет не вектор значений указанной колонки, а таблица с такой колонкой.
str(df_dataset[, 'var1', drop = FALSE])
## 'data.frame': 8 obs. of 1 variable:
## $ var1: int 7 6 10 6 4 8 4 4
str(df_dataset[, 3, drop = FALSE])
## 'data.frame': 8 obs. of 1 variable:
## $ var3: num 8 7 6 5 4 3 2 1
Выбор нескольких колонок в data.frame
Выбор нескольких колонок осуществляется с помощью либо вектора номеров колонок в таблице, либо вектора названий колонок (если только не используется drop = FALSE
). Вектор значений можно задать как отдельный объект или же для лаконичности сразу с помощью c()
. Оператор $
в данном случае неприменим, как и [[
:
# выделяем с помощью вектора индексов
df_dataset[, c(1, 2)]
## var1 var2
## 1 7 e
## 2 6 h
## 3 10 d
## 4 6 h
## 5 4 c
## 6 8 d
## 7 4 o
## 8 4 o
# выделяем с помощью вектора названий колонок
df_dataset[, c('var1', 'var2')]
## var1 var2
## 1 7 e
## 2 6 h
## 3 10 d
## 4 6 h
## 5 4 c
## 6 8 d
## 7 4 o
## 8 4 o
Сортировка и упорядочивание векторов и таблиц
Для сортировки векторов используются функции sort()
и order()
. Функция sort()
просто сортирует и упорядочивает значения по возрастанию (действие по умолчанию, для обратной сортировки надо задать значение TRUE
для аргумента decreasing
):
## int [1:10] 10 6 5 4 1 8 2 7 9 3
# сортируем по возрастанию и по убыванию
sort(x)
## [1] 1 2 3 4 5 6 7 8 9 10
sort(x, decreasing = TRUE)
## [1] 10 9 8 7 6 5 4 3 2 1
Функция order()
действует немного сложнее — она возвращает вектор номеров позиций, на которых должны стоять соответствующие значения сортируемого вектора. Например, в векторе x
содержатся числа от 1 до 10 в случайном порядке. Результат функции order()
демонстрирует, что если мы хотим упорядочить по возрастанию исходный вектор, то нам необходимо первым поставить пятое значение из вектора x
(x[5]
), вторым — седьмое значение (x[7]
), третьим — десятое значение вектора x
(x[10]
) и так далее.
x
## [1] 10 6 5 4 1 8 2 7 9 3
order(x)
## [1] 5 7 10 4 3 2 8 6 9 1
В том случае если использовать функцию order()
для сортировки и упорядочивания значений векторов, то необходимо результат функции использовать с помощью оператора [
. В таком случае получаем аналогичный функции sort()
результат:
x[order(x)]
## [1] 1 2 3 4 5 6 7 8 9 10
sort(x)
## [1] 1 2 3 4 5 6 7 8 9 10
Для задач сортировки строк или столбцов при работе с таблицами используется функция order()
. Функция sort()
здесь не подходит и при неосмысленном использовании может привести к ошибкам или неверному результату. Это возможно, потому что функция sort()
отдаёт вектор сортированных значений, а не индексы, в отличие от order()
. Например, в таблице df_dataset
всего восемь строк. Если использовать sort()
в виде выражения df_dataset[sort(df_dataset$var1), ]
, то мы фактически запрашиваем таблицу, которая составлена из строк по номерам согласно значениям в колонке var1
по возрастанию (1, 4, 4, 6, 6, 8, 11, 12). В этом векторе запрашиваются 11-я и 12-я строки датасета, несмотря на то что сам датасет состоит из восьми строк. В результате получается несколько иной результат, чем предполагался:
df_dataset
## var1 var2 var3 var4
## 1 7 e 8 abc
## 2 6 h 7 xyz
## 3 10 d 6 abc
## 4 6 h 5 xyz
## 5 4 c 4 abc
## 6 8 d 3 xyz
## 7 4 o 2 abc
## 8 4 o 1 xyz
df_dataset[sort(df_dataset$var1), ]
## var1 var2 var3 var4
## 4 6 h 5 xyz
## 4.1 6 h 5 xyz
## 4.2 6 h 5 xyz
## 6 8 d 3 xyz
## 6.1 8 d 3 xyz
## 7 4 o 2 abc
## 8 4 o 1 xyz
## NA NA <NA> NA <NA>
Функция order()
в качестве результата возвращает не сам упорядоченный вектор значений, а вектор-инструкцию, как надо переставить значения в исходном векторе, чтобы получить упорядоченный вектор. Соответственно, применительно к таблицам — как надо переставить местами строки, чтобы получить таблицу с упорядоченными значениями по одной или нескольким колонкам:
df_dataset
## var1 var2 var3 var4
## 1 7 e 8 abc
## 2 6 h 7 xyz
## 3 10 d 6 abc
## 4 6 h 5 xyz
## 5 4 c 4 abc
## 6 8 d 3 xyz
## 7 4 o 2 abc
## 8 4 o 1 xyz
df_dataset[order(df_dataset$var1), ]
## var1 var2 var3 var4
## 5 4 c 4 abc
## 7 4 o 2 abc
## 8 4 o 1 xyz
## 2 6 h 7 xyz
## 4 6 h 5 xyz
## 1 7 e 8 abc
## 6 8 d 3 xyz
## 3 10 d 6 abc
Так как при сортировке таблиц происходит их перезапись, но уже с новым порядком строк или колонок, можно обойтись без использования функции order()
и вручную указать, как и в какой последовательности должна быть упорядочена таблица. Также это подходит и для ситуаций, когда неприменима сортировка по возрастанию или убыванию:
# сортируем колонки в произвольном порядке
df_dataset <- df_dataset[, c('var2', 'var1', 'var4', 'var3')]
df_dataset
## var2 var1 var4 var3
## 1 e 7 abc 8
## 2 h 6 xyz 7
## 3 d 10 abc 6
## 4 h 6 xyz 5
## 5 c 4 abc 4
## 6 d 8 xyz 3
## 7 o 4 abc 2
## 8 o 4 xyz 1
Изменение, создание и удаление элементов объектов
В задачах на изменение значений элементов векторов, списков или таблиц используется следующая логика: указывается элемент объекта, с которым надо произвести какое-то действие, и этому элементу присваивается новое значение. Например, у нас есть вектор из 10 значений в случайном порядке от 1 до 10, и мы хотим возвести в квадрат третий элемент:
## int [1:10] 10 6 5 4 1 8 2 7 9 3
# возводим в квадрат третий элемент
x[3] <- x[3] ^ 2
str(x)
## num [1:10] 10 6 25 4 1 8 2 7 9 3
Создание новых элементов или удаление уже существующих производятся аналогично: указывается индекс элемента (или его название, если применимо) и присваивается какое-то значение. Для создания элемента — любой объект, если он не нарушает уже существующую структуру (например, в таблице на пять строк нельзя создать колонку с шестью значениями). Если в векторе создать значение иного типа, чем был, то все значения будут преобразованы к более общему по правилам преобразования.
# создаём 11-й элемент вектора x, текстовых
x[11] <- 'x'
str(x)
## chr [1:11] "10" "6" "25" "4" "1" "8" "2" "7" "9" "3" "x"
Для удаления элемента вектора можно просто переприсвоить этому объекту те же значения, за исключением того, которое требуется удалить (строго говоря, создать объект с тем же именем, но другими значениями):
# удалим 3-е по счёту значение вектора x
x <- x[-3]
str(x)
## chr [1:10] "10" "6" "4" "1" "8" "2" "7" "9" "3" "x"
В тех случаях, когда необходимо удалить строку таблицы, логика аналогичная: перезаписываем таблицу, но уже с нужным порядком строк. Удалить колонку таблицы или элемент списка можно ещё одним способом — присвоить удаляемой колонке/подсписку значение NULL
(не путать с NA
, это разные типы и объекты!):
# смотрим структуру и удаляем var4
str(df_dataset)
## 'data.frame': 8 obs. of 4 variables:
## $ var2: chr "e" "h" "d" "h" ...
## $ var1: int 7 6 10 6 4 8 4 4
## $ var4: chr "abc" "xyz" "abc" "xyz" ...
## $ var3: num 8 7 6 5 4 3 2 1
df_dataset[, 'var4'] <- NULL
# ещё раз смотрим структуру
str(df_dataset)
## 'data.frame': 8 obs. of 3 variables:
## $ var2: chr "e" "h" "d" "h" ...
## $ var1: int 7 6 10 6 4 8 4 4
## $ var3: num 8 7 6 5 4 3 2 1
Манипуляции с таблицами
Построчное объединение таблиц, rbind()
Функция rbind()
(от row bind
) используется для объединения двух или более таблиц по строкам. В результате получается таблица с таким же количеством колонок, но с увеличенным числом строк — по количеству строк в объединяемых таблицах.
Нередко в объединяемых таблицах отсутствует какая-нибудь колонка или колонки перепутаны. В таких случаях необходимо первоначально создать и заполнить NA
недостающие колонки.
# создаём первую таблицу
df1 <- data.frame(tb = 'table_1',
col1 = sample(9, 3),
col3 = 'only in table1',
col2 = sample(letters, 3))
str(df1)
## 'data.frame': 3 obs. of 4 variables:
## $ tb : chr "table_1" "table_1" "table_1"
## $ col1: int 6 7 9
## $ col3: chr "only in table1" "only in table1" "only in table1"
## $ col2: chr "t" "n" "x"
# создаём вторую таблицу
df2 <- data.frame(tb = 'table_2',
col4 = 'only in table2',
col1 = sample(9, 3),
col2 = sample(letters, 3))
str(df2)
## 'data.frame': 3 obs. of 4 variables:
## $ tb : chr "table_2" "table_2" "table_2"
## $ col4: chr "only in table2" "only in table2" "only in table2"
## $ col1: int 4 9 5
## $ col2: chr "h" "t" "x"
# создаём недостающие колонки
df1$col4 <- NA
df2$col3 <- NA
# объединяем по строкам
rbind(df1, df2)
## tb col1 col3 col2 col4
## 1 table_1 6 only in table1 t <NA>
## 2 table_1 7 only in table1 n <NA>
## 3 table_1 9 only in table1 x <NA>
## 4 table_2 4 <NA> h only in table2
## 5 table_2 9 <NA> t only in table2
## 6 table_2 5 <NA> x only in table2
Еще один нюанс, который стоит учитывать при использовании rbind()
: желательно, чтобы типы объектов в колонках объединяемых таблиц были идентичны. То есть если в одной таблице колонка типа numeric
, то и во всех других таблицах она должна быть numeric
. В противном случае произойдет неявное преобразование типов, и все значения будут приведены к наиболее общему типу.
Поколоночное объединение таблиц, cbind()
Функция cbind()
(от columns bind
) используется для объединения нескольких векторов или таблиц равной длины. В результате получается объединённая таблица такой же длины, как каждый из объединяемых векторов. При объединении таблиц — с таким же количеством строк, как в каждой из объединяемых таблиц, и с суммарным количеством колонок.
При использовании cbind()
в работе с таблицами необходимо помнить, что это буквально “склейка” таблиц независимо от порядка наблюдений по строкам. В противном случае можно получить наблюдение, где часть колонок описывает характеристики этого наблюдения, а другая часть — каких-то других наблюдений.
Другой нюанс, который также необходимо учитывать: при объединении таблиц названия колонок останутся прежними. Так что если в нескольких таблицах встречается, например, колонка col1
, то в финальном датасете будет несколько колонок с таким названием — по количеству объединяемых таблиц, в которых она была. В свою очередь, это усложняет задачи выбора колонки по названию и мешает понять, в какой колонке какое содержание.
# выведем ранее созданные таблицы
print(df1)
## tb col1 col3 col2 col4
## 1 table_1 6 only in table1 t NA
## 2 table_1 7 only in table1 n NA
## 3 table_1 9 only in table1 x NA
print(df2)
## tb col4 col1 col2 col3
## 1 table_2 only in table2 4 h NA
## 2 table_2 only in table2 9 t NA
## 3 table_2 only in table2 5 x NA
# объединим по колонкам
cbind(df1, df2)
## tb col1 col3 col2 col4 tb col4 col1 col2 col3
## 1 table_1 6 only in table1 t NA table_2 only in table2 4 h NA
## 2 table_1 7 only in table1 n NA table_2 only in table2 9 t NA
## 3 table_1 9 only in table1 x NA table_2 only in table2 5 x NA
## tb col1 col3 col2 col4 tb col1 col4
## 1 table_1 6 only in table1 t NA table_2 4 only in table2
## 2 table_1 7 only in table1 n NA table_2 9 only in table2
## 3 table_1 9 only in table1 x NA table_2 5 only in table2
В целом, cbind()
— весьма редко используемый способ объединения таблиц. Его стоит использовать только тогда, когда есть однозначная уверенность в структуре данных (одинаковое количество строк, разные названия колонок, идентичная сортировка и т. д.), в противном случае это место, в котором очень легко ошибиться, при этом эту ошибку будет очень сложно найти.
Объединение таблиц по ключу, merge()
Одна из самых, наверное, важных операций при работе с таблицами — построчное объединение двух или нескольких таблиц. При использовании функции merge()
с каждым значенем в ключевой колонке первой таблицы сопоставляется строка параметров наблюдения другой таблицы с таким же значением в ключевой колонке, как и в первой таблице. В других языках программирования, в частности, в SQL, аналогичная функция может называться join
.
Несмотря на сложность формулировки, выглядит это достаточно просто:
# создаём датасет 1, в синтаксисе data.frame
dt1 <- data.frame(key_col = c('r1', 'r2', 'r3'),
col_num = seq_len(3))
# создаём датасет 2, в синтасисе data.frame
dt2 <- data.frame(key_col = c('r3', 'r1', 'r2'),
col_char = c('c', 'a', 'b'))
# объединяем построчно по значениям в колонке key_col
merge(x = dt1, y = dt2, by = 'key_col')
## key_col col_num col_char
## 1 r1 1 a
## 2 r2 2 b
## 3 r3 3 c
Здесь первая таблица задаётся аргументом x
, вторая таблица — аргументом y
, а колонка (или колонки), по значениям которой происходит объединение таблиц, задаётся аргументом by
. Если аргумент by
не указан, то объединение происходит по тем колонкам, которые имеют одинаковое название в объединяемых таблицах.
В SQL аналогичная операция выполняется следующим образом:
SELECT *
FROM dt1
INNER JOIN dt2 using(key_col)
Притом таблицы можно объединять по значениям колонок разными именами, тогда надо отдельно указать, по значениям каких колонок в первой и второй таблице происходит слияние. Для этого вместо общего аргумента by
используют аргументы by.x
и by.y
для первой и второй таблицы соответственно.
В первом приближении операция объединения merge()
похожа на результат работы функции cbind()
. Однако из-за того, что при объединении происходит сопоставление по значениям ключевых колонок, в результате решается проблема объединения колонок, в которых разный порядок строк. Сравним:
cbind(dt1, dt2)
## key_col col_num key_col col_char
## 1 r1 1 r3 c
## 2 r2 2 r1 a
## 3 r3 3 r2 b
merge(x = dt1, y = dt2, by = 'key_col')
## key_col col_num col_char
## 1 r1 1 a
## 2 r2 2 b
## 3 r3 3 c
Второе существенное отличие от cbind()
— обработка ситуаций, когда в таблицах разное количество наблюдений. Например, в первой таблице данные по первой волне опросов, а во второй — данные по тем, кто из принявших участие в первой волне принял участие и во второй, а также какие-то новые опрошенные респонденты. Разное количество наблюдений в объединяемых таблицах порождает четыре варианта объединения, все они задаются аргументом all
с постфиксами:
-
all
= FALSE (INNER JOIN в SQL). Значение аргумента по умолчанию, в результате объединения будет таблица с наблюдениями, которые есть и в первой, и во второй таблице. То есть наблюдения из первой таблицы, которым нет сопоставления из второй таблицы, отбрасываются, и наоборот. В примере с волнами это будет таблица только по тем, кто принял участие и в первой, и во второй волнах опросов:
# создаём данные первой волны
wave1 <- data.frame(id = paste0('id_', seq_len(5)),
col1 = sample(10, 5))
print(wave1)
## id col1
## 1 id_1 3
## 2 id_2 4
## 3 id_3 7
## 4 id_4 8
## 5 id_5 5
# создаём данные второй волны
wave2 <- data.frame(id = paste0('id_', c(1, 3, 5, 6, 7, 8)),
col2 = sample(letters, 6))
print(wave2)
## id col2
## 1 id_1 z
## 2 id_3 e
## 3 id_5 b
## 4 id_6 o
## 5 id_7 h
## 6 id_8 t
# объединяем так, чтобы оставить только тех, кто был в обеих волнах
merge(x = wave1, y = wave2, by = 'id', all = FALSE)
## id col1 col2
## 1 id_1 3 z
## 2 id_3 7 e
## 3 id_5 5 b
-
all.x
= TRUE (LEFT JOIN). Всем наблюдениям из первой таблицы сопоставляются значения из второй. Если во второй таблице нет соответствующих наблюдений, то пропуски заполняютсяNA
-значениями (в нашем примере в колонкеcol2
):
# сливаем так, чтобы оставить тех, кто был в первой волне
merge(x = wave1, y = wave2, by = 'id', all.x = TRUE)
## id col1 col2
## 1 id_1 3 z
## 2 id_2 4 <NA>
## 3 id_3 7 e
## 4 id_4 8 <NA>
## 5 id_5 5 b
-
all.y
= TRUE (RIGHT JOIN). Обратная ситуация, когда всем наблюдениям из второй таблицы сопоставляются значения из первой и пропущенные значения заполняютсяNA
-значениями (в нашем примере в колонкеco12
):
# сливаем так, чтобы оставить тех, кто был во второй волне
merge(x = wave1, y = wave2, by = 'id', all.y = TRUE)
## id col1 col2
## 1 id_1 3 z
## 2 id_3 7 e
## 3 id_5 5 b
## 4 id_6 NA o
## 5 id_7 NA h
## 6 id_8 NA t
-
all
= TRUE (FULL OUTER JOIN). Сочетание предыдущих двух вариантов — создаётся таблица по всему набору уникальных значений из ключевых таблиц, по которым происходит объединение, и если в какой-то из таблиц нет соответствующих наблюдений, то пропуски также заполняютсяNA
-значениями:
# сливаем так, чтобы оставить тех, кто был в какой-то из обеих волн
merge(x = wave1, y = wave2, by = 'id', all = TRUE)
## id col1 col2
## 1 id_1 3 z
## 2 id_2 4 <NA>
## 3 id_3 7 e
## 4 id_4 8 <NA>
## 5 id_5 5 b
## 6 id_6 NA o
## 7 id_7 NA h
## 8 id_8 NA t
При работе с несколькими таблицами можно столкнуться с ограничением, что базовая функция merge()
работает только с парами таблиц. То есть, если вдруг необходимо объединить по одному ключу сразу несколько таблиц (например, не две волны опросов, а пять), то придется строить последовательные цепочки попарных объединений. Правда, возможен альтернативный вариант — писать собственные функции или использовать соответствующие функции из других пакетов.
wide-long трансформации, reshape
Обычная форма представления данных в таблицах — когда одна строка является одним наблюдением, а в значениях колонок отражены те или иные характеристики этого наблюдения. Такой формат традиционно называется wide
-форматом, потому что при увеличении количества характеристик таблица будет расти вширь путем увеличения числа колонок. Пример таблицы в wide
-формате.
# создаём таблицу с идентификатором респондента, его возрастом, ростом и весом
dt_wide <- data.frame(
wave = paste0('wave_', rep(1:2, each = 2)),
id = paste0('id_', seq_len(4)),
age = c(45, 13, 29, 69),
height = c(163, 142, 178, 155),
weight = c(55, 40, 85, 63))
dt_wide
## wave id age height weight
## 1 wave_1 id_1 45 163 55
## 2 wave_1 id_2 13 142 40
## 3 wave_2 id_3 29 178 85
## 4 wave_2 id_4 69 155 63
Тем не менее, нередко встречается другой формат, в котором на одно наблюдение может приходиться несколько строк (по количеству измеренных характеристик этого наблюдения). В таком случае таблица состоит из колонки, в которой содержится какой-то идентификатор объекта, одной или нескольких колонок, в которых содержатся идентификаторы характеристик объекта, и колонки, в которой содержатся значения этих характеристик. Такой формат называется длинным, long
-форматом данных, потому что при увеличении количества измеряемых характеристик таблица будет расти в длину увеличением строк.
# создаём таблицу с идентификатором респондента, его возрастом, ростом и весом
dt_long <- data.frame(
# две волны, по два респондента в каждой
wave = paste0('wave_', rep(1:2, each = 6)),
# на каждого респондента задаём три строки
id = paste0('id_', rep(1:4, each = 3)),
# три характеристики повторяем для четырёх респондентов
variable = rep(c('age', 'height', 'weight'), 4),
# задаём значения характеристик с учётом того, как упорядочены первые две колонки
value = c(45, 163, 55,
13, 142, 40,
29, 178, 85,
69, 155, 63))
dt_long
## wave id variable value
## 1 wave_1 id_1 age 45
## 2 wave_1 id_1 height 163
## 3 wave_1 id_1 weight 55
## 4 wave_1 id_2 age 13
## 5 wave_1 id_2 height 142
## 6 wave_1 id_2 weight 40
## 7 wave_2 id_3 age 29
## 8 wave_2 id_3 height 178
## 9 wave_2 id_3 weight 85
## 10 wave_2 id_4 age 69
## 11 wave_2 id_4 height 155
## 12 wave_2 id_4 weight 63
Из long в wide, dcast()
Для перевода long
-формата в wide
-формат, используется функция dcast()
пакета reshape2
(либо dcast()
пакета data.table
, в tidyverse
используются другие названия для аналогичных операций). Также можно использовать функцию reshape()
из базового набора функций R, однако эта функция достаточно медленная и отличается весьма неудобным синтаксисом.
Для того чтобы превратить созданную выше таблицу в long
-формате в широкий формат, выражение будет выглядеть следующим образом. Сама операция называется решейп (в Excel и python есть близкая, но не эквивалентная операция пивота, создания сводной таблицы):
## wave id age height weight
## 1 wave_1 id_1 45 163 55
## 2 wave_1 id_2 13 142 40
## 3 wave_2 id_3 29 178 85
## 4 wave_2 id_4 69 155 63
Здесь аргумент data
определяет таблицу, которую мы хотим трансформировать.
Аргумент formula
задает, что в результирующей таблице будет представлять уникальное наблюдение и значения какой колонки будут разделены на самостоятельные колонки. Формулу можно прочитать как строки ~ колонки
в результирующей таблице. В нашем случае уникальное наблюдение мы задаём парой переменных wave
и id
, поэтому указываем их до тильды через +
. Колонки же мы создаём по значениям переменной variable
после тильды. Следует отметить, что ситуация, когда строка задаётся несколькими переменными через оператор +
, весьма частая, а вот в правой части формулы несколько переменных встречаются достаточно редко, обычно всё же на колонки раскладывают по значениям одной переменной.
Аргумент value.var
содержит текстовое название переменной, значения которой будут отражены в результирующей таблице по колонкам для каждого наблюдения.
Иногда случаются ситуации, когда необходимо провести сначала агрегацию по одной из колонок, описывающих наблюдение. Например, вычислить средние значения возраста, роста и веса для каждой волны. Это можно сделать в два этапа: сначала провести агрегацию, а потом решейп. Также можно сразу сделать решейп и воспользоваться дополнительным аргументом fun.aggregate
, который сразу при решейпе агрегирует данные (и в таком использовании можно говорить о пивоте таблицы). Например, если использовать сначала агрегацию, а потом трансформацию в wide
-формат:
# агрегируем наблюдения по волнам и характеристикам
tmp <- aggregate(value ~ wave + variable, dt_long, mean)
tmp
## wave variable value
## 1 wave_1 age 29.0
## 2 wave_2 age 49.0
## 3 wave_1 height 152.5
## 4 wave_2 height 166.5
## 5 wave_1 weight 47.5
## 6 wave_2 weight 74.0
# трансформируем в wide-формат, колонки id уже нет в таблице, поэтому удаляем из формулы
dcast(data = tmp, formula = wave ~ variable, value.var = 'value')
## wave age height weight
## 1 wave_1 29 152.5 47.5
## 2 wave_2 49 166.5 74.0
Аналогично, но с использованием аргумента fun.aggregate
. В значения аргумента передаём название функции без кавычек и скобок, в нашем случае это fun.aggregate = mean
:
dcast(data = tmp, formula = wave ~ variable, value.var = 'value', fun.aggregate = mean)
## wave age height weight
## 1 wave_1 29 152.5 47.5
## 2 wave_2 49 166.5 74.0
Из wide в long, melt()
Обратная трансформация из wide
-формата в long
-формат также возможна. Для этого используется функция melt()
:
melt(data = dt_wide,
id.vars = c('wave', 'id'),
measure.vars = c('age', 'height', 'weight'),
variable.name = 'variable',
value.name = 'value')
## wave id variable value
## 1 wave_1 id_1 age 45
## 2 wave_1 id_2 age 13
## 3 wave_2 id_3 age 29
## 4 wave_2 id_4 age 69
## 5 wave_1 id_1 height 163
## 6 wave_1 id_2 height 142
## 7 wave_2 id_3 height 178
## 8 wave_2 id_4 height 155
## 9 wave_1 id_1 weight 55
## 10 wave_1 id_2 weight 40
## 11 wave_2 id_3 weight 85
## 12 wave_2 id_4 weight 63
Здесь аргумент id.vars
задаёт переменные, которые будут использоваться для уникальной идентификации наблюдения. Аргумент measure.vars
определяет те колонки, которые войдут в длинную таблицу как значения переменной характеристик наблюдений (когда каждая строка — отдельная характеристика наблюдения, несколько строк на одного пользователя). Аргументы variable.name
и value.name
задают соответственно названия колонок характеристик наблюдения и значений этих характеристик в финальной таблице. В принципе, эти аргументы не обязательны, так как melt()
самостоятельно присваивает названия этим колонкам, если они не указаны, но могут сильно упростить и сделать более прозрачным код. Пример решейпа в long-формат без указания названий колонок характеристик и их значений:
## wave id variable value
## 1 wave_1 id_1 age 45
## 2 wave_1 id_2 age 13
## 3 wave_2 id_3 age 29
## 4 wave_2 id_4 age 69
## 5 wave_1 id_1 height 163
## 6 wave_1 id_2 height 142
## 7 wave_2 id_3 height 178
## 8 wave_2 id_4 height 155
## 9 wave_1 id_1 weight 55
## 10 wave_1 id_2 weight 40
## 11 wave_2 id_3 weight 85
## 12 wave_2 id_4 weight 63
Манипуляции с датами и временем
Форматирование и парсинг дат и времени
При работе с датами нередко возникает ситуация, когда необходимо либо привести текстовую запись даты (или даты и времени) к какому-то виду, либо извлечь из даты месяц, день недели или какой-либо другой параметр. Для всех этих задач используются функции as.Date()
и strftime()
, где в аргумент format
передаётся буквенный код требуемого формата (по стандарту ISO 8601). Наиболее часто используемые коды:
-
%Y
— год со столетием, четырёхзначный формат; -
%y
— год в двузначном формате; -
%m
— месяц в двузначном формате (01-12); -
%B
— полное название месяца на языке локали; -
%b
— сокращённое название месяца на языке локали; -
%U
— номер недели месяца в двузначном формате (00–53), неделя начинается с первого воскресенья года (первый день первой недели); -
%W
— Номер недели месяца в двузначном формате (00–53), неделя начинается с первого понедельника года (первый день первой недели); -
%V
— Номер недели месяца в двузначном формате (00–53) согласно стандарту ISO 8601, неделя начинается с первого четверга года (если четыре и больше дней недели в новом году, считая неделю с понедельника, то это первая неделя года, иначе — неделя предыдущего года), не используется на ввод (создание даты из текстовой записи); -
%j
— день года в трехзначном формате (001–366); -
%d
— день месяца в двузначном формате (01–31); -
%w
— номер дня недели (0–6, 0 означает воскресенье); -
%u
— номер дня недели (1–7, 1 означает понедельник), не используется на вывод (извлечение данных из даты) в Windows; -
%A
— полное название дня недели на языке локали; -
%a
— сокращённое название дня недели на языке локали; -
%H
— час в двузначном формате (00–23); -
%M
— минута в двузначном формате (00–59); -
%S
— секунда в двузначном формате (00–61), в том числе с поддержкой високосных секунд (в POSIX-совместимых системах високосные секунды игнорируются); -
%F
— аналог стандартной ISO 8601 записи даты,%Y-%m-%d;
-
%D
— локально-специфичная запись даты по стандарту ISO C99,%m/%d/%y;
-
%T
— эквивалентно%H:%M:%S
.
Коды можно использовать как для создания дат (input), так и извлечения из дат требуемой информации (output), например, номера дня или месяца. Следует учитывать, что, согласно справке функции strftime()
, некоторые коды могут неодинаково работать на ввод и вывод, а также зависеть от используемой операционной системы.
При создании дат на вход подаётся текстовая запись, а в записи формата, помимо кодов, указывается использованный разделитель. Например, когда есть номер недели и день недели, сначала создаём текстовую запись с пробелом в виде разделителя, а потом в функции as.Date()
прописываем, как форматировать полученную запись:
## chr "2018 40 1"
## Date[1:1], format: "2018-10-01"
Для обратной операции извлечения данных из даты используем strftime()
и также указываем, какие значения нужны, как именно парсить дату:
## [1] "40" "1"
Вычисление дат и времени
Так как форматы Date
и POSIXct
представляют собой количество дней и секунд с 1 января 1970 года (по умолчанию), то при работе с датами и временем можно использовать операции сложения и вычитания (другие арифметические операции не определены для объектов Date
и POSIXct
):
## Date[1:1], format: "2022-02-04"
# прибавляем семь дней к текущей дате
x_7 <- x + 7
str(x_7)
## Date[1:1], format: "2022-02-11"
# вычитаем 10 дней
x_10 <- x - 10
str(x_10)
## Date[1:1], format: "2022-01-25"
Подобным образом поступаем и с POSIXct
-датой с учётом того, что изменение даты и времени происходит на определённое количество секунд. Если требуется изменить собственно дату, то надо количество дней умножить на 86 400 (60*60*24):
## POSIXct[1:1], format: "2022-02-04 18:53:57"
# прибавляем семь секунд к текущему времени
x_px7 <- x_px + 7
str(x_px7)
## POSIXct[1:1], format: "2022-02-04 18:54:04"
# вычитаем 10 дней
x_px10 <- x_px - 10 * 86400
str(x_px10)
## POSIXct[1:1], format: "2022-01-25 18:53:57"
Сложно представить такую необходимость, однако при желании для дат и времени можно определить собственные арифметические операторы, так как выражение Sys.Date() - 1
сводится к выражению structure(unclass(Sys.Date()) - 1, class = "Date")
. Выражение Sys.time() - 1
аналогично сводится к .POSIXct(unclass(Sys.time()) - 1, attr(Sys.time(), "tzone"))
.
Вычисление интервалов
Помимо сложения и вычитания дат с числами, можно также вычислять длительность интервала между двумя датами. Для этого используется функция difftime()
, которая в качестве аргументов принимает даты конца и начала интервала, а также указание, в каком виде должен быть представлен интервал (секунды, минуты, часы, дни, недели). В результате получается числовое значение с мерой интервала в атрибуте:
## Time difference of 1 weeks
str(x_diff)
## 'difftime' num 1
## - attr(*, "units")= chr "weeks"
Простое вычитание дат дает такой же результат, как и использование функции difftime()
(что неудивительно, так как в она используется в функции бинарного вычитания дат). По умолчанию полученный интервал представлен в днях или в секундах, для Date
и POSIXct
соответственно. В последнем выражении в примере ниже время меньше семи секунд, так как само вычисление выражения требует времени, а Sys.time()
возвращает текущее системное время:
## Time difference of 7 days
## Time difference of 6.999948 secs
Полученный в результате вычитания дат объект имеет свой собственный класс, difftime
. С этим объектом также можно работать — совершать арифметические операции сложения и вычитания (и даже умножения и деления), использовать в вычислении интервалов:
print(x_diff)
## Time difference of 1 weeks
# умножаем интервал на 3
print(x_diff * 3)
## Time difference of 3 weeks
## Date[1:1], format: "2022-02-11"
Округление дат
Инструменты в base R для работы с датами хороши, однако некоторые задачи всё же не покрывают (округления дат, високосные года и тому подобное). В подобных ситуациях лучше обращаться к специализированным пакетам, в частности, lubridate
.
Некоторые отчёты требуют представления данных за более крупный период, чем день или час — например, за неделю или за месяц. Чтобы создать такие интервалы, необходимо вычислять из даты номер месяца или недели. Другой вариант — округлить дату до начала интервала (или начала следующего, хотя это уже намного более редкая операция). Например, день 2019-07-03
— это среда. Получить из этой даты дату начала недели (воскресенье или понедельник в зависимости от стандарта) можно с помощью функции lubridate::floor_date()
, где в аргументе unit
можно задать порядок округления:
library(lubridate)
# создаём объект даты
my_date <- as.Date('2019-07-03')
# округляем до даты понедельника той же недели
floor_date(my_date, unit = 'week', week_start = 1)
## [1] "2019-07-01"
# округляем до даты понедельника следующей недели
ceiling_date(my_date, unit = 'week', week_start = 1)
## [1] "2019-07-08"