1 Введение в data.table

Эта виньетка представляет собой введение в синтаксис data.table, его общий вид, способы создания поднаборов строк (subset), выбора и вычисления колонок (select, compute), агрегирования по группам (by group). Знакомство с базовой структурой данных data.frame является полезным, но не обязательным.

1.1 Анализ данных с data.table

Все операции манипулирования данными, такие как subset, group, update, join и др., по сути взаимосвязаны. С учетом этого, вместе они обеспечивают:

  • краткий и согласованный синтаксис независимо от набора операций, которые вы хотели бы выполнить для достижения вашей конечной цели.

  • плавное выполнение анализа без когнитивной нагрузки, связанной с помещением каждой операции в отдельную функцию из набора доступных функций для выполнения анализа.

  • автоматическая и эффективная внутренняя оптимизация за счет точного знания, какие данные нужны для каждой операции, что делает вычисления очень быстрыми и экономно расходующими память.

Вкратце, если вы заинтересованы в радикальном сокращении программного кода и времени вычислений, то этот пакет для вас. Философия, которой следует data.table, делает это возможным. Наша цель состоит в иллюстрации этого в серии виньеток.

1.2 Данные

В этой виньетке мы используем набор данных NYC-flights14. Он содержит данные о времени полетов от Bureau of Transporation Statistics для всех рейсов из аэропортов Нью-Йорка в 2014 г. (по аналогии с nycflights13). Данные доступны только с января по октябрь 2014 г.

Мы можем использовать быструю функцию для чтения fread из data.table для непосредственной загрузки набора данных flights:

flights <- fread("https://raw.githubusercontent.com/wiki/arunsrinivasan/flights/NYCflights14/flights14.csv")
flights
#         year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
#      1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1
#      2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3
#      3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21
#      4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29
#      5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117
#     ---                                                                                      
# 253312: 2014    10  31     1459         1     1747       -30         0      UA  N23708   1744
# 253313: 2014    10  31      854        -5     1147       -14         0      UA  N33132   1758
# 253314: 2014    10  31     1102        -8     1311        16         0      MQ  N827MQ   3591
# 253315: 2014    10  31     1106        -4     1325        15         0      MQ  N511MQ   3592
# 253316: 2014    10  31      824        -5     1045         1         0      MQ  N813MQ   3599
#         origin dest air_time distance hour min
#      1:    JFK  LAX      359     2475    9  14
#      2:    JFK  LAX      363     2475   11  57
#      3:    JFK  LAX      351     2475   19   2
#      4:    LGA  PBI      157     1035    7  22
#      5:    JFK  LAX      350     2475   13  47
#     ---                                       
# 253312:    LGA  IAH      201     1416   14  59
# 253313:    EWR  IAH      189     1400    8  54
# 253314:    LGA  RDU       83      431   11   2
# 253315:    LGA  DTW       75      502   11   6
# 253316:    LGA  SDF      110      659    8  24
dim(flights)
# [1] 253316     17

Так как мы будем использовать этот набор данных во всех виньетках, может быть, лучше скачать файл один раз и затем загружать его с диска.

1.3 Введение

В этой виньетке мы:

  1. начнем с основ - что такое пакет data.table, его общий вид, способы выделения поднаборов строк, выбор и вычисление колонок

  2. затем перейдем к выполнению агрегирования данных по группам.

1.4 1. Основы

1.4.1 a) Что такое data.table?

data.table является пакетом, обеспечивающим улучшенную версию таблиц данных data.frames. В разделе “Данные” мы уже создали таблицу data.table при помощи fread(). Мы также можем создать таблицу, используя функцию data.table(). Пример:

DT = data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c=13:18)
DT
#    ID a  b  c
# 1:  b 1  7 13
# 2:  b 2  8 14
# 3:  b 3  9 15
# 4:  a 4 10 16
# 5:  a 5 11 17
# 6:  c 6 12 18
class(DT$ID)
# [1] "character"

Вы также можете конвертировать существующие объекты в data.table, используя as.data.table().

1.4.1.1 Обратите внимание, что:

  • В отличие от data.frames, столбцы символьного типа по умолчанию никогда не конвертируются в факторы (factor).

  • Номера строк выводятся вместе с : для их визуального отделения от первого столбца.

  • Когда количество строк для вывода превышает значение глобального параметра datatable.print.nrows (по умолчанию оно равно 100), автоматически выводятся только первые 5 и последние 5 строк (как можно видеть в разделе “Данные”).

getOption("datatable.print.nrows")
# [1] 100
  • data.table никогда не устанавливает имена строк (row names). Почему - мы увидим в виньетке “Keys and fast binary search based subset”.

1.4.2 b) Общий вид - каким образом реализованы улучшения data.table?

