---
title: "Быстрое изменение формы данных с помощью data.table"
date: "`r Sys.Date()`"
output:
  markdown::html_format
vignette: >
  %\VignetteIndexEntry{Быстрое изменение формы данных с помощью data.table}
  %\VignetteEngine{knitr::knitr}
  \usepackage[utf8]{inputenc}
---

```{r echo=FALSE, file='../_translation_links.R'}
```
`r .write.translation.links("Данное руководство также доступно на следующих языках: %s")`

```{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)
```

В этом руководстве рассматривается стандартное использование функций
изменения формы данных `melt` (из широкой в длинную) и `dcast` (из длинной в
широкую) для класса *data.table*, а также **новые расширенные возможности**
«расплавления» и «отлива» для *нескольких столбцов*, доступные с `v1.9.6`.

***

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

## Данные

Мы будем загружать наборы данных непосредственно по ходу текста.

## Введение

Функции `melt` и `dcast` для `data.table` предназначены для изменения формы
таблиц между «длинными» и «широкими»; реализации специально разработаны с
учетом больших данных в памяти (например, 10 ГБ).

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

1. Сначала кратко рассмотрим стандартные операции `melt` и `dcast` для
   `data.table`, чтобы преобразовать их из широкого формата в длинный и
   _наоборот_

2. Рассмотрим сценарии, в которых текущие функции становятся громоздкими и
   неэффективными

3. Наконец, обратим внимание на новые улучшения методов `melt` и `dcast`,
   позволяющие обрабатывать несколько столбцов таблиц `data.table`
   одновременно.

Расширенная функциональность соответствует философии `data.table`, которая
заключается в эффективном и понятном выполнении операций.

## 1. Функциональность по умолчанию

### a) «плавление» данных в таблице (от широких к длинным)

Предположим, у нас есть `data.table` (заполненная искусственными данными),
как показано ниже:

```{r}
s1 <- "family_id age_mother dob_child1 dob_child2 dob_child3
1         30 1998-11-26 2000-01-29         NA
2         27 1996-06-22         NA         NA
3         26 2002-07-11 2004-04-05 2007-09-02
4         32 2004-10-10 2009-08-27 2012-07-21
5         29 2000-12-05 2005-02-28         NA"
DT <- fread(s1)
DT
## dob значит "дата рождения" (date of birth).

str(DT)
```


#### — Преобразование `DT` в *длинную* форму, где каждое значение `dob` представляет собой отдельное наблюдение.

Мы могли бы добиться этого с помощью `melt()`, указав аргументы `id.vars` и
`measure.vars` следующим образом:

```{r}
DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"),
                measure.vars = c("dob_child1", "dob_child2", "dob_child3"))
DT.m1
str(DT.m1)
```

* `measure.vars` указывает набор столбцов, которые мы хотели бы свернуть
  (или объединить) вместе.

* Мы также можем указать *номера* столбцов вместо их *имен*.

* По умолчанию столбец `variable` является фактором. Установите для
  аргумента `variable.factor` значение `FALSE`, если вместо этого вы хотите
  вернуть вектор типа *`character`*.

* По умолчанию «расплавленные» столбцы автоматически получают имена
  `variable` и `value`.

* `melt` сохраняет атрибуты столбцов в результатах.

#### — Назовём столбцы `variable` и `value` соответственно `child` и `dob`


```{r}
DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"),
               variable.name = "child", value.name = "dob")
DT.m1
```

* По умолчанию, если один из аргументов `id.vars` или `measure.vars`
  отсутствует, остальные столбцы *автоматически достаются* отсутствующему
  аргументу.

* Если не указаны ни `id.vars`, ни `measure.vars`, как указано в `?melt`,
  все *не*-числовые, целые, логические столбцы достанутся `id.vars`.

    Кроме того, выдается предупреждающее сообщение, в котором выделяются столбцы, которые автоматически считаются `id.vars`.

### b) `dcast` («отливка») `data.table` (из длинной в широкую)

В предыдущем разделе мы рассмотрели, как перейти от широкой формы к
длинной. В этом разделе мы рассмотрим обратную операцию.

#### - Как нам вернуться к исходной таблице данных `DT` из `DT.m1`?

Мы бы хотели собрать все наблюдения *child*, соответствующие каждой паре
`family_id, age_mother`, вместе в одной строке. Мы можем сделать это с
помощью `dcast` следующим образом:

```{r}
dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
```

