4 Эффективное переформатирование данных при помощи data.table
В этой виньетке обсуждается обычное использование функций для переформатирования данных в таблицах data.tables melt
(из “широкого” формата в “длинный”) и dcast
(из “длинного” формата в широкий), а также новая расширенная функциональность для множественных столбцов, доступная начиная с v1.9.6
.
4.1 Данные
Мы будет загружать наборы данных непосредственно в разделах.
4.2 Введение
Функции melt
и dcast
для таблиц data.tables являются расширениями соответствующих функций из пакета reshape2.
В этой виньетке мы:
сперва кратко рассмотрим переформатирование таблиц data.tables из “широкого” формата в “длинный”, и наоборот
затем рассмотрим сценарии, когда базовая функциональность становится неэффективной
и, наконец, рассмотрим новые улучшения для методов
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) melt
ing таблиц 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) cast
ing таблиц 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 Вопросы
- Мы хотели объединить все столбцы типа
dob
, а затем все столбцы типаgender
. Вместо этого мы сперва объединили их все вместе, а затем разделили снова. Я думаю, легко заметить, что это весьма окольный и неэффективный путь.
В качестве аналогии, представьте, что у вас есть шкаф с четырьма полками для одежды, и вы хотите собрать вместе одежду с полок 1 и 2 (на полку 1) и с полок 3 и 4 (на полку 3). Что мы делаем: собираем всю одежду вместе, а затем снова разделяем на полки 1 и 3!
Объединяемые столбцы могут иметь разный тип, как в этом случае (символьный и числовой тип). При “расплавлении” столбцов их тип был приведен, как объясняется в предупреждении и показано в выводе
str(DT.c1)
, гдеgender
был сконвертирован в символьный тип.Мы создаем дополнительный столбец путем разделения
variable
на два новых столбца с весьма загадочной целью. Мы делаем это для использования функцииdcast()
на следующем шаге.Наконец, мы собрали набор данных. Но дело в том, что это гораздо более вычислительно дорогостоящая операция, чем “плавление”. В частности, это требует дорогостоящего вычисления порядка переменных в формуле.
На самом деле, функция 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
, иллюстрирующие эту функциональность.