Данное руководство также доступно на следующих языках: en | fr | ru
Описание пакета data.table
: его синтаксис, содержание и
функции; выбор подмножества (subset) строк, выбор столбцов и
проведение на них арифметических действий (select and compute),
объединение в группы по определенному признаку. Опыт использования
структуры данных data.frame
из базового R будет полезен, но
не необходим для понимания описания.
Анализ данных с использованием data.table
Действия манипуляции данных (subset, group, update, join и т.д.) связаны между собой. Объединение этих родственных действий позволяет:
использовать краткий и последовательный синтакс независимо от набора действий, необходимых для достижения конечной цели.
свободно проводить анализ данных без необходимости соотнесения каждого действия с одной из огромного множества функций, доступных перед проведением анализа.
автоматически и эффективно оптимизировать действия в зависимости от данных, к которым применяются эти действия; в результате получается очень быстрый код с эффективным использованием памяти.
Вкратце, этот пакет для вас, если вы заинтересованы в радикальном
сокращении времени, затрачиваемого на написание кода и
обсчет данных. Принципы data.table
это позволяют,
что мы и стремимся продемонстрировать в данном описании.
Данные
В этом документе мы используем данные NYC-flights14 из пакета flights (доступен только на GitHub). Он содержит данные о своевременности рейсов от Бюро транспортной статистики для всех рейсов, вылетевших из аэропортов Нью-Йорка в 2014 году (вдохновлено nycflights13). Данные доступны только за период с января по октябрь 2014 года.
Мы можем использовать функцию чтения файлов fread
из
data.table
, чтобы загрузить рейсы (flights
)
следующим образом:
input <- if (file.exists("../flights14.csv")) {
"../flights14.csv"
} else {
"https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv"
}
flights <- fread(input)
flights
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 -8 -26 AA LGA PBI 157 1035 7
# 5: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# ---
# 253312: 2014 10 31 1 -30 UA LGA IAH 201 1416 14
# 253313: 2014 10 31 -5 -14 UA EWR IAH 189 1400 8
# 253314: 2014 10 31 -8 16 MQ LGA RDU 83 431 11
# 253315: 2014 10 31 -4 15 MQ LGA DTW 75 502 11
# 253316: 2014 10 31 -5 1 MQ LGA SDF 110 659 8
dim(flights)
# [1] 253316 11
Примечание: fread
напрямую поддерживает URL-адреса с
http
и https
, а также команды операционной
системы, такие как вывод sed
и awk
. Примеры
можно найти в справке (?fread
).
Введение
В этом описании мы
Начнем с основ - что такое
data.table
, его общая структура, как выбирать строки, как выбирать и вычислять значения по столбцам;Затем мы рассмотрим выполнение агрегации данных по группам.
1. Основы
a) Что такое data.table
?
data.table
— это пакет R, который предоставляет
расширенную версию data.frame
, стандартной
структуры данных для хранения данных в базовом (base
) R. В
разделе Данные выше мы увидели, как создать
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
# <char> <int> <int> <int>
# 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
с помощью setDT()
(для структур
data.frame
и list
) или
as.data.table()
(для других структур). Для получения более
подробной информации о различиях (выходит за рамки этого руководства)
обратитесь к ?setDT
и ?as.data.table
.
Примечания:
Номера строк выводятся с
:
, чтобы визуально отделить номер строки от первого столбца.-
Когда количество строк для вывода превышает глобальную опцию
datatable.print.nrows
(по умолчаниюr getOption("datatable.print.nrows")
), автоматически выводятся только первые 5 и последние 5 строк (как видно в разделе Данные). Для большогоdata.frame
можно заметить, что приходится очень долго ждать, пока вся таблица будет напечатана. Это ограничение помогает справиться с данной проблемой, и вы можете узнать число выводимых строк следующим образом:getOption("datatable.print.nrows")
data.table
никогда не устанавливает и не использует имена строк. Мы увидим, почему это так, в руководствеvignette("datatable-keys-fast-subset", package="data.table")
.
b) Общая структура - в чем заключается усовершенствование
data.table
?
По сравнению с data.frame
, с использованием оператора
[ ... ]
в рамках data.table
можно делать
гораздо больше, чем просто выбирать строки и столбцы
(примечание: мы также можем называть запись внутри DT[...]
«запросом к DT
», по аналогии с SQL). Для понимания этого
сначала нужно рассмотреть общую форму синтаксиса
data.table
, как показано ниже:
DT[i, j, by]
## R: i j by
## SQL: where | order by select | update group by
Для пользователей с опытом работы в SQL этот синтаксис может выглядеть знакомо.
в) Выбор строк в i
– Вывести все рейсы, вылетевшие из аэропорта “JFK” в июне.
ans <- flights[origin == "JFK" & month == 6L]
head(ans)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 6 1 -9 -5 AA JFK LAX 324 2475 8
# 2: 2014 6 1 -10 -13 AA JFK LAX 329 2475 12
# 3: 2014 6 1 18 -1 AA JFK LAX 326 2475 7
# 4: 2014 6 1 -6 -16 AA JFK LAX 320 2475 10
# 5: 2014 6 1 -4 -45 AA JFK LAX 326 2475 18
# 6: 2014 6 1 -6 -23 AA JFK LAX 329 2475 14
В рамках
data.table
к столбцам можно обращаться, как если бы они были переменными, как в SQL или Stata. Поэтому мы просто ссылаемся наorigin
иmonth
, как на переменные. Не нужно добавлять префиксflights$
каждый раз. Тем не менее,flights$origin
иflights$month
также будет работать без проблем.Вычисляются индексы строк, удовлетворяющие условию
origin == "JFK" & month == 6L
, и, поскольку больше ничего не требуется делать, все столбцы изflights
в строках, соответствующих этим индексам строк, просто возвращаются какdata.table
.Запятая после условия в
i
не требуется, тем не менее,flights[origin == "JFK" & month == 6L, ]
тоже будет работать без проблем. Вdata.frame
, однако, запятая обязательна.
– Вывести первые две строки из flights
.
ans <- flights[1:2]
ans
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
- В этом случае условия нет. Индексы строк уже указаны в
i
. Поэтому возвращаетсяdata.table
со всеми столбцами изflights
в строках, соответствующих этим индексам строк.
– Отсортировать flights
по столбцу origin
в возрастающем порядке, а затем по столбцу dest
в
убывающем порядке:
Для этого мы можем использовать функцию R order()
.
ans <- flights[order(origin, -dest)]
head(ans)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 5 6 49 EV EWR XNA 195 1131 8
# 2: 2014 1 6 7 13 EV EWR XNA 190 1131 8
# 3: 2014 1 7 -6 -13 EV EWR XNA 179 1131 8
# 4: 2014 1 8 -7 -12 EV EWR XNA 184 1131 8
# 5: 2014 1 9 16 7 EV EWR XNA 181 1131 8
# 6: 2014 1 13 66 66 EV EWR XNA 188 1131 9
order()
оптимизировано внутри функции
Мы можем использовать “-” для столбцов типа
character
в рамкахdata.table
для сортировки в порядке убывания.Кроме того,
order(...)
в рамкахdata.table
использует внутреннюю быструю сортировкуforder()
. Эта сортировка показала настолько значительное улучшение по сравнению сbase::order
в R, что проект R принял алгоритмdata.table
как стандартный способ сортировки начиная с версии R 3.3.0 в 2016 году (для справки см.?sort
и R Release NEWS).
Мы более подробно обсудим быструю сортировку data.table
в руководстве data.table
internals.
d) Выбрать столбец/столбцы в j
– Выбрать столбец arr_delay
, но вывести его как
вектор.
ans <- flights[, arr_delay]
head(ans)
# [1] 13 13 9 -26 1 0
Поскольку к столбцам можно обращаться как к переменным в рамках
data.table
, мы напрямую ссылаемся на переменную, по которой хотим выбрать строки. Поскольку нам нужны все строки, мы просто пропускаемi
.Это возвращает все строки из столбца
arr_delay
.
– Выбрать столбец arr_delay
и вернуть его в виде
data.table
.
ans <- flights[, list(arr_delay)]
head(ans)
# arr_delay
# <int>
# 1: 13
# 2: 13
# 3: 9
# 4: -26
# 5: 1
# 6: 0
Мы оборачиваем переменные (имена столбцов) в
list()
, что позволяет получить в ответ объектdata.table
. При выборе одного столбца, не обёрнутого вlist()
, будет возвращён один вектор, как в предыдущем примере.data.table
также позволяет заворачивать столбцы в.()
вместоlist()
. Это синоним дляlist()
: эти конструкции значат одно и то же. Используйте ту, которая кажется более удобной; далее мы будем использовать.()
для краткости.
data.table
(как и data.frame
) также
является списком (list
) с условием, что у него есть атрибут
class
, а каждый его элемент имеет одинаковую длину.
Возможность возвращать list
с помощью j
позволяет эффективно преобразовывать и возвращать
data.table
.
Подсказка:
Пока выражение j
возвращает список (list
),
каждый элемент списка будет преобразован в столбец в результирующем
data.table
. Это делает j
очень мощным
инструментом, как мы скоро увидим. Также это очень важно понимать, когда
вы захотите составлять более сложные запросы.
– Выбрать оба столбца: arr_delay
и
dep_delay
.
е) Вычисление или выполнение в j
– Сколько рейсов имели общую задержку < 0?
ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans
# [1] 141814
Что здесь происходит?
-
j
вdata.table
может выполнять не только выбор столбцов, но и выражения, то есть вычисления на основе столбцов. Это неудивительно, так как столбцы можно рассматривать как переменные. Следовательно, мы можем вычислять значения, вызывая функции для этих переменных. Именно это мы здесь и делаем.
ж) Выбор в i
и выполнение в
j
– Рассчитать среднее время задержки прибытия и отправления для всех рейсов, вылетевших из аэропорта “JFK” в июне.
ans <- flights[origin == "JFK" & month == 6L,
.(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
ans
# m_arr m_dep
# <num> <num>
# 1: 5.839349 9.807884
Сначала мы выбираем строки в
i
, чтобы найти соответствующие индексы строк, в которых аэропортorigin
равен"JFK"
, аmonth
равен6L
. Мы пока не выбираем целыйdata.table
, соответствующий этим строкам.Теперь мы видим, что в
j
используется только два столбца. Нам нужно вычислить ихmean()
. Поэтому мы выбираем только те столбцы, которые соотносятся с соответствующими строками, и вычисляем ихmean()
.
Поскольку три основные компонента запроса (i
,
j
и by
) находятся вместе внутри
[...]
, data.table
видит все три и может
оптимизировать запрос целиком до выполнения, а не
оптимизировать каждый компонент отдельно. Таким образом, мы можем
избежать выбора всего набора данных (то есть, выбора столбцов
кроме arr_delay
и dep_delay
), что
повышает как скорость, так и эффективность использования памяти.
– Сколько рейсов было вылетело в 2014 году из аэропорта “JFK” в июне?
ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422
Функция length()
требует аргумента. Нам нужно вычислить
количество строк в выбранном подмножестве. Мы могли бы использовать
любой другой столбец в качестве аргумента для length()
.
Этот подход напоминает
SELECT COUNT(dest) FROM flights WHERE origin = 'JFK' AND month = 6
в SQL.
Этот тип действий встречается довольно часто, особенно при
группировке (как мы увидим в следующем разделе), и поэтому
data.table
предоставляет специальный символ
.N
для этого.
з) Обработка несуществующих элементов в i
– Что происходит при запросе несуществующих элементов?
При запросе data.table
для элементов, которые не
существуют, поведение зависит от используемого метода.
setkeyv(flights, "origin")
-
Выбор на основе ключа:
dt["d"]
Это выполняет правое соединение по ключевому столбцу
x
, в результате чего получается строка сd
иNA
для столбцов, которые не найдены. При использованииsetkeyv
таблица сортируется по указанным ключам и создается внутренний индекс, позволяющий выполнять бинарный поиск для эффективного выбора подмножеств.flights["XYZ"] # Возвращает: # origin year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum ... # 1: XYZ NA NA NA NA NA NA NA NA NA NA NA NA ...
-
Логический выбор:
dt[x == "d"]
Это выполняет стандартную операцию выбора, которая не находит совпадающих строк и, следовательно, возвращает пустой
data.table
.flights[origin == "XYZ"] # Возвращает: # Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...
-
Точное соответствие с использованием
nomatch=NULL
Для точного соответствия без
NA
для несуществующих элементов используйтеnomatch=NULL
:flights["XYZ", nomatch=NULL] # Возвращает: # Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...
Понимание этих особенностей поможет избежать путаницы при работе с несуществующими элементами в ваших данных.
Специальный символ .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
.
и) Отлично! Но как я могу ссылаться на столбцы по именам в
j
(как в data.frame
)?
Если вы явно указываете имена столбцов, разницы по сравнению с
data.frame
нет (начиная с версии 1.9.8).
– Выбрать оба столбца: arr_delay
и
dep_delay
способом data.frame
.
ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 0 4
Если вы сохранили нужные столбцы в векторе символов, есть два
варианта: использовать префикс ..
или использовать аргумент
with
.
– Выбрать столбцы по именам из переменной, используя префикс
..
select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# ---
# 253312: -30 1
# 253313: -14 -5
# 253314: 16 -8
# 253315: 15 -4
# 253316: 1 -5
Знакомым с терминалом Unix префикс ..
напоминает команду
“на уровень выше”, что аналогично происходящему здесь — ..
указывает data.table
искать переменную
select_cols
“на уровне выше”, то есть, в данном случае, в
глобальном пространстве переменных.
– Выбрать столбцы по именам из переменной, используя
with = FALSE
flights[ , select_cols, with = FALSE]
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# ---
# 253312: -30 1
# 253313: -14 -5
# 253314: 16 -8
# 253315: 15 -4
# 253316: 1 -5
Аргумент называется with
, как и функция R
with()
, из-за схожей функциональности. Допустим, у вас есть
data.frame
DF
, и вы хотите выбрать все строки,
где x > 1
. В base
R вы можете сделать
следующее:
DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8)
## (1) обычный способ
DF[DF$x > 1, ] # для data.frame эта запятая также понадобится
# x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
## (2) с использованием 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
вdata.table
. Установкаwith = FALSE
отключает возможность ссылаться на столбцы как на переменные, возвращая режим “data.frame
”. -
Мы также можем исключить столбцы, используя
-
или!
. Например: -
Начиная с версии
v1.9.5+
, мы также можем выбирать столбцы, указывая начальное и конечное имена столбцов, например,year:day
для выбора первых трёх столбцов.## этот код не выполняется # вернуть year, month, day ans <- flights[, year:day] # вернуть day, month, year ans <- flights[, day:year] # вернуть все столбцы, кроме year, month, day ans <- flights[, -(year:day)] ans <- flights[, !(year:day)]
Это особенно удобно при работе в интерактивном режиме.
with = TRUE
в data.table
является значением
по умолчанию, поскольку это позволяет j
обрабатывать
выражения — особенно в сочетании с by
, как мы вскоре
увидим.
2. Объединения
Мы уже рассмотрели i
и j
в общем виде
data.table
в предыдущем разделе. В этом разделе мы увидим,
как их можно объединить с by
, чтобы выполнять действия
по группам. Давайте рассмотрим несколько примеров.
а) Группировка с использованием by
– Как узнать количество рейсов, соответствующих каждому аэропорту отправления?
ans <- flights[, .(.N), by = .(origin)]
ans
# origin N
# <char> <int>
# 1: JFK 81483
# 2: LGA 84433
# 3: EWR 87400
## либо со строковым вектором в '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 # <char> <int> # 1: JFK 81483 # 2: LGA 84433 # 3: EWR 87400
Мы будем использовать эту более удобную форму и в дальнейших случаях, где она будет применима.
– Как рассчитать количество рейсов для каждого аэропорта отправления
для кода перевозчика "AA"
?
Уникальный код перевозчика "AA"
соответствует
American Airlines Inc.
ans <- flights[carrier == "AA", .N, by = origin]
ans
# origin N
# <char> <int>
# 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
# <char> <char> <int>
# 1: JFK LAX 3387
# 2: LGA PBI 245
# 3: EWR LAX 62
# 4: JFK MIA 1876
# 5: JFK SEA 298
# 6: EWR MIA 848
## или, как вариант, с использованием вектора символов в `by`
# ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]
– Как получить среднюю задержку прибытия и отправления для каждой
пары origin, dest
для каждого месяца для кода перевозчика
"AA"
?
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
by = .(origin, dest, month)]
ans
# origin dest month V1 V2
# <char> <char> <int> <num> <num>
# 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) Сортировка по by
: keyby
data.table
намеренно сохраняет исходный порядок групп. В
некоторых случаях сохранение исходного порядка является важным. Однако
иногда нам необходимо автоматически отсортировать данные по переменным,
которые используются для группировки.
– Тогда как же мы можем отсортировать данные по всем переменным группировки напрямую?
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
keyby = .(origin, dest, month)]
ans
# Key: <origin, dest, month>
# origin dest month V1 V2
# <char> <char> <int> <num> <num>
# 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
. Это автоматически сортирует результат по переменным группировки в порядке возрастания. На самом деле, из-за внутренней реализацииby
, которая сначала требует сортировки перед восстановлением исходного порядка таблицы,keyby
обычно работает быстрее, так как не требует этого второго шага.
Ключи сортировки: На самом деле keyby
делает немного больше, чем просто сортировка. Он также устанавливает
ключ после сортировки, добавляя атрибут с названием
sorted
.
Мы подробнее рассмотрим keys
в руководстве vignette("datatable-keys-fast-subset", package="data.table")
.
Пока что вам нужно знать, что вы можете использовать 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
# <char> <char> <int>
# 1: EWR PHX 121
# 2: EWR MIA 848
# 3: EWR LAX 62
# 4: EWR DFW 1618
# 5: JFK STT 229
# 6: JFK SJU 690
Вспомним, что мы можем использовать
-
для столбцаcharacter
в функцииorder()
в рамкахdata.table
. Это возможно благодаря внутренней оптимизации запросов вdata.table
.Также вспомним, что
order(...)
в рамкахdata.table
автоматически оптимизируется для использования внутреннего быстрого алгоритма сортировкиforder()
для повышения скорости.
Но это требует присваивания промежуточного результата и последующей его замены. Мы можем сделать лучше и избежать этого промежуточного присваивания временной переменной, используя цепочки вызовов.
ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)]
head(ans, 10)
# origin dest N
# <char> <char> <int>
# 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
# <lgcl> <lgcl> <int>
# 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-expression
. Поэтому имена были присвоены автоматически. Как и в случае сj
, вы можете назвать эти выражения так же, как и для элементов любогоlist
, например,DT[, .N, .(dep_delayed = dep_delay>0, arr_delayed = arr_delay>0)]
.Вы можете предоставить другие столбцы вместе с выражениями, например:
DT[, .N, by = .(a, b>0)]
.
e) Множественные столбцы в j
- .SD
– Нужно ли вычислять mean()
для каждого столбца по
отдельности?
Конечно, неудобно набирать mean(myCol)
для каждого
столбца по отдельности. Что если у вас 100 столбцов, для которых нужно
вычислить mean()
?
Как сделать это эффективно и лаконично? Для этого вспомните этот совет - “Если выражение в j
возвращает list
, каждый элемент списка будет преобразован в
столбец в результирующем data.table
”. Если мы можем
ссылаться на подмножество данных для каждой группы как на
переменную во время группировки, мы можем использовать уже
знакомую базовую функцию lapply()
для обработки всех
столбцов этой переменной. Никаких новых названий, специфичных для
data.table
, учить не нужно.
Специальный символ .SD
:
data.table
предоставляет специальный символ
.SD
, который означает подмножество данных
(Subset of Data). Это
data.table
, который содержит данные для текущей
группы, определенной с помощью by
.
Помните, что data.table
внутренне является
list
, в котором все столбцы имеют одинаковую длину.
Давайте используем data.table
DT из предыдущего
примера, чтобы увидеть, как выглядит .SD
.
DT
# ID a b c
# <char> <int> <int> <int>
# 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
# <int> <int> <int>
# 1: 1 7 13
# 2: 2 8 14
# 3: 3 9 15
# a b c
# <int> <int> <int>
# 1: 4 10 16
# 2: 5 11 17
# a b c
# <int> <int> <int>
# 1: 6 12 18
# Empty data.table (0 rows and 1 cols): ID
.SD
содержит все столбцы, кроме столбцов группировки по умолчанию.Он также создаётся с сохранением оригинального порядка — сначала данные для
ID = "b"
, затемID = "a"
, и наконецID = "c"
.
Для выполнения вычислений на (многих) столбцах можно использовать
базовую функцию R lapply()
.
DT[, lapply(.SD, mean), by = ID]
# ID a b c
# <char> <num> <num> <num>
# 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()
из базового R.Каждая группа возвращает список из трёх элементов с вычисленными средними значениями, которые становятся столбцами в результирующем
data.table
.Поскольку
lapply()
возвращаетlist
, то нет необходимости оборачивать его дополнительным.()
(при необходимости обратитесь к этой подсказке).
Мы почти закончили. Осталось прояснить только одну вещь. В нашем
data.table
flights
мы хотели рассчитать
mean()
только для двух столбцов - arr_delay
и
dep_delay
. Однако по умолчанию .SD
будет
содержать все столбцы, кроме группирующих переменных.
.SDcols
С помощью аргумента .SDcols
. Он принимает как имена
столбцов, так и их индексы. Например,
.SDcols = c("arr_delay", "dep_delay")
гарантирует, что
.SD
будет содержать только эти два столбца для каждой
группы.
Аналогично пункту и), вы также можете указать
столбцы для удаления вместо столбцов для сохранения, используя
-
или !
. Также можно выбирать последовательные
столбцы как colA:colB
и исключать их как
!(colA:colB)
или -(colA:colB)
.
Теперь давайте попробуем использовать .SD
вместе с
.SDcols
, чтобы вычислить mean()
для столбцов
arr_delay
и dep_delay
, сгруппированных по
origin
, dest
и month
.
flights[carrier == "AA", ## Для перелётов авиакомпанией "AA":
lapply(.SD, mean), ## посчитать среднее
by = .(origin, dest, month), ## для каждой комбинации 'origin,dest,month'
.SDcols = c("arr_delay", "dep_delay")] ## для столбцов, указанных в .SDcols
# origin dest month arr_delay dep_delay
# <char> <char> <int> <num> <num>
# 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
для каждой группы:
– Как вернуть первые две строки для каждого месяца?
ans <- flights[, head(.SD, 2), by = month]
head(ans)
# month year day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 1 2014 1 14 13 AA JFK LAX 359 2475 9
# 2: 1 2014 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2 2014 1 -1 1 AA JFK LAX 358 2475 8
# 4: 2 2014 1 -5 3 AA JFK LAX 358 2475 11
# 5: 3 2014 1 -11 36 AA JFK LAX 375 2475 8
# 6: 3 2014 1 -3 14 AA JFK LAX 368 2475 11
g) Почему j
настолько многофункционален?
Таким образом мы обеспечиваем консистентный синтаксис и продолжаем
использовать уже существующие (и знакомые) базовые функции, вместо того
чтобы изучать новые. Для иллюстрации используем data.table
DT
, который мы создали в самом начале в разделе Что такое data.table?.
– Как мы можем объединить столбцы a
и b
для каждой группы в ID
?
DT[, .(val = c(a,b)), by = ID]
# ID val
# <char> <int>
# 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()
, которая объединяет векторы, и предыдущий совет.
– Что если мы хотим, чтобы все значения столбцов a
и
b
были объединены, но возвращены как столбец-список?
DT[, .(val = list(c(a,b))), by = ID]
# ID val
# <char> <list>
# 1: b 1,2,3,7,8,9
# 2: a 4, 5,10,11
# 3: c 6,12
Здесь мы сначала объединяем значения с помощью
c(a,b)
для каждой группы, а затем оборачиваем это вlist()
. Таким образом, для каждой группы мы возвращаем список всех объединённых значений.Обратите внимание, что запятые здесь используются только для отображения. Столбец типа “список” может содержать любые объекты в каждой ячейке, и в этом примере каждая ячейка сама по себе является вектором, причём некоторые ячейки содержат более длинные векторы, чем другие.
Когда вы начнёте привыкать к использованию синтаксиса в
j
, вы поймёте, насколько это мощный инструмент. Чтобы
попрактиковаться с ним и попробовать разные вещи, вы можете
поэкспериментировать с помощью функции print()
.
Например:
## обратите внимание на разницу между
DT[, print(c(a,b)), by = ID] # (1)
# [1] 1 2 3 7 8 9
# [1] 4 5 10 11
# [1] 6 12
# Empty data.table (0 rows and 1 cols): ID
## и
DT[, print(list(c(a,b))), by = ID] # (2)
# [[1]]
# [1] 1 2 3 7 8 9
#
# [[1]]
# [1] 4 5 10 11
#
# [[1]]
# [1] 6 12
# Empty data.table (0 rows and 1 cols): ID
В случае (1) для каждой группы возвращается по вектору вектор, длины
которых равны 6, 4, 2. Однако (2) возвращает список длиной 1 для каждой
группы, где первый элемент содержит векторы длиной 6, 4, 2. Поэтому (1)
даёт общую длину 6 + 4 + 2 = 12
, тогда как (2) возвращает
1 + 1 + 1 = 3
.
При помощи аргумента j
можно поместить внутрь data.table
любой список. Например, при построении статистических моделей на группах
строк список с этими моделями может стать столбцом data.table. Такой код
лаконичен и легко читается.
## Удаётся ли дальним перелётам сократить отставание лучше, чем ближним?
## Различается ли сокращение отставания по месяцам?
flights[, `:=`(makeup = dep_delay - arr_delay)]
makeup.models <- flights[, .(fit = list(lm(makeup ~ distance))), by = .(month)]
makeup.models[, .(coefdist = coef(fit[[1]])[2], rsq = summary(fit[[1]])$r.squared), by = .(month)]
# month coefdist rsq
# <int> <num> <num>
# 1: 1 0.0042864543 2.664617e-02
# 2: 2 -0.0036042523 2.211601e-02
# 3: 3 0.0012742633 3.661327e-03
# 4: 4 0.0018003305 5.912241e-03
# 5: 5 0.0021486474 7.794517e-03
# 6: 6 -0.0000427658 3.261486e-06
# 7: 7 0.0028011128 1.199733e-02
# 8: 8 0.0029923379 1.910536e-02
# 9: 9 0.0014305778 4.917775e-03
# 10: 10 0.0022125344 1.099980e-02
С использованием data.frame требуется более сложный код, чтобы добиться такого же результата.
setDF(flights)
flights.split <- split(flights, f = flights$month)
makeup.models.list <- lapply(flights.split, function(df) c(month = df$month[1], fit = list(lm(makeup ~ distance, data = df))))
makeup.models.df <- do.call(rbind, makeup.models.list)
sapply(makeup.models.df[, "fit"], function(model) c(coefdist = coef(model)[2], rsq = summary(model)$r.squared)) |> t() |> data.frame()
# coefdist.distance rsq
# 1 0.0042864543 2.664617e-02
# 2 -0.0036042523 2.211601e-02
# 3 0.0012742633 3.661327e-03
# 4 0.0018003305 5.912241e-03
# 5 0.0021486474 7.794517e-03
# 6 -0.0000427658 3.261486e-06
# 7 0.0028011128 1.199733e-02
# 8 0.0029923379 1.910536e-02
# 9 0.0014305778 4.917775e-03
# 10 0.0022125344 1.099980e-02
setDT(flights)
Подведение итогов
Общий синтаксис data.table
выглядит следующим
образом:
DT[i, j, by]
Как мы теперь видим,
Использование i
:
Мы можем выбирать строки аналогично
data.frame
, но при этом не нужно повторно использоватьDT$
, так как столбцы в рамкахdata.table
рассматриваются как переменные.Мы также можем сортировать
data.table
с помощьюorder()
, который внутренне использует быстрый метод сортировкиdata.table
для повышения производительности.
Мы можем сделать гораздо больше в i
, установив ключи для
data.table
, что позволит нам очень быстро извлекать
подмножества и выполнять соединения. Мы рассмотрим это в разделах vignette("datatable-keys-fast-subset", package="data.table")
и vignette("datatable-joins", package="data.table")
.
Использование j
:
Выберите столбцы способом
data.table
:DT[, .(colA, colB)]
.Выберите столбцы способом
data.frame
:DT[, c("colA", "colB")]
.Выполните вычисления по столбцам:
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
возвращает list
, каждый элемент
этого списка станет столбцом в результирующей
data.table
.
В следующем
руководстве
(vignette("datatable-reference-semantics", package="data.table")
)
мы рассмотрим, как добавлять/обновлять/удалять столбцы по
ссылке и как комбинировать эти операции с i
и
by
.