* `dcast` использует *формульный* интерфейс. Переменные в *левой части*
  формулы соответствуют *id* (строкам), а в *правой части* — *measure*
  (столбцам).

* `value.var` обозначает столбец, который будет заполнен при приведении к
  широкому формату.

* `dcast` также старается сохранять атрибуты в результатах, где это
  возможно.

#### - Начиная с `DT.m1`, как нам узнать количество детей в каждой семье?

Вы также можете передать функцию для агрегирования в `dcast` с аргументом
`fun.aggregate`. Это особенно важно, когда переданная формула даёт больше
одного наблюдения для каждой описываемой ею ячейки.

```{r}
dcast(DT.m1, family_id ~ ., fun.aggregate = function(x) sum(!is.na(x)), value.var = "dob")
```

Ознакомьтесь с `?dcast`, чтобы узнать о других полезных аргументах и
примерах.

## 2. Ограничения прежнего подхода `melt`/`dcast`

До сих пор мы видели функции `melt` и `dcast`, которые эффективно
реализованы для `data.table`, используя внутренние механизмы `data.table`
(*быстрая поразрядная сортировка*, *двоичный поиск* и т.д.).

Однако бывают ситуации, когда требуемая операция не может быть выражена
простым способом. Например, рассмотрим `data.table`, показанную ниже:

```{r}
s2 <- "family_id age_mother name_child1 name_child2 name_child3 gender_child1 gender_child2 gender_child3
1         30         Ben        Anna          NA             1             2            NA
2         27         Tom          NA          NA             2            NA            NA
3         26         Lia         Sam         Amy             2             2             1
4         32         Max         Zoe         Joe             1             1             1
5         29         Dan         Eva          NA             2             1            NA"
DT <- fread(s2)
DT
## 1 = female, 2 = male
```

Предположим, Вы хотите объединить (`melt`) все столбцы `name` вместе, а
также столбцы `gender` вместе. Используя описанные выше функции, можно
сделать примерно следующее:

```{r}
DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"))
DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)]
DT.c1 = dcast(DT.m1, family_id + age_mother + child ~ variable, value.var = "value")
DT.c1

str(DT.c1) ## столбец gender теперь класса character!
```

#### Проблемы

1. Мы хотели объединить все столбцы типа `name` и `gender` попарно. Вместо
   этого мы объединяем их *все*, а затем снова разделяем. Думаю, легко
   заметить, что это довольно запутанный путь (и неэффективный).

    В качестве аналогии представьте, что у вас есть шкаф с четырьмя полками для одежды, и вы хотите сложить одежду с полок 1 и 2 вместе (в 1), а также 3 и 4 вместе (в 3). Мы делаем примерно следующее: объединяем всю одежду в одну кучу, а затем рассовываем ее обратно по полкам 1 и 3!

2. Столбцы, передаваемые в `melt`, могут быть разных типов. Если «сплавлять»
   их вместе, результат будет приведён к единому типу.

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

4. Наконец, мы «отливаем» набор данных. Проблема в том, что эта операция
   требует гораздо больше вычислений, чем *melt*. В частности, она требует
   вычисления порядка переменных в формуле, а это дорого.

Между прочим, `stats::reshape` способна выполнить эту операцию очень простым
способом. Это чрезвычайно полезная и часто недооцененная функция. Вам
определенно стоит её попробовать!

## 3. Расширенная (новая) функциональность

### a) Улучшенный `melt`

Поскольку мы хотели бы, чтобы `data.table` выполняла эту операцию просто и
эффективно, используя тот же интерфейс, мы пошли дальше и реализовали
*дополнительную функциональность*, которая позволяет «выплавлять» несколько
столбцов *одновременно*.

#### - `melt` для нескольких столбцов одновременно

Идея довольно проста. Мы передаем список столбцов в `measure.vars`, где
каждый элемент списка содержит столбцы, которые должны быть объединены
вместе.

```{r}
colA = paste0("name_child", 1:3)
colB = paste0("gender_child", 1:3)
DT.m2 = melt(DT, measure.vars = list(colA, colB), value.name = c("name", "gender"))
DT.m2

str(DT.m2) ## тип столбца сохранён
```

* При необходимости мы можем удалить столбец `variable`.

* Эта функция реализована полностью на языке C, поэтому является *быстрой* и
  *экономичной по памяти*, а также *простой*.

#### - Использование `patterns()`

Обычно в таких задачах столбцы, которые мы хотели бы объединить, можно
выделить по шаблону. Для этого мы можем использовать функцию
`patterns()`. Приведенную выше операцию можно переписать так:

