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.


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 (заполненная искусственными данными), как показано ниже:

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 следующим образом:

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)

— Назовём столбцы 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

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

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

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

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

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

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

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

dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob")

Ознакомьтесь с ?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
## 1 = female, 2 = male

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

DT.m1 = melt(DT, id = 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 теперь класса IDate!

Проблемы

  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 = list(colA, colB), value.name = c("dob", "gender"))
DT.m2

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

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

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

DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2

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

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

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

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

melt(two.iris, measure.vars = measure(part, dim, sep="."))

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

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

melt(two.iris, measure.vars = measure(value.name, dim, sep="."))

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

melt(two.iris, measure.vars = measure(part, value.name, sep="."))

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

DT.m3 = melt(DT, measure = measure(value.name, child=as.integer, sep="_child"))
DT.m3

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

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

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

Аргумент 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}))"
))

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

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

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

setDTthreads(.old.th)




Rdatatable/data.table documentation built on March 1, 2025, 6:11 a.m.