지난 포스팅에서는 R data.table 을 참조하여 := 연산자로 data.table의 칼럼 추가, 갱신, 삭제하는 방법을 소개하였습니다. 그리고 본문 중간에 얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해서도 소개를 하였습니다. 


다시 한번 얕은 복사와 깊은 복사의 정의를 보면요, 


얕은 사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터(위치)의 벡터만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하는 것은 아니라고 했습니다. 


반면, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적을 복사고 했습니다. 


이번 포스팅에서는 지난번 포스팅에서 한발 더 나아가서,

 

(1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용

(2) copy() 함수를 사용해 깊은 복사하기


하는 방법에 대해 알아보겠습니다. 

이번 포스팅은 R data.table vignette "Reference semantics" 를 번역하여 작성하였습니다.





  (1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용


먼저, 예제 데이터로 사용하기 위해 MASS 패키지에 내장되어 있는 Cars93 데이터프레임에서 변수 몇 개만 가져와서 DT 라는 이름의 data.table을 만들어보겠습니다.



library(data.table)
library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69




이제 := 연산자로 data.table을 참조하여 얕은 복사한 객체에 대해 새로운 칼럼을 추가하고, 이 새 칼럼에 대해 차종(Type) 그룹별 최대값(maximum)을 구하는 shallow_copy_func() 라는 이름의 사용자 정의함수를 정의하여 실행시켜 보겠습니다.



## (1) Shallow copy by reference
shallow_copy_func <- function(data_tbl){
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_2 <- shallow_copy_func(DT)



위에서 정의한 shallow_copy_func()에 DT data.table을 input으로 넣어서 실행시켜 DT_2 라는 이름의 새로운 객체를 반환하였더니, 원래의 객체(original object)인 DT에 'area'라는 이름의 새로운 칼럼이 추가되었습니다. 이것은 := 연산자가 원래의 객체인 DT를 참조하여 바라보고 있다가, 얕은 복사한 객체에 수정사항이 생겼을 경우 원래의 DT에도 이를 반영하여 갱신하기 때문입니다.


새로 얻은 DT_2 에는 차종(Type) 그룹별로 최대 면적(max_area)이 계산되어 있습니다.


만약 DT_2의 차종 그룹별 최대값 계산 결과만을 원하였고, 원래의 객체인 DT 는 아무런 변경도 원하지 않았다면 지금 := 를 사용해서 발생한 원본 data.table의 수정은 := 를 사용했을 때의 참조에 의한 얕은 복사의 부작용(side effect of reference, shalloe copy)이라고 할 수 있습니다.



## Note that the new column Area has been added to DT data.table.
## This is because := performs operations by reference. Side effect of := reference symantics.
head(DT)
# Model    Type Price Length Width  area
# 1: Integra   Small  15.9    177    68 12036
# 2:  Legend Midsize  33.9    195    71 13845
# 3:      90 Compact  29.1    180    67 12060
# 4:     100 Midsize  37.7    193    70 13510
# 5:    535i Midsize  30.0    186    69 12834
# 6: Century Midsize  15.7    189    69 13041


## DT_2 contains the maximum Price for each Type.
head(DT_2)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





  (2) copy() 함수를 사용해 깊은 복사하기


얕은 복사(shallow copy)가 효율적인 메모리 사용 측면에서는 장점이 있지만, 만약 복사한 객체에 수정한 내용이 원본 객체에는 반영되는 것을 원하지 않는다면 깊은 복사(deep copy)를 명시적으로 해줘야 합니다. R data.table 에서 깊은 복사(deep copy)를 하려면 copy() 함수를 사용하면 됩니다.


아래의 예에서는 위 (1)번에서 작성했던 함수에다가 data_tbl <- copy(data_tbl) 코드를 한 줄 더 추가함으로써 원본 data.table 객체를 깊은 복사한 하도록 하고, 깊은 복사된 새로운 객체에 대해 := 로 참조하여 새로운 칼럼을 추가하고, 차종(Type) 그룹별로 최대값을 계산하는 사용자정의함수를 정의하고 실행해 보겠습니다.


## (2) Deep copy
## When we don't want to update the original object, we use copy() function.
DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])

deep_copy_func <- function(data_tbl){
  data_tbl <- copy(data_tbl)
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_3 <- deep_copy_func(DT)




deep_copy_func() 사용자정의함수를 실행시키고 나서 원래의 객체인 DT를 다시 확인해보니, 위의 (1)번 얕은 복사 예에서와는 달리 이번 (2)번 copy()에 의한 깊은 복사된 객체의 수정사항이 원본 DT data.table에는 아무런 영향을 끼치지 못했음을 알 수 있습니다.

(즉, 이번에는 원본 DT data.table에 'area'라는 칼럼이 안생겼음)



## Using copy() function did not update DT data.table by reference.
## It doesn't contain the column 'Area'.
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69


## DT_3 contains the maximum Price for each Type.
head(DT_3)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





data.table의 칼럼 이름(names(DT))을 변수로 저장할 경우 칼럼 이름이 얕은 복사(shallow copy)되며, 만약 원본 data.table에 칼럼을 추가/ 갱신/ 삭제할 경우, 이를 참조하여 생성된 칼럼 이름을 저장한 변수도 연동하여 변경이 발생게 됩니다.


아래의 예에서는 원본 DT data.table의 칼럼 이름을 DT_names 라는 변수에 저장하여 만들었습니다.(얕은 복사가 됨).  그런 다음에 원본 DT data.table에 "z"라는 이름의 새로운 칼럼을 ':=' 연산자로 참조하여 추가하였습니다. 그랬더니 직전에 생성한 칼럼 이름을 저장해놓은 DT_names 변수에 자동으로 "z" 도 새로 추가되었습니다. DT_names 가 DT를 참조하면서 바라보고 있다가, DT에 수정이 발생하자 DT_names이 DT를 참조하여 같이 수정이 발생한 것입니다.



## When we store the column names on to a variable, e.g. DT_names = names(DT),
## and then add/ update/ delete columns(s) by referene,
## it would also modify DT_names, unless we do copy(names(DT)).
DT = data.table(x = 1L, y = 2L)
DT_names = names(DT)
print(DT_names)
# [1] "x" "y"

## add a new column by reference.
DT[, z := 3L]

## DT_names also gets updated.
print(DT_names)
# [1] "x" "y" "z"

 



만약 DT의 칼럼 이름을 저장해 놓은 DT_names 변수가 원본 객체 DT에 변경사항이 발생하더라도 영향을 받지않기를 원한다면 copy() 함수를 사용하여 명시적으로 DT 칼럼 이름(names(DT))을 깊은 복사(deep copy) 해놓으면 됩니다.



## use 'copy()' function, deep copy.
DT_names = copy(names(DT))
DT[, w := 4L]

## DT_names doesn't get updated. No "w" column in DT_names.
print(DT_names)
# [1] "x" "y" "z"

 


[Reference]

R data.table vignette, "Reference Semantics"
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!


반응형
Posted by Rfriend

댓글을 달아 주세요