지난 포스팅에서는 R data.table에서 mult와 nomatch 매개변수를 사용하여 Key 값의 매칭되는 모든 행, 첫번째 행, 마지막 행, 값이 존재하는 행만 가져오는 방법(https://rfriend.tistory.com/571)을 소개하였습니다.
이번 포스팅에서는 R data.table의 칼럼을 참조하여 := 연산자를 사용하여 칼럼을 추가, 갱신, 삭제하는 참조 구문(Reference semantics)에 대해서 알아보겠습니다.
(1) 참조 구문(reference semantics) 배경과 := 연산자
(2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy)
(3) 참조에 의한 칼럼 추가/ 갱신/ 삭제
(4) 그룹 by와 참조 := 연산자 함께 사용하기
(5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기
(1) data.table의 참조 의미론 배경과 := 연산자 |
data.table을 다루기 이전에, 먼저 data.frame의 칼럼 생성/변경/삭제에 대해서 살펴보겠습니다. R 버전이 3.1 미만(R version < 3.1) 일 경우, 아래의 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경하거나(칼럼 x1을 101:106 값으로 재할당), 혹은 (b) 처럼 칼럼의 일부 값만을 새로 할당(grp 가 "a"인 행의 x1 칼럼 값을 500으로 할당) 하면, 전체 data.frame 의 깊은 복사(deep copy)가 발생합니다.
R version >= 3.1 부터는 data.frame 에서 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경 시 깊은 복사(deep copy) 대신 얕은 복사(shallow copy)로 변경하여 성능을 대폭적으로 향상시켰습니다. 하지만 (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 R version >= 3.1 에서도 여전히 전체 칼럼에 대한 깊은 복사(deep copy)가 발생합니다.
data.table 은 (a)처럼 특정 칼럼 전체를 다른 값으로 변경하거나, (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 모두 얕은 복사(shallow copy)를 함으로써 메모리 효율과 성능을 높였습니다.
## data.frame ## replacing entire column or subassigning in a column by deep coly (R versions < 3.1) DF <- data.frame(grp = c("a", "a", "b", "b", "c", "c"), x1 = 1:6, x2 = 7:12) DF # grp x1 x2 # 1 a 1 7 # 2 a 2 8 # 3 b 3 9 # 4 b 4 10 # 5 c 5 11 # 6 c 6 12
## (a) replace entire column DF$x1 <- 101:106 print(DF) # grp x1 x2 # 1 a 101 7 # 2 a 102 8 # 3 b 103 9 # 4 b 104 10 # 5 c 105 11 # 6 c 106 12
## (b) subassign in column 'x1' DF$x1[DF$grp == "a"] <- 500 print(DF) # grp x1 x2 # 1 a 500 7 # 2 a 500 8 # 3 b 103 9 # 4 b 104 10 # 5 c 105 11 # 6 c 106 12 |
(2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy) |
그러면 위에서 말한 얕은 복사(shallow copy)와 깊은 복사(deep copy)가 무엇인지 알아보겠습니다.
얕은 복사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터의 벡터(the vector of column pointers)만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하지는 않습니다. 따라서 칼럼 값을 중복해서 물리적으로 복사하지 않기 때문에 메모리를 효율적으로 사용할 수 있습니다.
반면에, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적으로 복사를 합니다. 따라서 똑같은 값이 물리적으로 복사되기 때문에 메모리가 중복 사용됩니다.
해들리 위크햄은 "Advanced R book"에서 말하길, 복사는 비싼 연산작업이므로 가능한한 게으르게 얕은 복사만 해놓고 놀고 있다가, 새로운 객체에 실제 수정이 가해질 때에 가서야 마지못해 깊은 복사를 하면서 일을 한다고 설명해주고 있습니다. R이 메모리를 가장 효율적으로 사용하기 위한 방안이며, Python도 R과 마찬가지로 이런 게으른 실행 전략을 취하고 있습니다.
"R 의미론에서, 객체는 값으로 복사됩니다. 이것은 복사된 객체를 수정하더라도 원래의 값은 그대로 동일하게 남아있다는 것을 의미합니다. 메모리에 데이터를 복사하는 것은 비싼 연산작업이기 때문에, R에서 복사는 가능한 게으르게(lazy) 수행합니다. 데이터 복사는 새로운 객체가 실제로 변경이 될 때에만 발생합니다."
("In R semantics, objects are copied by value. This means that modifying the copy leaves the original object intact. Since copying data in memory is an expensive operation, copies in R are as lazy as possible. They only happen when the new object is actually modified.")
- "Advanced R book", Hadley Wickham
아래의 예에서 보면 x에 c(1, 2, 3) 의 벡터 값이 할당되어 있으며, 이 x를 y에 할당하였습니다. lobstr 패키지로 x와 y가 저장된 메모리 상의 위치(memory address)를 확인해보면 x와 y의 저장 주소가 동일함을 알 수 있습니다. 즉, y는 x를 얕게 복사(shallow copy)하여 동일한 위치를 가리키고(pointer)만 있습니다.
반면에 y <- c(y, -3)과 같이 y에 새로운 값을 추가하여 기존 벡터를 변형해 새로운 벡터를 만들면 이때서야 비로서 y 는 새로운 물리적인 메모리 주소에 할당이 되어 깊게 복사(deep copy)가 됩니다.
## shallow vs. deep copy
install.packages("lobstr") library(lobstr)
## shallow copy x <- c(1, 2, 3) lobstr::obj_addr(x) # [1] "0x10ccef808"
## The location ("memory address") of x and y in memory are the same.
## ie., "shallow copy" until now. y <- x lobstr::obj_addr(y) # [1] "0x10ccef808"
## When new object is actually modified, then "deep copy" occurs.
## ie., new physicial copy in memory. y <- c(y, -3) print(lobstr::obj_addr(y)) # [1] "0x10d6dacf8"
|
아래에 x와 y를 인쇄해서 비교해보니 x는 아무런 영향을 받지 않아서 그대로 이고, y는 '-3' 값이 추가되어 새로운 벡터로 변경되었습니다. (즉, y에 깊은 복사(deep copy)가 일어났음.)
## no change in x
print(x) # [1] 1 2 3
## only change in y
print(y) # [1] 1 2 3 -3 |
R data.table은 참조하여 새로운 칼럼을 추가하거나 갱신하는 경우 얕은 복사(shallow copy)를 합니다.
(3) 참조에 의한 칼럼 추가/ 갱신/ 삭제
(Add/ update/ delete columns by refreence)
|
data.table의 기본 구문 DT[i, j, by]에서 칼럼 j 를 추가/ 갱신/ 삭제할 때는 특수부호 := 연산자를 사용하며, 아래처럼 왼쪽처럼 (방법 1) 'LHS (Left Hand Side) := RHS (Right Hand Side)' 구문이나, 또는 오른쪽에 있는 (방법 2) ':='(colA = valA, colB = vlaB, ...) 의 함수 형식(functional form)으로 구문을 작성합니다.
(방법 1) 로는 왼쪽(LHS)에 문자 벡터(Charater vector)로 칼럼 이름을 부여하고, 오른쪽(RHS)에는 리스트 값(list of values)을 받습니다. 오른쪽(RHS)은 lapply(), list(), mget(), mapply() 등 어떤 방식으로 생성되었든지간에 단지 리스트이기만 하면 되고 왼쪽은 문자 벡터를 칼럼 이름으로 받으므로, 특히 미리 칼럼에 무슨 값을 할당할지 모르는 경우에 프로그래밍하는데 사용하기에 편리합니다.
(방법 2)의 경우 칼럼과 값을 1:1로 나란히 짝을 지어 코딩하기 때문에 칼럼 개수가 적을 경우 사람이 읽기에 좀더 가독성이 좋고, 칼럼 = 값의 오른쪽 여백에 '#' 코멘트 기호로 부연설명을 추가해 넣기에 유용합니다.
만약, 대상 칼럼이 1개 뿐이라면 왼쪽 아래부분처럼 DT[, colA := valA] 처럼 구문을 간편하게 작성할 수도 있습니다.
## way 1. The LHS := RHS form DT[, c("colA", "colB", ...) := list(valA, valB, ...)]
## in case only for 1 column
DT[, colA := valA]
|
## way 2. The functional form Dt[, ':='(colA = valA, # valA is assigned to colA colB = valB, # valB is assigned to colB ...)]
|
MASS 패키지에 내장되어 있는 Cars93 데이터셋으로 부터 칼럼 몇개만 선택해 가져와서 DT 라는 이름의 예제 data.table을 만들어보겠습니다. 그리고 위의 두 가지 방법을 이용하여 "Range.Price", "Area"라는 이름의 새로운 칼럼 2개를 추가해보겠습니다.
'최대 가격(Max.Price)'에서 '최소 가격(Min.Price)'의 차이를 계산해서 '가격 범위(Range.Price)' 칼럼을 새로 생성하여 추가하고, '길이(Length)'와 '폭(Width)'을 곱하여 '넓이(Area)' 칼럼을 새로 생성하여 추가하시오.
## a) Add columns by reference library(data.table) library(MASS)
DT <- data.table(Cars93[, c("Model", "Type", "Min.Price", "Max.Price", "Length", "Width")]) head(DT) # Model Type Min.Price Max.Price Length Width # 1: Integra Small 12.9 18.8 177 68 # 2: Legend Midsize 29.2 38.7 195 71 # 3: 90 Compact 25.9 32.3 180 67 # 4: 100 Midsize 30.8 44.6 193 70 # 5: 535i Midsize 23.7 36.2 186 69 # 6: Century Midsize 14.2 17.3 189 69
## way 1: LHS := RHS form DT[, c("Range.Price", "Area") := list((Max.Price - Min.Price), Length * Width)]
## (equivalently) way 2: The functional form DT[, ':='(Range.Price = (Max.Price - Min.Price), Area = Length * Width)] head(DT) # Model Type Min.Price Max.Price Length Width Range.Price Area # 1: Integra Small 12.9 18.8 177 68 5.9 12036 # 2: Legend Midsize 29.2 38.7 195 71 9.5 13845 # 3: 90 Compact 25.9 32.3 180 67 6.4 12060 # 4: 100 Midsize 30.8 44.6 193 70 13.8 13510 # 5: 535i Midsize 23.7 36.2 186 69 12.5 12834 # 6: Century Midsize 14.2 17.3 189 69 3.1 13041
|
이번에는 DT[i, j, by] 구문에서 i 로 조건을 만족하는 일부 행을 선별하고, colA := valB 구문으로 특정 칼럼의 값을 갱신(update) 하여 보겠습니다.
차종(Type)이 'Large'인 행의 차종의 값을 'Big'으로 변경하시오.
## b) Update some rows of columns by reference - sub-assign by reference DT[, .N, by = Type] # Type N # 1: Small 21 # 2: Midsize 22 # 3: Compact 16 # 4: Large 11 <--- # 5: Sporty 14 # 6: Van 9
## replace those rows where Type == 'Large' with the value 'Big' DT[Type == 'Large', Type := 'Big']
DT[, .N, by = Type] # Type N # 1: Small 21 # 2: Midsize 22 # 3: Compact 16 # 4: Big 11 <--- # 5: Sporty 14 # 6: Van 9
|
위처럼 DT[Type == 'Large', Type := 'Big'] 을 실행시키면 칼럼 값 갱신이 눈에 보이지 않는(invisibly) 상태로 실행됩니다. 만약 갱신 결과를 눈에 보이도록 출력하려면 제일 뒤에 [] 를 붙여주면 됩니다.
## We can see the result by adding an empty [] at the end of the query DT[Type == 'Large', Type := 'Big'][] # Model Type Min.Price Max.Price Length Width Range.Price Area # 1: Integra Small 12.9 18.8 177 68 5.9 12036 # 2: Legend Midsize 29.2 38.7 195 71 9.5 13845 # 3: 90 Compact 25.9 32.3 180 67 6.4 12060 # 4: 100 Midsize 30.8 44.6 193 70 13.8 13510 # 5: 535i Midsize 23.7 36.2 186 69 12.5 12834 # --- # 89: Eurovan Van 16.6 22.7 187 72 6.1 13464 # 90: Passat Compact 17.6 22.4 180 67 4.8 12060 # 91: Corrado Sporty 22.9 23.7 159 66 0.8 10494 # 92: 240 Compact 21.8 23.5 190 67 1.7 12730 # 93: 850 Midsize 24.8 28.5 184 69 3.7 12696 |
- data.table 참조하여 칼럼 삭제 (Delete column(s) by Reference in data.table)
data.table에서 칼럼을 삭제하려면 colA := NULL 처럼 NULL 값을 할당해주면 됩니다. rm() 이나 del() 같은 함수가 아니라 '없음(NULL)'을 할당하는 방식이라 좀 생소하기는 합니다. ^^;
삭제하려는 칼럼이 여러개라면 c("colA", "colB", ...) 처럼 c() 로 칼럼 문자 벡터를 묶어서 써줍니다.
DT data.table에서 "Range.Price"와 "Area" 칼럼을 삭제하시오.
## c) Delete column by reference ## Assigning NULL to a column deletes that column. And it happens instantly. DT[, c("Range.Price", "Area") := NULL] head(DT, 3) # Model Type Min.Price Max.Price Length Width # 1: Integra Small 12.9 18.8 177 68 # 2: Legend Midsize 29.2 38.7 195 71 # 3: 90 Compact 25.9 32.3 180 67
|
만약 삭제하려는 칼럼이 단 1개라면 c() 나 문자형 칼럼 이름을 쓸 필요없이, 그냥 colA := NULL 처럼 바로 써주면 됩니다.
## When there is just one column to delete, we can drop the c() and bouble quotes DT[, Width := NULL] head(DT, 3) # Model Type Min.Price Max.Price Length # 1: Integra Small 12.9 18.8 177 # 2: Legend Midsize 29.2 38.7 195 # 3: 90 Compact 25.9 32.3 180
|
(4) 그룹 by와 참조 := 연산자 함께 사용하기 |
data.table의 기본구문인 DT[i, j, by] 에서 그룹 by 별로 연산을 한 결과를 참조하여 DT[, colA := calculation(colB), by = Group] 형식으로 새로운 칼럼을 추가할 수 있습니다.
data.table DT에서 차종(Type) 그룹별로 길이(Length)의 최대값을 구하여, 각 행의 차종(Type)에 맞게 "Max.Length"라는 이름의 칼럼에 값을 추가하시오.
## d) := along with grouping using 'by' DT[, Max.Length := max(Length), by = Type]
## or alternatively, providing with a character vector DT[, Max.Length := max(Length), by = c("Type")]
head(DT) # Model Type Min.Price Max.Price Length Max.Length # 1: Integra Small 12.9 18.8 177 177 # 2: Legend Midsize 29.2 38.7 195 205 <-- # 3: 90 Compact 25.9 32.3 180 190 # 4: 100 Midsize 30.8 44.6 193 205 <-- # 5: 535i Midsize 23.7 36.2 186 205 <-- # 6: Century Midsize 14.2 17.3 189 205 <--
## check the max value of Length by Type DT[, max(Length), by = Type] # Type V1 # 1: Small 177 # 2: Midsize 205 <-- # 3: Compact 190 # 4: Big 219 # 5: Sporty 196 # 6: Van 194
|
(5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기 |
data.table을 참조하여 여러개의 칼럼을 참조하여 여러개의 새로운 칼럼을 한꺼번에 추가하거나 갱신, 삭제하고 싶으면 (a) .SDcols = c("colA", "colB", ...) 에서 지정한 복수의 칼럼에 대하여, (b) lapply(.SD, function) 으로 (a)에서 지정한 모든 칼럼(.SD)에 함수를 적용하여, (c) := 연산자로 (b)의 결과를 참조하여 새로운 칼럼을 추가합니다.
참고로, R data.table에서 여러개의 변수를 처리하는 특수부호 .SD, .SDcols 에 대한 자세한 소개는 https://rfriend.tistory.com/568 를 참조하세요.
DT data.table에서 차종(Type) 그룹별로 '가격(Price)'과 '길이(Length)' 변수의 평균을 구하여 '평균 가격(mean_price)', '평균 길이(mean_length)' 칼럼을 추가하시오.
## e) Multiple columns and :=
## new data.table as an sample DT2 <- data.table(Cars93[, c("Model", "Type", "Price", "Length")]) head(DT2) # Model Type Price Length # 1: Integra Small 15.9 177 # 2: Legend Midsize 33.9 195 # 3: 90 Compact 29.1 180 # 4: 100 Midsize 37.7 193 # 5: 535i Midsize 30.0 186 # 6: Century Midsize 15.7 189
## LHS := RHS form in_cols = c("Price", "Length") out_cols = c("mean_price", "mean_length") DT2[, c(out_cols) := lapply(.SD, mean), # lapply() for multiple columns by = Type, # by Type groups .SDcols = in_cols] # compute the mean on columns specified in .SDcols
head(DT2) # Model Type Price Length mean_price mean_length # 1: Integra Small 15.9 177 10.16667 167.1905 # 2: Legend Midsize 33.9 195 27.21818 192.5455 # 3: 90 Compact 29.1 180 18.21250 182.1250 # 4: 100 Midsize 37.7 193 27.21818 192.5455 # 5: 535i Midsize 30.0 186 27.21818 192.5455 # 6: Century Midsize 15.7 189 27.21818 192.5455
|
[ Reference ]
* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html
다음번 포스팅에서는 R data.table에서 copy() 함수를 사용해서 깊은 복사(deep copy)를 하는 방법을 소개하겠습니다.
이번 포스팅이 많은 도움이 되었기를 바랍니다.
행복한 데이터 과학자 되세요! :-)