Быстрое изменение формы данных с помощью data.table
2025-04-18
Source:vignettes/ru/datatable-reshape.Rmd
datatable-reshape.Rmd
Данное руководство также доступно на следующих языках: en | fr | ru
В этом руководстве рассматривается стандартное использование функций
изменения формы данных melt
(из широкой в длинную) и
dcast
(из длинной в широкую) для класса
data.table, а также новые расширенные
возможности «расплавления» и «отлива» для нескольких
столбцов, доступные с v1.9.6
.
Введение
Функции melt
и dcast
для
data.table
предназначены для изменения формы таблиц между
«длинными» и «широкими»; реализации специально разработаны с учетом
больших данных в памяти (например, 10 ГБ).
В этом описании мы
Сначала кратко рассмотрим стандартные операции
melt
иdcast
дляdata.table
, чтобы преобразовать их из широкого формата в длинный и наоборотРассмотрим сценарии, в которых текущие функции становятся громоздкими и неэффективными
Наконец, обратим внимание на новые улучшения методов
melt
иdcast
, позволяющие обрабатывать несколько столбцов таблицdata.table
одновременно.
Расширенная функциональность соответствует философии
data.table
, которая заключается в эффективном и понятном
выполнении операций.
1. Функциональность по умолчанию
a) «плавление» данных в таблице (от широких к длинным)
Предположим, у нас есть data.table
(заполненная
искусственными данными), как показано ниже:
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
# family_id age_mother dob_child1 dob_child2 dob_child3
# <int> <int> <IDat> <IDat> <IDat>
# 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 значит "дата рождения" (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: IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" ...
# $ dob_child2: IDate, format: "2000-01-29" NA "2004-04-05" ...
# $ dob_child3: IDate, format: NA NA "2007-09-02" ...
# - attr(*, ".internal.selfref")=<externalptr>
— Преобразование 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
# <int> <int> <fctr> <IDat>
# 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 : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" ...
# - attr(*, ".internal.selfref")=<externalptr>
measure.vars
указывает набор столбцов, которые мы хотели бы свернуть (или объединить) вместе.Мы также можем указать номера столбцов вместо их имен.
По умолчанию столбец
variable
является фактором. Установите для аргументаvariable.factor
значениеFALSE
, если вместо этого вы хотите вернуть вектор типаcharacter
.По умолчанию «расплавленные» столбцы автоматически получают имена
variable
иvalue
.melt
сохраняет атрибуты столбцов в результатах.
— Назовём столбцы 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
# <int> <int> <fctr> <IDat>
# 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
, все не-числовые, целые, логические столбцы достанутсяid.vars
.Кроме того, выдается предупреждающее сообщение, в котором выделяются столбцы, которые автоматически считаются
id.vars
.
b) dcast
(«отливка») data.table
(из
длинной в широкую)
В предыдущем разделе мы рассмотрели, как перейти от широкой формы к длинной. В этом разделе мы рассмотрим обратную операцию.
- Как нам вернуться к исходной таблице данных DT
из
DT.m1
?
Мы бы хотели собрать все наблюдения child, соответствующие
каждой паре family_id, age_mother
, вместе в одной строке.
Мы можем сделать это с помощью dcast
следующим образом:
dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
# Key: <family_id, age_mother>
# family_id age_mother dob_child1 dob_child2 dob_child3
# <int> <int> <IDat> <IDat> <IDat>
# 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
использует формульный интерфейс. Переменные в левой части формулы соответствуют id (строкам), а в правой части — measure (столбцам).value.var
обозначает столбец, который будет заполнен при приведении к широкому формату.dcast
также старается сохранять атрибуты в результатах, где это возможно.
- Начиная с DT.m1
, как нам узнать количество детей в
каждой семье?
Вы также можете передать функцию для агрегирования в
dcast
с аргументом fun.aggregate
. Это особенно
важно, когда переданная формула даёт больше одного наблюдения для каждой
описываемой ею ячейки.
dcast(DT.m1, family_id ~ ., fun.aggregate = function(x) sum(!is.na(x)), value.var = "dob")
# Key: <family_id>
# family_id .
# <int> <int>
# 1: 1 2
# 2: 2 1
# 3: 3 3
# 4: 4 3
# 5: 5 2
Ознакомьтесь с ?dcast
, чтобы узнать о других полезных
аргументах и примерах.
2. Ограничения прежнего подхода
melt
/dcast
До сих пор мы видели функции melt
и dcast
,
которые эффективно реализованы для data.table
, используя
внутренние механизмы data.table
(быстрая поразрядная
сортировка, двоичный поиск и т.д.).
Однако бывают ситуации, когда требуемая операция не может быть
выражена простым способом. Например, рассмотрим data.table
,
показанную ниже:
s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
1 30 1998-11-26 2000-01-29 NA 1 2 NA
2 27 1996-06-22 NA NA 2 NA NA
3 26 2002-07-11 2004-04-05 2007-09-02 2 2 1
4 32 2004-10-10 2009-08-27 2012-07-21 1 1 1
5 29 2000-12-05 2005-02-28 NA 2 1 NA"
DT <- fread(s2)
DT
# family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# <int> <int> <IDat> <IDat> <IDat> <int> <int> <int>
# 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
Предположим, Вы хотите объединить (melt
) все столбцы
dob
вместе, а также столбцы gender
вместе.
Используя описанные выше функции, можно сделать примерно следующее:
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
# Key: <family_id, age_mother, child>
# family_id age_mother child dob gender
# <int> <int> <char> <IDat> <IDat>
# 1: 1 30 child1 1998-11-26 1970-01-02
# 2: 1 30 child2 2000-01-29 1970-01-03
# 3: 1 30 child3 <NA> <NA>
# 4: 2 27 child1 1996-06-22 1970-01-03
# 5: 2 27 child2 <NA> <NA>
# 6: 2 27 child3 <NA> <NA>
# 7: 3 26 child1 2002-07-11 1970-01-03
# 8: 3 26 child2 2004-04-05 1970-01-03
# 9: 3 26 child3 2007-09-02 1970-01-02
# 10: 4 32 child1 2004-10-10 1970-01-02
# 11: 4 32 child2 2009-08-27 1970-01-02
# 12: 4 32 child3 2012-07-21 1970-01-02
# 13: 5 29 child1 2000-12-05 1970-01-03
# 14: 5 29 child2 2005-02-28 1970-01-02
# 15: 5 29 child3 <NA> <NA>
str(DT.c1) ## столбец gender теперь класса IDate!
# 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 : IDate, format: "1998-11-26" "2000-01-29" NA ...
# $ gender : IDate, format: "1970-01-02" "1970-01-03" NA ...
# - attr(*, ".internal.selfref")=<externalptr>
# - attr(*, "sorted")= chr [1:3] "family_id" "age_mother" "child"
Проблемы
-
Мы хотели объединить все столбцы типа
dob
иgender
соответственно. Вместо этого мы объединяем их все, а затем снова разделяем. Думаю, легко заметить, что это довольно запутанный путь (и неэффективный).В качестве аналогии представьте, что у вас есть шкаф с четырьмя полками для одежды, и вы хотите сложить одежду с полок 1 и 2 вместе (в 1), а также 3 и 4 вместе (в 3). Мы делаем примерно следующее: объединяем всю одежду в одну кучу, а затем рассовываем ее обратно по полкам 1 и 3!
Столбцы, передаваемые в
melt
, могут быть разных типов. Если «сплавлять» их вместе, результат будет приведён к единому типу.Нам приходится создавать дополнительный столбец, разбивая столбец
variable
на два других, назначение которых совершенно неочевидно. Мы делаем это потому, что он нужен нам для вызоваdcast
на следующем шаге.Наконец, мы «отливаем» набор данных. Проблема в том, что эта операция требует гораздо больше вычислений, чем melt. В частности, она требует вычисления порядка переменных в формуле, а это дорого.
Между прочим, stats::reshape
способна выполнить эту
операцию очень простым способом. Это чрезвычайно полезная и часто
недооцененная функция. Вам определенно стоит её попробовать!
3. Расширенная (новая) функциональность
a) Улучшенный melt
Поскольку мы хотели бы, чтобы data.table
выполняла эту
операцию просто и эффективно, используя тот же интерфейс, мы пошли
дальше и реализовали дополнительную функциональность, которая
позволяет «выплавлять» несколько столбцов одновременно.
- melt
для нескольких столбцов одновременно
Идея довольно проста. Мы передаем список столбцов в
measure.vars
, где каждый элемент списка содержит столбцы,
которые должны быть объединены вместе.
colA = paste0("dob_child", 1:3)
colB = paste0("gender_child", 1:3)
DT.m2 = melt(DT, measure.vars = list(colA, colB), value.name = c("dob", "gender"))
DT.m2
# family_id age_mother variable dob gender
# <int> <int> <fctr> <IDat> <int>
# 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) ## тип столбца сохранён
# 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 : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" ...
# $ gender : int 1 2 2 1 2 2 NA 2 1 1 ...
# - attr(*, ".internal.selfref")=<externalptr>
При необходимости мы можем удалить столбец
variable
.Эта функция реализована полностью на языке C, поэтому является быстрой и экономичной по памяти, а также простой.
- Использование patterns()
Обычно в таких задачах столбцы, которые мы хотели бы объединить,
можно выделить по шаблону. Для этого мы можем использовать функцию
patterns()
. Приведенную выше операцию можно переписать
так:
DT.m2 = melt(DT, measure.vars = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2
# family_id age_mother variable dob gender
# <int> <int> <fctr> <IDat> <int>
# 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
- Использование measure()
для задания
measure.vars
через шаблон или разделитель
Если, как в приведенных выше данных, входные столбцы для расплавления
имеют обычные имена, то можно использовать measure
, что
позволяет указать столбцы для «переплавки» через разделитель или
регулярное выражение. Например, рассмотрим данные по ирисам,
(two.iris = data.table(datasets::iris)[c(1,150)])
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# <num> <num> <num> <num> <fctr>
# 1: 5.1 3.5 1.4 0.2 setosa
# 2: 5.9 3.0 5.1 1.8 virginica
Про ирисы известно четыре числовых столбца, имена которых имеют
регулярную структуру: сначала цветочная часть, затем точка, затем длина
или ширина. Чтобы указать, что мы хотим «расплавить» эти четыре столбца,
мы можем использовать measure
с sep="."
, что
означает использование strsplit
для всех имен столбцов; те
столбцы, в которых после разбиения получается максимальное количество
групп, будут использованы в качестве measure.vars
:
melt(two.iris, measure.vars = measure(part, dim, sep="."))
# Species part dim value
# <fctr> <char> <char> <num>
# 1: setosa Sepal Length 5.1
# 2: virginica Sepal Length 5.9
# 3: setosa Sepal Width 3.5
# 4: virginica Sepal Width 3.0
# 5: setosa Petal Length 1.4
# 6: virginica Petal Length 5.1
# 7: setosa Petal Width 0.2
# 8: virginica Petal Width 1.8
Первые два аргумента measure
в приведенном выше коде
(part
и dim
) используются для названия
выходных столбцов; количество аргументов должно быть равно максимальному
количеству групп после разбиения с помощью sep
.
Если мы хотим получить два столбца значений, по одному для каждой
части, мы можем использовать специальное ключевое слово
value.name
, которое означает вывод столбца значений для
каждого уникального имени, найденного в данной группе:
melt(two.iris, measure.vars = measure(value.name, dim, sep="."))
# Species dim Sepal Petal
# <fctr> <char> <num> <num>
# 1: setosa Length 5.1 1.4
# 2: virginica Length 5.9 5.1
# 3: setosa Width 3.5 0.2
# 4: virginica Width 3.0 1.8
Используя приведенный выше код, мы получаем один столбец значений для каждой части цветка. Если мы вместо этого хотим получить столбец значений для каждого измерения, мы можем сделать следующее:
melt(two.iris, measure.vars = measure(part, value.name, sep="."))
# Species part Length Width
# <fctr> <char> <num> <num>
# 1: setosa Sepal 5.1 3.5
# 2: virginica Sepal 5.9 3.0
# 3: setosa Petal 1.4 0.2
# 4: virginica Petal 5.1 1.8
Возвращаясь к примеру с данными о семьях и детях, можно предложить
более сложное использование measure
, включающее функцию,
которая используется для преобразования строковых значений
child
в целые числа:
DT.m3 = melt(DT, measure.vars = measure(value.name, child=as.integer, sep="_child"))
DT.m3
# family_id age_mother child dob gender
# <int> <int> <int> <IDat> <int>
# 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
В приведенном выше коде мы использовали sep="_child"
,
что привело к «расплавлению» только столбцов, содержащих эту строку
(шесть имен столбцов, разбитых на две группы). Аргумент
child=as.integer
означает, что вторая группа приведет к
созданию выходного столбца с именем child
и значениями,
определенными путем подстановки строк из этой группы в функцию
as.integer
.
Наконец, мы рассмотрим пример (заимствованный из пакета
tidyr
), в котором нам необходимо определить группы с
помощью регулярного выражения, а не разделителя.
(who <- data.table(id=1, new_sp_m5564=2, newrel_f65=3))
# id new_sp_m5564 newrel_f65
# <num> <num> <num>
# 1: 1 2 3
melt(who, measure.vars = measure(
diagnosis, gender, ages, pattern="new_?(.*)_(.)(.*)"))
# id diagnosis gender ages value
# <num> <char> <char> <char> <num>
# 1: 1 sp m 5564 2
# 2: 1 rel f 65 3
Аргумент pattern
должен быть Perl-совместимым регулярным
выражением, захватывающим столько же групп (подвыражений, заключенных в
скобки), сколько и других аргументов (имен групп). В приведенном ниже
коде показано, как использовать более сложное регулярное выражение с
пятью группами, два числовых столбца вывода и анонимную функцию
преобразования типов,
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}))"
))
# id diagnosis gender ages ymin ymax value
# <num> <char> <char> <char> <num> <num> <num>
# 1: 1 sp m 5564 55 64 2
# 2: 1 rel f 65 65 Inf 3
b) Улучшенный dcast
Отлично! Теперь мы можем «расплавлять» данные в нескольких столбцах
одновременно. Теперь, если взять набор данных DT.m2
,
полученный выше, как мы можем вернуться к тому же формату, что и
исходные данные, с которых мы начали?
Если мы используем обычный dcast
, то нам придется дважды
выполнять «отлив» и связывать результаты вместе. Но это опять же
многословно, не совсем просто и к тому же неэффективно.
- «Отлив» нескольких value.var
одновременно
Теперь можно передавать несколько столбцов
value.var
для dcast
, чтобы операции
выполнялись внутри data.table
и эффективным образом.
## новая функция 'dcast' - несколько value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender"))
DT.c2
# Key: <family_id, age_mother>
# family_id age_mother dob_1 dob_2 dob_3 gender_1 gender_2 gender_3
# <int> <int> <IDat> <IDat> <IDat> <int> <int> <int>
# 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
Атрибуты по возможности сохраняются в результате.
Всё выполнено внутренними средствами, быстро и экономично по памяти.
Несколько функций в fun.aggregate
:
Вы также можете передать несколько функций в
fun.aggregate
для dcast
. Ознакомьтесь с
примерами в ?dcast
, которые иллюстрируют эту
функциональность.