По сравнению с data.frame, мы можете сделать гораздо больше, чем выбор строк и столбцов в таблице при помощи [ ... ]. Для понимания этого посмотрим на общий вид синтаксиса data.table, как показано ниже:

DT[i, j, by]

##   R:      i                 j        by
## SQL:  where   select | update  group by

Пользователи с опытом использования SQL могут сразу понять этот синтаксис.

1.4.2.1 Как прочитать это (вслух):

Взять DT, выбрать строки при помощи i, затем вычислить j, сгруппировав по by.

Давайте начнем с рассмотрения i и j - выбора строк и операций над столбцами.

1.4.3 c) Выбор строк в i

1.4.3.1 - Выбрать все рейсы с начальной точкой (аэропортом) “JFK” за июнь.

ans <- flights[origin == "JFK" & month == 6L]
head(ans)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     6   1      851        -9     1205        -5         0      AA  N787AA      1    JFK
# 2: 2014     6   1     1220       -10     1522       -13         0      AA  N795AA      3    JFK
# 3: 2014     6   1      718        18     1014        -1         0      AA  N784AA      9    JFK
# 4: 2014     6   1     1024        -6     1314       -16         0      AA  N791AA     19    JFK
# 5: 2014     6   1     1841        -4     2125       -45         0      AA  N790AA     21    JFK
# 6: 2014     6   1     1454        -6     1757       -23         0      AA  N785AA    117    JFK
#    dest air_time distance hour min
# 1:  LAX      324     2475    8  51
# 2:  LAX      329     2475   12  20
# 3:  LAX      326     2475    7  18
# 4:  LAX      320     2475   10  24
# 5:  LAX      326     2475   18  41
# 6:  LAX      329     2475   14  54
  • В таблице data.table к столбцам можно обращаться, как если бы они были переменными. Таким образом, мы просто обратились к dest (origin в примере выше - прим. пер.) и month как к переменным. Нам не нужно каждый раз добавлять префикс flights$. Тем не менее, использование flights$dest и flights$month будет нормально работать.

  • Были рассчитаны индексы строк, удовлетворяющих условию origin == "JFK" & month == 6L, и, поскольку больше ничего делать не нужно, data.table просто возвратил все столбцы flights в соответствии с этими индексами строк.

  • Запятая после условия также не требуется в составе i. Но flights[dest == "JFK" & month == 6L, ] также будет нормально работать. В data.frames, тем не менее, запятая обязательна.

1.4.3.2 - Выбрать первые две строки из таблицы flights.

ans <- flights[1:2]
ans
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
  • В этом случае условия нет. Индексы строк уже предоставлены в составе i. Поэтому мы возвращаем data.table со всеми столбцами из flights для этих индексов строк.

1.4.3.3 - Упорядочить flights сначала по столбцу origin в порядке возрастания, а затем по dest в порядке убывания.

Мы можем использовать для этого базовую функцию R order().

ans <- flights[order(origin, -dest)]
head(ans)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   5      836         6     1151        49         0      EV  N12175   4419    EWR
# 2: 2014     1   6      833         7     1111        13         0      EV  N24128   4419    EWR
# 3: 2014     1   7      811        -6     1035       -13         0      EV  N12142   4419    EWR
# 4: 2014     1   8      810        -7     1036       -12         0      EV  N11193   4419    EWR
# 5: 2014     1   9      833        16     1055         7         0      EV  N14198   4419    EWR
# 6: 2014     1  13      923        66     1154        66         0      EV  N12157   4419    EWR
#    dest air_time distance hour min
# 1:  XNA      195     1131    8  36
# 2:  XNA      190     1131    8  33
# 3:  XNA      179     1131    8  11
# 4:  XNA      184     1131    8  10
# 5:  XNA      181     1131    8  33
# 6:  XNA      188     1131    9  23

1.4.3.4 Функция order() является внутренне оптимизированной

  • Мы можем использовать “-” перед именем столбца в таблице data.table для сортировки в порядке убывания.
  • Кроме того, order(...) в таблице data.table использует внутренний способ упорядочивания forder(), который гораздо быстрее, чем base::order. Вот небольшой пример, чтобы проиллюстрировать разницу.
odt = data.table(col=sample(1e7))
(t1 <- system.time(ans1 <- odt[base::order(col)]))  ## uses order from base R
#    user  system elapsed 
#   8.610   0.056   8.708
(t2 <- system.time(ans2 <- odt[order(col)]))        ## uses data.table's forder
#    user  system elapsed 
#   0.526   0.024   0.553
(identical(ans1, ans2))
# [1] TRUE

Ускорение составило ~16x. Мы обсудим быстрое упорядочивание в виньетке “data.table internals”.

  • Таким образом, вы можете улучшить производительность, используя хорошо знакомые функции.

