Понимание того, когда data.table является ссылкой на (против копии) другого data.table

194

У меня небольшие проблемы с пониманием свойств передачи по ссылке data.table. Некоторые операции, кажется, «ломают» ссылку, и я хотел бы точно понять, что происходит.

При создании data.tableиз другого data.table(через <-, затем обновляя новую таблицу :=, исходная таблица также изменяется. Это ожидается согласно:

?data.table::copy и stackoverflow: передача по ссылке-оператору-в-таблице-данных-пакета

Вот пример:

library(data.table)

DT <- data.table(a=c(1,2), b=c(11,12))
print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

newDT <- DT        # reference, not copy
newDT[1, a := 100] # modify new DT

print(DT)          # DT is modified too.
#        a  b
# [1,] 100 11
# [2,]   2 12

Однако, если я вставлю неосновную :=модификацию между <-назначением и :=строками выше, DTтеперь больше не будет изменено:

DT = data.table(a=c(1,2), b=c(11,12))
newDT <- DT        
newDT$b[2] <- 200  # new operation
newDT[1, a := 100]

print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

Так что кажется, что newDT$b[2] <- 200строка как-то «ломает» ссылку. Я предполагаю, что это как-то вызывает копию, но я хотел бы полностью понять, как R обрабатывает эти операции, чтобы не допустить потенциальных ошибок в моем коде.

Я был бы очень признателен, если бы кто-то мог мне это объяснить.

Питер Файн
источник
1
Я только что обнаружил эту «особенность», и она ужасна. В интернете широко рекомендуется использовать <-вместо =базового назначения в R (например, Google: google.github.io/styleguide/Rguide.xml#assignment ). Но это означает, что манипулирование данными.таблицы не будет функционировать так же, как манипулирование фреймами данных, и, следовательно, это далеко не полная замена фрейма данных.
Cmo

Ответы:

141

Да, это суб-назначение в R с использованием <-(или =или ->), которое делает копию всего объекта. Вы можете отследить это, используя tracemem(DT)и .Internal(inspect(DT)), как показано ниже. В data.tableособенности :=и set()правопреемник по отношению к любому объекту , они передаются. Таким образом, если этот объект был ранее скопирован (с помощью суб- <-присвоения или явного copy(DT)), то это копия, которая модифицируется по ссылке.

DT <- data.table(a = c(1, 2), b = c(11, 12)) 
newDT <- DT 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))   # precisely the same object at this point
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

tracemem(newDT)
# [1] "<0x0000000003b7e2a0"

newDT$b[2] <- 200
# tracemem[0000000003B7E2A0 -> 00000000040ED948]: 
# tracemem[00000000040ED948 -> 00000000040ED830]: .Call copy $<-.data.table $<- 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),TR,ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,200
# ATTRIB:  # ..snip..

Обратите внимание, что даже aвектор был скопирован (другое шестнадцатеричное значение указывает на новую копию вектора), даже если aон не был изменен. Даже все bкопирование было скопировано, а не просто изменены элементы, которые необходимо изменить. Это важно, чтобы избежать для больших данных, и почему :=и set()были представлены data.table.

Теперь, с нашим скопированным, newDTмы можем изменить его по ссылке:

newDT
#      a   b
# [1,] 1  11
# [2,] 2 200

newDT[2, b := 400]
#      a   b        # See FAQ 2.21 for why this prints newDT
# [1,] 1  11
# [2,] 2 400

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,400
# ATTRIB:  # ..snip ..

Обратите внимание, что все 3 шестнадцатеричных значения (вектор точек столбцов и каждый из 2 столбцов) остаются неизменными. Так что он был действительно изменен по ссылке без копий вообще.

Или мы можем изменить оригинал DTпо ссылке:

DT[2, b := 600]
#      a   b
# [1,] 1  11
# [2,] 2 600

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,600
#   ATTRIB:  # ..snip..

Эти шестнадцатеричные значения совпадают с исходными значениями, которые мы видели DTвыше. Введите example(copy)дополнительные примеры с использованием tracememи сравнение с data.frame.

Кстати, если вы, tracemem(DT)то DT[2,b:=600]вы увидите одну копию сообщили. Это копия первых 10 строк, которые printделает метод. При переносе invisible()или при вызове внутри функции или скрипта printметод не вызывается.

