1. ホーム
  2. r

[解決済み】data.tableが他のdata.tableの参照である場合(コピーである場合)を正確に理解する。

2022-04-08 01:10:14

質問

の参照渡しのプロパティを理解するのに少し苦労しています。 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がこれらの操作をどのように扱っているかを完全に理解したいと思います。

どなたか解説していただけると大変ありがたいです。

どのように解決するのですか?

そうです、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 ベクターがコピーされました(異なる16進数の値はベクターの新しいコピーを示します)。 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つの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 メソッドが行います。でラップした場合 invisible() または関数やスクリプトの中で呼び出された場合は print メソッドは呼び出されません。

これはすべて、関数の内部にも当てはまります。 :=set() は、たとえ関数内であっても、書き込み時にコピーされません。ローカルなコピーを変更する必要がある場合は x=copy(x) を関数の先頭で実行します。ただし data.table は大きなデータのためのものです(小さなデータのためのプログラミングの高速化という利点もあります)。我々は意図的に大きなオブジェクトをコピーしたくないのです(絶対に)。その結果、通常のワーキングメモリー倍率3倍を許容する必要はありません。私たちは、1列分のワーキングメモリしか必要としないようにしています(つまり、ワーキングメモリの係数は3ではなく1/ncolです)。