```{r}
DT.m2 = melt(DT, measure = patterns("^name", "^gender"), value.name = c("name", "gender"))
DT.m2
```

#### - Использование `measure()` для задания `measure.vars` через шаблон или разделитель

Если, как в приведенных выше данных, входные столбцы для расплавления имеют
обычные имена, то можно использовать `measure`, что позволяет указать
столбцы для «переплавки» через разделитель или регулярное
выражение. Например, рассмотрим данные по ирисам,

```{r}
(two.iris = data.table(datasets::iris)[c(1,150)])
```

Про ирисы известно четыре числовых столбца, имена которых имеют регулярную
структуру: сначала цветочная часть, затем точка, затем длина или
ширина. Чтобы указать, что мы хотим «расплавить» эти четыре столбца, мы
можем использовать `measure` с `sep="."`, что означает использование
`strsplit` для всех имен столбцов; те столбцы, в которых после разбиения
получается максимальное количество групп, будут использованы в качестве
`measure.vars`:

```{r}
melt(two.iris, measure.vars = measure(part, dim, sep="."))
```

Первые два аргумента `measure` в приведенном выше коде (`part` и `dim`)
используются для названия выходных столбцов; количество аргументов должно
быть равно максимальному количеству групп после разбиения с помощью `sep`.

Если мы хотим получить два столбца значений, по одному для каждой части, мы
можем использовать специальное ключевое слово `value.name`, которое означает
вывод столбца значений для каждого уникального имени, найденного в данной
группе:

```{r}
melt(two.iris, measure.vars = measure(value.name, dim, sep="."))
```

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

```{r}
melt(two.iris, measure.vars = measure(part, value.name, sep="."))
```

Возвращаясь к примеру с данными о семьях и детях, можно предложить более
сложное использование `measure`, включающее функцию, которая используется
для преобразования строковых значений `child` в целые числа:

```{r}
DT.m3 = melt(DT, measure.vars = measure(value.name, child=as.integer, sep="_child"))
DT.m3
```

В приведенном выше коде мы использовали `sep="_child"`, что привело к
«расплавлению» только столбцов, содержащих эту строку (шесть имен столбцов,
разбитых на две группы). Аргумент `child=as.integer` означает, что вторая
группа приведет к созданию выходного столбца с именем `child` и значениями,
определенными путем подстановки строк из этой группы в функцию `as.integer`.

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

```{r}
(who <- data.table(id=1, new_sp_m5564=2, newrel_f65=3))
melt(who, measure.vars = measure(
  diagnosis, gender, ages, pattern="new_?(.*)_(.)(.*)"))
```

Аргумент `pattern` должен быть Perl-совместимым регулярным выражением,
захватывающим столько же групп (подвыражений, заключенных в скобки), сколько
и других аргументов (имен групп). В приведенном ниже коде показано, как
использовать более сложное регулярное выражение с пятью группами, два
числовых столбца вывода и анонимную функцию преобразования типов,

```{r}
melt(who, measure.vars = measure(
  diagnosis, gender, ages,
  ymin=as.numeric,
  ymax=function(y) ifelse(nzchar(y), as.numeric(y), Inf),
  pattern="new_?(.*)_(.)(([0-9]{2})([0-9]{0,2}))"
))
```

### b) Улучшенный `dcast`

Отлично! Теперь мы можем «расплавлять» данные в нескольких столбцах
одновременно. Теперь, если взять набор данных `DT.m2`, полученный выше, как
мы можем вернуться к тому же формату, что и исходные данные, с которых мы
начали?

Если мы используем обычный `dcast`, то нам придется дважды выполнять «отлив»
и связывать результаты вместе. Но это опять же многословно, не совсем просто
и к тому же неэффективно.

#### - «Отлив» нескольких `value.var` одновременно

Теперь можно передавать **несколько столбцов `value.var`** для `dcast`,
чтобы операции выполнялись внутри `data.table` и эффективным образом.

```{r}
## новая функция 'dcast' - несколько value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("name", "gender"))
DT.c2
```

* Атрибуты по возможности сохраняются в результате.

* Всё выполнено внутренними средствами, быстро и экономично по памяти.

#

#### Несколько функций в `fun.aggregate`:

Вы также можете передать *несколько функций* в `fun.aggregate` для
`dcast`. Ознакомьтесь с примерами в `?dcast`, которые иллюстрируют эту
функциональность.

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

#

***
