require(data.table) knitr::opts_chunk$set( comment = "#", error = FALSE, tidy = FALSE, cache = FALSE, collapse = TRUE ) .old.th = setDTthreads(1)
Описание пакета 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
) следующим образом:
options(width = 100L)
input <- if (file.exists("../flights14.csv")) { "../flights14.csv" } else { "https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv" } flights <- fread(input) flights dim(flights)
Примечание: fread
напрямую поддерживает URL-адреса с http
и https
, а
также команды операционной системы, такие как вывод sed
и awk
. Примеры
можно найти в справке (?fread
).
В этом описании мы
Начнем с основ - что такое data.table
, его общая структура, как
выбирать строки, как выбирать и вычислять значения по столбцам;
Затем мы рассмотрим выполнение агрегации данных по группам.
data.table
? {#what-is-datatable-1a}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 class(DT$ID)
Вы также можете преобразовать существующие объекты в 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
можно заметить, что приходится очень долго ждать,
пока вся таблица будет напечатана. Это ограничение помогает справиться с
данной проблемой, и вы можете узнать число выводимых строк следующим
образом:
{.r}
getOption("datatable.print.nrows")
data.table
никогда не устанавливает и не использует имена строк. Мы
увидим, почему это так, в руководстве
vignette("datatable-keys-fast-subset",
package="data.table")
.
data.table
? {#enhanced-1b}По сравнению с 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 этот синтаксис может выглядеть знакомо.
Взять DT
, выбрать/переставить строки, используя i
, затем вычислить j
,
сгруппировав по by
.
Начнем с рассмотрения i
и j
— выбора строк и операций со столбцами.
i
{#subset-i-1c}ans <- flights[origin == "JFK" & month == 6L] head(ans)
В рамках 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
. {#subset-rows-integer}ans <- flights[1:2] ans
i
. Поэтому
возвращается data.table
со всеми столбцами из flights
в строках,
соответствующих этим индексам строк.flights
по столбцу origin
в возрастающем порядке, а затем по столбцу dest
в убывающем порядке:Для этого мы можем использовать функцию R order()
.
ans <- flights[order(origin, -dest)] head(ans)
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.
j
{#select-j-1d}arr_delay
, но вывести его как вектор.ans <- flights[, arr_delay] head(ans)
Поскольку к столбцам можно обращаться как к переменным в рамках
data.table
, мы напрямую ссылаемся на переменную, по которой хотим
выбрать строки. Поскольку нам нужны все строки, мы просто пропускаем
i
.
Это возвращает все строки из столбца arr_delay
.
arr_delay
и вернуть его в виде data.table
.ans <- flights[, list(arr_delay)] head(ans)
Мы оборачиваем переменные (имена столбцов) в 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
.ans <- flights[, .(arr_delay, dep_delay)] head(ans) ## другой вариант # ans <- flights[, list(arr_delay, dep_delay)]
.()
или list()
. Этого достаточно.arr_delay
и dep_delay
, и переименовать их в delay_arr
и delay_dep
.Поскольку .()
— это просто псевдоним для list()
, мы можем присваивать
имена столбцам так же, как при создании list
.
ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)] head(ans)
j
ans <- flights[, sum( (arr_delay + dep_delay) < 0 )] ans
j
в data.table
может выполнять не только выбор столбцов, но и
выражения, то есть вычисления на основе столбцов. Это неудивительно,
так как столбцы можно рассматривать как переменные. Следовательно, мы
можем вычислять значения, вызывая функции для этих переменных. Именно
это мы здесь и делаем.i
и выполнение в j
ans <- flights[origin == "JFK" & month == 6L, .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))] ans
Сначала мы выбираем строки в i
, чтобы найти соответствующие индексы
строк, в которых аэропорт origin
равен "JFK"
, а month
равен
6L
. Мы пока не выбираем целый data.table
, соответствующий этим
строкам.
Теперь мы видим, что в j
используется только два столбца. Нам нужно
вычислить их mean()
. Поэтому мы выбираем только те столбцы, которые
соотносятся с соответствующими строками, и вычисляем их mean()
.
Поскольку три основные компонента запроса (i
, j
и by
) находятся
вместе внутри [...]
, data.table
видит все три и может оптимизировать
запрос целиком до выполнения, а не оптимизировать каждый компонент
отдельно. Таким образом, мы можем избежать выбора всего набора данных (то
есть, выбора столбцов кроме arr_delay
и dep_delay
), что повышает как
скорость, так и эффективность использования памяти.
ans <- flights[origin == "JFK" & month == 6L, length(dest)] ans
Функция 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
таблица сортируется по указанным ключам и создается внутренний индекс, позволяющий выполнять бинарный поиск для эффективного выбора подмножеств.
r
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
.
r
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
:
r
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
: {#special-N}.N
— это специальная встроенная переменная, которая содержит количество
наблюдений в текущей группе. Она особенно полезна при использовании с
by
, как мы увидим в следующем разделе. В отсутствие операций группировки
она просто возвращает количество строк в выбранном подмножестве.
Теперь, когда мы знаем это, мы можем выполнить ту же задачу, используя .N
,
следующим образом:
ans <- flights[origin == "JFK" & month == 6L, .N] ans
Снова выбираем строки в i
, чтобы получить индексы строк, где аэропорт
origin
равен "JFK", а month
равен 6.
Мы видим, что в j
используется только .N
и никакие другие
столбцы. Поэтому подмножество не отображается целиком. Мы просто
возвращаем количество строк в подмножестве (что эквивалентно длине
индексов строк).
Обратите внимание, что мы не оборачивали .N
в list()
или
.()
. Поэтому возвращается вектор.
Мы могли бы выполнить то же действие, используя nrow(flights[origin ==
"JFK" & month == 6L])
. Однако сначала нужно было бы выбрать весь
data.table
, соответствующий индексам строк в i
, а затем возвращать
количество строк с помощью nrow()
, что является ненужным и
неэффективным. Мы подробно рассмотрим это и другие аспекты оптимизации в
руководстве дизайн data.table
.
j
(как в data.frame
)? {#refer-j}Если вы явно указываете имена столбцов, разницы по сравнению с data.frame
нет (начиная с версии 1.9.8).
arr_delay
и dep_delay
способом data.frame
.ans <- flights[, c("arr_delay", "dep_delay")] head(ans)
Если вы сохранили нужные столбцы в векторе символов, есть два варианта:
использовать префикс ..
или использовать аргумент with
.
..
select_cols = c("arr_delay", "dep_delay") flights[ , ..select_cols]
Знакомым с терминалом Unix префикс ..
напоминает команду "на уровень
выше", что аналогично происходящему здесь — ..
указывает data.table
искать переменную select_cols
"на уровне выше", то есть, в данном случае,
в глобальном пространстве переменных.
with = FALSE
flights[ , select_cols, with = FALSE]
Аргумент называется 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 эта запятая также понадобится ## (2) с использованием with() DF[with(DF, x > 1), ]
Использование with()
в (2) позволяет обращаться к столбцу x
в DF
,
как если бы это была переменная.
Поэтому аргумент называется with
в data.table
. Установка with = FALSE
отключает возможность ссылаться на столбцы как на переменные, возвращая режим "data.frame
".
Мы также можем исключить столбцы, используя -
или !
. Например:
```r
ans <- flights[, !c("arr_delay", "dep_delay")]
ans <- flights[, -c("arr_delay", "dep_delay")] ```
Начиная с версии v1.9.5+
, мы также можем выбирать столбцы, указывая
начальное и конечное имена столбцов, например, year:day
для выбора
первых трёх столбцов.
```r
ans <- flights[, year:day]
ans <- flights[, day:year]
ans <- flights[, -(year:day)] ans <- flights[, !(year:day)] ```
Это особенно удобно при работе в интерактивном режиме.
with = TRUE
в data.table
является значением по умолчанию, поскольку это
позволяет j
обрабатывать выражения — особенно в сочетании с by
, как мы
вскоре увидим.
Мы уже рассмотрели i
и j
в общем виде data.table
в предыдущем
разделе. В этом разделе мы увидим, как их можно объединить с by
, чтобы
выполнять действия по группам. Давайте рассмотрим несколько примеров.
by
ans <- flights[, .(.N), by = .(origin)] ans ## либо со строковым вектором в 'by' # ans <- flights[, .(.N), by = "origin"]
Нам известно, что .N
является специальной переменной,
которая содержит количество строк в текущей группе. Группировка по
origin
позволяет получить количество строк, .N
, для каждой группы.
Выполнив head(flights)
, вы увидите, что аэропорты отправления
упорядочены как "JFK", "LGA", и "EWR". Исходный порядок переменных
группировки сохраняется. Это важно помнить.
Поскольку мы не указали имя для столбца, возвращаемого в j
, он был
автоматически назван N
, поскольку был распознан специальный символ .N
.
by
также принимает вектор символов с именами столбцов. Это особенно
полезно для программного кодирования, например, при создании функции с
колонками группировки (в виде вектора символов) в качестве аргумента
функции.
Когда есть только один столбец или выражение, на которое нужно ссылаться в
j
и by
, мы можем опустить обозначение .()
. Это сделано только для
удобства. Вместо этого можно сделать:
r
ans <- flights[, .N, by = origin]
ans
Мы будем использовать эту более удобную форму и в дальнейших случаях, где она будет применима.
"AA"
? {#origin-N}Уникальный код перевозчика "AA"
соответствует American Airlines Inc.
ans <- flights[carrier == "AA", .N, by = origin] ans
Сначала мы получаем индексы строк для выражения carrier == "AA"
из i
.
Используя эти индексы строк, мы получаем количество строк при
группировке по origin
. Опять же, никакие столбцы здесь не отображаются,
поскольку j-выражение
не требует фактической выборки столбцов, что
делает данное действие быстрым и экономит память.
origin, dest
для кода перевозчика "AA"
? {#origin-dest-N}ans <- flights[carrier == "AA", .N, by = .(origin, dest)] head(ans) ## или, как вариант, с использованием вектора символов в `by` # ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]
by
может принимать несколько столбцов. Мы просто указываем все столбцы,
по которым нужно сгруппировать данные. Обратите внимание на использование
.()
в by
— это всего лишь сокращение для list()
, и вместо него здесь
также можно использовать list()
. В этом руководстве мы будем
придерживаться .()
.origin, dest
для каждого месяца для кода перевозчика "AA"
? {#origin-dest-month}ans <- flights[carrier == "AA", .(mean(arr_delay), mean(dep_delay)), by = .(origin, dest, month)] ans
Поскольку мы не задали имена для столбцов в выражениях j
, они были
автоматически сгенерированы как V1
и V2
.
Еще раз обратите внимание, что порядок указания столбцов группировки в результате сохраняется.
Что если мы захотим отсортировать результат по столбцам группировки
origin
, dest
и month
?
by
: keyby
data.table
намеренно сохраняет исходный порядок групп. В некоторых случаях
сохранение исходного порядка является важным. Однако иногда нам необходимо
автоматически отсортировать данные по переменным, которые используются для
группировки.
ans <- flights[carrier == "AA", .(mean(arr_delay), mean(dep_delay)), keyby = .(origin, dest, month)] ans
by
на keyby
. Это автоматически
сортирует результат по переменным группировки в порядке возрастания. На
самом деле, из-за внутренней реализации by
, которая сначала требует
сортировки перед восстановлением исходного порядка таблицы, keyby
обычно
работает быстрее, так как не требует этого второго шага.Ключи сортировки: На самом деле keyby
делает немного больше, чем просто сортировка. Он также устанавливает ключ после сортировки, добавляя атрибут с названием sorted
.
Мы подробнее рассмотрим keys
в руководстве
vignette("datatable-keys-fast-subset",
package="data.table")
. Пока что вам
нужно знать, что вы можете использовать keyby
, чтобы автоматически
отсортировать результат по столбцам, указанным в by
.
Давайте ещё раз рассмотрим задачу получения общего количества рейсов для
каждой пары origin, dest
для перевозчика "AA".
ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
ans
по столбцу origin
в порядке возрастания и по столбцу dest
в порядке убывания?Мы можем сохранить промежуточный результат в переменной, а затем
использовать order(origin, -dest)
для этой переменной. Это выглядит
довольно просто.
ans <- ans[order(origin, -dest)] head(ans)
Вспомним, что мы можем использовать -
для столбца character
в функции
order()
в рамках data.table
. Это возможно благодаря внутренней
оптимизации запросов в data.table
.
Также вспомним, что order(...)
в рамках data.table
автоматически
оптимизируется для использования внутреннего быстрого алгоритма
сортировки forder()
для повышения скорости.
Но это требует присваивания промежуточного результата и последующей его замены. Мы можем сделать лучше и избежать этого промежуточного присваивания временной переменной, используя цепочки вызовов.
ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)] head(ans, 10)
Мы можем соединять выражения одно за другим, формируя цепочку действий,
то есть DT[ ... ][ ... ][ ... ]
.
Либо вы можете также соединять их вертикально:
r
DT[ ...
][ ...
][ ...
]
by
by
принимать выражения, или он принимает только столбцы?Да, может. Например, если мы хотим узнать, сколько рейсов вылетели с опозданием, но прибыли раньше (или вовремя), вылетели и прибыли с опозданием и т.д.
ans <- flights[, .N, .(dep_delay>0, arr_delay>0)] ans
Последняя строка соответствует dep_delay > 0 = TRUE
и arr_delay > 0 =
FALSE
. Мы видим, что r flights[!is.na(arr_delay) & !is.na(dep_delay),
.N, .(dep_delay>0, arr_delay>0)][, N[4L]]
рейсы вылетели с опозданием, но
прибыли раньше (или вовремя).
Обратите внимание, что мы не задали имена для by-expression
. Поэтому
имена были присвоены автоматически. Как и в случае с j
, вы можете
назвать эти выражения так же, как и для элементов любого list
, например,
DT[, .N, .(dep_delayed = dep_delay>0, arr_delayed = arr_delay>0)]
.
Вы можете предоставить другие столбцы вместе с выражениями, например:
DT[, .N, by = .(a, b>0)]
.
j
- .SD
mean()
для каждого столбца по отдельности?Конечно, неудобно набирать mean(myCol)
для каждого столбца по
отдельности. Что если у вас 100 столбцов, для которых нужно вычислить
mean()
?
Как сделать это эффективно и лаконично? Для этого вспомните этот
совет - "Если выражение в j
возвращает list
, каждый элемент
списка будет преобразован в столбец в результирующем data.table
". Если мы
можем ссылаться на подмножество данных для каждой группы как на переменную
во время группировки, мы можем использовать уже знакомую базовую функцию
lapply()
для обработки всех столбцов этой переменной. Никаких новых
названий, специфичных для data.table
, учить не нужно.
.SD
: {#special-SD}data.table
предоставляет специальный символ .SD
, который означает
подмножество данных (Subset of Data). Это data.table
, который
содержит данные для текущей группы, определенной с помощью by
.
Помните, что data.table
внутренне является list
, в котором все столбцы
имеют одинаковую длину.
Давайте используем data.table
DT из предыдущего
примера, чтобы увидеть, как выглядит .SD
.
DT DT[, print(.SD), by = ID]
.SD
содержит все столбцы, кроме столбцов группировки по умолчанию.
Он также создаётся с сохранением оригинального порядка — сначала данные
для ID = "b"
, затем ID = "a"
, и наконец ID = "c"
.
Для выполнения вычислений на (многих) столбцах можно использовать базовую
функцию R lapply()
.
DT[, lapply(.SD, mean), by = ID]
.SD
содержит строки, соответствующие столбцам a
, b
и c
для данной
группы. Мы вычисляем mean()
для каждой из этих колонок, используя уже
знакомую функцию lapply()
из базового R.
Каждая группа возвращает список из трёх элементов с вычисленными средними
значениями, которые становятся столбцами в результирующем data.table
.
Поскольку lapply()
возвращает list
, то нет необходимости оборачивать
его дополнительным .()
(при необходимости обратитесь к этой
подсказке).
Мы почти закончили. Осталось прояснить только одну вещь. В нашем
data.table
flights
мы хотели рассчитать mean()
только для двух
столбцов - arr_delay
и dep_delay
. Однако по умолчанию .SD
будет
содержать все столбцы, кроме группирующих переменных.
mean()
?С помощью аргумента .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
.SD
для каждой группы:ans <- flights[, head(.SD, 2), by = month] head(ans)
.SD
— это data.table
, который содержит все строки для этой
группы. Мы просто выбираем первые две строки, как уже обсуждалось
здесь.
Для каждой группы head(.SD, 2)
возвращает первые две строки в виде
data.table
, который также является list
, поэтому нет необходимости
оборачивать его в .()
.
j
настолько многофункционален?Таким образом мы обеспечиваем консистентный синтаксис и продолжаем
использовать уже существующие (и знакомые) базовые функции, вместо того
чтобы изучать новые. Для иллюстрации используем data.table
DT
, который
мы создали в самом начале в разделе Что такое
data.table?.
a
и b
для каждой группы в ID
?DT[, .(val = c(a,b)), by = ID]
c()
, которая объединяет векторы, и
предыдущий совет.a
и b
были объединены, но возвращены как столбец-список?DT[, .(val = list(c(a,b))), by = ID]
Здесь мы сначала объединяем значения с помощью c(a,b)
для каждой группы,
а затем оборачиваем это в list()
. Таким образом, для каждой группы мы
возвращаем список всех объединённых значений.
Обратите внимание, что запятые здесь используются только для отображения. Столбец типа "список" может содержать любые объекты в каждой ячейке, и в этом примере каждая ячейка сама по себе является вектором, причём некоторые ячейки содержат более длинные векторы, чем другие.
Когда вы начнёте привыкать к использованию синтаксиса в j
, вы поймёте,
насколько это мощный инструмент. Чтобы попрактиковаться с ним и попробовать
разные вещи, вы можете поэкспериментировать с помощью функции print()
.
Например:
## обратите внимание на разницу между DT[, print(c(a,b)), by = ID] # (1) ## и DT[, print(list(c(a,b))), by = ID] # (2)
В случае (1) для каждой группы возвращается по вектору вектор, длины которых
равны 6, 4, 2. Однако (2) возвращает список длиной 1 для каждой группы, где
первый элемент содержит векторы длиной 6, 4, 2. Поэтому (1) даёт общую длину
6 + 4 + 2 =
r 6+4+2, тогда как (2) возвращает `1 + 1 + 1 = `r 1+1+1
.
При помощи аргумента 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)]
С использованием 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() 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
.
setDTthreads(.old.th)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.