Все это относится и к внутренним функциям; т.е. :=и set()не копировать при записи даже внутри функций. Если вам нужно изменить локальную копию, вызовите ее x=copy(x)в начале функции. Но помните, data.tableчто для больших данных (а также преимущества быстрого программирования для небольших данных). Мы сознательно не хотим копировать большие объекты (никогда). В результате нам не нужно учитывать обычное эмпирическое правило с 3 * рабочим коэффициентом памяти. Мы стараемся использовать рабочую память размером не более одного столбца (т.е. коэффициент рабочей памяти равен 1 / ncol, а не 3).

Мэтт Доул
источник
1
Когда это поведение желательно?
Colin
Интересно, что поведение копирования всего объекта не происходит для объекта data.frame. В скопированном файле data.frame только область памяти, которая была изменена непосредственно с помощью ->назначения. Неизмененные векторы сохраняют в памяти местоположение векторов исходного data.frame. Поведение data.tables, описанное здесь, является текущим поведением на 1.12.2.
LMO
105

Просто быстрое подведение итогов.

<-с data.tableпросто как база; т. е. копия не берется до тех пор, пока не будет выполнен поднабор с помощью <-(например, изменение имен столбцов или изменение элемента, такого как DT[i,j]<-v). Затем он берет копию всего объекта, как база. Это известно как копирование при записи. Думаю, будет более известным как copy-on-subassign! Он НЕ копирует, когда вы используете специальный :=оператор или set*функции, предоставляемые data.table. Если у вас есть большие данные, вы, вероятно, хотите использовать их вместо. :=и не set*будет копировать data.table, даже в рамках функций.

Учитывая данные этого примера:

DT <- data.table(a=c(1,2), b=c(11,12))

Следующее просто «привязывает» другое имя DT2к тому же объекту данных, привязанному в данный момент к имени DT:

DT2 <- DT

Это никогда не копирует и никогда не копирует в базу. Он просто помечает объект данных, чтобы R знал, что два разных имени ( DT2и DT) указывают на один и тот же объект. И поэтому R нужно будет скопировать объект , если либо являются subassigned на потом.

Это также идеально подходит для data.table. :=Не за это. Таким образом, следующее является преднамеренной ошибкой, поскольку :=не только для привязки имен объектов:

DT2 := DT    # not what := is for, not defined, gives a nice error

:=предназначен для поднабора по ссылке. Но вы не используете его как в базе:

DT[3,"foo"] := newvalue    # not like this

вы используете это так:

DT[3,foo:=newvalue]    # like this

Это изменилось DTпо ссылке. Скажем, вы добавляете новый столбец newпо ссылке на объект данных, нет необходимости делать это:

DT <- DT[,new:=1L]

потому что RHS уже изменился DTпо ссылке. Дополнительным DT <-является неправильное понимание того, что :=делает. Вы можете написать это там, но это лишнее.

DTизменяется по ссылке :=, ДАЖЕ В ФУНКЦИЯХ:

f <- function(X){
    X[,new2:=2L]
    return("something else")
}
f(DT)   # will change DT

DT2 <- DT
f(DT)   # will change both DT and DT2 (they're the same data object)

data.tableдля больших наборов данных, помните. Если у вас есть 20 ГБ data.tableпамяти, вам нужен способ сделать это. Это очень обдуманное дизайнерское решение data.table.

Копии могут быть сделаны, конечно. Вам просто нужно сообщить data.table, что вы уверены, что хотите скопировать набор данных объемом 20 ГБ, используя copy()функцию:

DT3 <- copy(DT)   # rather than DT3 <- DT
DT3[,new3:=3L]     # now, this just changes DT3 because it's a copy, not DT too.

Чтобы избежать копирования, не используйте базовое назначение типа или обновление:

DT$new4 <- 1L                 # will make a copy so use :=
attr(DT,"sorted") <- "a"      # will make a copy use setattr() 

Если вы хотите быть уверены, что вы обновляете по ссылке, используйте .Internal(inspect(x))и посмотрите значения адресов памяти компонентов (см. Ответ Мэтью Доула).

Запись :=в jтаком виде позволяет вам назначать ссылки по группам . Вы можете добавить новый столбец по ссылке по группе. Вот почему так :=делается внутри [...]:

DT[, newcol:=mean(x), by=group]
statquant
источник