1.4.4 d) Выбор столбцов в j

1.4.4.1 - Выбрать столбец arr_delay, но вернуть как вектор.

ans <- flights[, arr_delay]
head(ans)
# [1]  13  13   9 -26   1   0
  • Поскольку можно обращаться к столбцам, как если бы они были переменными в таблице, мы напрямую обратились к переменным, которые хотим выбрать. Поскольку нам нужны все строки, мы просто пропустили i.

  • Были возвращены все строки для столбца arr_delay.

1.4.4.2 - Выбрать столбец arr_delay, но вернуть как data.table.

ans <- flights[, list(arr_delay)]
head(ans)
#    arr_delay
# 1:        13
# 2:        13
# 3:         9
# 4:       -26
# 5:         1
# 6:         0
  • Мы обернули переменные (имена столбцов) вызовом list(), что гарантирует возврат объекта data.table. В случае отдельного имени столбца, не обернутого в list(), вместо этого возвращается вектор, как было показано в предыдущем примере.

  • data.table также позволяет использовать .(). Это псевдоним (alias) для list(). Используйте тот или иной вариант в зависимости от своих предпочтений.

Далее мы продолжим использовать .().

data.tablesdata.frames) внутри являются списками со столбцами равной длины и с атрибутом класса. Разрешение j возвращать список позволяет конвертировать и возвращать data.table очень эффективно.

1.4.4.3 Совет

Поскольку j-выражение возвращает список, каждый элемент списка будет сконвертирован в столбец итоговой таблицы data.table. Это делает j допольно мощным средством, как мы вскоре увидим.

1.4.4.4 - Выбрать столбцы arr_delay и dep_delay.

ans <- flights[, .(arr_delay, dep_delay)]
head(ans)
#    arr_delay dep_delay
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

## alternatively
# ans <- flights[, list(arr_delay, dep_delay)]
  • Оберните обе колонки в .() или list(). Вот и все.

1.4.4.5 - Выбрать столбцы arr_delay и dep_delay и переименовать их в delay_arr и delay_dep.

ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)
#    delay_arr delay_dep
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

Вот и все.

1.4.5 e) Вычислить или выполнить в j

1.4.5.1 – сколько рейсов имели общую задержку (delay) < 0?

ans <- flights[, sum((arr_delay + dep_delay) < 0)]
ans
# [1] 141814

1.4.5.2 Что здесь происходит?

  • j в data.table может не только выбирать столбцы - этот элемент может обрабатывать выражения, т.е. вычислять столбцы. Это не удивительно, ведь к столбцам можно обращаться, как к переменным. Тогда мы должны быть способны выполнять вычисления, вызывая функции для этих переменных. И это именно то, что здесь происходит.

1.4.6 f) Выбрать в i и выполнить в j

1.4.6.1 - Рассчитать среднюю задержку прибытия и отлета для всех рейсов с начальной точкой (аэропортом) “JFK” за июнь.

ans <- flights[origin == "JFK" & month == 6L, 
               .(m_arr=mean(arr_delay), m_dep=mean(dep_delay))]
ans
#       m_arr    m_dep
# 1: 5.839349 9.807884
  • Мы сперва выбрали строки в i, найдя индексы, для которых значение origin равно “JFK”, а значение month равно 6. В этот момент мы не выбрали часть целой таблицы data.table, соответствующую этим строкам.

  • Теперь мы смотрим на j и видим, что это выражение использует только два столбца. И мы должны рассчитать их средние значения mean(). Поэтому мы выбираем только столбцы с соответствующими строками и рассчитываем mean().

Поскольку три компонента запроса (i, j и by) находятся вместе внутри [...], data.table может видеть все три и оптимизировать запрос целиком перед вычислением, а не каждый по отдельности. Следовательно, мы можем избежать выбора всего поднабора данных для большей скорости и эффективного использования памяти.

1.4.6.2 - Как много рейсов было сделано в 2014 г. из аэропорта “JFK” за июнь?

ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422

Функция length() требует передачи ей аргумента. Нам нужно лишь рассчитать количество строк в поднаборе. На самом деле, вы могли бы использовать любой столбец в качестве аргумента length().

Этот тип операций встречается довольно часто, особенно в процессе группировки, как мы увидим в следующем разделе. data.table предоставляет для этого специальный символ .N.

1.4.6.3 Специальный символ .N

.N является специальной встроенной переменной, которая содержит число наблюдений в данной группе. Это особенно полезно в сочетании с by, как мы увидим в следующем разделе. При отсутствии операции группировки просто возвращает количество строк в поднаборе.

Так что теперь мы можем выполнить эту задачу с помощью .N следующим образом:

