Memahami kapan tepatnya data.table adalah referensi ke (vs salinan) data.table lain

194

Saya mengalami sedikit kesulitan memahami properti pass-by-reference dari data.table. Beberapa operasi tampaknya 'mematahkan' referensi, dan saya ingin memahami persis apa yang terjadi.

Pada membuat data.tabledari yang lain data.table(via <-, kemudian memperbarui tabel baru dengan :=, tabel asli juga diubah. Ini diharapkan, sesuai:

?data.table::copy dan stackoverflow: pass-by-referensi-operator-dalam-data-tabel-paket

Ini sebuah contoh:

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

Namun, jika saya menyisipkan :=modifikasi tidak berbasis antara <-penugasan dan :=baris di atas, DTsekarang tidak lagi dimodifikasi:

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

Jadi sepertinya newDT$b[2] <- 200garis entah bagaimana 'mematahkan' referensi. Saya kira ini entah bagaimana meminta salinan, tetapi saya ingin memahami sepenuhnya bagaimana R memperlakukan operasi ini, untuk memastikan saya tidak memperkenalkan bug potensial dalam kode saya.

Saya akan sangat menghargai jika seseorang dapat menjelaskan hal ini kepada saya.

Peter Fine
sumber
1
Saya baru saja menemukan "fitur" ini, dan itu mengerikan. Ini banyak dianjurkan di Internet untuk digunakan <-alih-alih =untuk penugasan dasar di R (misalnya oleh Google: google.github.io/styleguide/Rguide.xml#assignment ). Tetapi ini berarti bahwa manipulasi data.tabel tidak akan berfungsi dengan cara yang sama seperti manipulasi bingkai data dan karenanya jauh dari penggantian drop-in ke bingkai data.
cmo

Jawaban:

141

Ya, subassignment di R menggunakan <-(atau =atau ->) yang membuat salinan seluruh objek. Anda dapat melacaknya menggunakan tracemem(DT)dan .Internal(inspect(DT)), seperti di bawah ini. The data.tablefitur :=dan set()menetapkan dengan mengacu apapun keberatan mereka berlalu. Jadi, jika objek itu sebelumnya disalin (oleh subassigning <-atau eksplisit copy(DT)) maka itu adalah salinan yang akan dimodifikasi oleh referensi.

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..

Perhatikan bagaimana bahkan avektor disalin (nilai heks yang berbeda menunjukkan salinan vektor baru), meskipun atidak diubah. Bahkan keseluruhan bsudah disalin, bukan hanya mengubah elemen yang perlu diubah. Itu penting untuk dihindari untuk data besar, dan mengapa :=dan set()diperkenalkan data.table.

Sekarang, dengan salinan newDTkami, kami dapat memodifikasinya dengan referensi:

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 ..

Perhatikan bahwa semua 3 nilai hex (vektor titik kolom, dan masing-masing dari 2 kolom) tetap tidak berubah. Jadi itu benar-benar dimodifikasi dengan referensi tanpa salinan sama sekali.

Atau, kita dapat memodifikasi yang asli DTdengan referensi:

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..

Nilai hex tersebut sama dengan nilai asli yang kami lihat di DTatas. Ketik example(copy)untuk lebih banyak contoh menggunakan tracememdan membandingkan data.frame.

Btw, jika Anda tracemem(DT)maka DT[2,b:=600]Anda akan melihat satu salinan dilaporkan. Itu adalah salinan dari 10 baris pertama yang dilakukan oleh printmetode ini. Ketika dibungkus dengan invisible()atau ketika dipanggil dalam suatu fungsi atau skrip, printmetode tersebut tidak dipanggil.

Semua ini berlaku di dalam fungsi juga; yaitu, :=dan set()jangan salin saat menulis, bahkan dalam fungsi. Jika Anda perlu memodifikasi salinan lokal, hubungi x=copy(x)di awal fungsi. Tetapi, yang diingat data.tableadalah untuk data besar (juga keunggulan pemrograman lebih cepat untuk data kecil). Kami sengaja tidak ingin menyalin objek besar (pernah). Akibatnya, kita tidak perlu mengizinkan aturan praktis faktor memori 3 * yang biasa digunakan. Kami mencoba hanya perlu memori yang bekerja sebesar satu kolom (yaitu faktor memori kerja 1 / ncol daripada 3).

Matt Dowle
sumber
1
Kapan perilaku ini diinginkan?
Colin
Menariknya, perilaku menyalin seluruh objek tidak terjadi untuk objek data.frame. Dalam bingkai data yang disalin, hanya vektor yang diubah secara langsung melalui ->penugasan yang mengubah lokasi memori. Vektor yang tidak berubah mempertahankan lokasi memori dari vektor dari data.frame asli. Perilaku data.tables yang dijelaskan di sini adalah perilaku saat ini pada 1.12.2.
lmo
105

Hanya ringkasan singkat.

<-dengan data.tableseperti basis; yaitu, tidak ada salinan yang diambil sampai subassign dilakukan setelahnya dengan <-(seperti mengubah nama kolom atau mengubah elemen seperti DT[i,j]<-v). Kemudian dibutuhkan salinan seluruh objek seperti basis. Itu dikenal sebagai copy-on-write. Akan lebih dikenal sebagai copy-on-subassign, saya pikir! JANGAN menyalin ketika Anda menggunakan :=operator khusus , atau set*fungsi yang disediakan oleh data.table. Jika Anda memiliki data besar, Anda mungkin ingin menggunakannya. :=dan set*TIDAK akan menyalin data.table, BAHKAN DALAM FUNGSI.

Diberikan contoh data ini:

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

Berikut ini hanya "mengikat" nama lain DT2ke objek data yang sama terikat saat ini terikat pada nama DT:

DT2 <- DT

Ini tidak pernah menyalin, dan tidak pernah menyalin di pangkalan juga. Itu hanya menandai objek data sehingga R tahu bahwa dua nama berbeda ( DT2dan DT) menunjuk ke objek yang sama. Maka R perlu menyalin objek jika salah satunya ditransfer ke bawah sesudahnya.

Itu sempurna untuk data.tablejuga. The :=bukan untuk melakukan hal itu. Jadi berikut ini adalah kesalahan yang disengaja karena :=bukan untuk hanya mengikat nama objek:

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

:=adalah untuk subassigning dengan referensi. Tetapi Anda tidak menggunakannya seperti pada basis:

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

Anda menggunakannya seperti ini:

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

Itu berubah DTdengan referensi. Katakanlah Anda menambahkan kolom baru newdengan referensi ke objek data, tidak perlu melakukan ini:

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

karena RHS sudah diubah DTdengan referensi. Kelebihannya DT <-adalah salah paham apa yang :=dilakukannya. Anda dapat menulisnya di sana, tetapi itu berlebihan.

DTdiubah dengan referensi, oleh :=, BAHKAN DALAM FUNGSI:

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.tableuntuk dataset besar, ingat. Jika Anda memiliki data.tablememori 20GB maka Anda perlu cara untuk melakukan ini. Ini adalah keputusan desain yang sangat disengaja data.table.

Salinan dapat dibuat, tentu saja. Anda hanya perlu memberi tahu data.tabel bahwa Anda yakin ingin menyalin dataset 20GB Anda, dengan menggunakan copy()fungsi:

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

Untuk menghindari salinan, jangan gunakan penetapan atau pembaruan tipe dasar:

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

Jika Anda ingin memastikan bahwa Anda memperbarui dengan menggunakan referensi .Internal(inspect(x))dan melihat nilai-nilai alamat memori konstituen (lihat jawaban Matthew Dowle).

Menulis :=dengan cara jseperti itu memungkinkan Anda melakukan subassign dengan referensi berdasarkan grup . Anda dapat menambahkan kolom baru dengan referensi berdasarkan grup. Jadi itu sebabnya :=dilakukan seperti itu di dalam [...]:

DT[, newcol:=mean(x), by=group]
statquant
sumber