1 Data manipulations

1.1 data.table

1.1.1 Операции со строками и колонками, группировка

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"

Данные

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

# импортируем по ссылке
sw <- fread('http://bit.ly/39aOUne')

# смотрим структуру объекта
str(sw)
## 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-таблица с вычисленным значением:

sw[, list(median(mass, na.rm = TRUE))]
##    V1
## 1: 79

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

sw[, list(mass_md = median(mass, na.rm = TRUE))]
##    mass_md
## 1:      79

Группировка

Простая группировка

В синтаксисе data.table есть конструкция by, которая отвечает за примененим операций над колонками отдельно для каждой группы (общая структура выглядит следующим образом: dataset[выбор строк, операции над колонками, группировка]).

Общая логика группировки стандартная - split - apply - combine. То есть, датасет разделяется на блоки по значениям группирующей переменной, к колонкам каждого сабсета применяется какое-то выражение, и результат обратно собирается в таблицу. Результатом группировки в data.table всегда будет таблица.

Можно использовать группировку при применении функции к таблице, но удобнее результат операции с колонкой оборачивать в list(), так как это дает возможность переименовать колонку. В примере ниже мы считаем количество уникальных значений в колонке name для каждой группы по значениям колонки gender:

sw[, list(n_chars = uniqueN(name)), by = gender]
##           gender n_chars
## 1:          male      57
## 2:         droid       3
## 3:        female      16
## 4: hermaphrodite       1

Функция uniqueN() - data.table-аналог выражения length(unique()), просто короче и быстрее (особенно на больших датасетах с большим количеством маленьких групп).

Можно выполнять операции сразу с несколькими колонками:

sw[, list(
  n_chars = uniqueN(name),
  mass_md = median(mass, na.rm = TRUE)
), by = gender]
##           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.

1.1.2 Слияние и решейпинг, .SD

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

1.1.3 Полезные функции, семплы колонок

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

Также можно указать названия колонок:

my_dt_str[, tstrsplit(e2, '_', names = c('letter', 'month_abb'))]
##    letter month_abb
## 1:      a       Jan
## 2:      b       Feb
## 3:      c       Mar

Либо создать в этой же таблице колонки с результатами сплита. Впрочем, это возможно только если мы знаем количество колонок (сколько раз встречается знак, по которому идет сплит, в строке).

my_dt_str[, (c('letter', 'month_abb')) := tstrsplit(e2, '_')]
my_dt_str
##    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. Используется в семплинге и фильтрациях, когда надо выбрать строки из диапазона значений:

my_dt[mass %between% c(15, 40), .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
## 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

функции set*

Неполный список функций семейства set*:

  • setDT: конвертация из data.frame или list в data.table
  • setDF: обратная конвертация в data.fra,e
  • setattr: добавить атрибут
  • setorder: отсортировать
  • setindex: установить индексы
  • setkey: установить ключи

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