ans <- flights[origin == "JFK" & month == 6L, .N]
ans
# [1] 8422
  • Еще раз: мы выбираем поднабор в i для получения индексов строк, для которых значение origin равно “JFK”, а значение month равно 6.

  • Мы видим, что j использует только .Nи не использует никаких столбцов. Поэтому весь поднабор не “материализовался”. Мы просто вернули число строк в поднаборе (которое является всего лишь длиной индекса строк).

  • Обратите внимание, что мы не обернули .N в list() или в .(), поэтому возвращен вектор.

Мы могли бы выполнить ту же операцию при помощи nrow(flights[origin == "JFK" & month == 6L]). Тем не менее, это привело бы сначала к выбору поднабора всей таблицы data.table для соответствующих индексов строк в i, а затем возврату числа строк при помощи nrow(), что является ненужным и неэффективным. Мы подробно рассмотрим этот и другие аспекты оптимизации в виньетке “data.table design”.

1.4.7 g) Отлично! Но как я могу ссылаться на столбцы по именам в j (как в data.frame)?

Вы можете ссылаться на имена столбцов в стиле data.frame, используя with = FALSE (таким же образом можно передавать номера столбцов - прим. пер.).

1.4.7.1 – Выбрать столбцы arr_delay и dep_delay, как в data.frame.

ans <- flights[, c("arr_delay", "dep_delay"), with=FALSE]
head(ans)
#    arr_delay dep_delay
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

Этот аргумент назван with, как функция R with(), из-за подобной функциональности. Предположим, у вас есть data.frame DF, и вы хотите выбрать все строки, для которых x > 1.

DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8)

## (1) normal way
DF[DF$x > 1, ] # data.frame needs that ',' as well
#   x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8

## (2) using with
DF[with(DF, x > 1), ]
#   x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
  • Использование with() в (2) позволяет использовать столбец x в DF как переменную.

Отсюда и название аргумента в data.table. Установка with=FALSE отключает возможность ссылаться на столбцы как на переменные, тем самым восстанавливая “режим data.frame по умолчанию”.

  • Мы также можем исключать столбцы при помощи - или !. Например:
## not run

# returns all columns except arr_delay and dep_delay
ans <- flights[, !c("arr_delay", "dep_delay"), with=FALSE]
# or
ans <- flights[, -c("arr_delay", "dep_delay"), with=FALSE]
  • Начиная с v1.9.5+, мы также можем выбирать с указанием начального и конечного имен столбцов, например, year:day выберет первые три столбца.
## not run

# returns year,month and day
ans <- flights[, year:day, with=FALSE]
# returns day, month and year
ans <- flights[, day:year, with=FALSE]
# returns all columns except year, month and day
ans <- flights[, -(year:day), with=FALSE]
ans <- flights[, !(year:day), with=FALSE]

Это особенно удобно при работе в интерактивном режиме.

with = TRUE установлено по умолчанию в data.table, потому что мы можем сделать гораздо больше, позволяя j обрабатывать выражения - особенно в комбинации с by, что мы сейчас увидим.

1.5 2. Агрегирования

Мы уже видели общую форму i и j из data.table в предыдущем разделе. В этом разделе мы увидим, как они могут сочетаться с by для выполнения операций по группам. Давайте рассмотрим несколько примеров.

1.5.1 a) Группировка с использованием by

1.5.1.1 - Как мы можем получить количество рейсов, соответствующее каждому аэропорту отправления?

ans <- flights[, .(.N), by=.(origin)]
ans
#    origin     N
# 1:    JFK 81483
# 2:    LGA 84433
# 3:    EWR 87400

## or equivalently using a character vector in 'by'
# ans <- flights[, .(.N), by="origin"]
  • Мы знаем, что .N является специальной переменной, содержащей количество строк в данной группе. Группировка по origin позволяет получить количество строк, .N, для каждой группы.

  • Выполнив head(flights), вы увидите, что аэропорты идут в последовательности “JFK”, “LGA” и “EWR”. Исходная последовательность группирующих переменных сохраняется в результате.

  • Так как мы не указали в j имя для вовращаемого столбца, он был автоматически назван N из-за распознавания специального символа .N.

  • by также принимает символьный вектор из имен столбцов. Это особенно полезно для программ, содержащих, например, проектирование функции со столбцами для группировки в качестве аргумента.

  • Когда есть только один столбец или выражение в j и by, мы можем отбросить запись .() для удобства. Мы могли бы вместо этого выполнить:

ans <- flights[, .N, by=origin]
ans
#    origin     N
# 1:    JFK 81483
# 2:    LGA 84433
# 3:    EWR 87400

Мы будем использовать в будущем эту удобную форму, где это применимо.

