Когда следует использовать setDT() вместо data.table() для создания таблицы данных?

Мне трудно понять суть функции setDT(). Когда я читаю код на SO, я часто сталкиваюсь с использованием setDT() для создания data.table. Конечно, использование data.table() вездесущ. Я чувствую, что я твердо понимаю природу data.table(), но релевантность setDT() ускользает от меня. ?setDT говорит мне следующее:

setDT преобразует списки (как именные, так и неназванные) и data.frames в data.tables по ссылке.

а также:

В языке data.table все заданные * функции меняют свой вход по ссылке. То есть никакой копии не делается вообще, кроме временной рабочей памяти, которая равна одному столбцу.

Итак, это заставляет меня думать, что я должен использовать setDT() для создания data.table, правильно? Является ли setDT() просто списком для преобразователя data.table?

library(data.table)

a <- letters[c(19,20,1,3,11,15,22,5,18,6,12,15,23)]
b <- seq(1,41,pi)
ab <- data.frame(a,b)
d <- data.table(ab)
e <- setDT(ab)

str(d)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ a: Factor w/ 12 levels "a","c","e","f",..: 9 10 1 2 5 7 11 3 8 4 ...
# $ b: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

str(e)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ a: Factor w/ 12 levels "a","c","e","f",..: 9 10 1 2 5 7 11 3 8 4 ...
# $ b: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

По-видимому, никакой разницы в этом случае нет. В другом случае разница очевидна:

ba <- list(a,b)
f <- data.table(ba)
g <- setDT(ba)

str(f)
#Classes ‘data.table’ and 'data.frame': 2 obs. of  1 variable:
# $ ba:List of 2
#  ..$ : chr  "s" "t" "a" "c" ...
#  ..$ : num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

str(g)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ V1: chr  "s" "t" "a" "c" ...
# $ V2: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

Когда следует использовать setDT()? Что делает setDT() релевантным? Почему бы просто не сделать оригинальную функцию data.table() способной выполнять то, что может сделать setDT()?

Ответы

Ответ 1

Обновление:

@Roland делает несколько замечательных замечаний в разделе комментариев, и сообщение лучше для них. Хотя я изначально сосредоточился на проблемах переполнения памяти, он отметил, что даже если этого не происходит, управление памятью разных копий занимает значительное время, что является более распространенной повседневной заботой. Также были добавлены примеры обеих проблем.

Мне нравится этот вопрос в stackoverflow, потому что я думаю, что это действительно предотвращение в R при работе с большими наборами данных. 😊 Те, кто не знаком с data.table семейством операций set, могут воспользоваться этим обсуждением!

При работе с большими наборами данных следует использовать setDT(), которые занимают значительное количество ОЗУ, поскольку операция будет модифицировать каждый объект на месте, сохраняя память. Для данных, которые являются очень небольшим процентом ОЗУ, использование data.tables copy-and-modify прекрасное.

Создание функции setDT было фактически вдохновлено следующим потоком, который заключается в работе с большим набором данных (несколько ГБ). Вы увидите мелодию Мэтта Доула, предложив имя 'setDT'.

Преобразование фрейма данных в таблицу данных без копирования

Немного больше:

С R данные хранятся в памяти. Это значительно ускоряет работу, поскольку оперативная память намного быстрее, чем устройства хранения данных. Однако проблема может возникнуть, когда один набор данных является большой частью ОЗУ. Зачем? Поскольку база R имеет тенденцию делать копии каждого data.frame, когда к ним применяются некоторые операции. Это улучшилось после версии 3.1, но адресация выходит за рамки этой публикации. Если вы вытаскиваете несколько data.frame или list в один data.frame или data.table, использование вашей памяти будет расширяться довольно быстро, потому что в какой-то момент во время работы в ОЗУ имеется несколько копий ваших данных. Если набор данных достаточно велик, у вас может закончиться нехватка памяти, когда будут созданы все копии, и ваш стек будет переполняться. См. Пример ниже. Мы получаем ошибку, и исходный адрес памяти и класс объекта не изменяются.

