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


193

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

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

?data.table::copy およびstackoverflow:pass-by-reference-the-operator-in-the-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がこれらの操作をどのように処理しているかを完全に理解したいと思います。

これについて誰かに説明していただければ幸いです。


1
この「機能」を発見したばかりで、恐ろしいです。Rでの基本的な割り当ての<-代わりに使用することがインターネットで広く推奨されています=(たとえば、Google:google.github.io/styleguide/Rguide.xml#assignmentによって)。しかし、これはdata.table操作がデータフレーム操作と同じように機能しないため、データフレームへのドロップイン置換とはほど遠いことを意味します。
cmo

回答:


140

はい、オブジェクト全体のコピーを作成するのは、Rでのサブ割り当て<-(または=or ->)です。以下のように、とを使用してトレースできます。機能とそれらが渡されたどんなオブジェクトへの参照によって割り当てます。したがって、そのオブジェクトが以前に(サブ割り当てまたは明示的なによって)コピーされている場合、参照によって変更されるのはそのコピーです。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上記で見た元の値と同じです。example(copy)を使用してtracemem、と比較するその他の例を入力しますdata.frame

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

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


1
この動作はいつ望ましいですか?
コリン

興味深いことに、オブジェクト全体をコピーする動作は、data.frameオブジェクトでは発生しません。コピーされたdata.frameでは、->割り当てによって直接変更されたベクトルのみがメモリ位置を変更します。変更されていないベクトルは、元のdata.frameのベクトルのメモリ位置を維持します。動作data.tableここで説明するのは、1.12.2のように現在の動作です。
lmo

105

簡単にまとめます。

<-with data.tableはベースのようなものです。つまり、後で副割り当てが行われるまで<-(列名の変更やなどの要素の変更など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はオブジェクトをコピーする必要があります。

これ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大規模なデータセット用です。data.tableメモリに20GBがある場合、これを行う方法が必要です。これは、の非常に慎重な設計上の決定ですdata.table

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

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それはあなたが参照することによりsubassignことができますようにグループで。グループごとの参照で新しい列を追加できます。だからこそ:=、そのように内部で行われます[...]

DT[, newcol:=mean(x), by=group]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.