1.5.1.2 - Как мы можем рассчитать количество рейсов из каждого аэропорта отправления для авиаперевозчика с кодом “AA”?

Уникальный код авиаперевозчика “AA” соответствует American Airlines Inc.

ans <- flights[carrier == "AA", .N, by=origin]
ans
#    origin     N
# 1:    JFK 11923
# 2:    LGA 11730
# 3:    EWR  2649
  • Сперва мы получаем индексы строк для выражения carrier == "AA" в i.

  • Используя эти индексы строк, мы получаем количество строк для группировки по origin. И снова никакие столбцы на самом деле здесь не “материализуются”, потому что j-выражение не требует наличия столбцов, для которых выполнено создание поднабора, и поэтому оно работает быстро и эффективно расходует память.

1.5.1.3 - Как мы можем получить количество рейсов для каждой пары origin, dest для авиаперевозчика с кодом “AA”?

ans <- flights[carrier == "AA", .N, by=.(origin,dest)]
head(ans)
#    origin dest    N
# 1:    JFK  LAX 3387
# 2:    LGA  PBI  245
# 3:    EWR  LAX   62
# 4:    JFK  MIA 1876
# 5:    JFK  SEA  298
# 6:    EWR  MIA  848

## or equivalently using a character vector in 'by'
# ans <- flights[carrier == "AA", .N, by=c("origin", "dest")]
  • by принимает несколько столбцов. Мы просто предоставляем все столбцы, по которым проводится группировка.

1.5.1.4 - Как мы можем получить среднюю задержку прибытия и отлета для каждой пары origin, dest в каждом месяце для для авиаперевозчика с кодом “AA”?

ans <- flights[carrier == "AA", 
        .(mean(arr_delay), mean(dep_delay)), 
        by=.(origin, dest, month)]
ans
#      origin dest month         V1         V2
#   1:    JFK  LAX     1   6.590361 14.2289157
#   2:    LGA  PBI     1  -7.758621  0.3103448
#   3:    EWR  LAX     1   1.366667  7.5000000
#   4:    JFK  MIA     1  15.720670 18.7430168
#   5:    JFK  SEA     1  14.357143 30.7500000
#  ---                                        
# 196:    LGA  MIA    10  -6.251799 -1.4208633
# 197:    JFK  MIA    10  -1.880184  6.6774194
# 198:    EWR  PHX    10  -3.032258 -4.2903226
# 199:    JFK  MCO    10 -10.048387 -1.6129032
# 200:    JFK  DCA    10  16.483871 15.5161290
  • Мы не задали имена столбцов в выражении j, они были созданы автоматически (V1, V2).

  • И снова, обратите внимание, исходный порядок группирующих столбцов сохраняется в результате.

А что, если мы хотели упорядочить результат по этим группирующим столбцам: origin, dest и month?

1.5.2 b) keyby

Сохранение в data.table исходного порядка групп является преднамеренным. Есть случаи, когда сохранение оригинального порядка имеет важное значение. Но порой мы хотели бы, чтобы происходило автоматическое упорядочивание по группирующим переменным.

1.5.2.1 - Так как же мы можем непосредственно упорядочить по всем группирующим переменным?

ans <- flights[carrier == "AA", 
        .(mean(arr_delay), mean(dep_delay)), 
        keyby=.(origin, dest, month)]
ans
#      origin dest month         V1         V2
#   1:    EWR  DFW     1   6.427673 10.0125786
#   2:    EWR  DFW     2  10.536765 11.3455882
#   3:    EWR  DFW     3  12.865031  8.0797546
#   4:    EWR  DFW     4  17.792683 12.9207317
#   5:    EWR  DFW     5  18.487805 18.6829268
#  ---                                        
# 196:    LGA  PBI     1  -7.758621  0.3103448
# 197:    LGA  PBI     2  -7.865385  2.4038462
# 198:    LGA  PBI     3  -5.754098  3.0327869
# 199:    LGA  PBI     4 -13.966667 -4.7333333
# 200:    LGA  PBI     5 -10.357143 -6.8571429
  • Все, что мы сделали, это заменили by на keyby. Это автоматически упорядочило результаты по группирующим переменным в порядке возрастания. Обратите внимание, что keyby() применяется после выполнения операции, т.е. для рассчитанного результата.

Ключи: На самом деле keyby делает несколько больше, чем просто выполняет упорядочивание. Эта функция также задает ключ после упорядочивания путем установки атрибута под названием sorted. Но мы узнаем больше о ключах keys в следующей виньетке.

В настоящий момент вы знаете, как использовать keyby для автоматического упорядочивания столбцов, заданных в by.

1.5.3 c) Цепочки

