Подтвердить что ты не робот

Изменяет ли mutate tbl по ссылке?

То, что мне действительно нравится в data.table, - это идиома := для изменения таблицы по ссылке без необходимости в дорогостоящих копиях. Насколько я понимаю, это один из аспектов, который делает data.table настолько сверхбыстрым по сравнению с другими методами.

Теперь я начал играть с пакетом dplyr, который, кажется, был одинаково совершенен. Но поскольку результаты все еще нужно назначать с помощью оператора <-, я ожидал, что производительность будет снижена на этом уровне. Однако, кажется, нет никого.

В качестве примера:

library(dplyr)
library(Lahman)
library(microbenchmark)
library(ggplot2)

df <- Batting[ c("yearID", "teamID", "G_batting") ]

mb <- microbenchmark(
  dplyr = {
    tb <- tbl_df( df )
    tb <- tb %.%
      group_by( yearID, teamID ) %.%
      mutate( G_batting = max(G_batting) )
  },
  data.table = {
    dt <- as.data.table( df )
    dt[ , G_batting := max(G_batting), by = list( yearID, teamID ) ]
  },
  times = 500
)

qplot( data = mb, x = expr, y = time * 1E-6, geom = "boxplot", ylab="time [ms]", xlab = "approach" )

enter image description here

Мне просто интересно, как это возможно? Или есть концептуальная ошибка в том, как я ориентируюсь? Непонятно ли мое понимание <-?

4b9b3361

Ответ 1

Хороший вопрос. В общем, я бы оценил размер данных, который достаточно велик, чтобы полностью (полностью) не входить в кеш. Посмотрите здесь в разделе "Начальная настройка". Не имеет смысла сравнивать инструменты, разработанные для (в памяти) больших данных для запуска задач, выполняемых в миллисекундах. Мы планируем провести сравнительный анализ относительно более крупных данных в будущем.

Кроме того, если вы намерены выяснить, выполняет ли mutate копию, то все, что вам нужно сделать, это проверить address до и после (это можно сделать с помощью .Internal(inspect(.)) в базе R или используя функцию changes() в dplyr).


В том, делается ли копия или нет:

Здесь есть две разные вещи. A) создание нового столбца и B) изменение существующего столбца.

A) Создание нового столбца:

require(dplyr)
require(data.table)
df <- tbl_df(data.frame(x=1:5, y=6:10))

df2 <- mutate(df, z=1L)
changes(df, df2)
# Changed variables:
#           old new
# z             0x105ec36d0

Он сообщает вам, что никаких изменений в адресах x и y нет, и указывает z, который мы только что добавили. Что здесь происходит?

dplyr неглубоко копирует data.frame, а затем добавляет новый столбец. Неглубокая копия в отличие от глубокой копии просто копирует вектор указателей столбцов, а не самих данных. Поэтому он должен быть быстрым. В основном df2 создается с тремя столбцами, где первые два столбца указывают на то же адресное местоположение, что и df, а третий столбец только что был создан.

С другой стороны, data.table не имеет мелкой копии, так как он изменяет столбец по ссылке (на месте). data.table также (умно) перераспределяет список векторов столбцов, который позволяет быстро добавлять (новые) столбцы по ссылке.

Не должно быть большой разницы во времени до мелкой копии, если у вас слишком много столбцов. Вот небольшой ориентир на 5000 столбцов с 1e4 строками:

require(data.table) # 1.8.11
require(dplyr)      # latest commit from github

dt <- as.data.table(lapply(1:5e3, function(x) sample(1e4)))
ans1 <- sapply(1:1e2, function(x) {
    dd <- copy(dt) # so as to create the new column each time
    system.time(set(dd, i=NULL, j="V1001", value=1L))['elapsed'] 
    # or equivalently of dd[, V1001 := 1L]
})

