Hiểu chính xác khi data.table là một tham chiếu đến (so với bản sao của) data.table khác


193

Tôi gặp một chút khó khăn khi hiểu các thuộc tính tham chiếu của data.table. Một số hoạt động dường như 'phá vỡ' tài liệu tham khảo và tôi muốn hiểu chính xác những gì đang xảy ra.

Khi tạo một bảng data.tablekhác data.table(thông qua <-, sau đó cập nhật bảng mới :=, bảng gốc cũng bị thay đổi. Điều này được mong đợi, theo:

?data.table::copystackoverflow: pass-by-Reference-the-toán-in-the-data-bảng-gói

Đây là một ví dụ:

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

Tuy nhiên, nếu tôi chèn một :=sửa đổi không dựa trên <-chuyển nhượng và các :=dòng ở trên, DTthì bây giờ không còn được sửa đổi:

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

Vì vậy, có vẻ như newDT$b[2] <- 200dòng nào đó 'phá vỡ' tham chiếu. Tôi đoán rằng điều này sẽ gọi một bản sao bằng cách nào đó, nhưng tôi muốn hiểu đầy đủ về cách R xử lý các hoạt động này, để đảm bảo tôi không giới thiệu các lỗi tiềm ẩn trong mã của mình.

Tôi rất cảm kích nếu ai đó có thể giải thích điều này cho tôi.


1
Tôi mới phát hiện ra "tính năng" này, và nó thật kinh khủng. Nó được sử dụng rộng rãi trên các Internets để sử dụng <-thay vì chỉ =định cơ bản trong R (ví dụ: bởi Google: google.github.io/styleguide/Rguide.xml#assocation ). Nhưng điều này có nghĩa là thao tác data.table sẽ không hoạt động giống như thao tác khung dữ liệu và do đó không phải là sự thay thế thả vào khung dữ liệu.
cmo

Câu trả lời:


140

Có, đó là cấp dưới trong R bằng cách sử dụng <-( =hoặc ->) tạo bản sao của toàn bộ đối tượng. Bạn có thể theo dõi bằng cách sử dụng tracemem(DT).Internal(inspect(DT)), như dưới đây. Các data.tabletính năng :=set()gán bằng cách tham chiếu đến bất kỳ đối tượng nào chúng được thông qua. Vì vậy, nếu đối tượng đó đã được sao chép trước đó (bằng cách đăng ký phụ <-hoặc rõ ràng copy(DT)) thì đó là bản sao được sửa đổi bằng tham chiếu.

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

Lưu ý rằng ngay cả avectơ đã được sao chép (giá trị hex khác nhau biểu thị bản sao mới của vectơ), mặc dù akhông thay đổi. Ngay cả toàn bộ bđã được sao chép, thay vì chỉ thay đổi các yếu tố cần phải thay đổi. Đó là điều quan trọng để tránh dữ liệu lớn, và tại sao :=set()được giới thiệu data.table.

Bây giờ, với bản sao của newDTchúng tôi, chúng tôi có thể sửa đổi nó bằng cách tham khảo:

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

Lưu ý rằng tất cả 3 giá trị hex (vectơ của các điểm cột và mỗi cột trong 2 cột) không thay đổi. Vì vậy, nó đã thực sự được sửa đổi bằng cách tham khảo mà không có bản sao nào cả.

Hoặc, chúng tôi có thể sửa đổi bản gốc DTbằng cách tham khảo:

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

Các giá trị hex này giống như các giá trị ban đầu mà chúng ta đã thấy DTở trên. Nhập example(copy)để biết thêm ví dụ sử dụng tracememvà so sánh với data.frame.

Btw, nếu tracemem(DT)sau đó DT[2,b:=600]bạn sẽ thấy một bản sao được báo cáo. Đó là bản sao của 10 hàng đầu tiên mà printphương thức thực hiện. Khi được bao bọc bởi invisible()hoặc khi được gọi trong một hàm hoặc tập lệnh, printphương thức không được gọi.

Tất cả điều này áp dụng bên trong các chức năng quá; tức là :=set()không sao chép trên ghi, ngay cả trong các hàm. Nếu bạn cần sửa đổi một bản sao cục bộ, sau đó gọi x=copy(x)khi bắt đầu chức năng. Nhưng, hãy nhớ data.tablelà dành cho dữ liệu lớn (cũng như lợi thế lập trình nhanh hơn cho dữ liệu nhỏ). Chúng tôi cố tình không muốn sao chép các đối tượng lớn (bao giờ). Kết quả là chúng ta không cần phải cho phép quy tắc ngón tay cái làm việc 3 * thông thường. Chúng tôi cố gắng chỉ cần bộ nhớ làm việc lớn bằng một cột (tức là hệ số bộ nhớ làm việc là 1 / ncol chứ không phải 3).


1
Khi nào hành vi này là mong muốn?
colin

Thật thú vị, hành vi sao chép toàn bộ đối tượng không xảy ra đối với một đối tượng data.frame. Trong một data.frame được sao chép, chỉ có vectơ được thay đổi trực tiếp thông qua ->việc gán thay đổi vị trí bộ nhớ. Các vectơ không thay đổi duy trì vị trí bộ nhớ của các vectơ của data.frame gốc. Hành vi của data.tables được mô tả ở đây là hành vi hiện tại kể từ 1.12.2.
LMO

105

Chỉ cần một tổng hợp nhanh chóng.

<-với data.tablecũng giống như cơ sở; tức là, không có bản sao nào được thực hiện cho đến khi một lệnh con được thực hiện sau đó <-(chẳng hạn như thay đổi tên cột hoặc thay đổi một phần tử như DT[i,j]<-v). Sau đó, nó lấy một bản sao của toàn bộ đối tượng giống như cơ sở. Điều đó được gọi là sao chép trên văn bản. Tôi nghĩ sẽ được biết đến như là bản sao trên cấp dưới, tôi nghĩ vậy! Nó KHÔNG sao chép khi bạn sử dụng :=toán tử đặc biệt hoặc các set*chức năng được cung cấp bởi data.table. Nếu bạn có dữ liệu lớn, bạn có thể muốn sử dụng chúng thay thế. :=set*sẽ KHÔNG SAO CHÉP data.table, NGAY CẢ CÁC CHỨC NĂNG.

Cho dữ liệu ví dụ này:

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

Sau đây chỉ "liên kết" một tên khác DT2với cùng một đối tượng dữ liệu bị ràng buộc hiện tại bị ràng buộc với tên DT:

DT2 <- DT

Điều này không bao giờ sao chép, và không bao giờ sao chép trong cơ sở. Nó chỉ đánh dấu đối tượng dữ liệu để R biết rằng hai tên khác nhau ( DT2DT) trỏ đến cùng một đối tượng. Và do đó, R sẽ cần phải sao chép đối tượng nếu một trong hai được gán lại sau đó.

Điều đó cũng hoàn hảo cho data.table. Các :=không phải là để làm điều đó. Vì vậy, sau đây là một lỗi cố ý vì :=không chỉ ràng buộc tên đối tượng:

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

:=là để đăng ký lại bằng cách tham khảo. Nhưng bạn không sử dụng nó như trong căn cứ:

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

bạn sử dụng nó như thế này:

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

Điều đó đã thay đổi DTbằng cách tham khảo. Giả sử bạn thêm một cột mới newbằng cách tham chiếu đến đối tượng dữ liệu, không cần phải làm điều này:

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

bởi vì RHS đã thay đổi DTbằng cách tham chiếu. Thêm DT <-là để hiểu sai những gì :=làm. Bạn có thể viết nó ở đó, nhưng nó không cần thiết.

DTđược thay đổi bởi tham chiếu, bởi :=, NGAY CẢ TRONG CHỨC NĂNG:

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.tablelà cho các bộ dữ liệu lớn, hãy nhớ. Nếu bạn có data.tablebộ nhớ 20GB thì bạn cần một cách để làm điều này. Đó là một quyết định thiết kế rất có chủ ý của data.table.

Bản sao có thể được thực hiện, tất nhiên. Bạn chỉ cần thông báo cho data.table rằng bạn chắc chắn muốn sao chép tập dữ liệu 20GB của mình bằng cách sử dụng copy()chức năng:

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

Để tránh các bản sao, không sử dụng gán hoặc cập nhật loại cơ sở:

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

Nếu bạn muốn chắc chắn rằng bạn đang cập nhật bằng cách sử dụng tham chiếu .Internal(inspect(x))và xem các giá trị địa chỉ bộ nhớ của các thành phần (xem câu trả lời của Matthew Dowle).

Viết :=trong jnhư thế cho phép bạn subassign bằng cách tham chiếu theo nhóm . Bạn có thể thêm một cột mới theo tham chiếu theo nhóm. Vì vậy, đó là lý do tại sao :=được thực hiện theo cách đó bên trong [...]:

DT[, newcol:=mean(x), by=group]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.