Давайте вновь рассмотрим задачу получения количество рейсов для каждой пары origin, dest для авиаперевозчика с кодом “AA”.

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]

1.5.3.1 - Как мы можем упорядочить ans, используя столбец origin в порядке по возрастанию и столбец dest в порядке по убыванию?

Мы можем хранить промежуточный результат в переменной, а затем использовать order(origin, -dest) для этой переменной. Это кажется довольно простым.

ans <- ans[order(origin, -dest)]
head(ans)
#    origin dest    N
# 1:    EWR  PHX  121
# 2:    EWR  MIA  848
# 3:    EWR  LAX   62
# 4:    EWR  DFW 1618
# 5:    JFK  STT  229
# 6:    JFK  SJU  690
  • Напомним, что мы можем использовать “-” перед названием столбца внутри order() в таблице data.table. Это возможно благодаря внутренней оптимизации запросов data.table.

  • Также напомним, что order(...) в таблице data.table автоматически оптимизирована для использования быстрого внутреннего способа упорядочивания forder(). Таким образом, вы можете продолжать использовать уже знакомые базовые функции R без ущерба для скорости или эффективности использования памяти, предлагаемых data.table. Мы рассмотрим этот вопрос более подробно в виньетке “data.table internals”.

Но в данном случае приходится присваивать промежуточный результат, а затем его перезаписывать. Мы можем сделать лучше и избежать этого промежуточного присваивания переменной при помощи цепочечных выражений (chaining) .

ans <- flights[carrier == "AA", .N, by=.(origin, dest)][order(origin, -dest)]
head(ans, 10)
#     origin dest    N
#  1:    EWR  PHX  121
#  2:    EWR  MIA  848
#  3:    EWR  LAX   62
#  4:    EWR  DFW 1618
#  5:    JFK  STT  229
#  6:    JFK  SJU  690
#  7:    JFK  SFO 1312
#  8:    JFK  SEA  298
#  9:    JFK  SAN  299
# 10:    JFK  ORD  432
  • Мы можем прикреплять выражения одно за другим, формируя цепочку операций, т.е. DT[ ... ][ ... ][ ... ].

  • Или же мы можем объединять их в цепочки вертикально:

DT[ ... 
 ][ ... 
 ][ ... 
 ]

1.5.4 d) Выражения в by

1.5.4.1 - Может ли by также принимать выражения, или только столбцы?

Да, может. Например, если мы хотим найти, сколько рейсов вылетели с опозданием, но прибыли заранее (или вовремя), или вылетели и прибыли с опозданием, и т.д…

ans <- flights[, .N, .(dep_delay>0, arr_delay>0)]
ans
#    dep_delay arr_delay      N
# 1:      TRUE      TRUE  72836
# 2:     FALSE      TRUE  34583
# 3:     FALSE     FALSE 119304
# 4:      TRUE     FALSE  26593
  • Последняя строка соответствует dep_delay > 0 = TRUE и arr_delay > 0 = FALSE. мы можем видеть, что 26593 рейсов начались с опозданием, но прибыли заранее (или вовремя).

  • Обратите внимание, что мы не задали никаких имен для by-выражения. И имена были автоматически присвоены в результате.

  • Вы можете задать другие столбцы вместе с выражениями, например: DT[, .N, by=.(a, b>0)].

1.5.5 e) Множество столбцов в j - .SD

1.5.5.1 - Можем ли мы вычислить mean() отдельно для каждого столбца?

Конечно, нецелесообразно вводить mean(myCol) для каждого столбца. Что, если mean() нужно рассчитать для 100 столбцов?

Как мы можем сделать это эффективно? Чтобы понять, вспомните совет - “Поскольку j-выражение возвращает список, каждый элемент списка будет сконвертирован в столбец итоговой таблицы data.table”. Предположим, что мы можем обратиться к поднабору данных для каждой группы как к переменной при группировке, затем мы можем обработать в цикле все столбцы этой переменной, используя уже знакомую базовую функцию lapply(). Нам не нужно изучать никаких новых функций.

1.5.5.2 Специальный символ .SD:

data.table предоставляет специальный символ под названием .SD. Он обозначает поднабор данных (Subset of Data) и сам по себе является таблицей data.table, содержащей данные для текущей группы, определенной с использованием by.

Напомним, что data.table представляет собой список со столбцами равной длины.

Давайте используем таблицу data.table DT, чтобы получить представление о том, как выглядит .SD.

DT
#    ID a  b  c
# 1:  b 1  7 13
# 2:  b 2  8 14
# 3:  b 3  9 15
# 4:  a 4 10 16
# 5:  a 5 11 17
# 6:  c 6 12 18

