Введение в data.table
translated by Andrey Ogurtsov
2015-06-24
Анализ данных с data.table
Все операции манипулирования данными, такие как subset, group, update, join и др., по сути взаимосвязаны. С учетом этого, вместе они обеспечивают:
- краткий и согласованный синтаксис независимо от набора операций, которые вы хотели бы выполнить для достижения вашей конечной цели.
- плавное выполнение анализа без когнитивной нагрузки, связанной с помещением каждой операции в отдельную функцию из набора доступных функций для выполнения анализа.
- автоматическая и эффективная внутренняя оптимизация за счет точного знания, какие данные нужны для каждой операции, что делает вычисления очень быстрыми и экономно расходующими память.
Данные
В этой виньетке мы используем набор данных 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
Так как мы будем использовать этот набор данных во всех виньетках, может быть, лучше скачать файл один раз и затем загружать его с диска.Введение
В этой виньетке мы:- начнем с основ - что такое пакет data.table, его общий вид, способы выделения поднаборов строк, выбор и вычисление колонок
- затем перейдем к выполнению агрегирования данных по группам.
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()
.Обратите внимание, что:
- В отличие от 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”.
b) Общий вид - каким образом реализованы улучшения data.table?
По сравнению с data.frame, мы можете сделать гораздо больше, чем выбор строк и столбцов в таблице при помощи[ ... ]
. Для понимания этого посмотрим на общий вид синтаксиса data.table, как показано ниже:DT[i, j, by]
## R: i j by
## SQL: where select | update group by
Пользователи с опытом использования SQL могут сразу понять этот синтаксис.Как прочитать это (вслух):
ВзятьDT
, выбрать строки при помощи i
, затем вычислить j
, сгруппировав по by
.Давайте начнем с рассмотрения
i
и j
- выбора строк и операций над столбцами.
c) Выбор строк в i
- Выбрать все рейсы с начальной точкой (аэропортом) “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, тем не менее, запятая обязательна.
- Выбрать первые две строки из таблицы 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
для этих индексов строк.
- Упорядочить 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
Функция 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”.- Таким образом, вы можете улучшить производительность, используя хорошо знакомые функции.
d) Выбор столбцов в j
- Выбрать столбец arr_delay
, но вернуть как вектор.
ans <- flights[, arr_delay]
head(ans)
# [1] 13 13 9 -26 1 0
- Поскольку можно обращаться к столбцам, как если бы они были переменными в таблице, мы напрямую обратились к переменным, которые хотим выбрать. Поскольку нам нужны все строки, мы просто пропустили
i
. - Были возвращены все строки для столбца
arr_delay
.
- Выбрать столбец 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.tables (и data.frames) внутри являются списками со столбцами равной длины и с атрибутом класса. Разрешение
j
возвращать список позволяет конвертировать и возвращать data.table очень эффективно.Совет
Посколькуj-выражение
возвращает список, каждый элемент списка будет сконвертирован в столбец итоговой таблицы data.table. Это делает j
допольно мощным средством, как мы вскоре увидим.
- Выбрать столбцы 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()
. Вот и все.
- Выбрать столбцы 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
Вот и все.
e) Вычислить или выполнить в j
– сколько рейсов имели общую задержку (delay) < 0?
ans <- flights[, sum((arr_delay + dep_delay) < 0)]
ans
# [1] 141814
Что здесь происходит?
j
в data.table может не только выбирать столбцы - этот элемент может обрабатывать выражения, т.е. вычислять столбцы. Это не удивительно, ведь к столбцам можно обращаться, как к переменным. Тогда мы должны быть способны выполнять вычисления, вызывая функции для этих переменных. И это именно то, что здесь происходит.
f) Выбрать в i
и выполнить в j
- Рассчитать среднюю задержку прибытия и отлета для всех рейсов с начальной точкой (аэропортом) “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 может видеть все три и оптимизировать запрос целиком перед вычислением, а не каждый по отдельности. Следовательно, мы можем избежать выбора всего поднабора данных для большей скорости и эффективного использования памяти.- Как много рейсов было сделано в 2014 г. из аэропорта “JFK” за июнь?
ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422
Функция length()
требует передачи ей аргумента. Нам нужно лишь рассчитать количество строк в поднаборе. На самом деле, вы могли бы использовать любой столбец в качестве аргумента length()
.Этот тип операций встречается довольно часто, особенно в процессе группировки, как мы увидим в следующем разделе. data.table предоставляет для этого специальный символ
.N
.
Специальный символ .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”.
g) Отлично! Но как я могу ссылаться на столбцы по именам в j
(как в data.frame)?
Вы можете ссылаться на имена столбцов в стиле data.frame, используя with = FALSE
(таким же образом можно передавать номера столбцов - прим. пер.).
– Выбрать столбцы 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
как переменную.
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
, что мы сейчас увидим.2. Агрегирования
Мы уже видели общую формуi
и j
из data.table в предыдущем разделе. В этом разделе мы увидим, как они могут сочетаться с by
для выполнения операций по группам. Давайте рассмотрим несколько примеров.
a) Группировка с использованием by
- Как мы можем получить количество рейсов, соответствующее каждому аэропорту отправления?
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
Мы будем использовать в будущем эту удобную форму, где это применимо.- Как мы можем рассчитать количество рейсов из каждого аэропорта отправления для авиаперевозчика с кодом “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-выражение
не требует наличия столбцов, для которых выполнено создание поднабора, и поэтому оно работает быстро и эффективно расходует память.
- Как мы можем получить количество рейсов для каждой пары 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
принимает несколько столбцов. Мы просто предоставляем все столбцы, по которым проводится группировка.
- Как мы можем получить среднюю задержку прибытия и отлета для каждой пары 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
?b) keyby
Сохранение в data.table исходного порядка групп является преднамеренным. Есть случаи, когда сохранение оригинального порядка имеет важное значение. Но порой мы хотели бы, чтобы происходило автоматическое упорядочивание по группирующим переменным.- Так как же мы можем непосредственно упорядочить по всем группирующим переменным?
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
.c) Цепочки
Давайте вновь рассмотрим задачу получения количество рейсов для каждой парыorigin
, dest
для авиаперевозчика с кодом “AA”.ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
- Как мы можем упорядочить 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[ ...
][ ...
][ ...
]
d) Выражения в by
- Может ли 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)]
.
e) Множество столбцов в j
- .SD
- Можем ли мы вычислить mean()
отдельно для каждого столбца?
Конечно, нецелесообразно вводить mean(myCol)
для каждого столбца. Что, если mean()
нужно рассчитать для 100 столбцов?Как мы можем сделать это эффективно? Чтобы понять, вспомните совет - “Поскольку
j-выражение
возвращает список, каждый элемент списка будет сконвертирован в столбец итоговой таблицы data.table”. Предположим, что мы можем обратиться к поднабору данных для каждой группы как к переменной при группировке, затем мы можем обработать в цикле все столбцы этой переменной, используя уже знакомую базовую функцию lapply()
. Нам не нужно изучать никаких новых функций.
Специальный символ .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()
возвращает список, нет необходимости дополнительно заключать ее в.()
.
flights
мы хотим рассчитать mean()
только для двух столбцов - arr_delay
и dep_delay
. Но .SD
по умолчанию содержит все столбцы, кроме группирующих переменных.
- Как мы можем указать только те столбцы, для которых хотим рассчитать mean()
?
.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
f) Поднабор .SD
для каждой группы:
– Как мы можем вернуть первые две строки для каждого 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, которая также является списком. Поэтому нам не нужно использовать.()
.
g) Зачем делать j
настолько гибким?
Таким образом у нас есть единый синтаксис и возможность использования уже существующих (и знакомых) базовых функций вместо изучения новых. Для иллюстрации давайте использовать созданную в начале таблицу data.table DF
.
– Как мы можем соединить столбцы 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-выражение
возвращает список).
- Что, если бы мы хотели получить все значения соединенных столбцов 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
.Резюме
Общая форма синтаксиса data.table:DT[i, j, by]
Использование 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”.
Использование j
:
- Выбор столбцов в стиле data.table:
DT[, .(colA, colB)]
. - Выбор столбцов в стиле data.frame:
DT[, c("colA", "colB"), with=FALSE]
. - Вычисления на основе столбцов:
DT[, .(sum(colA), mean(colB))]
. - Указание имен, если нужно:
DT[, .(sA =sum(colA), mB = mean(colB))]
. - Комбинация с
i
:DT[colA > value, sum(colB)]
.
Использование by
:
- Используя
by
, мы можем осуществлять группировку по столбцам, указывая список столбцов или символьный вектор с именами столбцов, или даже используя выражения. Гибкостьj
в сочетании сby
иi
создает очень мощный синтаксис. by
может обрабатывать множественные столбцы, а также выражения.- Мы можем использовать
keyby
для группирующих столбцов с целью автоматического упорядочивания результатов группировки. - Мы можем использовать
.SD
и.SDcols
вj
для обработки множественных столбцов с использованием уже знакомых базовых функций. Вот несколько примеров:
DT[, lapply(.SD, fun), by=., .SDcols=...]
- применяетfun
ко всем столбцам, указанным в.SDcols
, в то время как группировка по столбцам задана вby
.DT[, head(.SD, 2), by=.]
- возвращает первые две строки для каждой группы.DT[col > val, head(.SD, 1), by=.]
- комбинацияi
,j
иby
.
И запомните совет:
Посколькуj-выражение
возвращает список, каждый элемент списка станет столбцом итоговой таблицы data.table.В следующей виньетке мы увидим, как добавлять/обновлять/удалять столбцы по ссылке и как комбинировать это с
i
и by
.P.S. Очень познавательные примеры на SO
Комментариев нет:
Отправить комментарий