1 Data manipulations
data.table intro pt1
why data.table?
высокая скорость IO / манипуляций (бенчмарки)
параллелизация вычислений по умолчанию
опирается только на base R
лаконичность выражений
бережные апдейты (поддерживается R 3.1)
забота об обратной совместимости
Создание data.table-таблиц
data.table()
Создать data.table
можно следующим образом (синтаксис немного напоминает создание именованного списка):
# если нет пакета - его надо установить
# install.pakages('data.table')
# после установки подключаем пакет
library(data.table)
# создаем data.table-объект, аналогично именованным спискам
my_dt <- data.table(
e1 = 1:3,
e2 = letters[1:3],
e3 = month.abb[1:3]
)
my_dt
## e1 e2 e3
## 1: 1 a Jan
## 2: 2 b Feb
## 3: 3 c Mar
Конвертация из списков
Также data.table-таблицу можно создать из списков или из data.frame-таблиц. Для этого используется функция as.data.table()
или setDT()
. Функция setDT()
предпочтительнее, так как меняет объект на месте, не создавая его копии.
# создаем список
my_list <- list(
e1 = 9:12,
e3 = month.name[9:12]
)
# конвертируем в dt-таблицу
setDT(my_list)
# смотрим класс объекта
class(my_list)
## [1] "data.table" "data.frame"
Данные
В вебинаре я использовал данные по персонажам Звездный Войн, для импорта датасета в рабочее окружение необходимо выполнить следующее выражение:
## Classes 'data.table' and 'data.frame': 77 obs. of 6 variables:
## $ name : chr "Luke Skywalker" "C-3PO" "Darth Vader" "Owen Lars" ...
## $ height : int 172 167 202 178 165 97 183 188 163 183 ...
## $ mass : num 77 75 136 120 75 32 84 84 NA NA ...
## $ skin_color : chr "fair" "gold" "white" "light" ...
## $ gender : chr "male" "n/a" "male" "male" ...
## $ planet_name: chr "Tatooine" "Tatooine" "Tatooine" "Tatooine" ...
## - attr(*, ".internal.selfref")=<externalptr>
Основная формула dt-синтаксиса
Общая формула data.table
выглядит как dataset[выбор строк, операции над колонками, группировка]
. То есть, указание, какие строки необходимо выделить, осуществляется в первой части (до первой запятой в синтаксисе data.table
). Если нет необходимости выделять какие-то строки, перед первой запятой ничего не ставится. Параметр группировки (как и прочие параметры, кроме i
и j
- опциональны).
Также можно провести параллели с синтаксисом SQL-запроса. В терминах SQL data.table-выражения выглядят как таблица[where, select, group by]
.
Фильтрация по строкам
Выбор строк в data.table
осуществляется аналогично выбору элементов в векторе - по номеру строки или по какому-то условию. При выборе по номеру строки также можно указать вектор номеров строк, которые необходимо вернуть. При выборке строки по условию проверяется, удовлетворяет ли условию каждый элемент строки в определенной колонке, и если удовлетворяет, выделяется вся строка.
по номеру строки
При указании номер строки можно передать сразу вектор номеров строк. Таким образом можно использовать как просто ранее созданный вектор номеров, так и любую функцию, которая возвращает целочисленный вектор, например, функцию order()
.
Если не предполагается операций с колонками, запятую можно опустить - два выражения ниже идентичны.
sw[c(1, 3, 5), ]
## name height mass skin_color gender planet_name
## 1: Luke Skywalker 172 77 fair male Tatooine
## 2: Darth Vader 202 136 white male Tatooine
## 3: Beru Whitesun lars 165 75 light female Tatooine
sw[c(1, 3, 5)]
## name height mass skin_color gender planet_name
## 1: Luke Skywalker 172 77 fair male Tatooine
## 2: Darth Vader 202 136 white male Tatooine
## 3: Beru Whitesun lars 165 75 light female Tatooine
по условию
При фильтрации по условию также неоходимо указать логическое выражение или выражение, возвращающее вектор значений TRUE/FALSE. В результате из таблицы будут выделены те строки, для которых соответствует значение TRUE.
Выделяем все строки, где в колонке planet_name есть значение Coruscant:
sw[planet_name == 'Coruscant']
## name height mass skin_color gender planet_name
## 1: Finis Valorum 170 NA fair male Coruscant
## 2: Adi Gallia 184 50 dark female Coruscant
## 3: Jocasta Nu 167 NA fair female Coruscant
Выделяем все строки, где в значениях колонки skin_color встречается строка yellow
:
sw[grep('yellow', skin_color)]
## name height mass skin_color gender planet_name
## 1: Ben Quadinaros 163 65.0 grey, green, yellow male Tund
## 2: Luminara Unduli 170 56.2 yellow female Mirial
## 3: Barriss Offee 166 50.0 yellow female Mirial
## 4: Zam Wesell 168 55.0 fair, green, yellow female Zolan
Операции с колонками
Создание, модификация и удаление колонок
Создать новую колонку в синтаксисе data.table можно с помощью оператора :=
. Это точно такая же операция над колонками, как и все прочие, просто поисходит создание новой колонки.
NB! Оператор :=
изменяет таблицу sw
на месте. То есть, не требует никаких дополнительных присвоений, выражение sw[, new_col := 'new']
уже создает новую колонку.
sw[, new_col := 'new']
sw[1:3]
## name height mass skin_color gender planet_name new_col
## 1: Luke Skywalker 172 77 fair male Tatooine new
## 2: C-3PO 167 75 gold n/a Tatooine new
## 3: Darth Vader 202 136 white male Tatooine new
Можно не просто создать новую колонку, но и модифицировать уже существующую (также на месте). Например, всем значениям колонки new_col
добавляем префикс second
и переписываем колонку:
sw[, new_col := paste('second', new_col, sep = '_')]
sw[1:3]
## name height mass skin_color gender planet_name new_col
## 1: Luke Skywalker 172 77 fair male Tatooine second_new
## 2: C-3PO 167 75 gold n/a Tatooine second_new
## 3: Darth Vader 202 136 white male Tatooine second_new
Значения колонок можно модифицировать точечно, только в конкретных ячейках. Для этого совмещается фильтрация по строкам и присвоение значения:
sw[gender == 'n/a', gender := 'droid']
sw[1:3]
## name height mass skin_color gender planet_name new_col
## 1: Luke Skywalker 172 77 fair male Tatooine second_new
## 2: C-3PO 167 75 gold droid Tatooine second_new
## 3: Darth Vader 202 136 white male Tatooine second_new
Удаление колонок осуществляется схожим образом, просто колонке присваивается значение NULL
:
sw[, new_col := NULL]
sw[1:3]
## name height mass skin_color gender planet_name
## 1: Luke Skywalker 172 77 fair male Tatooine
## 2: C-3PO 167 75 gold droid Tatooine
## 3: Darth Vader 202 136 white male Tatooine
Выбор колонок
В синтаксисе data.table
все операции над колонками производятся после первой запятой. При этом отличается, как именно указана колонка. Если дано просто название колонки, то в результате эта колонка будет возвращена как вектор:
sw[1:3, gender]
## [1] "male" "droid" "male"
Если же колонка указана как элемент списка (из колонок собирается новый список), то результатом выражения будет data.table-таблица, состоящая из указанной колонки/колонок.
sw[1:3, list(name, gender)]
## name gender
## 1: Luke Skywalker male
## 2: C-3PO droid
## 3: Darth Vader male
Выражения list()
и .()
в этом контексте идентичны:
sw[1:3, .(name, gender)]
## name gender
## 1: Luke Skywalker male
## 2: C-3PO droid
## 3: Darth Vader male
NB! Оба способа не изменяют исходную таблицу, а создают сэмпл из нее - в виде вектора или другой таблицы. Если необходимо сохранить результат этого выражения в отдельную таблицу, то следует сделать это явно:
sw_sample <- sw[1:3, .(name, gender)]
sw_sample
## name gender
## 1: Luke Skywalker male
## 2: C-3PO droid
## 3: Darth Vader male
Иначе результат sw[1:3, .(name, gender)]
будет просто выведен на печать в консоль (при интерактивной работе), как в примерах выше. То есть, новый объект не создается.
Операции над колонками
Операции над колонками аналогичны выделению одной или нескольких колонок, только вместо простого названия колонки используется какая-нибудь функция и название колонки передается в аргументы этой функции.
Результат зависит, как именно происходит обращение к колонке - если напрямую к колонке, то результат также будет вектором:
sw[, median(mass, na.rm = TRUE)]
## [1] 79
Если же результат операции с колонкой дополнительно представляется в виде списка, то результатом будет новая data.table-таблица с вычисленным значением:
## V1
## 1: 79
Когда колонка или результат операции над колонкой оборачиваются в list()
, можно сразу переименовывать колонки. Строго говоря, создается таблица с новой колонкой с требуемым именем, в которую записыватся значения колонки, которую надо переимновать:
## mass_md
## 1: 79
Группировка
Простая группировка
В синтаксисе data.table
есть конструкция by
, которая отвечает за примененим операций над колонками отдельно для каждой группы (общая структура выглядит следующим образом: dataset[выбор строк, операции над колонками, группировка]
).
Общая логика группировки стандартная - split - apply - combine
. То есть, датасет разделяется на блоки по значениям группирующей переменной, к колонкам каждого сабсета применяется какое-то выражение, и результат обратно собирается в таблицу. Результатом группировки в data.table
всегда будет таблица.
Можно использовать группировку при применении функции к таблице, но удобнее результат операции с колонкой оборачивать в list()
, так как это дает возможность переименовать колонку. В примере ниже мы считаем количество уникальных значений в колонке name
для каждой группы по значениям колонки gender
:
## gender n_chars
## 1: male 57
## 2: droid 3
## 3: female 16
## 4: hermaphrodite 1
Функция uniqueN()
- data.table-аналог выражения length(unique())
, просто короче и быстрее (особенно на больших датасетах с большим количеством маленьких групп).
Можно выполнять операции сразу с несколькими колонками:
## gender n_chars mass_md
## 1: male 57 80.0
## 2: droid 3 32.0
## 3: female 16 52.5
## 4: hermaphrodite 1 NA
Группировка по нескольким полям
Часто возникает необходимость группировки сразу по нескольким полям - для этого колонки групп также указываются через список. В выражении ниже мы сначала фильтруем датасет и оставляем только строки, где в колонке gender
есть значения male
и female
, после чего в группах по полу и цвету кожи считаем количество персонажей. Результат агрегации записываем в новый объект и выводим на печать только первые 5 строк (просто чтобы сократить вывод).
sw_grps <- sw[gender %in% c('male', 'female'),
list(n_chars = uniqueN(name)),
by = list(gender, skin_color)]
sw_grps[1:5]
## gender skin_color n_chars
## 1: male fair 12
## 2: male white 2
## 3: male light 4
## 4: female light 5
## 5: female fair 3
Вычисления над группирующей колонкой
В отдельных случаях хочется каким-то образом дополнительно обработать группирующую колонку, например, сократить количество групп. Создавать для этого отдельную переменную может быть и накладно по памяти, да и в целом не очень удобно, не следует замусоривать датасеты техническими колонками.
Для этого, аналогично операциями с колонками в блоке j
, делается операция над группирующей колонкой в блоке by
. В примере ниже мы на основе skin_color
создаем новую колонку skin_color_group
, которую и используем в группировке.
sw_skin <- sw[gender %in% c('male', 'female'),
list(n_chars = uniqueN(name)),
by = list(gender,
skin_color_group = ifelse(grepl(',', skin_color), 'multi', 'mono'))]
sw_skin
## gender skin_color_group n_chars
## 1: male mono 49
## 2: female mono 14
## 3: male multi 8
## 4: female multi 2
Вычисления в группах на месте
Результат группировки не всегда должен быть отдельной таблицей. Так, можно сделать какое-то вычисление по колонке в группах, и результат записать в эту же таблицу:
Считаем количество персонажей обоего пола в группах по цвету кожи, и результат записываем в колонку total_chars
. Так как результат для каждой группы - одно значение, а строк в группе несколько, это одно значение размножается по количеству строк.
sw_skin[, total_chars := sum(n_chars), by = skin_color_group]
sw_skin
## gender skin_color_group n_chars total_chars
## 1: male mono 49 63
## 2: female mono 14 63
## 3: male multi 8 10
## 4: female multi 2 10
Можно не создавать промежуточную колонку total_chars
и сразу вычислить долю мужчин и женщин в группах по цвету кожи:
sw_skin[, share := n_chars / sum(n_chars), by = skin_color_group]
sw_skin
## gender skin_color_group n_chars total_chars share
## 1: male mono 49 63 0.7777778
## 2: female mono 14 63 0.2222222
## 3: male multi 8 10 0.8000000
## 4: female multi 2 10 0.2000000
Дополнительные материалы
Шпаргалка по data.table. Не без нюансов, но вполне осмысленная
Для tidy-практиков: соответствие конструкций data.table и tidy. Правда, я не все перечисленные data.table-конструкции рекомендую применять на практике, так как они сделаны больше для наглядности или в попытке увеличить сходство с tidy-выражениями. Ну и большая часть выражений - тема других вебинаров.
Для питоно-говорящих: словарь соответствий конструкций data.table и pandas.
data.table intro pt2
Манипуляции с таблицами
Добавление строк и таблиц
rbind()
Функция rbind()
(от row bind
) используется для объединение двух или более таблиц по строкам. То есть, в результате получается таблица с таким же количеством колонок, но с увеличенным числом строк - по количеству строк в объединяемых таблицах.
Нередко в объединяемых таблицах отсутствует какая-нибудь колонка, или колонки перепутаны. В таких случаях необходимо использовать аргументы use.names = TRUE
(проверка названий колонок при объединение) и fill = TRUE
(создание колонки с NA
-значениями). Обратите внимание, это работает только с data.table
-объектами.
library(data.table)
dt1 <- data.table(v1 = 'row1', v2 = 3, v3 = 'b')
dt1
## v1 v2 v3
## 1: row1 3 b
dt2 <- data.table(v1 = 'row2', v2 = 5, v3 = 'z')
dt2
## v1 v2 v3
## 1: row2 5 z
rbind(dt1, dt2)
## v1 v2 v3
## 1: row1 3 b
## 2: row2 5 z
rbindlist()
Функция rbindlist()
- расширение rbind()
. Отличительная особенность этой функции - может работать с списком таблиц (то есть, объектом класса list()
, где в качестве элементов спиcка - таблицы или списки).
my_list <- list(
dt1 = data.table(v1 = 'row1', v2 = 3, v3 = 'b'),
dt2 = data.table(v1 = 'row2', v2 = 5, v3 = 'z')
)
my_list
## $dt1
## v1 v2 v3
## 1: row1 3 b
##
## $dt2
## v1 v2 v3
## 1: row2 5 z
rbindlist(my_list)
## v1 v2 v3
## 1: row1 3 b
## 2: row2 5 z
Если список именованный, то аргумент idcols
может быть использован для создания отдельной колонки с названиями подсписков. Либо же просто с указанием номера элемента списка, к котором относятся строки в полученной таблице:
rbindlist(my_list, idcol = 'dt_name')
## dt_name v1 v2 v3
## 1: dt1 row1 3 b
## 2: dt2 row2 5 z
Также, как и data.table::rbind()
, функция rbindlist()
может сортировать колонки при склеивании элементов списка в одну большую таблицу:
# создаем таблицу с перепутанным порядком колонок
my_list <- list(
dt1 = data.table(v1 = 'row1', v2 = 3, v4 = 'from v4'),
dt2 = data.table(v1 = 'row2', v5 = 'from v5', v2 = 5)
)
my_list
## $dt1
## v1 v2 v4
## 1: row1 3 from v4
##
## $dt2
## v1 v5 v2
## 1: row2 from v5 5
rbindlist(my_list, fill = TRUE)
## v1 v2 v4 v5
## 1: row1 3 from v4 <NA>
## 2: row2 5 <NA> from v5
Аналогично с аргументом fill
, если в каком-то из подсписков недостает какой-то колонки (колонок), то при fill = TRUE
в финальной таблице в значения этих колонок будут проставлены NA
:
my_list <- list(
dt1 = data.table(v1 = 'row1', v2 = 3, v4 = 'from v4'),
dt2 = data.table(v1 = 'row2', v5 = 'from v5', v2 = 5)
)
my_list
## $dt1
## v1 v2 v4
## 1: row1 3 from v4
##
## $dt2
## v1 v5 v2
## 1: row2 from v5 5
rbindlist(my_list, fill = TRUE)
## v1 v2 v4 v5
## 1: row1 3 from v4 <NA>
## 2: row2 5 <NA> from v5
Наиболее часто функция rbindlist()
используется в связке lapply() + rbindlist()
, когда результатом lapply()
получается список таблиц. Например, когда необходимо прочитать множество файлов одной структуры из какой-либо директории и объединить в одну таблицу. Типовой код для такой задачи может выглядеть следующим образом:
my_files <- list.files(path = 'my_dir', pattern = '\\.csv')
result <- lapply(my_files, fread)
result <- rbindlist(result, fill = TRUE)
Присоединение таблиц
classic merge()
Одна из самых, наверное, важных операций при работе с таблицами - построчное слияние двух или нескольких таблиц. При использовании функции merge()
каждому значению в ключевой колонке первой таблицы сопоставляется строка параметров наблюдения другой таблицы, с таким же значением в ключевой колонке, как и в первой таблице. В других языках программирования, в SQL, в частности, аналогичная функция может называться join
.
Несмотря на сложность формулировки, выглядит это достаточно просто:
wave1 <- data.table(id = paste0('id_', 1:3),
col1 = c(2, 3, 5))
wave1
## id col1
## 1: id_1 2
## 2: id_2 3
## 3: id_3 5
wave2 <- data.table(id = paste0('id_', c(1, 3, 5)),
col2 = c('z', 'j', 'x'))
wave2
## id col2
## 1: id_1 z
## 2: id_3 j
## 3: id_5 x
Первая таблица задается аргументом x
, вторая таблица - аргументом y
, а колонка (или колонки), по значениям которой происходит слияние таблиц, задается аргументом by
. Если аргумент by
не указан, то слияние происходит по тем колонкам, которые имеют одинаковое название в сливаемых таблицах. Притом, таблицы можно сливать по значениям колонок разными именами, тогда надо отдельно указать, по значениям каких колонок в первой и второй таблице происходит слияние, и для этого вместо общего аргумента by
используют аргументы by.x
и by.y
для первой и второй таблицы соответственно.
merge(x = wave1, y = wave2, by = 'id')
## id col1 col2
## 1: id_1 2 z
## 2: id_3 5 j
Варианты направлений слияния (мерджа) таблиц:
-
all
= FALSE. Значение аргумента по умолчанию, в результате слияния будет таблица с наблюдениями, которые есть и в первой, и во второй таблице. То есть, наблюдения из первой таблицы, которым нет сопоставления из второй таблицы, отбрасываются. В примере с волнами это будет таблица только по тем, кто принял участи и в первой, и во второй волнах опросов:
# сливаем так, чтобы оставить только тех, кто был в обеих волнах, это зачение по умолчанию
merge(x = wave1, y = wave2, by = 'id', all = FALSE)
## id col1 col2
## 1: id_1 2 z
## 2: id_3 5 j
-
all.x
= TRUE. Всем наблюдениям из первой таблицы сопоставляются значения из второй. Если во второй таблице нет соответствующих наблюдений, то пропуски заполняютсяNA
-значениями (в нашем примере в колонкеcol2
):
# сливаем так, чтобы оставить тех, кто был в первой волне
merge(x = wave1, y = wave2, by = 'id', all.x = TRUE)
## id col1 col2
## 1: id_1 2 z
## 2: id_2 3 <NA>
## 3: id_3 5 j
-
all.y
= TRUE. Обратная ситуация, когда всем наблюдениям из второй таблицы сопоставляются значения из первой, и пропущенные значения заполняютсяNA
-значениями (в нашем примере в колонкеco12
):
# сливаем так, чтобы оставить тех, кто был во второй волне
merge(x = wave1, y = wave2, by = 'id', all.y = TRUE)
## id col1 col2
## 1: id_1 2 z
## 2: id_3 5 j
## 3: id_5 NA x
-
all
= TRUE. Объединение предыдущих двух вариантов - создается таблица по всему набору уникальных значений из ключевых таблиц, по которым происходит слияние. и если в какой-то из таблиц нет соответствующих наблюдений, то пропуски также заполняютсяNA
-значениями:
# сливаем так, чтобы оставить тех, кто был в какой-то из обеих волн
merge(x = wave1, y = wave2, by = 'id', all = TRUE)
## id col1 col2
## 1: id_1 2 z
## 2: id_2 3 <NA>
## 3: id_3 5 j
## 4: id_5 NA x
dt1[dt2] merge()
В data.table есть альтернативный вариант мерджа таблиц, с синтаксисом вида dt1[dt2]
. Этот синтаксис использует логику фильтрации по строкам, i
, только в место вектора номеров строк или логического условия указывается таблица. Аргумент on
используется для указания ключей, по которым происходит мердж, если аргумент не указан, то используются колонки, которые были назначены ключами (setkey()
).
В таком случае происходит аналог right join (all.y = TRUE
), когда к указанной в блоке i
таблице присоединяется первая таблица:
wave1[wave2, on = 'id']
## id col1 col2
## 1: id_1 2 z
## 2: id_3 5 j
## 3: id_5 NA x
Как правило, подобный синтаксис используется в том случае, когда надо провести какую-то операцию над колонкой из присоединенной таблицы. В таком случае мы указываем таблицу, а также операцию над колонкой из присоединяемой таблицы (col2 в примере):
wave1[wave2, on = 'id', col3 := paste(col1, col2, sep = '_')]
Важно, что результатом такой операции получается не новая таблица, как в предыдущем примере, а именно созданная колонка в основной таблице (col3 в wave1):
wave1
## id col1 col3
## 1: id_1 2 2_z
## 2: id_2 3 <NA>
## 3: id_3 5 5_j
Подобный мердж и операции при мердже существенно оптимизируют выполнение кода - и за счет фильтрации таблицы по значениям колонки-ключа из второй таблицы, и за счет того, что не создается отдельная промежуточная таблица мерджа (как было бы в классическом случае), и нет необходимости хранить промежуточную для вычисления колонку col2, которая участвует в создании col3 (в примере), но сама по себе не очень нужна.
Смена формы таблицы
Обычная форма представления данных в таблицах - когда одна строка является одним наблюдением, а в значениях колонок отражены те или иные характеристики этого наблюдения. Такой формат традиционно называется wide
-форматом, потому что при увеличении количества характеристик таблица будет расти вширь, путем увеличения числа колонок. Пример таблицы в wide
-формате.
dt_wide <- data.table(
wave = paste0('wave_', rep(1:2, each = 2)),
id = paste0('id_', rep(1:2)),
age = c(45, 68, 47, 69),
height = c(163, 142, 164, 140),
weight = c(55, 40, 50, 47))
dt_wide
## wave id age height weight
## 1: wave_1 id_1 45 163 55
## 2: wave_1 id_2 68 142 40
## 3: wave_2 id_1 47 164 50
## 4: wave_2 id_2 69 140 47
Тем не менее, нередко встречается другой формат, в котором на одно наблюдение может приходиться несколько строк (по количеству измеренных характеристик этого наблюдения). В таком случае таблица состоит из колонки, в котором содержится какой-то идентификатор объекта, колонки одной или нескольких), в которых содержатся идентификаторы характеристик объекта, и колонки, в которой содержатся значения этих характеристик. Такой формат называется длинным, long
-форматом данных, потому что при увеличении количества измеряемых характеристик, таблица будет расти в длину, увеличением строк.
# создаем таблицу с идентификатором респондента, его возрастом, ростом и весом
dt_long <- data.table(
# две волны, по два респондента в каждой
wave = paste0('wave_', rep(1:2, each = 6)),
# на каждого респондента задаем три строки
id = paste0('id_', rep(rep(1:2, each = 3), 2)),
# три характеристики повторяем для четырех респондентов
variable = rep(c('age', 'height', 'weight'), 4),
# задаем значения характеристик, с учетом того, как упорядочены первые две колонки
value = c(45, 163, 55,
68, 142, 40,
47, 164, 50,
69, 140, 47))
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 68
## 5: wave_1 id_2 height 142
## 6: wave_1 id_2 weight 40
## 7: wave_2 id_1 age 47
## 8: wave_2 id_1 height 164
## 9: wave_2 id_1 weight 50
## 10: wave_2 id_2 age 69
## 11: wave_2 id_2 height 140
## 12: wave_2 id_2 weight 47
melt()
Трансформация из wide
-формата в long
-формат возможна с помощью функции melt()
(пакет data.table, а не reshape2, где есть одноименные функции):
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 68
## 3: wave_2 id_1 age 47
## 4: wave_2 id_2 age 69
## 5: wave_1 id_1 height 163
## 6: wave_1 id_2 height 142
## 7: wave_2 id_1 height 164
## 8: wave_2 id_2 height 140
## 9: wave_1 id_1 weight 55
## 10: wave_1 id_2 weight 40
## 11: wave_2 id_1 weight 50
## 12: wave_2 id_2 weight 47
Здесь аргумент id.vars
задает переменные, которые будут использоваться для уникальной идентификации наблюдения. Аргумент measure.vars
определяет те колонки, которые войдут длинную таблицу как значения переменной характеристик наблюдений (когда каждая строка - отдельная характеристика наблюдения, несколько строк на одного пользователя). Аргументы variable.name
и value.name
задают, соответственно, названия колонок характеристик наблюдения и значений этих характеристик в финальной таблице.
dcast()
Для того, чтобы трансформировать long
-формат в wide
-формат, используется функция dcast()
пакета data.table
. Также можно использовать функцию reshape()
из базового набора функций R, однако эта функция достаточно медленная по скорости работы.
Выражение будет выглядеть следующим образом (сама операция называется решейп):
dcast(data = dt_long, formula = wave + id ~ variable, value.var = 'value')
## wave id age height weight
## 1: wave_1 id_1 45 163 55
## 2: wave_1 id_2 68 142 40
## 3: wave_2 id_1 47 164 50
## 4: wave_2 id_2 69 140 47
Здесь аргумент data
- определяет таблицу, которую мы хотим трансформировать.
Аргумент formula
задает, что в результирующей таблице будет задавать уникальное наблюдение, и значения какой колонки будут разделены на самостоятельные колонки. Формулу можно прочитать как строки ~ колонки
в результирующей таблице. В нашем случае уникальное наблюдение мы задаем парой переменных wave
и id
, поэтому мы их указываем до тильды через +
. Колонки же мы создаем по значениям переменной variable
, после тильды. Следует отметить, что ситуация, когда строка задется несколькими переменными через оператор +
весьма частая, а вот в правой части формулы несколько переменных встречаются достаточно редко, обычно все же на колонки раскладывают по значениям одной переменной.
Аргумент value.var
содержит текстовое название переменной, значения которой будут отражены в результирующей таблице по колонкам для каждого наблюдения.
Иногда случаются ситуации, когда необходимо провести сначала агрегацию по одной из колонок, описывающих наблюдение. Например, вычислить средние значения возраста, роста и веса для каждой волны. Это можно сделать в два этапа - сначала провести агрегацию, и потом решейп. Также можно сразу сделать решейп, и воспользоваться дополнительным аргументом fun.aggregate
, который сразу, при решейпе, агрегирует данные. Например, если использовать сначала агрегацию, а потом трансформацию в wide
-формат:
# агрегируем наблюдения по волнам и характеристикам
tmp <- dt_long[, list(value = mean(value)), by = list(wave, variable)]
tmp
## wave variable value
## 1: wave_1 age 56.5
## 2: wave_1 height 152.5
## 3: wave_1 weight 47.5
## 4: wave_2 age 58.0
## 5: wave_2 height 152.0
## 6: wave_2 weight 48.5
# трансформируем в wide-формат. колонки id уже нет в таблице, поэтому удаляем из формулы
dcast(data = tmp, formula = wave ~ variable, value.var = 'value')
## wave age height weight
## 1: wave_1 56.5 152.5 47.5
## 2: wave_2 58.0 152.0 48.5
Аналогично, но с использованием аргумента fun.aggregate
. В значения аргумента передаем название функции без кавычек и скобок, в нашем случае это fun.aggregate = mean
:
dcast(data = tmp, formula = wave ~ variable, value.var = 'value', fun.aggregate = mean)
## wave age height weight
## 1: wave_1 56.5 152.5 47.5
## 2: wave_2 58.0 152.0 48.5
Операции с несколькими колонками
:=
Самый простой способ в одно выражении изменить несколько колонок - воспользоваться инфиксным оператором :=
в виде классической функции `:=`():
# создадим тестовую таблицу
my_dt <- data.table(
e1 = 1:3,
e2 = letters[1:3],
e3 = month.abb[1:3]
)
my_dt
## e1 e2 e3
## 1: 1 a Jan
## 2: 2 b Feb
## 3: 3 c Mar
# одновременно изменим колонку e1 и добавим колонки e3 и e5
my_dt[, `:=`(e1 = e1 + 5, e4 = paste(e2, e3, sep = '_'), e5 = 17:19)]
(colnames)
Также можно создать вектор названий колонок и, обернув его в скобки, использовать в множественном приcвоении с все тем же инфиксным оператором :=
:
# создадим вектор колонок
tg_cols = c('e1', 'e4', 'e5')
# одновременно изменим колонки
my_dt[, (tg_cols) := list(e1 * 2, gsub('_', '', e4), e5 - 1)]
my_dt
## e1 e2 e3 e4 e5
## 1: 12 a Jan aJan 16
## 2: 14 b Feb bFeb 17
## 3: 16 c Mar cMar 18
В данном случае происходит запись значений подсписка из правой части в колонку, названия которых перечислены в левой части (вектор названий tg_cols
), первый подсписок - в колонку, название которой идет первой в векторе tg_cols
и так далее.
.SD
Subset of Data
Также можно выделить колонки таблицы data.table c помощью конструкций .SD
и .SDcols
(.SD от Subset of Data)
.SD
служит ярлыком-указателем на колонки с которыми надо провести какое-то действие, а .SDcols
- собственно вектор названий колонок или порядковых номеров колонок в таблице. Если .SDcols
не указано, то подразумеваются все колонки таблицы. Оборачивать в list()
конструкцию .SD
не нужно.
# проверяем, что вся таблица и .SD без указания колонок идентичны
identical(my_dt, my_dt[, .SD])
## [1] TRUE
.SDcols
.SDcols
используются для указания, какие колонки должны быть сабсете таблицы (.SD
). Способы выделения могут быть разными:
- простой вектор номеров колонок
my_dt[, .SD, .SDcols = c(1, 3)]
## e1 e3
## 1: 12 Jan
## 2: 14 Feb
## 3: 16 Mar
- вектор названий колонок
my_dt[, .SD, .SDcols = c('e2', 'e4')]
## e2 e4
## 1: a aJan
## 2: b bFeb
## 3: c cMar
- диапазон колонок, записанный в виде col_start:col_end
my_dt[, .SD, .SDcols = e2:e4]
## e2 e3 e4
## 1: a Jan aJan
## 2: b Feb bFeb
## 3: c Mar cMar
- регулярное выражение, по которому можно отобрать названия колонок
my_dt[, .SD, .SDcols = patterns('4|5')]
## e4 e5
## 1: aJan 16
## 2: bFeb 17
## 3: cMar 18
- какая-то логическая функция, которая проверяет содержимое колонки
my_dt[, .SD, .SDcols = is.character]
## e2 e3 e4
## 1: a Jan aJan
## 2: b Feb bFeb
## 3: c Mar cMar
- или просто вектор логических значений
my_dt[, .SD, .SDcols = c(TRUE, FALSE, TRUE, TRUE, FALSE)]
## e1 e3 e4
## 1: 12 Jan aJan
## 2: 14 Feb bFeb
## 3: 16 Mar cMar
sapply() / lapply() + .SD
Чаще всего .SD используется в сочетании с lapply()
- таким образом к каждой колонке будет применена определенная функция, а результатом будет все также data.table (по аналогии с my_dt[, list(col1 = mean(col1))]
, когда на основе таблицы, фактически, создается новая таблица, тоже с помощью list()
).
Иногда можно использовать sapply()
, например, чтобы вернуть вектор результата проверки, является ли колонка численной:
my_dt[, sapply(.SD, is.numeric)]
## e1 e2 e3 e4 e5
## TRUE FALSE FALSE FALSE TRUE
# выделим в отдельный вектор названия численных колонок
tg_cols <- names(my_dt)[my_dt[, sapply(.SD, is.numeric)]]
tg_cols
## [1] "e1" "e5"
lapply() + .SD + .SDcols
Операции над всеми колонками делаются достаточно редко, чаще надо обработать несколько колонок всего датасета. Для этой задачи используется сочетание lapply() + .SD + .SDcols
- lapply()
применяется к тем колонкам, которые указаны в .SDcols
.
Выше мы уже выделили вектор численных колонок, вычислим корень из каждого значения колонок:
my_dt[, lapply(.SD, sqrt), .SDcols = tg_cols]
## e1 e5
## 1: 3.464102 4.000000
## 2: 3.741657 4.123106
## 3: 4.000000 4.242641
Выражение выше создает новую таблицу, в которой будет только результат операций над целевыми колонками. нередко бывает необходимо не выделить новую таблицу, а переписать уже существующие. В этой задаче помогает множественное присваивание с использованием вектора названий.
Вектор названий изменяемых колонок у нас уже есть и используется в .SDcols
, так что можно использовать его еще раз:
my_dt[, (tg_cols) := lapply(.SD, sqrt), .SDcols = tg_cols]
my_dt
## e1 e2 e3 e4 e5
## 1: 3.464102 a Jan aJan 4.000000
## 2: 3.741657 b Feb bFeb 4.123106
## 3: 4.000000 c Mar cMar 4.242641
data.table intro pt3
Полезные функции
Готовим данные для примеров:
library(data.table)
my_dt <- copy(dplyr::starwars)
setDT(my_dt)
Создаем вектор названий колонок, который будем использовать, чтобы не все колонки таблицы выводить на печать:
tg_cols <- c('name', 'mass', 'gender', 'skin_color', 'homeworld')
.N
В data.table реализовано несколько функций вида .FUNNAME
, в том числе .SD
и .SDcols
(см.материалы второго вебинара по data.table).
.N
возвращает количество строк в семпле данных. Если применяется без группировки - то количество строк в датасете.
my_dt[, .N]
## [1] 87
Если .N
применяется при группировке, то в результате будет количество строк в каждой группе.
my_dt[, .N, by = gender]
## gender N
## 1: masculine 66
## 2: feminine 17
## 3: <NA> 4
Точно также, если использовать .N
в блоке i
(фильтрация по строкам), то в результате будет последняя строка таблицы:
my_dt[.N, .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Padmé Amidala 45 feminine light Naboo
uniqueN
Для работы с уникальными значениями и поиска дубликатов используются функции unuque()
(метод для data.table, одноименный с base), uniqueN()
(аналог length() + unique()
, но быстрее), а также duplicated()
и anyDuplicated
.
my_dt[, uniqueN(name)]
## [1] 87
fifelse
fifelse()
- fast ifelse, быстрый аналог функции ifelse()
из base R, используется аналогичным образом. Например, можно создать новую колонку, в которой в зависимости от значений в колонке homeworld
пишем Tatooine
или other
:
my_dt[, new_col := fifelse(homeworld == 'Tatooine', 'Tatooine', 'other')]
Смотрим количество строк по каждому значению созданной таблицы:
my_dt[, .N, keyby = new_col]
## new_col N
## 1: <NA> 10
## 2: Tatooine 10
## 3: other 67
fifelse
оставляет NA
значения как отдельные самостоятельные значения, однако с ними тоже можно работать. В частности, можно с помощью аргумента na
задать значение:
my_dt[, new_col := fifelse(homeworld == 'Tatooine', 'Tatooine', 'other', na = 'unknown')]
Смотрим результат:
my_dt[, .N, by = new_col]
## new_col N
## 1: Tatooine 10
## 2: other 67
## 3: unknown 10
fcase
Аналог case()
из базового R, с одним важным изменением - она векторизована и может применяться к колонкам. Аргументы функции составляют пары логическое условие - значение
. Если хочется еще добавить значение "все остальные", то используется аргумент default
.
Создаем новую колонку, в которой оставляем значения 'Tatooine'
и 'Naboo'
, для пропусков ставим 'unknown'
. Для всего остального, что не подходит под эти фильтры - ставим 'other'
:
my_dt[, new_col2 := fcase(
homeworld == 'Tatooine', 'Tatooine',
homeworld == 'Naboo', 'Naboo',
is.na(homeworld), 'unknown',
default = 'other'
)]
Результат:
my_dt[, .N, by = new_col2]
## new_col2 N
## 1: Tatooine 10
## 2: Naboo 11
## 3: other 56
## 4: unknown 10
tstrsplit
Аналог функции strsplit()
из базового R. Разделяют строку по указанному символу и возвращает в виде таблицы.
# датасет для примера
my_dt_str <- data.table(
e1 = 1:3,
e2 = paste(letters[1:3], month.abb[1:3], sep = '_')
)
my_dt_str
## e1 e2
## 1: 1 a_Jan
## 2: 2 b_Feb
## 3: 3 c_Mar
Просто делим значения колонки e2
по символу _
.
my_dt_str[, tstrsplit(e2, '_')]
## V1 V2
## 1: a Jan
## 2: b Feb
## 3: c Mar
При желании можно указать, какую колонку результата надо сохранить:
my_dt_str[, tstrsplit(e2, '_', keep = 2)]
## V1
## 1: Jan
## 2: Feb
## 3: Mar
Также можно указать названия колонок:
## letter month_abb
## 1: a Jan
## 2: b Feb
## 3: c Mar
Либо создать в этой же таблице колонки с результатами сплита. Впрочем, это возможно только если мы знаем количество колонок (сколько раз встречается знак, по которому идет сплит, в строке).
## e1 e2 letter month_abb
## 1: 1 a_Jan a Jan
## 2: 2 b_Feb b Feb
## 3: 3 c_Mar c Mar
like
SQL-подобный алиас к поиску и фильтрации / сэмплингу по текстовым вхождениям (аналог base::grep()
).
my_dt[skin_color %like% 'green', .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Greedo 74 masculine green Rodia
## 2: Jabba Desilijic Tiure 1358 masculine green-tan, brown Nal Hutta
## 3: Yoda 17 masculine green <NA>
## 4: Bossk 113 masculine green Trandosha
## 5: Nute Gunray 90 masculine mottled green Cato Neimoidia
## 6: Rugor Nass NA masculine green Naboo
## 7: Ben Quadinaros 65 masculine grey, green, yellow Tund
## 8: Kit Fisto 87 masculine green Glee Anselm
## 9: Poggle the Lesser 80 masculine green Geonosis
## 10: Zam Wesell 55 feminine fair, green, yellow Zolan
## 11: Wat Tambor 48 masculine green, grey Skako
my_dt[skin_color %like% '^green', .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Greedo 74 masculine green Rodia
## 2: Jabba Desilijic Tiure 1358 masculine green-tan, brown Nal Hutta
## 3: Yoda 17 masculine green <NA>
## 4: Bossk 113 masculine green Trandosha
## 5: Rugor Nass NA masculine green Naboo
## 6: Kit Fisto 87 masculine green Glee Anselm
## 7: Poggle the Lesser 80 masculine green Geonosis
## 8: Wat Tambor 48 masculine green, grey Skako
Поддерживает регулярные выражения и как инфиксную форму, так и классическую форму функции:
my_dt[like(vector = skin_color, '^green'), .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Greedo 74 masculine green Rodia
## 2: Jabba Desilijic Tiure 1358 masculine green-tan, brown Nal Hutta
## 3: Yoda 17 masculine green <NA>
## 4: Bossk 113 masculine green Trandosha
## 5: Rugor Nass NA masculine green Naboo
## 6: Kit Fisto 87 masculine green Glee Anselm
## 7: Poggle the Lesser 80 masculine green Geonosis
## 8: Wat Tambor 48 masculine green, grey Skako
between
Знакомая пользователям SQL реализации оператора between. Используется в семплинге и фильтрациях, когда надо выбрать строки из диапазона значений:
## name mass gender skin_color homeworld
## 1: R2-D2 32 masculine white, blue Naboo
## 2: R5-D4 32 masculine white, red Tatooine
## 3: Yoda 17 masculine green <NA>
## 4: Wicket Systri Warrick 20 masculine brown Endor
## 5: Sebulba 40 masculine grey, red Malastare
## 6: Ratts Tyerell 15 masculine grey, blue Aleen Minor
При использовании в классической форме позволяет указать, включать или нет границы интервала в поиск (по умолчанию они включаются):
my_dt[between(mass, lower = 15, upper = 40, incbounds = FALSE), .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: R2-D2 32 masculine white, blue Naboo
## 2: R5-D4 32 masculine white, red Tatooine
## 3: Yoda 17 masculine green <NA>
## 4: Wicket Systri Warrick 20 masculine brown Endor
nafill
Функция nafill()
по поведению аналогична zoo::na.locf()
и используется в тех случаях, когда необходимо заполнить пропущенные значения в колонке последним непропущенным (или же в обратную сторону, следующим непропущенным)
# создаем датасет с пропусками
my_dt_na <- data.table(
start = seq(as.Date('2021-01-01'), as.Date('2021-01-30'), 5)
)
my_dt_na[, var1 := sample(.N)]
my_dt_na[c(2, 3, 5), var1 := NA]
my_dt_na
## start var1
## 1: 2021-01-01 3
## 2: 2021-01-06 NA
## 3: 2021-01-11 NA
## 4: 2021-01-16 4
## 5: 2021-01-21 NA
## 6: 2021-01-26 1
Заполняем пропуски последним непропущенным значением (type = 'locf'
):
my_dt_na[, filled := nafill(var1, type = 'locf')]
my_dt_na
## start var1 filled
## 1: 2021-01-01 3 3
## 2: 2021-01-06 NA 3
## 3: 2021-01-11 NA 3
## 4: 2021-01-16 4 4
## 5: 2021-01-21 NA 4
## 6: 2021-01-26 1 1
fcoalesce
fcoalesce()
по поведению аналогична функциям coalesce
в SQL и в dplyr. Используется как короткий аналог ifelse, когда надо заполнить пропущенные значения.
Заполняем все пропущенные значения в колонке var1
:
my_dt_na[, var1 := fcoalesce(var1, 99L)]
my_dt_na
## start var1 filled
## 1: 2021-01-01 3 3
## 2: 2021-01-06 99 3
## 3: 2021-01-11 99 3
## 4: 2021-01-16 4 4
## 5: 2021-01-21 99 4
## 6: 2021-01-26 1 1
shift
Функция shift()
позволяет сделать некоторое подобие оконных функций в SQL, аналогична функции dplyr::lag()
. По действию функция "сдвигает" значения колонки (а так же вектора или таблицы) на какое-то количество позиций назад или вперед. Недостающие значения заполняет NA
, чтобы сохранялась длина вектора.
Простой пример на векторах:
# сдвигаем вперед на 1 позицию
shift(1:5, n = 1)
## [1] NA 1 2 3 4
# сдвигаем назад на 1 позицию (можно использовать type = 'lead')
shift(1:5, n = -1)
## [1] 2 3 4 5 NA
Часто функция бывает полезна, когда надо вычислить какую-то разницу между значением на одной строке и значением на предыдущей строке другой колонки. Например, когда хочется вычислить интервал между сессиями пользователей:
# из даты старта создаем случайным образом дату конца сессии
my_dt_na[, end := start + sample(3, .N, replace = TRUE)]
my_dt_na
## start var1 filled end
## 1: 2021-01-01 3 3 2021-01-03
## 2: 2021-01-06 99 3 2021-01-08
## 3: 2021-01-11 99 3 2021-01-12
## 4: 2021-01-16 4 4 2021-01-17
## 5: 2021-01-21 99 4 2021-01-23
## 6: 2021-01-26 1 1 2021-01-29
Считаем интервал между каждым значением старта и соответствующим ему значением конца сеcсии на предыдущей строке. Таким образом мы получаем для каждой сессии количество дней, сколько прошло с окончания предыдущей сессии до начала этой сессии:
my_dt_na[, duration := start - shift(end, 1)]
my_dt_na
## start var1 filled end duration
## 1: 2021-01-01 3 3 2021-01-03 NA days
## 2: 2021-01-06 99 3 2021-01-08 3 days
## 3: 2021-01-11 99 3 2021-01-12 3 days
## 4: 2021-01-16 4 4 2021-01-17 4 days
## 5: 2021-01-21 99 4 2021-01-23 4 days
## 6: 2021-01-26 1 1 2021-01-29 3 days
rowid
Незатейливая функция, которая создает колонку с идентификаторами строк по группам. В качестве аргумента принимает колонку, по которой идет группировка. Не зависит от порядка строк разных групп, так как при выполнении происходит неявная сортировка колонки.
# создаем датасет
my_months <- data.table(
m_name = month.name,
m_abb = month.abb,
is_winter = month.name %like% c('Dec|Jan|Feb')
)
# создаем номера месяцев сезона (в зависимости от того, зима или нет)
my_months[, row_id := rowid(is_winter)]
my_months
## m_name m_abb is_winter row_id
## 1: January Jan TRUE 1
## 2: February Feb TRUE 2
## 3: March Mar FALSE 1
## 4: April Apr FALSE 2
## 5: May May FALSE 3
## 6: June Jun FALSE 4
## 7: July Jul FALSE 5
## 8: August Aug FALSE 6
## 9: September Sep FALSE 7
## 10: October Oct FALSE 8
## 11: November Nov FALSE 9
## 12: December Dec TRUE 3
rleid
Похожая на rowid()
функция, реализация base::rle()
для колонок data.table. При применении к колонке определяет, сколько в ней смен значений относительно предыдущей строки. Например, Jan и Feb - оба зимние месяцы, им выставлено значение 1. Март - уже не зима, то есть, новое значение is_winter, поэтому проставляется значение 2. До декабря все значения is_winter == FALSE, поэтому сохраняется значение 2. А в декабре меняется значение на TRUE, и проставляется новое значение, 3.
В принципе, можно назвать это как нумерация непрерывных последовательностей одинаковых значений:
my_months[, row_rle := rleid(is_winter)]
my_months
## m_name m_abb is_winter row_id row_rle
## 1: January Jan TRUE 1 1
## 2: February Feb TRUE 2 1
## 3: March Mar FALSE 1 2
## 4: April Apr FALSE 2 2
## 5: May May FALSE 3 2
## 6: June Jun FALSE 4 2
## 7: July Jul FALSE 5 2
## 8: August Aug FALSE 6 2
## 9: September Sep FALSE 7 2
## 10: October Oct FALSE 8 2
## 11: November Nov FALSE 9 2
## 12: December Dec TRUE 3 3
Смотрим группировку:
my_months[, .N, by = rleid(is_winter)]
## rleid N
## 1: 1 2
## 2: 2 9
## 3: 3 1
week, month, year
Набор функций для работы с датами и таймштампами. В какой-то мере полезно для того, чтобы не импортировать пакет lubridate
. Как обычно с датами, хорошо бы помнить, что могут быть разные стандарты (week()
vs isoweek()
, например):
# определяем номер недели
my_dt_na[, week(start)]
## [1] 1 1 2 3 4 4
my_dt_na[, isoweek(start)]
## [1] 53 1 2 2 3 4
# определяем номер месяца
my_dt_na[, month(start)]
## [1] 1 1 1 1 1 1
# определяем год
my_dt_na[, year(start)]
## [1] 2021 2021 2021 2021 2021 2021
Семплы колонок
colname[1]
Объекты data.table - такие же списки, как и data.frame. То есть, это коллекции векторов одной длины. Как следствие, эти векторы можно какимлибо образом фильтровать или семплировать. В data.table это реализовано как дополнительный семплинг при операции над колонкой / колонками.
В примере ниже мы берем первую строку всех указанных выделенных колонок.
my_dt[, .SD[1], .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Luke Skywalker 77 masculine fair Tatooine
Здесь мы берем уже указанный набор строк:
my_dt[, .SD[c(1, 3, 5)], .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: Luke Skywalker 77 masculine fair Tatooine
## 2: R2-D2 32 masculine white, blue Naboo
## 3: Leia Organa 49 feminine light Alderaan
при группировке
Выражение вида my_dt[, .SD[1], .SDcols = tg_cols]
тождественно my_dt[1, .SD, .SDcols = tg_cols]
. Однако если добавить группировку, то тождество нарушается. В примере ниже мы берем первую строку в каждом наборе строк по значению группирующей колонки (каждую первую строку группы). Подобное поведение уже нельзя реализовать при указании номеров строк в блоке i
:
my_dt[, .SD[1], .SDcols = tg_cols, by = sex]
## sex name mass gender skin_color
## 1: male Luke Skywalker 77 masculine fair
## 2: none C-3PO 75 masculine gold
## 3: female Leia Organa 49 feminine light
## 4: hermaphroditic Jabba Desilijic Tiure 1358 masculine green-tan, brown
## 5: <NA> Ric Olié NA <NA> fair
## homeworld
## 1: Tatooine
## 2: Tatooine
## 3: Alderaan
## 4: Nal Hutta
## 5: Naboo
Еще сильнее гибкость фильтрации колонок проявляется в ситуациях, когда надо посчитать какие-то статистики по части строк колонки, притом в нескольких разных вариантах фильтрации. Если не использовать механизм семплирования колонки, то пришлось бы делать две разных таблицы с фильтрацией по строкам для каждой задачи, и потом их джойнить.
В примере ниже мы считаем средний вес мужских персонажей и средний вес мужских персонажей с Татуина, и делаем это все в одной таблице и с одной и той же колонкой mass
, просто по разным условиям делаем подвыборки.
my_dt[, list(
male_mass = mean(mass[sex == 'male'], na.rm = TRUE),
male_mass_from_tt = mean(mass[sex == 'male' & homeworld == 'Tatooine'], na.rm = TRUE)
)]
## male_mass male_mass_from_tt
## 1: 81.00455 100.2
set
Функции семейства set* - похожи на обычные функции в R с одним важным отличием - они изменяют объект по ссылке (by reference). Это значит, что при изменении объекта не происходит его копирование в оперативной памяти, и операция происходит "на месте". Такое поведение может быть значимо для работы большими данными, например, когда таблицы большие и их удвоение просто не поместится в памяти.
setnames
Простая функция для переименования колонок таблицы. Можно переименовывать как всю таблицу, так и только некоторые колонки. Если переименовываются все колонки, то можно опустить аргумент old
.
setnames(my_months, old = 'm_name', new = 'month_name')
my_months
## month_name m_abb is_winter row_id row_rle
## 1: January Jan TRUE 1 1
## 2: February Feb TRUE 2 1
## 3: March Mar FALSE 1 2
## 4: April Apr FALSE 2 2
## 5: May May FALSE 3 2
## 6: June Jun FALSE 4 2
## 7: July Jul FALSE 5 2
## 8: August Aug FALSE 6 2
## 9: September Sep FALSE 7 2
## 10: October Oct FALSE 8 2
## 11: November Nov FALSE 9 2
## 12: December Dec TRUE 3 3
setcolorder
Похожа на setnames()
, только меняет порядок колонок.
setcolorder(my_months, c('month_name', 'm_abb', 'row_rle', 'row_id', 'is_winter'))
my_months
## month_name m_abb row_rle row_id is_winter
## 1: January Jan 1 1 TRUE
## 2: February Feb 1 2 TRUE
## 3: March Mar 2 1 FALSE
## 4: April Apr 2 2 FALSE
## 5: May May 2 3 FALSE
## 6: June Jun 2 4 FALSE
## 7: July Jul 2 5 FALSE
## 8: August Aug 2 6 FALSE
## 9: September Sep 2 7 FALSE
## 10: October Oct 2 8 FALSE
## 11: November Nov 2 9 FALSE
## 12: December Dec 3 3 TRUE
set в циклах
Функция set()
аналогична функции :=
(то есть, присвоение по ссылке), однако ее можно использовать в цикле. Общая формула цикла такая:
set(x, i = NULL, j, value)
Тут x
- объект, i
- фильтрация по строкам, j
- операция над колонками, value
- значение, которое надо присвоить в операции.
Например, у нас от функций rowid()
/ rleid()
остались соответствующие колонки. Заменим в них значение 2
на 99
:
# делаем цикл
for (iter in c('row_id', 'row_rle'))
set(
x = my_months,
# к колонке data.table мы можем обратиться как в именованном списке, по имени
i = which(my_months[[iter]] == 2),
j = iter,
value = 99
)
# смотрим результат
my_months
## month_name m_abb row_rle row_id is_winter
## 1: January Jan 1 1 TRUE
## 2: February Feb 1 99 TRUE
## 3: March Mar 99 1 FALSE
## 4: April Apr 99 99 FALSE
## 5: May May 99 3 FALSE
## 6: June Jun 99 4 FALSE
## 7: July Jul 99 5 FALSE
## 8: August Aug 99 6 FALSE
## 9: September Sep 99 7 FALSE
## 10: October Oct 99 8 FALSE
## 11: November Nov 99 9 FALSE
## 12: December Dec 3 3 TRUE
keys
Ключи - колонки, по которым таблица отсортирована и по которым идет первичное деление таблицы при операциях. Аналогичны индексам в SQL.
Когда ключи установлены, то:
поиск быстрее (семплинг), так как данные отсортированы
агрегации по ключам быстрее (группы находятся рядом)
немножко упрощает синтаксис семплинга (можно не указывать название колонки)
Установить ключи можно с помощью setkey()
, при этом таблица будет отсортирована по колонкам-ключам:
setkey(my_dt, name)
Проверяем наличие ключей:
haskey(my_dt)
## [1] TRUE
Вызываем список ключей:
key(my_dt)
## [1] "name"
Пример использования ключей для фильтрации. Аналогчиным образом можно использовать ключи при мердже в data.table-стиле (dt1[dt2]
). В частности, можно не указывать колонки мерджа (аргумент on
), так как в первую очередь мердж происходит по ключевым колонкам, если не указано явно иное.
Так как колонка name
у нас указана как ключ, можем при фильтрации по строкам просто указать какое-то значение, которое будет искаться в первую очередь в этой колонке. Так, мы просто указываем имя робота 'R2-D2'
и получаем по нему соответствующие значения:
my_dt['R2-D2', .SD, .SDcols = tg_cols]
## name mass gender skin_color homeworld
## 1: R2-D2 32 masculine white, blue Naboo