DT[, print(.SD), by=ID]
#    a b  c
# 1: 1 7 13
# 2: 2 8 14
# 3: 3 9 15
#    a  b  c
# 1: 4 10 16
# 2: 5 11 17
#    a  b  c
# 1: 6 12 18
# Empty data.table (0 rows) of 1 col: ID
  • .SD по умолчанию содержит все столбцы, кроме того, по которому выполнена группировка.

  • Создается с сохранением исходного порядка - данные, соответствующие ID = "b", затем ID = "a", а затем ID = "c".

Для расчетов по множеству столбцов мы может просто использовать базовую функцию lapply().

DT[, lapply(.SD, mean), by=ID]
#    ID   a    b    c
# 1:  b 2.0  8.0 14.0
# 2:  a 4.5 10.5 16.5
# 3:  c 6.0 12.0 18.0
  • .SD содержит строки, соответствующие столбцам a, b и c для этой группы. Мы рассчитываем mean() для каждого из этих столбцов, используя уже знакомую базовую функцию lapply().

  • Каждая группа возвращается список из трех элементов, содержащих средние значения, которые становятся столбцами итоговой таблицы data.table.

  • Поскольку функция lapply() возвращает список, нет необходимости дополнительно заключать ее в .().

Мы почти справились. Осталось рассмотреть одну маленькую деталь. В нашей таблице data.table flights мы хотим рассчитать mean() только для двух столбцов - arr_delay и dep_delay. Но .SD по умолчанию содержит все столбцы, кроме группирующих переменных.

1.5.5.3 - Как мы можем указать только те столбцы, для которых хотим рассчитать mean()?

1.5.5.4 .SDcols

Использование аргумента .SDcols. Он принимает имена или индексы столбцов. Например, .SDcols = c("arr_delay", "dep_delay") гарантирует, что .SD содержит только эти два столбца для каждой группы.

Так же как в разделе про with = FALSE, вы можете указывать столбцы, которые нужно удалить, вместо столбцов, которые нужно оставить, при помощи - или !, а также выбирать идущие подряд столбцы как colA:colB или исключать их как !(colA:colB) или -(colA:colB).

Теперь давайте попробуем использовать SD вместе с .SDcols, чтобы получить mean() для столбцов arr_delay и dep_delay, сгруппированных по origin, dest и month.

flights[carrier == "AA",                     ## Only on trips with carrier "AA"
        lapply(.SD, mean),                   ## compute the mean
        by=.(origin, dest, month),           ## for every 'origin,dest,month'
        .SDcols=c("arr_delay", "dep_delay")] ## for just those specified in .SDcols
#      origin dest month  arr_delay  dep_delay
#   1:    JFK  LAX     1   6.590361 14.2289157
#   2:    LGA  PBI     1  -7.758621  0.3103448
#   3:    EWR  LAX     1   1.366667  7.5000000
#   4:    JFK  MIA     1  15.720670 18.7430168
#   5:    JFK  SEA     1  14.357143 30.7500000
#  ---                                        
# 196:    LGA  MIA    10  -6.251799 -1.4208633
# 197:    JFK  MIA    10  -1.880184  6.6774194
# 198:    EWR  PHX    10  -3.032258 -4.2903226
# 199:    JFK  MCO    10 -10.048387 -1.6129032
# 200:    JFK  DCA    10  16.483871 15.5161290

1.5.6 f) Поднабор .SD для каждой группы:

1.5.6.1 – Как мы можем вернуть первые две строки для каждого month?

ans <- flights[, head(.SD, 2), by=month]
head(ans)
#    month year day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1:     1 2014   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2:     1 2014   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3:     2 2014   1      859        -1     1226         1         0      AA  N783AA      1    JFK
# 4:     2 2014   1     1155        -5     1528         3         0      AA  N784AA      3    JFK
# 5:     3 2014   1      849       -11     1306        36         0      AA  N784AA      1    JFK
# 6:     3 2014   1     1157        -3     1529        14         0      AA  N787AA      3    JFK
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
# 3:  LAX      358     2475    8  59
# 4:  LAX      358     2475   11  55
# 5:  LAX      375     2475    8  49
# 6:  LAX      368     2475   11  57
  • .SD является таблицей data.table, содержащей все строки для данной группы. Мы просто выбираем первые две строки, как мы уже видели.

  • head(.SD, 2) возвращается для каждой группы первые две строки как таблицу data.table, которая также является списком. Поэтому нам не нужно использовать .().

1.5.7 g) Зачем делать j настолько гибким?

Таким образом у нас есть единый синтаксис и возможность использования уже существующих (и знакомых) базовых функций вместо изучения новых. Для иллюстрации давайте использовать созданную в начале таблицу data.table DF.

1.5.7.1 – Как мы можем соединить столбцы a and b для каждой группы в ID?

