การทำความเข้าใจอย่างแน่นอนเมื่อ 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
ฉันเพิ่งค้นพบ "คุณสมบัติ" นี้และมันก็น่ากลัว มันได้รับการสนับสนุนอย่างกว้างขวางใน Internets ให้ใช้<-แทน=การมอบหมายพื้นฐานใน R (เช่นโดย Google: google.github.io/styleguide/Rguide.xml#assignment ) แต่นี่หมายความว่าการจัดการ data.table จะไม่ทำงานเหมือนกับการจัดการ data frame และอยู่ไกลจากการแทนที่แบบดรอปดาวน์เป็น data frame
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)ตัวอย่างเพิ่มขึ้นโดยใช้และเมื่อเปรียบเทียบกับtracememdata.frame

Btw ถ้าคุณtracemem(DT)แล้วDT[2,b:=600]คุณจะเห็นหนึ่งสำเนารายงาน นั่นคือสำเนาของ 10 แถวแรกที่printวิธีการทำ เมื่อห่อด้วยinvisible()หรือเมื่อถูกเรียกภายในฟังก์ชั่นหรือสคริปต์printวิธีการจะไม่ถูกเรียก

ทั้งหมดนี้ใช้ภายในฟังก์ชั่นเช่นกัน; คือ:=และset()ไม่คัดลอกเมื่อเขียนแม้ในฟังก์ชั่น หากคุณต้องการแก้ไขสำเนาในเครื่องให้โทรหาx=copy(x)ตอนเริ่มต้นของฟังก์ชัน แต่จำไว้ว่าdata.tableสำหรับข้อมูลขนาดใหญ่ (เช่นเดียวกับข้อได้เปรียบการเขียนโปรแกรมได้เร็วขึ้นสำหรับข้อมูลขนาดเล็ก) เราตั้งใจไม่ต้องการคัดลอกวัตถุขนาดใหญ่ (เคย) ด้วยเหตุนี้เราจึงไม่จำเป็นต้องอนุญาตให้ใช้กฎหน่วยความจำแบบ 3 * โดยทั่วไปในการทำงาน เราพยายามที่จะต้องใช้หน่วยความจำในการทำงานที่มีขนาดใหญ่เท่ากับหนึ่งคอลัมน์ (เช่นหน่วยความจำที่ใช้งานได้ของ 1 / ncol มากกว่า 3)


1
พฤติกรรมนี้เป็นที่พึงปรารถนาเมื่อใด
ลิน

ที่น่าสนใจพฤติกรรมการคัดลอกวัตถุทั้งหมดไม่ได้เกิดขึ้นกับวัตถุ data.frame ใน data.frame ที่คัดลอกเฉพาะเวกเตอร์ที่ถูกเปลี่ยนโดยตรงผ่านการ->มอบหมายการเปลี่ยนแปลงตำแหน่งหน่วยความจำ เวกเตอร์ไม่เปลี่ยนแปลงรักษาตำแหน่งหน่วยความจำของเวกเตอร์ของ data.frame เดิม พฤติกรรมของdata.tables ที่อธิบายที่นี่คือพฤติกรรมปัจจุบัน ณ 1.12.2
lmo

105

เพียงสรุปอย่างรวดเร็ว

<-ด้วยdata.tableก็เหมือนฐาน; เช่นจะไม่มีการคัดลอกจนกว่าจะมีการมอบหมายย่อยในภายหลังด้วย<-(เช่นการเปลี่ยนชื่อคอลัมน์หรือเปลี่ยนองค์ประกอบเช่นDT[i,j]<-v) จากนั้นจะคัดลอกวัตถุทั้งหมดเช่นเดียวกับฐาน ที่เรียกว่า copy-on-write จะเป็นที่รู้จักกันดีกว่าในฐานะ 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สำหรับชุดข้อมูลขนาดใหญ่อย่าลืม หากคุณมีdata.tableหน่วยความจำ20GB จากนั้นคุณต้องมีวิธีในการทำเช่นนี้ มันเป็นการตัดสินใจที่รอบคอบมากในการออกแบบdata.tableมันเป็นการตัดสินใจการออกแบบโดยเจตนามาก

สามารถทำสำเนาได้แน่นอน คุณเพียงแค่ต้องบอก data.table ว่าคุณแน่ใจว่าคุณต้องการคัดลอกชุดข้อมูล 20GB ของคุณโดยใช้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))และดูค่าที่อยู่หน่วยความจำขององค์ประกอบ (ดูคำตอบของ Matthew Dowle)

เขียน:=ในjเช่นที่ช่วยให้คุณ subassign โดยการอ้างอิงโดยกลุ่ม คุณสามารถเพิ่มคอลัมน์ใหม่โดยอ้างอิงโดยกลุ่ม ดังนั้นนั่นคือเหตุผลที่:=ทำแบบนั้นภายใน[...]:

DT[, newcol:=mean(x), by=group]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.