---
title: "Введение в data.table"
date: "`r Sys.Date()`"
output:
  markdown::html_format
vignette: >
  %\VignetteIndexEntry{Введение в data.table}
  %\VignetteEngine{knitr::knitr}
  \usepackage[utf8]{inputenc}
---

```{r, echo = FALSE, message = FALSE}
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` это позволяют, что мы и стремимся продемонстрировать в данном
описании.

## Данные {#data}

В этом документе мы используем данные
[NYC-flights14](https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv)
из пакета [flights](https://github.com/arunsrinivasan/flights) (доступен
только на GitHub). Он содержит данные о своевременности рейсов от Бюро
транспортной статистики для всех рейсов, вылетевших из аэропортов Нью-Йорка
в 2014 году (вдохновлено
[nycflights13](https://github.com/tidyverse/nycflights13)). Данные доступны
только за период с января по октябрь 2014 года.

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

```{r echo = FALSE}
options(width = 100L)
```

```{r}
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`).

## Введение

В этом описании мы

1. Начнем с основ - что такое `data.table`, его общая структура, как
   *выбирать* строки, как *выбирать и вычислять* значения по столбцам;

2. Затем мы рассмотрим выполнение агрегации данных по группам.

## 1. Основы {#basics-1}

### a) Что такое `data.table`? {#what-is-datatable-1a}

`data.table` — это пакет R, который предоставляет **расширенную версию**
`data.frame`, стандартной структуры данных для хранения данных в базовом
(`base`) R. В разделе [Данные](#data) выше мы увидели, как создать
`data.table` с помощью функции `fread()`, но также можно создать его,
используя функцию `data.table()`. Вот пример:

```{r}
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)). Для
  большого `data.frame` можно заметить, что приходится очень долго ждать,
  пока вся таблица будет напечатана. Это ограничение помогает справиться с
  данной проблемой, и вы можете узнать число выводимых строк следующим
  образом:

    ```{.r}
    getOption("datatable.print.nrows")
    ```

* `data.table` никогда не устанавливает и не использует *имена строк*. Мы
  увидим, почему это так, в руководстве
  [`vignette("datatable-keys-fast-subset",
  package="data.table")`](../datatable-keys-fast-subset.html).

### b) Общая структура - в чем заключается *усовершенствование* `data.table`? {#enhanced-1b}

По сравнению с `data.frame`, с использованием оператора `[ ... ]` в рамках
`data.table` можно делать *гораздо больше*, чем просто выбирать строки и
столбцы (примечание: мы также можем называть запись внутри `DT[...]`
«запросом к `DT`», по аналогии с SQL). Для понимания этого сначала нужно
рассмотреть *общую форму* синтаксиса `data.table`, как показано ниже:

```{r eval = FALSE}
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}

#### -- Вывести все рейсы, вылетевшие из аэропорта "JFK" в июне.

```{r}
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}

```{r}
ans <- flights[1:2]
ans
```

* В этом случае условия нет. Индексы строк уже указаны в `i`. Поэтому
  возвращается `data.table` со всеми столбцами из `flights` в строках,
  соответствующих этим *индексам строк*.

#### -- Отсортировать `flights` по столбцу `origin` в *возрастающем* порядке, а затем по столбцу `dest` в *убывающем* порядке:

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

```{r}
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](https://cran.r-project.org/doc/manuals/r-release/NEWS.pdf)).

Мы более подробно обсудим быструю сортировку `data.table` в руководстве
*`data.table` internals*.

### d) Выбрать столбец/столбцы в `j` {#select-j-1d}

#### -- Выбрать столбец `arr_delay`, но вывести его как *вектор*.

```{r}
ans <- flights[, arr_delay]
head(ans)
```

* Поскольку к столбцам можно обращаться как к переменным в рамках
  `data.table`, мы напрямую ссылаемся на *переменную*, по которой хотим
  выбрать строки. Поскольку нам нужны *все строки*, мы просто пропускаем
  `i`.

* Это возвращает *все* строки из столбца `arr_delay`.

#### -- Выбрать столбец `arr_delay` и вернуть его в виде `data.table`.

```{r}
ans <- flights[, list(arr_delay)]
head(ans)
```

* Мы оборачиваем *переменные* (имена столбцов) в `list()`, что позволяет
  получить в ответ объект `data.table`. При выборе одного столбца, не
  обёрнутого в `list()`, будет возвращён один вектор, как в [предыдущем
  примере](#select-j-1d).

* `data.table` также позволяет заворачивать столбцы в `.()` вместо
  `list()`. Это *синоним* для `list()`: эти конструкции значат одно и то
  же. Используйте ту, которая кажется более удобной; далее мы будем
  использовать `.()` для краткости.

`data.table` (как и `data.frame`) также является списком (`list`) с
условием, что у него есть атрибут `class`, а каждый его элемент имеет
одинаковую длину. Возможность возвращать `list` с помощью `j` позволяет
эффективно преобразовывать и возвращать `data.table`.

#### Подсказка: {#tip-1}

Пока выражение `j` возвращает список (`list`), каждый элемент списка будет
преобразован в столбец в результирующем `data.table`. Это делает `j` очень
мощным инструментом, как мы скоро увидим. Также это очень важно понимать,
когда вы захотите составлять более сложные запросы.

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

```{r}
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`.

```{r}
ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)
```

### е) Вычисление или *выполнение* в `j`

#### -- Сколько рейсов имели общую задержку < 0?

```{r}
ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans
```

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

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

### ж) Выбор в `i` *и* выполнение в `j`

#### -- Рассчитать среднее время задержки прибытия и отправления для всех рейсов, вылетевших из аэропорта "JFK" в июне.

```{r}
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`), что повышает как
скорость, так и эффективность использования памяти.

#### -- Сколько рейсов было вылетело в 2014 году из аэропорта "JFK" в июне?

```{r}
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` для элементов, которые не существуют, поведение
зависит от используемого метода.

```r
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`,
следующим образом:

```{r}
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`.

```{r j_cols_no_with}
ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)
```

Если вы сохранили нужные столбцы в векторе символов, есть два варианта:
использовать префикс `..` или использовать аргумент `with`.

#### -- Выбрать столбцы по именам из переменной, используя префикс `..`

```{r j_cols_dot_prefix}
select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]
```

Знакомым с терминалом Unix префикс `..` напоминает команду "на уровень
выше", что аналогично происходящему здесь — `..` указывает `data.table`
искать переменную `select_cols` "на уровне выше", то есть, в данном случае,
в глобальном пространстве переменных.

#### -- Выбрать столбцы по именам из переменной, используя `with = FALSE`

```{r j_cols_with}
flights[ , select_cols, with = FALSE]
```

Аргумент называется `with`, как и функция R `with()`, из-за схожей функциональности. Допустим, у вас есть `data.frame` `DF`, и вы хотите выбрать все строки, где `x > 1`. В `base` R вы можете сделать следующее:

```{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 eval = FALSE}
    ## этот код не выполняется

    # вернуть все столбцы, кроме arr_delay и dep_delay
    ans <- flights[, !c("arr_delay", "dep_delay")]
    # либо
    ans <- flights[, -c("arr_delay", "dep_delay")]
    ```

* Начиная с версии `v1.9.5+`, мы также можем выбирать столбцы, указывая
  начальное и конечное имена столбцов, например, `year:day` для выбора
  первых трёх столбцов.

    ```{r eval = FALSE}
    ## этот код не выполняется

    # вернуть 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`

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

```{r}
ans <- flights[, .(.N), by = .(origin)]
ans

## либо со строковым вектором в 'by'
# ans <- flights[, .(.N), by = "origin"]
```

* Нам известно, что `.N` [является специальной переменной](#special-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.*

```{r}
ans <- flights[carrier == "AA", .N, by = origin]
ans
```

* Сначала мы получаем индексы строк для выражения `carrier == "AA"` из `i`.

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

#### -- Как получить общее количество рейсов для каждой пары `origin, dest` для кода перевозчика `"AA"`? {#origin-dest-N}

```{r}
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}

```{r}
ans <- flights[carrier == "AA",
        .(mean(arr_delay), mean(dep_delay)),
        by = .(origin, dest, month)]
ans
```

* Поскольку мы не задали имена для столбцов в выражениях `j`, они были
  автоматически сгенерированы как `V1` и `V2`.

* Еще раз обратите внимание, что порядок указания столбцов группировки в
  результате сохраняется.

Что если мы захотим отсортировать результат по столбцам группировки
`origin`, `dest` и `month`?

### b) Сортировка по `by`: `keyby`

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

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

```{r}
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")`](../datatable-keys-fast-subset.html). Пока что вам
нужно знать, что вы можете использовать `keyby`, чтобы автоматически
отсортировать результат по столбцам, указанным в `by`.

### c) Цепочки вызовов

Давайте ещё раз рассмотрим задачу [получения общего количества рейсов для
каждой пары `origin, dest` для перевозчика *"AA"*](#origin-dest-N).

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

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

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

```{r}
ans <- ans[order(origin, -dest)]
head(ans)
```

* Вспомним, что мы можем использовать `-` для столбца `character` в функции
  `order()` в рамках `data.table`. Это возможно благодаря внутренней
  оптимизации запросов в `data.table`.

* Также вспомним, что `order(...)` в рамках `data.table` *автоматически
  оптимизируется* для использования внутреннего быстрого алгоритма
  сортировки `forder()` для повышения скорости.

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

```{r}
ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)]
head(ans, 10)
```

* Мы можем соединять выражения одно за другим, *формируя цепочку* действий,
  то есть `DT[ ... ][ ... ][ ... ]`.

* Либо вы можете также соединять их вертикально:

    ```{r eval = FALSE}
    DT[ ...
       ][ ...
         ][ ...
           ]
    ```

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

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

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

```{r}
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)]`.

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

#### -- Нужно ли вычислять `mean()` для каждого столбца по отдельности?

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

Как сделать это эффективно и лаконично? Для этого вспомните [этот
совет](#tip-1) - *"Если выражение в `j` возвращает `list`, каждый элемент
списка будет преобразован в столбец в результирующем `data.table`"*. Если мы
можем ссылаться на *подмножество данных для каждой группы* как на переменную
*во время группировки*, мы можем использовать уже знакомую базовую функцию
`lapply()` для обработки всех столбцов этой переменной. Никаких новых
названий, специфичных для `data.table`, учить не нужно.

#### Специальный символ `.SD`: {#special-SD}

`data.table` предоставляет специальный символ `.SD`, который означает
подмножество данных (**S**ubset of **D**ata). Это `data.table`, который
содержит данные для *текущей группы*, определенной с помощью `by`.

Помните, что `data.table` внутренне является `list`, в котором все столбцы
имеют одинаковую длину.

Давайте используем [`data.table` DT из предыдущего
примера](#what-is-datatable-1a), чтобы увидеть, как выглядит `.SD`.

```{r}
DT

DT[, print(.SD), by = ID]
```

* `.SD` содержит все столбцы, *кроме столбцов группировки* по умолчанию.

* Он также создаётся с сохранением оригинального порядка — сначала данные
  для `ID = "b"`, затем `ID = "a"`, и наконец `ID = "c"`.

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

```{r}
DT[, lapply(.SD, mean), by = ID]
```

* `.SD` содержит строки, соответствующие столбцам `a`, `b` и `c` для данной
  группы. Мы вычисляем `mean()` для каждой из этих колонок, используя уже
  знакомую функцию `lapply()` из базового R.

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

* Поскольку `lapply()` возвращает `list`, то нет необходимости оборачивать
  его дополнительным `.()` (при необходимости обратитесь к [этой
  подсказке](#tip-1)).

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

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

#### .SDcols

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

Аналогично [пункту и)](#refer-j), вы также можете указать столбцы для
удаления вместо столбцов для сохранения, используя `-` или `!`. Также можно
выбирать последовательные столбцы как `colA:colB` и исключать их как
`!(colA:colB)` или `-(colA:colB)`.

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

```{r}
flights[carrier == "AA",                       ## Для перелётов авиакомпанией "AA":
        lapply(.SD, mean),                     ## посчитать среднее
        by = .(origin, dest, month),           ## для каждой комбинации 'origin,dest,month'
        .SDcols = c("arr_delay", "dep_delay")] ## для столбцов, указанных в .SDcols
```

### f) Подмножество `.SD` для каждой группы:

#### -- Как вернуть первые две строки для каждого месяца?

```{r}
ans <- flights[, head(.SD, 2), by = month]
head(ans)
```

* `.SD` — это `data.table`, который содержит все строки для *этой
  группы*. Мы просто выбираем первые две строки, как уже обсуждалось
  [здесь](#subset-rows-integer).

* Для каждой группы `head(.SD, 2)` возвращает первые две строки в виде
  `data.table`, который также является `list`, поэтому нет необходимости
  оборачивать его в `.()`.

### g) Почему `j` настолько многофункционален?

Таким образом мы обеспечиваем консистентный синтаксис и продолжаем
использовать уже существующие (и знакомые) базовые функции, вместо того
чтобы изучать новые. Для иллюстрации используем `data.table` `DT`, который
мы создали в самом начале в разделе [Что такое
data.table?](#what-is-datatable-1a).

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

```{r}
DT[, .(val = c(a,b)), by = ID]
```

* Вот и всё. Не требуется никакого специального синтаксиса. Всё, что нам
  нужно знать, это базовая функция `c()`, которая объединяет векторы, и
  [предыдущий совет](#tip-1).

#### -- Что если мы хотим, чтобы все значения столбцов `a` и `b` были объединены, но возвращены как столбец-список?

```{r}
DT[, .(val = list(c(a,b))), by = ID]
```

* Здесь мы сначала объединяем значения с помощью `c(a,b)` для каждой группы,
  а затем оборачиваем это в `list()`. Таким образом, для каждой группы мы
  возвращаем список всех объединённых значений.

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

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

Например:

```{r}
## обратите внимание на разницу между
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.  Такой код лаконичен и легко
читается.

```{r}
## Удаётся ли дальним перелётам сократить отставание лучше, чем ближним?
## Различается ли сокращение отставания по месяцам?
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 требуется более сложный код, чтобы добиться
такого же результата.
```{r}
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` выглядит следующим образом:

```{r eval = FALSE}
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")`](../datatable-keys-fast-subset.html) и
[`vignette("datatable-joins",
package="data.table")`](../datatable-joins.html).

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

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

2. Выберите столбцы способом `data.frame`: `DT[, c("colA", "colB")]`.

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

4. Укажите имена, если это необходимо: `DT[, .(sA = sum(colA), mB =
   mean(colB))]`.

5. Сочетание с `i`: `DT[colA > value, sum(colB)]`.

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

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

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

* Мы можем использовать `keyby` для группировки по столбцам, чтобы
  автоматически сортировать результат группировки.

* Мы можем использовать `.SD` и `.SDcols` в `j`, чтобы работать с
  несколькими столбцами, используя уже знакомые базовые функции. Вот
  несколько примеров:

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

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

    3. `DT[col > val, head(.SD, 1), by = ...]` - объединяет `i` с `j` и
       `by`.

#### Также не забывайте:

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

В [следующем руководстве (`vignette("datatable-reference-semantics",
package="data.table")`)](../datatable-reference-semantics.html) мы
рассмотрим, как *добавлять/обновлять/удалять* столбцы *по ссылке* и как
комбинировать эти операции с `i` и `by`.

***

```{r, echo=FALSE}
setDTthreads(.old.th)
```