DT[, .(val = c(a,b)), by=ID]
#     ID val
#  1:  b   1
#  2:  b   2
#  3:  b   3
#  4:  b   7
#  5:  b   8
#  6:  b   9
#  7:  a   4
#  8:  a   5
#  9:  a  10
# 10:  a  11
# 11:  c   6
# 12:  c  12
  • Вот и все. Не нужно никакого специального синтаксиса. Все, что нужно - это знание базовой функции c(), которая соединяет векторы, и совет про списки выше (j-выражение возвращает список).

1.5.7.2 - Что, если бы мы хотели получить все значения соединенных столбцов a и b, но в виде столбца-списка?

DT[, .(val = list(c(a,b))), by=ID]
#    ID         val
# 1:  b 1,2,3,7,8,9
# 2:  a  4, 5,10,11
# 3:  c        6,12
  • Тут мы сперва соединили значения при помощи c(a,b) для каждой группы и обернули их вызовом list(). Таким образом, для каждой группы мы верули список из всех соединенных значений.

  • Заметим, что запятые служат только для отображения. Столбец-список может содержать любой объект в каждой ячейке, и в этом примере каждая ячейка является вектором, и некоторые ячейки содержат более длинные векторы, чем другие.

Как только вы начнете использовать j, вы поймете, насколько мощным может быть этот синтаксис. Очень полезный способ, чтобы понять это - поиграть с данными при помощи функции print().

Например:

## (1) look at the difference between
DT[, print(c(a,b)), by=ID]
# [1] 1 2 3 7 8 9
# [1]  4  5 10 11
# [1]  6 12
# Empty data.table (0 rows) of 1 col: ID

## (2) and
DT[, print(list(c(a,b))), by=ID]
# [[1]]
# [1] 1 2 3 7 8 9
# 
# [[1]]
# [1]  4  5 10 11
# 
# [[1]]
# [1]  6 12
# Empty data.table (0 rows) of 1 col: ID

В примере (1) для для каждой группы возвращается вектор с длиной = 6, 4, 2. Однако (2) возвращает список длиной 1 для каждой группы с первым элементом, содержащим векторы с длиной 6, 4, 2. Поэтому (1) в результате имеет длину 6+4+2=12, в то время как (2) возвращает 1+1+1=3.

1.6 Резюме

Общая форма синтаксиса data.table:

DT[i, j, by]

1.6.1 Использование i:

  • мы можем выбирать строки, как в data.frame - за исключением того, что не нужно постоянно использовать DT$1, так как столбцы в data.table ведут себя так, как если бы они были переменными.

  • Мы также можем упорядочивать data.table, используя функцию order(), которая использует для большей производительности быструю внутреннюю реализацию упорядочивания в data.table.

Мы можем делать гораздо больше в i при помощи установки ключей в data.table, что позволяет выполнять молниеносные выборки поднаборов и объединения. Мы это увидим в виньетках “Keys and fast binary search based subsets” и “Joins and rolling joins”.

1.6.2 Использование j:

  1. Выбор столбцов в стиле data.table: DT[, .(colA, colB)].

  2. Выбор столбцов в стиле data.frame: DT[, c("colA", "colB"), with=FALSE].

  3. Вычисления на основе столбцов: DT[, .(sum(colA), mean(colB))].

  4. Указание имен, если нужно: DT[, .(sA =sum(colA), mB = mean(colB))].

  5. Комбинация с i: DT[colA > value, sum(colB)].

1.6.3 Использование by:

  • Используя by, мы можем осуществлять группировку по столбцам, указывая список столбцов или символьный вектор с именами столбцов, или даже используя выражения. Гибкость j в сочетании с by и i создает очень мощный синтаксис.

  • by может обрабатывать множественные столбцы, а также выражения.

  • Мы можем использовать keyby для группирующих столбцов с целью автоматического упорядочивания результатов группировки.

  • Мы можем использовать .SD и .SDcols в j для обработки множественных столбцов с использованием уже знакомых базовых функций. Вот несколько примеров:

    1. DT[, lapply(.SD, fun), by=., .SDcols=...] - применяет fun ко всем столбцам, указанным в .SDcols, в то время как группировка по столбцам задана в by.

    2. DT[, head(.SD, 2), by=.] - возвращает первые две строки для каждой группы.

    3. DT[col > val, head(.SD, 1), by=.] - комбинация i, j и by.

1.6.4 И запомните совет:

Поскольку j-выражение возвращает список, каждый элемент списка станет столбцом итоговой таблицы data.table.

В следующей виньетке мы увидим, как добавлять/обновлять/удалять столбцы по ссылке и как комбинировать это с i и by.

P.S. Очень познавательные примеры на SO