df <- tbl_df(as.data.frame(dt))
ans2 <- sapply(1:1e2, function(x) {
    system.time(mutate(df, V1001 = 1L))['elapsed']
})
> summary(ans1) # data.table
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
0.00000 0.00000 0.00100 0.00061 0.00100 0.00100
> summary(ans2) # dplyr
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
0.03800 0.03900 0.03900 0.04178 0.04100 0.07900

Здесь вы можете увидеть разницу в "среднем времени" (0.00061 против 0.04178).

B) Изменить существующий столбец:

df2 <- mutate(df, y=1L)
changes(df, df2)
# Changed variables:
#           old         new
# y         0x105e5a850 0x105e590e0 

Он сообщает вам, что y был изменен - ​​была сделана копия столбца y. Для изменения значений y необходимо было создать новое место памяти, поскольку оно указывало на то же место, что и раньше, на df y.

Однако, поскольку data.table изменяется на месте, в случае (B) копия не будет сделана. Он изменит df на месте. Поэтому, если вы изменяете столбцы, вы должны увидеть разницу в производительности.

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

И из-за этого было бы невозможно изменить значения некоторых строк определенного столбца data.frame без глубокой копии. То есть:

DT[x >= 5L, y := 1L] # y is an existing column

Это невозможно сделать без полной копии data.frame с использованием базовых R и dplyr, насколько мне известно.


Кроме того, рассмотрите набор данных из двух столбцов размером 20 ГБ (два столбца каждый 10 ГБ) на машине с 32 ГБ ОЗУ. Философия data.table заключается в том, чтобы предоставить способ изменить подмножество этих столбцов 10GB по ссылке, не копируя ни одного столбца один раз. Копия одного столбца потребует дополнительных 10 ГБ и может выйти из строя с out-out-memory, не говоря уже о быстроте или нет. Эта концепция (:=) аналогична UPDATE в SQL.

Ответ 2

Чтобы понять, что происходит, вам нужно понять, что именно копируется. Кадр данных на самом деле довольно дешев для копирования, потому что он в основном представляет собой массив указателей на столбцы. Выполнение мелкой копии кадра данных очень дешево, потому что вам просто нужно скопировать эти указатели.

Однако большинство базовых функций R выполняют глубокую копию. Поэтому, когда вы делаете:

df <- data.frame(x = 1:10, y = 1:10)
transform(df, z = x + y)

не только R копирует кадр данных, он фактически копирует каждый отдельный столбец. dplyr предоставляет функцию changes(), чтобы сделать это легче. Для каждого столбца в кадре данных он отображает ячейку памяти, в которой находится этот столбец. Если он изменился, то скопирован полный столбец:

df2 <- transform(df, z = x + y)
changes(df, df2)
#> Changed variables:
#>           old            new           
#> x         0x7fb19adcd378 0x7fb19ab9bcb8
#> y         0x7fb19adcd3d0 0x7fb19ab9bd10
#> z                        0x7fb19ab9bd68
#> 
#> Changed attributes:
#>           old            new           
#> names     0x7fb19adcce98 0x7fb1944e4558
#> row.names 0x7fb19ab2bd10 0x7fb19ab2bf20
#> class     0x7fb19ad5d208 0x7fb19ab51b28

Если мы делаем то же самое в dplyr, исходные столбцы не копируются:

df3 <- dplyr::mutate(df, z = x + y)
changes(df, df3)
#> Changed variables:
#>           old new           
#> z             0x7fb19adcd060
#> 
#> Changed attributes:
#>           old            new           
#> names     0x7fb19adcce98 0x7fb1944e8b18
#> row.names 0x7fb19ab9c0d8 0x7fb19ab9c340
#> class     0x7fb19ad5d208 0x7fb19ad69408

Это делает dplyr намного быстрее, чем основание R.

Data.table немного быстрее, потому что она позволяет вам изменять таблицу данных на месте - ей даже не нужно копировать указатели на столбцы. Я думаю, что не модифицирование на месте делает dplyr немного легче понять (потому что он сохраняет обычную семантику R), ценой немного медленнее (но стоимость растет с количеством столбцов, а не количеством строк).