> N <- 1e8
> P <- 1e2
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> 
> pryr::object_size(data)
800 MB
> 
> tracemem(data)
[1] "<0000000006D2DF18>"
> 
> data <- data.table(data)
Error: cannot allocate vector of size 762.9 Mb
> 
> tracemem(data)
[1] "<0000000006D2DF18>"
> class(data)
[1] "data.frame"
>

Возможность просто изменить объект на месте без копирования - большое дело. Это то, что setDT делает, когда требуется list или data.frame и возвращает data.table. Тот же пример, что и выше, используя setDT, теперь работает нормально и без ошибок. Изменение адреса класса и памяти и отсутствие копий.

> tracemem(data)
[1] "<0000000006D2DF18>"
> class(data)
[1] "data.frame"
> 
> setDT(data)
>  
> tracemem(data)
[1] "<0000000006A8C758>"
> class(data)
[1] "data.table" "data.frame"

@Roland указывает, что для большинства людей большая проблема - это скорость, которая страдает как побочный эффект такого интенсивного использования управления памятью. Ниже приведен пример с меньшими данными, которые не приводят к сбою процессора и иллюстрируют, насколько быстрее setDT для этого задания. Обратите внимание на результаты "tracemem" после data <- data.table(data), делая копии data. Сравните это с setDT(data), который не печатает одну копию. Затем мы вызываем tracemem(data), чтобы увидеть новый адрес памяти.

> N <- 1e5
> P <- 1e2
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> pryr::object_size(data)
808 kB

> # data.table method
> tracemem(data)
[1] "<0000000019098438>"
> data <- data.table(data)
tracemem[0x0000000019098438 -> 0x0000000007aad7d8]: data.table 
tracemem[0x0000000007aad7d8 -> 0x0000000007c518b8]: copy as.data.table.data.frame as.data.table data.table 
tracemem[0x0000000007aad7d8 -> 0x0000000018e454c8]: as.list.data.frame as.list vapply copy as.data.table.data.frame as.data.table data.table 
> class(data)
[1] "data.table" "data.frame"
> 
> # setDT method
> # back to data.frame
> data <- as.data.frame(data)
> class(data)
[1] "data.frame"
> tracemem(data)
[1] "<00000000125BE1A0>"
> setDT(data)
> tracemem(data)
[1] "<00000000125C2840>"
> class(data)
[1] "data.table" "data.frame"
> 

Как это влияет на время? Как мы видим, setDT для него намного быстрее.

> # timing example
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> microbenchmark(setDT(data), data <- data.table(data))
Unit: microseconds
                     expr       min         lq        mean    median            max neval        uq
              setDT(data)    49.948    55.7635    69.66017    73.553        100.238   100    79.198
 data <- data.table(data) 54594.289 61238.8830 81545.64432 64179.131     611632.427   100 68647.917

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

library(data.table)    
vignette("datatable-reference-semantics")

Это отличный вопрос, и те, кто думает об использовании R с большими наборами данных или просто хотят ускорить активацию данных, могут извлечь выгоду из понимания значительных улучшений производительности data.table эталонной семантики.

Ответ 2

setDT() не заменяет data.table(). Это более эффективная замена для as.data.table(), которая может использоваться с определенными типами объектов.

  • mydata <- as.data.table(mydata) скопирует объект за mydata, преобразует копию в data.table, а затем изменит символ mydata, чтобы указать на копию.
  • setDT(mydata) изменит объект позади mydata на data.table. Копирование не выполняется.

Итак, какая реалистичная ситуация для использования setDT()? Если вы не можете управлять классом исходных данных. Например, большинство пакетов для работы с базами данных дают вывод data.frame. В этом случае ваш код будет похож на

mydata <- dbGetQuery(conn, "SELECT * FROM mytable")  # Returns a data.frame
setDT(mydata)                                        # Make it a data.table

Когда вы должны использовать as.data.table(x)? Всякий раз, когда x не является list или data.frame. Наиболее частое использование - для матриц.