web-dev-qa-db-ja.com

data.tableが別のdata.tableへの参照(コピー)である場合を正確に理解する

data.tableの参照渡しプロパティを理解するのに少し苦労しています。いくつかの操作は参照を「壊す」ように思われ、何が起こっているのかを正確に理解したいと思います。

別のdata.tableからdata.tableを作成すると(<-を介して、:=で新しいテーブルを更新すると、元のテーブルも変更されます。これは次のとおりです。

?data.table::copyおよび stackoverflow:data-table-packageのオペレーターによる参照渡し

以下に例を示します。

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がこれらの操作をどのように処理しているかを完全に理解して、コードに潜在的なバグを導入しないようにします。

誰かがこれを私に説明できたらとても感謝しています。

181
Peter Fine

はい、Rで_<-_(または_=_または_->_)を使用してwholeオブジェクトのコピーを作成するサブ割り当てです。以下のように、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ベクトルがコピーされたことに注意してください(異なる16進値はベクトルの新しいコピーを示します)。変更が必要な要素を変更するだけでなく、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つの16進値(列ポイントのベクトル、および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..
_

これらの16進値は、上記のDTで見た元の値と同じです。 tracememの使用例と_data.frame_との比較については、example(copy)と入力してください。

ところで、tracemem(DT) then _DT[2,b:=600]_の場合、1つのコピーが報告されます。これは、printメソッドが行う最初の10行のコピーです。 invisible()でラップされた場合、または関数またはスクリプト内で呼び出された場合、printメソッドは呼び出されません。

これはすべて、関数内でも適用されます。つまり、_:=_およびset()は、関数内であっても書き込み時にコピーしません。ローカルコピーを変更する必要がある場合は、関数の開始時にx=copy(x)を呼び出します。ただし、_data.table_は大きなデータ用であることに注意してください(小さなデータ用のプログラミングの高速化と同様)。意図的に大きなオブジェクト(決して)をコピーしたくない。その結果、通常の3 *作業メモリ係数の経験則を考慮する必要はありません。 1列の作業メモリ(つまり、3ではなく1/ncolの作業メモリ係数)のみが必要です。

134
Matt Dowle

簡単にまとめます。

_<-_と_data.table_は、baseに似ています。つまり、後で_<-_でサブ割り当てが行われるまで(列名の変更や_DT[i,j]<-v_などの要素の変更など)、コピーは行われません。次に、ベースのようにオブジェクト全体のコピーを取ります。それはコピーオンライトとして知られています。コピーオンサブアサインとしてよく知られていると思います!特別な_:=_演算子、または_set*_が提供する_data.table_関数を使用した場合、コピーは行われません。大きなデータがある場合は、代わりにそれらを使用することをお勧めします。 _:=_および_set*_は、関数内であっても_data.table_をコピーしません。

この例のデータを考えると:

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

次は、別の名前_DT2_を、名前DTに現在バインドされている同じデータオブジェクトに単に「バインド」します。

_DT2 <- DT
_

これは決してコピーせず、ベースでもコピーしません。 Rが2つの異なる名前(_DT2_およびDT)が同じオブジェクトを指していることを認識するように、データオブジェクトをマークするだけです。そのため、Rはsubassignedが後である場合、オブジェクトをコピーする必要があります。

それは_data.table_にも最適です。 _:=_はそうするためのものではありません。 _:=_はオブジェクト名をバインドするためだけのものではないため、次は意図的なエラーです。

_DT2 := DT    # not what := is for, not defined, gives a Nice error
_

_:=_は、参照によるsubassigning用です。ただし、baseで使用するようには使用しません。

_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_は大規模なデータセット用です、覚えておいてください。メモリに20GB _data.table_がある場合、これを行う方法が必要です。 _data.table_の非常に慎重な設計決定です。

もちろん、コピーを作成できます。 copy()関数を使用して、data.tableに20GBデータセットをコピーすることを確認する必要があります。

_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))を使用して、構成要素のメモリアドレス値を調べます(Matthew Dowleの回答を参照)。

_:=_をjに記述すると、参照による副割り当てが可能になりますby group。グループごとの参照により、新しい列を追加できます。そのため、_:=_は_[...]_内でそのように行われます。

_DT[, newcol:=mean(x), by=group]
_
98
statquant