Skip to contents

Данное руководство также доступно на следующих языках: en | fr | ru

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


Данные

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

Введение

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

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

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

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

  3. Наконец, обратим внимание на новые улучшения методов 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"
Проблемы
  1. Мы хотели объединить все столбцы типа dob и 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, где каждый элемент списка содержит столбцы, которые должны быть объединены вместе.

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, которые иллюстрируют эту функциональность.