4 Эффективное переформатирование данных при помощи data.table

В этой виньетке обсуждается обычное использование функций для переформатирования данных в таблицах data.tables melt (из “широкого” формата в “длинный”) и dcast (из “длинного” формата в широкий), а также новая расширенная функциональность для множественных столбцов, доступная начиная с v1.9.6.

4.1 Данные

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

4.2 Введение

Функции melt и dcast для таблиц data.tables являются расширениями соответствующих функций из пакета reshape2.

В этой виньетке мы:

  1. сперва кратко рассмотрим переформатирование таблиц data.tables из “широкого” формата в “длинный”, и наоборот

  2. затем рассмотрим сценарии, когда базовая функциональность становится неэффективной

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

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

4.2.0.1 Обратите внимание

Начиная с v1.9.6, вам не нужно загружать пакет reshape2, чтобы использовать функции для таблиц data.tables. Вам нужно только загрузить data.table. Если вам нужно загрузить пакет reshape2 для манипуляций с матрицами или таблицами данных, убедитесь, что он загружается перед пакетом data.table.

4.3 1. Базовая функциональность

4.3.1 a) melting таблиц data.tables (переформатирование из “широкого” формата в “длинный”)

Предположим, у нас есть искусственные данные, показанные ниже:

DT = fread("https://raw.githubusercontent.com/wiki/Rdatatable/data.table/data/melt_default.csv")
DT 
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA
## dob stands for date of birth.

str(DT)
# Classes 'data.table' and 'data.frame':    5 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5
#  $ age_mother: int  30 27 26 32 29
#  $ dob_child1: chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ dob_child2: chr  "2000-01-29" NA "2004-04-05" "2009-08-27" ...
#  $ dob_child3: chr  NA NA "2007-09-02" "2012-07-21" ...
#  - attr(*, ".internal.selfref")=<externalptr>

4.3.1.1 - Переформатировать DT в “длинную” форму, в которой каждый dob является отдельным наблюдением.

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

DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"), 
                measure.vars = c("dob_child1", "dob_child2", "dob_child3"))
DT.m1
#     family_id age_mother   variable      value
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA
str(DT.m1)
# Classes 'data.table' and 'data.frame':    15 obs. of  4 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "dob_child1","dob_child2",..: 1 1 1 1 1 2 2 2 2 2 ...
#  $ value     : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  - attr(*, ".internal.selfref")=<externalptr>
  • measure.vars задает набор столбцов, которые мы хотим скомбинировать.

  • Мы также можем задать индексы столбцов вместо имен.

  • По умолчанию столбец variable имеет тип factor. Присвойте аргументу variable.factor значение FALSE, если вместо этого вы хотите вернуть символьный вектор. Аргумент variable.factor доступен только в функции melt из пакета data.table и не доступен в пакете reshape2

  • По умолчанию, “расплавленные” столбцы автоматически названы variable и value.

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

4.3.1.2 - Назвать столбцы variable и value соответственно child и dob.

DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"), 
               variable.name = "child", value.name = "dob")
DT.m1
#     family_id age_mother      child        dob
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA
  • По умолчанию, если аргумент id.vars или measure.vars пропущен, оставшиеся столбцы автоматически присваиваются пропущенному аргументу.

  • Если не задан ни аргумент id.vars, ни аргумент measure.vars, как было упомянуто в справке ?melt, столбцы всех типов, кроме numeric, integer и logical присваиваются аргументу id.vars.

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

4.3.2 b) casting таблиц data.tables (переформатирование из “длинного” формата в “широкий”)

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

4.3.2.1 - Как мы можем получить обратно исходную таблицу DT из DT.m?

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

dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA
  • dcast использует интерфейс формул. Переменные в левой части формулы представляют идентификаторы, а переменные в правой части - измерения.

  • value.var задает столбец, который заполняется при приведении к “широкому” формату.

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

4.3.2.2 - Имея DT.m, как мы можем получить количество детей в каждой семье?

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

dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob")
#    family_id .
# 1:         1 2
# 2:         2 1
# 3:         3 3
# 4:         4 3
# 5:         5 2

См. ?dcast для других полезных аргументов и дополнительных примеров.

4.4 2. Ограничения базового подхода melt/dcast

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

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

DT = fread("https://raw.githubusercontent.com/wiki/Rdatatable/data.table/data/melt_enhanced.csv")
DT
#    family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# 1:         1         30 1998-11-26 2000-01-29         NA             1             2            NA
# 2:         2         27 1996-06-22         NA         NA             2            NA            NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
# 5:         5         29 2000-12-05 2005-02-28         NA             2             1            NA
## 1 = female, 2 = male 

И вы хотите скомбинировать (“слить”): столбцы dob; столбцы gender. С использованием базовой функциональности мы могли бы сделать что-нибудь подобное:

DT.m1 = melt(DT, id = c("family_id", "age_mother"))
# Warning in melt.data.table(DT, id = c("family_id", "age_mother")): 'measure.vars' [dob_child1,
# dob_child2, dob_child3, gender_child1, gender_child2, gender_child3] are not all of the same
# type. By order of hierarchy, the molten data value column will be of type 'character'. All measure
# variables not of type 'character' will be coerced to. Check DETAILS in ?melt.data.table for more on
# coercion.
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
#     family_id age_mother  child        dob gender
#  1:         1         30 child1 1998-11-26      1
#  2:         1         30 child2 2000-01-29      2
#  3:         1         30 child3         NA     NA
#  4:         2         27 child1 1996-06-22      2
#  5:         2         27 child2         NA     NA
#  6:         2         27 child3         NA     NA
#  7:         3         26 child1 2002-07-11      2
#  8:         3         26 child2 2004-04-05      2
#  9:         3         26 child3 2007-09-02      1
# 10:         4         32 child1 2004-10-10      1
# 11:         4         32 child2 2009-08-27      1
# 12:         4         32 child3 2012-07-21      1
# 13:         5         29 child1 2000-12-05      2
# 14:         5         29 child2 2005-02-28      1
# 15:         5         29 child3         NA     NA

str(DT.c1) ## gender column is character type now!
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 1 1 2 2 2 3 3 3 4 ...
#  $ age_mother: int  30 30 30 27 27 27 26 26 26 32 ...
#  $ child     : chr  "child1" "child2" "child3" "child1" ...
#  $ dob       : chr  "1998-11-26" "2000-01-29" NA "1996-06-22" ...
#  $ gender    : chr  "1" "2" NA "2" ...
#  - attr(*, ".internal.selfref")=<externalptr> 
#  - attr(*, "sorted")= chr  "family_id" "age_mother" "child"

4.4.0.1 Вопросы

  1. Мы хотели объединить все столбцы типа dob, а затем все столбцы типа gender. Вместо этого мы сперва объединили их все вместе, а затем разделили снова. Я думаю, легко заметить, что это весьма окольный и неэффективный путь.

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

  1. Объединяемые столбцы могут иметь разный тип, как в этом случае (символьный и числовой тип). При “расплавлении” столбцов их тип был приведен, как объясняется в предупреждении и показано в выводе str(DT.c1), где gender был сконвертирован в символьный тип.

  2. Мы создаем дополнительный столбец путем разделения variable на два новых столбца с весьма загадочной целью. Мы делаем это для использования функции dcast() на следующем шаге.

  3. Наконец, мы собрали набор данных. Но дело в том, что это гораздо более вычислительно дорогостоящая операция, чем “плавление”. В частности, это требует дорогостоящего вычисления порядка переменных в формуле.

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

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

4.5.1 a) Расширенная функция melt

Поскольку мы хотим, чтобы таблицы data.tables выполняли эти операции быстро и эффективно, используя тот же интерфейс, мы пошли дальше и реализовали дополнительную функциональность, где мы можем применять функцию melt() для нескольких столбцов одновременно.

4.5.1.1 - melt() для нескольких столбцов одновременно

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

colA = paste("dob_child", 1:3, sep="")
colB = paste("gender_child", 1:3, sep="")
DT.m2 = melt(DT, measure = list(colA, colB), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

str(DT.m2) ## col type is preserved
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "1","2","3": 1 1 1 1 1 2 2 2 2 2 ...
#  $ dob       : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ gender    : int  1 2 2 1 2 2 NA 2 1 1 ...
#  - attr(*, ".internal.selfref")=<externalptr>

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

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

DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

Вот и все!

  • Мы можем удалить столбец variable, если нужно.

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

4.5.2 b) Расширенная функция dcast

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

Используя базовую функциональность dcast, мы должны были бы “слить” данные дважды, а затем объединить результаты. Но это, опять же, многословно, сложно и неэффективно.

4.5.2.1 - Слияние нескольких value.var одновременно.

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

## new 'cast' functionality - multiple value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender"))
DT.c2
#    family_id age_mother      dob_1      dob_2      dob_3 gender_1 gender_2 gender_3
# 1:         1         30 1998-11-26 2000-01-29         NA        1        2       NA
# 2:         2         27 1996-06-22         NA         NA        2       NA       NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02        2        2        1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21        1        1        1
# 5:         5         29 2000-12-05 2005-02-28         NA        2        1       NA
  • Атрибуты сохраняются для результатов, когда это возможно.

  • Обеспечивается внутреннее и эффективное выполнение - не только быстрое, но и эффективно использующее память.

4.5.2.2 Множественные функции для fun.aggregate:

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