지난 포스팅에서는 R data.table에서 := 를 사용한 참조에 의한 얕은 복사(shallow copy)의 부작용과 copy() 함수를 사용한 깊은 복사(deep copy)에 대하여 알아보았습니다. 


이번 포스팅에서는 R data.table 에서 melt() 와 dcast() 함수를 사용해서 효율적으로 재구조화 (efficient reshaping of R data.table) 하는 방법을 소개하겠습니다. 


R reshape 패키지의 melt(), cast() 함수와 유사하므로 활용법에 있어서 어렵거나 특별한 것은 없습니다. 다만 R data.table은 재구조화의 과정이 내부적으로 전부 C 언어로 수행되므로 매우 빠르고 또 메모리 효율적입니다. 


(1) data.table 을 녹여서 넓은 자료구조를 길게 (wide to long) 재구조화 해주는 melt() 함수

(2) data.table 을 주조하여 긴 자료구조를 넓게 (long to wide) 재구조화 해주는 dcast() 함수





  (1) melt() : data.table 을 녹여서 넓은 자료구조를 길게 (wide to long) 재구조화


data.table 패키지를 불러오고, MASS 패키지에 내장되어 있는 Cars93 데이터프레임에서 행 1~5번 까지, 그리고 6개 칼럼만 선별해와서 예제로 사용할 간단한 data.table을 만들어보겠습니다. 



library(data.table)

library(MASS)


DT <- data.table(Cars93[1:5, c("Model", "Type", "DriveTrain", "Length", "Width", "Weight")])

print(DT)

# Model    Type DriveTrain Length Width Weight

# 1: Integra   Small      Front    177    68   2705

# 2:  Legend Midsize      Front    195    71   3560

# 3:      90 Compact      Front    180    67   3375

# 4:     100 Midsize      Front    193    70   3405

# 5:    535i Midsize       Rear    186    69   3640




이제 위에서 만든 data.table DT를 melt() 함수를 사용하여 ID(id.vars) 변수는 모델("Model"), 차종("Type"), 동력전달장치("DriveTrain") 의 3개 변수로 하고, 측정값 변수(measure.vars)로는 길이("Length"), 폭("Width"), 무게("Weight")의 3개 변수를 variable, value 의 2개 변수로 녹여서(melting) 재구조화함으로써, 옆으로 넓은 형태를 세로로 긴 형태 (wide to long)의 data.table로 재구조화 해보겠습니다. 



## -- 1. Melting data.tables (wide to long)

DT_melt_1 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measure.vars = c("Length", "Width", "Weight"))


## By default, the molten columns are automatically named 'variable' and 'value'.

print(DT_melt_1)

# Model    Type DriveTrain variable value

# 1: Integra   Small      Front   Length   177

# 2:  Legend Midsize      Front   Length   195

# 3:      90 Compact      Front   Length   180

# 4:     100 Midsize      Front   Length   193

# 5:    535i Midsize       Rear   Length   186

# 6: Integra   Small      Front    Width    68

# 7:  Legend Midsize      Front    Width    71

# 8:      90 Compact      Front    Width    67

# 9:     100 Midsize      Front    Width    70

# 10:    535i Midsize       Rear    Width    69

# 11: Integra   Small      Front   Weight  2705

# 12:  Legend Midsize      Front   Weight  3560

# 13:      90 Compact      Front   Weight  3375

# 14:     100 Midsize      Front   Weight  3405

# 15:    535i Midsize       Rear   Weight  3640


str(DT_melt_1)

# Classes 'data.table' and 'data.frame': 15 obs. of  5 variables:

#   $ Model     : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 49 56 9 1 6 ...

# $ Type      : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 4 3 1 3 3 ...

# $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 2 2 3 ...

# $ variable  : Factor w/ 3 levels "Length","Width",..: 1 1 1 1 1 2 2 2 2 2 ...

# $ value     : int  177 195 180 193 186 68 71 67 70 69 ...

# - attr(*, ".internal.selfref")=<externalptr>




위의 str()함수로 각 변수의 데이터 형태를 보면 "variable" 변수는 요인형(Factor) 입니다. melt() 함수로 재구조화 시 "variable" 칼럼의 기본 설정값은 요인형(Factor) 인데요, 만약 요인형 말고 문자형(charactor) 으로 하고 싶다면 variable.factor = FALSE 로 매개변수를 설정해주면 됩니다. 



## By default, 'variable' column is of type factor. 

## Set variable.factor argument to FALSE if you like to return a character vector. 

DT_melt_2 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measure.vars = c("Length", "Width", "Weight"), 

                  variable.factor = FALSE)


str(DT_melt_2)

# Classes 'data.table' and 'data.frame': 15 obs. of  5 variables:

#   $ Model     : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 49 56 9 1 6 ...

# $ Type      : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 4 3 1 3 3 ...

# $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 2 2 3 ...

# $ variable  : chr  "Length" "Length" "Length" "Length" ...  # <--- charactr vector

# $ value     : int  177 195 180 193 186 68 71 67 70 69 ...

# - attr(*, ".internal.selfref")=<externalptr>




만약 녹여서(melting) 길게(wide to long) 재구조화한 후의 "variable", "value" 변수 이름을 사용자가 지정해서 다른 이름으로 부여를 하고 싶다면 variable.name = "new_variable_name", value.name = "new_value_name" 처럼 매개변수에 새로운 칼럼 이름을 부여해주면 됩니다. 



## Name the 'variable' and 'value' columns to 'measure' and 'val' respectively.

DT_melt_3 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measures.vars = c("Length", "Width", "Weight"), 

                  variable.name = "measure"

                  value.name = "val")


head(DT_melt_3)

# Model    Type DriveTrain measure val

# 1: Integra   Small      Front  Length 177

# 2:  Legend Midsize      Front  Length 195

# 3:      90 Compact      Front  Length 180

# 4:     100 Midsize      Front  Length 193

# 5:    535i Midsize       Rear  Length 186

# 6: Integra   Small      Front   Width  68





  (2) dcast() : data.table 을 주조하여 긴 자료구조를 넓게 (long to wide) 재구조화


위의 (1)번에서 세로로 길게 재구조화한 data.table을 원래의 옆으로 넓은 형태로 역으로 재구조화를 하고 싶으면 dcast() 함수를 사용하면 됩니다. 


dcast(DT, ID1 + ID2 + ID3 ~ variable) 처럼 함수 형태의 구문을 사용합니다. 



## -- 2. dcasting data.tables (long to wide)

## reverse operation of melting

## dcast uses formula interface.

dcast(DT_melt_1, Model + Type + DriveTrain ~ variable)

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560




만약 위의 (1)번에서 "variable"과 "value" 칼럼이름을 사용자가 지정해서 melt()를 수행했다면 dcast() 를 하여 역으로 넓게 재구조화하려고 할 때 사용자가 지정한 변수(variable)와 값(value)의 칼럼 이름을 사용해주면 됩니다. 


위의 (1-3) 예에서 만든 DT_melt_3 이름의 data.table을 dcast()로 재구조화하려면 

dcast(DT_melt_3, Model + Type + DriveTrain ~ measure, value.var = "val"

처럼 위 (1-3)에서 지정했던 변수(variable) 이름인 'measure', 값(value) 이름인 value.var = "val" 을 써주면 됩니다. 



## 'value.var' denotes the column to be filled in with while casting to wide format. 

dcast(DT_melt_3, Model + Type + DriveTrain ~ measure

      value.var = "val")

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560

 



dcast() 함수에 집계함수(aggregation function)를 사용하여 ID 그룹별로 요약통계량을 계산한 결과를 재구조화하여 반환할 수도 있습니다.


위의 (1)에서 data.table을 길게 녹여서 재구조화했던 DT_melt_1에 대해서 차종(Type)을 기준으로 녹여서 만든 변수(variable)에 대해 평균(fun.aggregate = mean)을 집계하여 역으로 옆으로 넓게 재구조화한 data.table을 반환해보겠스니다. 



## You can pass a function to aggregate by in dcast with the argument 'fun.aggregate. 

dcast(DT_melt_1, Type ~ variable, fun.aggregate = mean)

# Type   Length Width Weight

# 1: Compact 180.0000    67   3375

# 2: Midsize 191.3333    70   3535

# 3:   Small 177.0000    68   2705




fun.aggregate 는 fun.agg 로 줄여서 쓸 수 있으며, fun.agg 뒤에는 function(x) 로 해서 어떠한 R 함수도 써줄 수 있습니다. 아래 예에서는 x가 결측값인지(1) 아닌지(0) 여부에 대해서 합(sum)을 구하는 집계함수로서 fun.agg = function(x) sum(!is.na(x)) 를 써주었습니다. (즉, 결측값이 아닌 행의 개수)


그리고 subset 매개변수를 사용하면 dcast()의 대상이 되는 data.table에서 특정 조건을 만족하는 부분집합만 필터링해와서 dcast() 재구조화를 할 수도 있습니다. 



## 'fun.agg' is the same with 'fun.aggregate'

## subset: Specified if casting should be done on a subset of the data.

dcast(DT_melt_1, Type ~ variable, 

      fun.agg = function(x) sum(!is.na(x))

      subset = .(variable != "Length"))

# Type Width Weight

# 1: Compact     1      1

# 2: Midsize     3      3

# 3:   Small     1      1



[Reference]

* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reshape.html



다음번 포스팅에서는 특정 패턴이 있는 data.table의 러개 칼럼을 동시에 녹이고 주조하여 재구조화하는 방법을 소개하겠습니다. (https://rfriend.tistory.com/576)



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

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



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 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


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

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


728x90
반응형
Posted by Rfriend
,
지난 포스팅에서는 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()로 여러개의 칼럼 한꺼번에 만들기

이번 포스팅은 R data.table vignette 의 Reference semantics 페이지참조하여 작성하였습니다.




  (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
          ...)]




  • data.table 참조하여 칼럼 추가 (Add column by Reference in data.table)
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



  • data.table 참조하여 칼럼 갱신 (Update column(s) by Reference in data.table)
이번에는 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)를 하는 방법을 소개하겠습니다.


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

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



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table에서 Key를 칼럼 j 와 그룹 by와 함께 사용하는 방법을 소개하였습니다. 


이번 포스팅에서는 R data.table에서 Key설정된 칼럼에 대해서 mult, nomatch 매개변수를 사용하여 조건이 매칭되는 행을 가져오는 다양한 방법을 소개하겠습니다. 


(1) Key 값이 매칭되는 모든 행 가져오기: mult = "all"

(2) Key 값이 매칭되는 첫번째 행 가져오기: mult = "first"

(3) Key 값이 매칭되는 마지막 행 가져오기: mult = "last"

(4) Key 값이 매칭되는 마지막 행의 값이 존재하는 행만 가져오기: mult = "last", nomatch = NULL





  (1) Key 값이 매칭되는 모든 행 가져오기: mult = "all"


먼저 data.table 패키지를 불러오고, MASS 패키지의 Cars93에서 칼럼 몇개만 선해서 예제로 사용할 간단한 data.table 자료를 만들어보겠습니다. 



library(data.table)

library(MASS)


DT <- data.table(Cars93[, c("Model", "Type", "Price", "DriveTrain")])


> str(DT)

Classes 'data.table' and 'data.frame': 93 obs. of  4 variables:

 $ Model     : Factor w/ 93 levels "100","190E","240",..: 55 9 22 30 52 83 90 11 7 14 ...

 $ Type      : Factor w/ 6 levels "Compact","Large",..: 1 1 1 1 1 1 1 1 1 1 ...

 $ Price     : num  19.5 29.1 13.4 11.4 15.8 13.3 11.3 17.5 16.5 15.7 ...

 $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 1 2 2 2 2 2 2 2 2 2 ...

 - attr(*, ".internal.selfref")=<externalptr> 

 - attr(*, "sorted")= chr [1:2] "Type" "DriveTrain"

 



다음으로, setkey() 함수를 사용하여 DT data.table에 차종(Type)과 동력전달장치(DriveTrain) 칼럼을 키로 설정(set the Key)해 보겠습니다. 



> # Set the key with Type and DriveTrain columns

> setkey(DT, Type, DriveTrain)

> key(DT)

[1] "Type"       "DriveTrain"

 



이제 준비가 되었으니 mult = "all" 매개변수를 사용해서 키 값과 매칭하는 모든 행을 가져와보겠습니다. 


키로 설정된 차종 동력전달치에서 (a) 차종이 "Compact", "Van", "Sporty" 이고 (Type == c("Compact", "Van", "Sporty")) & (b) 동력전달장치가 후륜 (DriveTrain == "Rear") 인 차를 모두 (mult = "all") 가져오시오. 


모두 9개의 행을 반환했는데요, 이중에서 3번, 4번 행을 보면, 차종이 "Van" (Type == "Van")이고 & 동력전달장치가 후륜(DriveTrain == "Rear") 차는 DT data.table 에 존재하지 않기 때문에 <NA>를 반환하였습니다.  



# Subset 'all' rows where  

# where Type matches ("Compact", "Sporty") and DriveTrain matches "Rear".  

DT[.(c("Compact", "Van", "Sporty"), "Rear")] # default mult = "all"


# or equivalently

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "all"] # default


      Model    Type Price DriveTrain

1:     190E Compact  31.9       Rear      #<--- first of "Compact" Type & "Rear" DriveTrain

2:      240 Compact  22.7       Rear      #<--- last of "Compact" Type & "Rear" DriveTrain


3:     <NA>     Van    NA       Rear    #<--- first of "Van" Type & "Rear" DriveTrain

4:     <NA>     Van    NA       Rear    #<--- last of "Van" Type & "Rear" DriveTrain


5:   Camaro  Sporty  15.1       Rear      #<--- first of "Sporty" Type & "Rear" DriveTrain

6: Corvette  Sporty  38.0       Rear

7:  Mustang  Sporty  15.9       Rear

8:     RX-7  Sporty  32.5       Rear

9: Firebird  Sporty  17.7       Rear         #<--- last of "Sporty" Type & "Rear" DriveTrain





  (2) Key 값이 매칭되는 첫번째 행 가져오기: mult = "first"


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 첫번째 행(mult = "first")을 가져오시오



# Subset only the 'first' matching row from all rows 

# where Type matches ("Compact", "Van", "Sporty") and DriveTrain matches "Rear".  

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "first"]

    Model    Type Price DriveTrain

1:   190E Compact  31.9       Rear

2:   <NA>     Van    NA       Rear

3: Camaro  Sporty  15.1       Rear






  (3) Key 값이 매칭되는 마지막 행 가져오기: mult = "last"


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 마지막 행(mult = "last")을 가져오시오



# Subset only the 'last' matching row from all rows

#  where Type matches ("Compact", "Van", "Sporty") and DriveTrain matches "Rear".  

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "last"]

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2:     <NA>     Van    NA       Rear

3: Firebird  Sporty  17.7       Rear

 





  (4) Key 값이 매칭되는 마지막 행의 값이 존재하는 행만 가져오기

      : mult = "last", nomatch = NULL


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 마지막 (mult = "last")을 가져오되, 매칭되는 값이 존재하는 행만(nomatch = NULL) 가져오시오



# We can choose if queries that do not match should return NA 

# or be skipped altogether using the nomatch argument.(nomatch = NULL)

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), 

+    mult = "last"

+    nomatch = NULL]

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2: Firebird  Sporty  17.7       Rear

 



[Reference]

* R data.table vignettes: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html


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

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



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 이 키(Key)와 빠른 이진 탐색(fast binary search) 기반의 부분집합 선택을 하는 원리, 함수 사용방법에 대하여 소개하였습니다.


지난 포스팅이 R data.table에 Key 를 활용하여 행(row, i)에 대해서만 subset 하는 방법에 대해 한정해서 소개했습니다. 반면, 이번 포스팅은 지난 포스팅을 이어받아 더 확장하여, DT[i, j, by][order()] 기본 구문에서 행뿐만 아니라 칼럼(column, j), 그룹(group, by) 별 연산, 정렬, 키 재설정 하는 방법까지 소개하겠습니다.


1. 키 설정 후, 특정 키(Key) 값에 행을 한정해 칼럼 j 가져오기 (select in j)

2. 체인연산으로 정렬하기 (chaining)

3. 특정 키(Key) 값에 행을 한정해 칼럼 j 에 연산 수행하기 (compute or do in j)

4. 칼럼 j에 := 를 사용하여 키의 부분집합에 할당하기(sub-assign by reference)

5. by 를 사용하여 그룹별 집계하기 (aggregation using by)





  1. 키 설정 후, 특정 키(Key) 값에 행을 한정해 칼럼 j 가져오기 (select in j)


먼저 Cars93 data.frame에서 칼럼 몇 개만 가져와서 예제로 사용할 간단할 data.table DT 를 만들어보겠습니다. 그리고 DT의 Type, DriveTrain 칼럼에 대해 키를 설정(setkey) 하겠습니다.


이렇게 키를 설정하면 data.table DT는 키인 Type, DriveTrain을 참조으로 오름차순으로 재정렬됩니다.



library(data.table)
library(MASS)
DT <- data.table(Cars93[, c("Model", "Type", "Price", "MPG.highway", "DriveTrain")])

# Combining keys with j and by
# Set keys with Type and DriveTrain
setkey(DT, Type, DriveTrain)


> key(DT)
[1] "Type"       "DriveTrain"

>

> # reordered by Keys

> head(DT, 10)
       Model    Type Price MPG.highway DriveTrain
 1:   Legacy Compact  19.5          30        4WD
 2:       90 Compact  29.1          26      Front
 3: Cavalier Compact  13.4          36      Front
 4:  Corsica Compact  11.4          34      Front
 5:  LeBaron Compact  15.8          28      Front
 6:   Spirit Compact  13.3          27      Front
 7:    Tempo Compact  11.3          27      Front
 8:   Accord Compact  17.5          31      Front
 9:      626 Compact  16.5          34      Front
10:   Altima Compact  15.7          30      Front




이제 위에서 설정한 (a) Type, DriveTrain 키(Key) 값이 Type == "Compact" & DriveTrain == "Front" 인 행을 선별하고 (subset row i), (b) 이들 부분집합에 대해서 칼럼(column j) Model, MPG.highway 를 선택해서 가져오겠습니다.


이렇게 data.table의 특정 키 값의 행만 가져오기(subset) 할 때 Key를 설정하면서 재정렬 된 상태이고 빠른 이진 탐색(fast binary search)로 키 값 데이터를 선별하므로 매우 빠르게 수행됩니다. 그리고 이렇게 선별한 subset에 한정해서 칼럼 j를 선택해서 가져오므로 메모리 효율적이고 빠릅니다.



> # Select in j
> DT[.("Compact", "Front"), .(Model, MPG.highway)]


       Model MPG.highway
 1:       90          26
 2: Cavalier          36
 3:  Corsica          34
 4:  LeBaron          28
 5:   Spirit          27
 6:    Tempo          27
 7:   Accord          31
 8:      626          34
 9:   Altima          30
10:  Achieva          31
11:  Sunbird          31
12:      900          26
13:   Passat          30




아래에는 위에서 수행한 행과 열 선택 과정을 data.table과 dplyr로 각각 했을 때의 코드를 비교해 보았습니다. 코드의 간결성면에서나 수행 속도면에서 data.table이 dplyr보다 우수합니다. (단, 코드의 가독성은 dplyr이 더 나아보입니다.)

data.table

dplyr

library(data.table)


setkey(DT, Type, DriveTrain)

DT[.("Compact", "Front"), .(Model, MPG.highway)]



library(dplyr)


DT %>%
  filter(Type == "Compact"
         & DriveTrain == "Front") %>%
  select(Model, MPG.highway)



위의 코드에서 뒤의 칼럼 j 를 선택할 때 c("Model", "MPG.highway") 처럼 큰 따옴표(" ")를 대신 사용할 수도 있으며, 프로그래밍을 할 때 유용하게 쓸 수 있습니다.



# or alternatively
DT[.("Compact", "Front"), c("Model", "MPG.highway")] # useful for programming

 





  2.  체인연산으로 결과 정렬하기 (chaining)


위의 (1) 번 결과를 MPG.highway 를 기준으로 오름차순 정렬하려면 Chaining 으로 [order(MPG.highway)] 를 뒤에 이어서 써주면 됩니다.



> # Chaining
> # - use chaining to order the MPG.highway column.
> # in ascending order
> DT[.("Compact", "Front"), .(Model, MPG.highway)][order(MPG.highway)]
       Model MPG.highway
 1:       90          26
 2:      900          26
 3:   Spirit          27
 4:    Tempo          27
 5:  LeBaron          28
 6:   Altima          30
 7:   Passat          30
 8:   Accord          31
 9:  Achieva          31
10:  Sunbird          31
11:  Corsica          34
12:      626          34
13: Cavalier          36
>



위의 (1)번 결과를 MPG.highway를 기준으로 내림차순(in decreasing order)으로 정렬을 하고 싶으면 마이너스 부호('-') 를 같이 써주어서 Chaining으로 [order(-MPG.highway)] 을 이어서 써주면 됩니다.



> # in descending order
> DT[.("Compact", "Front"), .(Model, MPG.highway)][order(-MPG.highway)]
       Model MPG.highway
 1: Cavalier          36
 2:  Corsica          34
 3:      626          34
 4:   Accord          31
 5:  Achieva          31
 6:  Sunbird          31
 7:   Altima          30
 8:   Passat          30
 9:  LeBaron          28
10:   Spirit          27
11:    Tempo          27
12:       90          26
13:      900          26






  3. 특정 키(Key) 값에 행을 한정해 칼럼 j 에 연산 수행하기 (compute or do in j)


(1)에서 설정한 키의 특정값인 Type == "Compact" & DriveTrain == "Front" 인 행을 선별하여, 이들 subset에 대해서만 MPG.highway 칼럼 j의 최대값(max), 평균(mean), 중앙값(median), 최소값(min), 1/4분위수(Q1), 3/4분위수(Q3)를 구해보겠습니다.


이때 요약통계량 연산을 하는데 사용하는 함수는 base R 함수의 max(), mean(), median(), min(), quantile() 과 동일하되, 속도는 base R 보다 data.table이 훨씬 빠릅니다!



> # Compute or do in j
> # - Find the maximum, minimum MPG.highway corresponding to Type == "Compact" and DriveTrain == "Front".
> DT[.("Compact", "Front"), max(MPG.highway)] # max
[1] 36
>
> DT[.("Compact", "Front"), mean(MPG.highway)] # mean
[1] 30.07692
>
> DT[.("Compact", "Front"), median(MPG.highway)] # median
[1] 30
>
> DT[.("Compact", "Front"), min(MPG.highway)] # min
[1] 26
>
> DT[.("Compact", "Front"), quantile(MPG.highway, c(0.25, 0.75))] # Q1, Q3
25% 75%
 27  31

 




  4. 칼럼 j에 := 를 사용하여 키의 부분집합에 할당하기(sub-assign by reference)


이번에는 Type을 키로 설정하고, 키의 값이 Type == "Large" 이면 ':=' 특수부호를 사용하여 Type == "Big" 의 다른 값으로 재할당(sub-assign by reference)해보겠습니다.


이러한 부분집합 재할당 때도 data.frame 대비 data.table이 훨씬 더 빠르게 수행이 됩니다.



> # sub-assign by reference using := in j
> DT <- data.table(Cars93[, c("Model", "Type", "Price", "MPG.highway", "DriveTrain")])
> setkey(DT, Type)
> key(DT)
[1] "Type"
>
> DT[, .N, by=Type]
      Type  N
1: Compact 16
2:   Large 11
3: Midsize 22
4:   Small 21
5:  Sporty 14
6:     Van  9

>

> DT[.("Large"), Type := "Big"]

>

> DT[, .N, by=Type]
      Type  N
1: Compact 16
2:     Big 11
3: Midsize 22
4:   Small 21
5:  Sporty 14
6:     Van  9
>



다만, 키의 특정 값을 재할당하고 나면 애초 키를 설정할 때 재정렬(reorder) 되었던 것이 이제 더이상 유효하지 않으므로 키가 해제됩니다.(Key 가 NULL로 설정됨)



> key(DT) # the key is removed by setting to NULL
NULL

 




  5. by 를 사용하여 그룹별 집계하기 (aggregation using by)


다시 data.table DT에 차종(Type)을 키로 설정한 후에, 동력전달장치(DriveTrain) 그룹별로 고속도로연비(MPG.highway) 의 최대값(max)을 집계해보겠습니다. 집계된 칼럼은 mpg_max 라는 이름을 부여하겠습니다.


이때 그룹별 집계의 결과값은 keyby 의 기준인 동력전달장치(DriveTrain)을 기준으로 정렬됩니다.



> # Aggregation using by
> setkey(DT, Type)
> key(DT) # key is Type.
[1] "Type"
>
> agg <- DT["Compact",
+           .(mpg_max = max(MPG.highway)),
+           keyby = DriveTrain] # keyby sets DriveTrain as the key and order it.
>
> head(agg)
   DriveTrain mpg_max
1:        4WD      30
2:      Front      36
3:       Rear      29




그리고, 그룹별 집계의 결과는 keyby 칼럼인 동력전달장치(DriveTrain)을 키로 설정됩니다.



> key(agg) # key is changed to DriveTrain.
[1] "DriveTrain"

 



만약 그룹별 집계를 할 때 그룹 연산 부호에 'keyby' 대신에 'by'를 사용하면 나중에 setkey(agg_2, DriveTrain) 처럼 별도의 키를 설정하는 코드를 추가해주어야 키가 설정이 되고 키를 기준으로 재정렬이 됩니다. (조금 번거롭습니다.)



> agg_2 <- DT["Compact",
+    .(mpg_max = max(MPG.highway)),
+    by = DriveTrain]
>

> # set the column 'DriveTrain' as a Key using setkey() explicitely
> setkey(agg_2, DriveTrain)
> head(agg_2)
   DriveTrain mpg_max
1:        4WD      30
2:      Front      36
3:       Rear      29
> key(agg_2)
[1] "DriveTrain"

 


[Reference] 

* data.table vignette: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html


다음번 포스팅에서는 R data.table에서 mult, nomatch 매개변수를 사용하여 Key값에 매칭되는 행 중에서 모든 행, 첫번째 행, 마지막 행, 값이 존재하는 행만 가져오는 방법을 소개하겠습니다. 


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

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



728x90
반응형
Posted by Rfriend
,

이전 포스팅에서는 R data.table 에서 여러개의 변수를 처리하는 특수부호 .SD, .SDcols, data.table의 기본구문 DT[i, j, by]를 이용하여 행 subset과 열 select하고 계산하기에 대해서 알아보았습니다. 


이번 포스팅에서는 data.table 에서 "키와 빠른 이진탐색 기반의 부분집합 선택 (Key and fast binary search based subset)" 에 대해서 소개하겠습니다. data.frame 대비 data.table의 subset 속도가 미친 듯이(!) 더 빠르고 메모리 효율적인 이유가 "Key and binary search based subset" 에 있습니다


R data.table에서 key 설정해서 정렬을 해두어 이진탐색으로 빠르게 관측치를 선별할 수 있는 것은 마치 DB에서 index를 설정해서 select 나 join 할 때 성능을 향상할 수 있는 것과 유사한 면이 있습니다. 


이번 포스팅은 data.table의 vignette을 거의 번역하다시피 대부분 참조하여 작성하였습니다. 


(1) data.table에서 키란 무엇인가? (What is Key in R data.table?)

(2) 이진 탐색이란 무엇인가? (What is binary search?)

(3) data.table에서 키를 설정하고, 확인하고, 사용하 (Set, get and use keys on a data.table)

(4) 느린 벡터 스캔(slow vector scan)과 빠른 이진 탐색(fast binary search) 속도 비교





  (1) data.table에서 키란 무엇인가? (What is Key in R data.table?)


data.table 의 Key에 대해 알아보기 전에 먼저 data.frame 의 행 이름 속성(row names attributes)에 대해서 먼저 알아보겠습니다. 아래와 같이 'DF'라는 이름의 간단한 data.frame을 만들어서 예를 들어보겠습니다. 



library(data.table)


set.seed(1)

DF = data.frame(g1 = c(rep("a", 6), rep("b", 4)), 

                g2 = sample(1:3, 10, TRUE), 

                x = sample(10), 

                stringsAsFactors = FALSE, 

                row.names = sample(LETTERS[1:10]))


> DF

  g1 g2  x

F  a  1  2

D  a  3  6

I  a  2  9

H  a  2  8

B  a  3  1

G  a  3  3

A  b  2  5

C  b  2 10

J  b  2  7

E  b  2  4

> rownames(DF)

 [1] "F" "D" "I" "H" "B" "G" "A" "C" "J" "E"

 



우리는 data.frame의 행(row)을 아래 예시의 DF["A", ] 처럼 부분집합을 선택해서 가져올 수 있습니다.  



> # subset a particular row using its row name

> DF["A", ]

  g1 g2 x

A  b  2 5




즉, data.frame의 행이름은 data.frame의 행에 대한 인덱스(index)와 다름없습니다. 그렇지만, 

1. 각 행은 정확하게 하나의 행 이름(excactly one row name)으로 한정됩니다.
   (만약 2개 이상의 이름을 가져야 하는 경우 제약)

2. 그리고, 행 이름은 유일(unique)해야 합니다. 아래의 예시와 이 행 이름에 중복이 있으면 "duplicate 'row.names' are not allowed" 라는 에러가 발생합니다. 



> # row names should be unique

> rownames(DF) = sample(LETTERS[1:3], 10, TRUE)

Error in `.rowNamesDF<-`(x, value = value) : 

  duplicate 'row.names' are not allowed

In addition: Warning message:

non-unique values when setting 'row.names': 'A', 'B'

 



위의 예제 data.frame data.table로 변환해보겠습니다. 



> # convert a data.frame to a data.table

> DT = as.data.table(DF)

> DT

    g1 g2  x

 1:  a  1  2

 2:  a  3  6

 3:  a  2  9

 4:  a  2  8

 5:  a  3  1

 6:  a  3  3

 7:  b  2  5

 8:  b  2 10

 9:  b  2  7

10:  b  2  4


> # Note that row names have been reset.

> rownames(DT)

 [1] "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10"

 



위에서 data.frame을 data.table로 변환한 결과 행 이름이 재설정되었습니다. 비록 data.table이 data.frame을 상속하기 때문에 행 이름 속성을 가지고 있기는 하지만, data.table은 행 이름을 절대 사용하지 않습니다



만약 data.table에서 행 이름을 계속 유지하고 싶다면 as.data.table(DF, keep.rownames = TRUE) 라는 옵션을 설정해주면 아래의 예시처럼 'rn'(row names) 이라 이름의 칼에 행 이름을 생성합니다. 



> # if you like to preserve the row naems, keep.rownames = TRUE

> # new column 'rn' is created

> DT2 = as.data.table(DF, keep.rownames = TRUE)

> DT2

    rn g1 g2  x

 1:  F  a  1  2

 2:  D  a  3  6

 3:  I  a  2  9

 4:  H  a  2  8

 5:  B  a  3  1

 6:  G  a  3  3

 7:  A  b  2  5

 8:  C  b  2 10

 9:  J  b  2  7

10:  E  b  2  4

 



data.frame의 행 이름(row names) 대신에 data.table 에서는 키(keys)를 설정하고 사용합니다. data.frame의 키(keys)를 한층 강화된 행 이름 (supercharged rownames) 이라고 생각할 수 있습니다. 


data.frame의 행 이름과는 다른 data.table 의 키 특성 (Keys properties)은 다음과 같습니다. 


  1. 키를 여러개의 칼럼에 설정할 수 있고, 칼럼은 다른 데이터 유형(예: 정수형, 실수형, 문자형, 요인형, 정수64형 등. 단, 리스트와 복소수형은 미지원) 을 사용할 수 있다. 
  2. 유일성(Uniqueness) 약이 없으며, 중복 키 값을 허용한다. 행은 키를 기준으로 정렬되므로, 키 칼럼의 어떤 중복값도 연속적으로 나타날 것이다. 
  3. 키를 설정하는 것은 두가지 일을 한다. 
    a. 물리적으로 data.table의 행을 Reference에 의해 제공되는 칼럼을 기준으로 항상 오름차순으로 재정렬한다 (reorder by reference in increasing order)
    b. 키에 해당하는 칼럼을 data.table의 'sorted'라고 불리는 속성을 설정함으로써 키 칼럼으로한다.  


  (2) 이진 탐색이란 무엇인가? (What is binary search?)


컴퓨터공학에서 이진 탐색(bianry search, 또는 half-interval search, logarithmic search, binary chop 이라고도 함)이란 정렬이 되어 있는 배열(within a sorted array) 안에서 목표값(targer value)의 위치를 찾는 탐색 알고리즘을 말합니다. 


이진탐색 알고리즘은 목표 값(Target value)와 정렬된 배열의 중간 원소(middle element of the sorted array)와 비교를 해서, 이 둘의 값이 같지 않으면 목표 이 존재하지 않을 나머지 반절은 배제합니다. 그리고 다시 남아있는 반절의 배열에서의 새로운 중간 위치 원소와 목표 값을 비교하고, 이 둘의 값이 같지 않으면 목표 값이 존재하지 않을 나머지 반절을 배제합니다. 이 단순한 중간 원소와 목표값과의 비교하는 과정을 목표 값을 찾을 때까지 반복합니다.  


아래의 예에서는 [1, 4, 5, 7, 9, 13, 14, 19, 27, 41, 44, 48, 64, 80, 85, 91, 94] 와 같이 정렬이 된 배열(sorted array)에 대해서 목표값 '9'를 찾아가는 과을 이진 탐색 알고리즘으로 실행해 본 것입니다. 모두 4번의 실행이 필요했네요. 




그러면 이진 탐색(binary search) 알고리즘이 순차 탐색 (seqneuce search) 알고리즘 보다 얼마나 더 효율적인지 최악의 경우의 시간 복잡도를 한번 비교해보겠습니다. 


데이터의 전체 개수가 n개라고 하고 연산회수의 함수를 T(n) 이라고 했을 때, 순차 탐색 알고리즘의 최악의 경우의 연산회수 함수는 T(n) = n 이 됩니다. 


반면에 이진 탐색 알고리즘의 최악의 경우, 아래와 같이 표값과 비교를 할 때 마다 데이터의 반절 씩을 배제해 나가게 되면 마지막 1개 데이터만 남을 때까지 k + 1 번의 연산회수가 필요하고 했을 때 T(n)= k + 1 에서 k를 구해보면, 이 됩니다. 

 



 


가령, 100억명의 사람 중에 특정 1명을 찾으려고 했을 때, 순차 탐색 알고리즘으로 찾으려면 최악의 경우 100억명을 모두 뒤져서 제일 끝에 찾게 되면 100억번의 비교가 필요하게 됩니다. 반면에 이진 탐색 알고리즘으로 100억명 중에서 특정 1명을 찾으려면 최악의 경우 k = log2(10,000,000,000) = 33.2 로서 약 34 (=33+1)번만 비교 연산을 하면 어떤 경우에도 찾을 수가 있게 됩니다.  


순차 탐색 알고리즘 vs. 이진 탐색 알고리즘 간의 최악의 연산회수는 10,000,000,000 번 연산 vs. 34 (=33+1)번 연산으로서 매우 매우 매우 큰 차이가 납니다. 


* source: https://en.wikipedia.org/wiki/Binary_search_algorithm



data.table에서 key 를 설정하면 자동으로 물리적으로 오름차순으로 정렬하여 data.table을 생성한다고 했는데요, 이렇게 정렬해 놓은 배열에 대해 이진 탐색 알고리즘을 적용였으니 data.table의 데이터 조회가 매우 매우 빠른 것입니다. 




  (3) data.table에서 키를 설정하고, 확인하고, 사용하기 

      (Set, get, and use keys on a data.table)


(3-1) data.table에서 키 설정하기 (Set keys on a data.table)


data.table에서 Key를 설정할 때는 setkey(DT, column_nm) 을 사용합니다. 그러면 왼쪽에 1, 2, 3, ..., n 처럼 정수로 key 가 부여되고 데이터는 키를 기준으로 오름차순 정렬이 됩니다. 


또는 setkeyv(DT, "column_nm") 식으로 setkeyv() 함수안에 큰 따옴표로 키 기준 칼럼이름을 넣어서 사용하면 프로그래밍을 하기에 유용합니다.  


MASS 패키지에 있는 Cars93 데이터셋에서 변수 몇개만 선별해서 DT 라는 이름의 data.table로 간단한 예제 데이터를 만들어서 예를 들어보겠습니다.


setkey(DT, Type) 으로 차종(Type)을 기준으로 키를 설정해주면 DT 데이터셋이 차종(Type)의 알파벳 오름차순 기준으로 물리적으로 정렬(reordered, sorted)이 된 data.table이 됩니다. 이때 키를 참조(reference)하여 재정렬이 되기 때문에 data.table의 행의 수와 같은 길이의 한 개의 칼럼에 대한 추가 메모리만 있으면 되므로 메모리 효율적입니다.



# b) Set, get adn use keys on a data.table

# How can we set the column 'Type' as key in the data.table 'DT'?

library(data.table)

library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Price", "MPG.highway", "DriveTrain")])


> str(DT)

Classes 'data.table' and 'data.frame': 93 obs. of  5 variables:

 $ Model      : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ...

 $ Type       : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ...

 $ Price      : num  15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ...

 $ MPG.highway: int  31 25 26 26 30 31 28 25 27 25 ...

 $ DriveTrain : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3 2 2 ...

 - attr(*, ".internal.selfref")=<externalptr>


# The data.table is now "reordered( or sorted)" by the column we provided - 'Type'. 

# Memory efficient, because we reorder by reference, we only require additional memory of 

# one column of length equal to the number of nrows in the data.table


setkey(DT, Type)


> head(DT)

      Model    Type Price MPG.highway DriveTrain

1:       90 Compact  29.1          26      Front

2: Cavalier Compact  13.4          36      Front

3:  Corsica Compact  11.4          34      Front

4:  LeBaron Compact  15.8          28      Front

5:   Spirit Compact  13.3          27      Front

6:    Tempo Compact  11.3          27      Front



# alternatively we can provide character vectors to the function 'setkeyv()'

setkeyv(DT, "Type") # useful to program with





(3-2) data.table에서 키 확인하기 (Get keys on a data.table)


Key 확인은 key() 함수로 합니다. 만약 키가 설정되어 있지 않다면 'NULL'을 반환합니다.



# How can we get the column(s) a data.table is keyed by?

# if no key is set, it returns 'NULL'.

> key(DT)
[1] "Type"





(3-3) data.table에서 키 사용하여 부분집합 가져오기 (Subset using keys on a data.table)


이렇게 키가 설정된 data.table에서 키의 특정 값과 일치하는 행의 부분집합을 가져오는 방법은 매우 쉽고 빠르며 (정렬된 상태에서 이진 탐색을 하므로 매우 빠름!), 여러가지 방법이 있습니다.


차종(Type) 중에서 "Van" 차종만을 가져오고 싶다면, DT[.("Van"), DT[J("Van")], DT[list("Van")], DT["Van"], DT[Type == "Van"] 중에서 아무거나 사용할 수 있습니다. (이게 좋을 수도 있는데요, 좀 헷갈리기도 합니다. ^^;)



# Subset all rows where the Type matches "Van"

> DT[.("Van")]

        Model Type Price MPG.highway DriveTrain

1: Lumina_APV  Van  16.3          23      Front

2:      Astro  Van  16.6          20        4WD

3:    Caravan  Van  19.0          21        4WD

4:   Aerostar  Van  19.9          20        4WD

5:        MPV  Van  19.1          24        4WD

6:      Quest  Van  19.1          23      Front

7: Silhouette  Van  19.5          23      Front

8:     Previa  Van  22.7          22        4WD

9:    Eurovan  Van  19.7          21      Front


# alternatively as below

DT[J("Van")]

DT[list("Van")]

DT["Van"]

DT[Type == "Van"]

 



만약 키를 설정한 칼럼에서 여러개의 값에 해당하는 모든 행을 부분집합으로 가져오고 싶다면 c()concatenate() 를 이용하여 원하는 모든 값을 써주면 됩니다.



# We can subset any amount of values s required.

# This returns all columns corresponding to those rows where Type column matches either "Compact" or "Van".

DT[c("Compact", "Van")]

DT[.(c("Compact", "Van"))]

 




(3-4) 여러개의 칼럼에 복수의 키를 설정하기 (Keys and multiple columns)


복수의 칼럼에 대해 키를 설정하려면 setkey(DT, Col_1, Col_2, ...) 또는 프로그래밍에 유용하도록 setkeyv(DT, c("Col_1", "Col_2", ...) 하는 형식으로 여러개 칼럼을 써주면 됩니다.


아래 예에서는 차종(Type), 동력전달장치(DriveTrain) 의 두 개 칼럼에 대해 키를 설정해보겠습니다.



# c) Keys and multiple columns
# We can set key on multiple columns and they can be of multiple types.
# How can I set keys on both 'Type' and 'Model' columns?
setkey(DT, Type, DriveTrain)

# or alternatively, provide a character vector of column names
setkeyv(DT, c("Type", "DriveTrain")) # useful for programming

> key(DT)
[1] "Type"       "DriveTrain"



이러면 (a) 차종(Type)을 참조하여 먼저 정렬을 하고, (b) 동력전달장치(DriveTrain)를 참조하여 다음으로 정렬을 하게 됩니다.



> # It sorts the data.table by the column 'Type' and then by 'Model' by reference
> head(DT)
      Model    Type Price MPG.highway DriveTrain
1:   Legacy Compact  19.5          30        4WD
2:       90 Compact  29.1          26      Front
3: Cavalier Compact  13.4          36      Front
4:  Corsica Compact  11.4          34      Front
5:  LeBaron Compact  15.8          28      Front
6:   Spirit Compact  13.3          27      Front

 



이번에는 차종(Type)이 "Compact"이고, 동력전달장치(DriveTrain)이 "Rear"인 모든 행을 부분집합으로 가져와보겠습니다. 이때 (a) 첫번째 키인 차종(Type)을 참조하여 차종이 "Compact"인 행을 매칭하고, (b) 차종이 "Compact"인 행에 한정해서 두번째 키인 동력전달장치(DriveTrain)를 참조하여 동력전달장치가 "Rear"인 행을 가져옮으로써 속도가 매우 빠르게 됩니다.


(즉, 키 설정을 통해 재정렬 + 이진 탐색 알고리즘 + 복수 키 subset 시 첫번째 키의 부분집합에 한정해서 두번째 키 탐색함으로써 속도 획기적 향상)



> # Subset all rows using key columns where first key column 'Type' matches 'Compact'
> # and second key column 'DriveTrain' matches 'Rear'
> # -- "Compact" is first matched against the first key column 'Type',
> # and within those matching rows, "Rear" is mached against the second key columns 'DriveTrain'
> # to obtain row indices where both 'Type' and 'DriveTrain' match the given values. --
> DT[.("Compact", "Rear")]
   Model    Type Price MPG.highway DriveTrain
1:  190E Compact  31.9          29       Rear
2:   240 Compact  22.7          28       Rear




위에서 data.table DT에 차종(Type), 동력전달장치(DriveTrain) 의 두 개 칼럼에 키를 설정하였는데요, 첫번째 키인 차종(Type)을 참조하여 차종이 "Compact"인 모든 행의 부분집합을 가져오는 것은 DT[.("Compact")] 또는 간단하게 DT["Compact"] 하면 됩니다.


반면에 두번째 키인 동력전달장치(DriveTrain)를 참조하여 "Rear"(후륜)인 모든 행의 부분집합을 가져오고 싶으면 첫번째 키인 차종(Type)의 고유한 값들을 무시할 수 없으므로 첫번째 키인 차종(.(unique(Type)))의 고유한 값도 함께 가져와야 해서 DT[.(unique(Type), "Rear"] 처럼 써줘야 합니다.


아래의 두번째 예인 DT[.(unique(Type), "Rear"] 의 결과에서 보면 12번째, 18번째 결과행에 차종(Type)이 "Small"과 "Van"인 경우 <NA> 로서 관측치가 없음에도 불구하고 unique(Type)에 "Small", "Van"도 포함이 되어있으므로 <NA>라는 표기와 함께 결과가 반환되었습니다.



> key(DT)
[1] "Type"       "DriveTrain"

>

> # Subset all rows where just the first key column 'Type' matches "Compact".

> DT[.("Compact")] # or in this case simply DT["Compact"]
       Model    Type Price MPG.highway DriveTrain
 1:   Legacy Compact  19.5          30        4WD
 2:       90 Compact  29.1          26      Front
 3: Cavalier Compact  13.4          36      Front
 4:  Corsica Compact  11.4          34      Front
 5:  LeBaron Compact  15.8          28      Front
 6:   Spirit Compact  13.3          27      Front
 7:    Tempo Compact  11.3          27      Front
 8:   Accord Compact  17.5          31      Front
 9:      626 Compact  16.5          34      Front
10:   Altima Compact  15.7          30      Front
11:  Achieva Compact  13.5          31      Front
12:  Sunbird Compact  11.1          31      Front
13:      900 Compact  28.7          26      Front
14:   Passat Compact  20.0          30      Front
15:     190E Compact  31.9          29       Rear
16:      240 Compact  22.7          28       Rear

>

>

> # Subset all rows where just the second key column 'DriveTrain' matches "Rear".
> DT[.(unique(Type), "Rear")]
             Model    Type Price MPG.highway DriveTrain
 1:           190E Compact  31.9          29       Rear
 2:            240 Compact  22.7          28       Rear
 3:     Roadmaster   Large  23.7          25       Rear
 4:        Caprice   Large  18.8          26       Rear
 5: Crown_Victoria   Large  20.9          26       Rear
 6:       Town_Car   Large  36.1          26       Rear
 7:           535i Midsize  30.0          30       Rear
 8:            Q45 Midsize  47.9          22       Rear
 9:          SC300 Midsize  35.2          23       Rear
10:           300E Midsize  61.9          25       Rear
11:         Cougar Midsize  14.9          26       Rear
12:           <NA>   Small    NA          NA       Rear
13:         Camaro  Sporty  15.1          28       Rear
14:       Corvette  Sporty  38.0          25       Rear
15:        Mustang  Sporty  15.9          29       Rear
16:           RX-7  Sporty  32.5          25       Rear
17:       Firebird  Sporty  17.7          28       Rear
18:           <NA>     Van    NA          NA       Rear

 



참고로, 설정되어 있는 키를 제거하려면 setkey(DT, NULL) 처럼 NULL 을 키로 설정해주면 됩니다. 




 (4) 느린 벡터 스캔(slow vector scan)과 빠른 이진 탐색(fast binary search) 속도 비교


data.table에서 벡터 스캔(Vector scan) 방식과 이진 탐색(Binary search) 방식의 subset 속도 차이를 비교하기 위해서 1억개의 행과 3개의 칼럼을 가진 data.table 샘플 데이터를 만들어보겠습니다. 데이터 크기가 1,907.4 Mb 이군요. 



## -- Performance of binary search approach

## - Create a sample data.table with 100 million rows and 3 columns. 

set.seed(1)

N = 1e8

DT = data.table(k1 = sample(LETTERS, N, replace = TRUE), 

                k2 = sample(1000, N, replace = TRUE), 

                val = rnorm(N, mean = 0, sd = 1))


## - number of rows

DT[, .N]

# [1] 100000000


## - size in Mb units

print(object.size(DT), units = "Mb")

# 1907.4 Mb

 




1억개 행을 가진 샘플 데이터가 준비되었으니, 


(a) Key가 없는 상태에서 전체 행을 모조리 뒤져서 (k1 == "A" & k2 == 750) 를 만족하는 행에는 TRUE, 만족하지 않는 행에는 FALSE 논리벡터를 생성하고 (TRUE & TRUE) 인 행만 subset 해오는  벡터 스캔(vector scan) 과, 


(b) Key가 있고, Key를 기준으로 정렬(sorted)이 된 data.table에서 (k1 == "A" & k2 == 750) 를 만족하는 행을 이진 탐색(binary search) 방식으로 subset 해오는 방식의 총 소요시간(elapsed time)을 비교해보겠습니다. 


아래에 결과를 보면 벡터 스캔 방식은 2.850 초 vs. 이진 탐색 방식은 0.001 초 걸렸다고 나오네요. 이 결과로 보면 이진 탐색 방식이 벡터 스캔 방식보다 2,850배 더 빠른 셈이군요!  비교 불가일 정도로 data.table의 Key를 활용한 이진탐색 방식이 겁나게 빠릅니다. 


벡터 스캔 방식은 subset 조건 만족 여부에 대한 논리 벡터 (TRUE, FALSE)를 행의 개수만큼 생성해야 하므로 메모리 효율 측면에서도 이진 탐색보다 비효율적입니다. 



 구분

 (a) 느린 벡터 스캔 

(slow vector scan)

(b) 빠른 이진 탐색 

(fast binary search) 

 key

 setkey(DT, NULL

 key(DT)
# NULL

 setkey(DT, k1, k2)

 key(DT)

# [1] "k1" "k2"

 syntax

 t_vc <- system.time(vc <- DT[k1 == "A" 

                             & k2 == 750])

 t_bs <- system.time(bs <- DT[.("A", 750)])


 elapsed 

 time

 t_vc

# user  system elapsed 

# 2.094   0.720   2.850

 t_bs   

# user  system elapsed 

# 0.001   0.000   0.001

 head

 head(vc)

# k1  k2         val

# 1:  A 750  1.11941277

# 2:  A 750 -0.06405564

# 3:  A 750  1.67845850

# 4:  A 750  0.32760125

# 5:  A 750 -0.08287104

# 6:  A 750 -0.97940166

 head(bs)

# k1  k2         val

# 1:  A 750  1.11941277

# 2:  A 750 -0.06405564

# 3:  A 750  1.67845850

# 4:  A 750  0.32760125

# 5:  A 750 -0.08287104

# 6:  A 750 -0.97940166 

 dimension

 dim(vc)

# [1] 3849    3

 dim(bs)

# [1] 3849    3



[Reference]

* R data.table vignette: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html

* Binary search algorithmhttps://en.wikipedia.org/wiki/Binary_search_algorithm



많은 도움이 되었기를 바랍니다. 

행복한 R 데이터 분석가 되세요.



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table의 DT[i, j, by] 에서 by 구문을 사용하여 "그룹별 집계" 하는 방법을 소개하였습니다. 


Base R만 쓰다가 몇 년 전에 처음으로 dplyr 의 chaining 형태의 구문을 봤을 때 '이게 R 맞나?' 갸우뚱 했었는데요, data.table 의 .N, .SD, .SDcols 등의 특수 부호가 포함된 data.table의 구문을 처음으로 봤을 때 역시나 '이게 R 맞나? 무슨 언어지?' 하고 의아해했었고 또 낯설기에 거부감도 들었습니다. 하지만 지난 포스팅에 이어 이번 포스팅까지 보고 나면 data.table에 대해 한결 친숙하게 느껴지실 거예요.  


이번 포스팅에서는 data.table 에서만 볼 수 있는 특수 부호로서 여러개의 칼럼을 처리할 때 사용하는 .SD, .SDcols  특수 부호의 기능과 사용법을 소개하겠습니다. 


(1) data.table .SD 특수 부호를 사용하여 여러개의 모든 칼럼 처리하기

(2) data.table .SDcols 특수 부호를 사용하여 특정 다수의 칼럼을 지정하여 처리하기

(3) data.table .SD, .SDcols 와 lapply 함수를 사용하여 여러개의 숫자형 변수를 표준화하기

     (How to standardize or transform the multiple numeric columns using .SD, .SDcols, lapply in R data.table?)


하는 방법을 소개하겠습니다. 




먼저, 예제로 사용할 1개의 문자형 변수와 3개의 숫자형 변수로 구성된 간단한 data.table 을 만들어보겠습니다. 



library(data.table)

library(dplyr)


grp <- c(rep('F', 5), rep('B', 3), rep('A', 2))

x1 <- c(1:10)

x2 <- seq(from = 11, to = 29, by = 2)

x3 <- seq(from = 30, to = 59, by = 3)

DT <- data.table(grp, x1, x2, x3)


> print(DT)

    grp x1 x2 x3

 1:   F  1 11 30

 2:   F  2 13 33

 3:   F  3 15 36

 4:   F  4 17 39

 5:   F  5 19 42

 6:   B  6 21 45

 7:   B  7 23 48

 8:   B  8 25 51

 9:   A  9 27 54

10:   A 10 29 57

 




  (1) data.table .SD 특수 부호를 사용하여 여러개의 모든 칼럼 처리하기


data.table 의 .SD 는 'Subset of Data' 의 첫머리 글자를 따서 만든 특수 부호로서, 그룹 칼럼(by grouping columns)을 제외한 모든 칼럼을 대상으로 연산을 수행할 때 사용합니다. 


아래 예에서는 'grp' 칼럼 ('F' 5행, 'B' 3행, 'A' 2행) 의 그룹을 기준으로 모든 변수(.SD)를 출력 (print(.SD))한 예입니다. 



# Special symbol .SD : Subset of Data

# It by itself is a data.table that holds the data for the current group defined using by.

# .SD contains all the columns except the grouping columns by default.

> DT[, print(.SD), by = grp]


   x1 x2 x3

1:  1 11 30

2:  2 13 33

3:  3 15 36

4:  4 17 39

5:  5 19 42


   x1 x2 x3

1:  6 21 45

2:  7 23 48

3:  8 25 51


   x1 x2 x3

1:  9 27 54

2: 10 29 57


Empty data.table (0 rows and 1 cols): grp

 



'grp' 그룹 별(by = grp)로 모든 변수(.SD)에 대해 요약통계량(summary statistics)을 계산하려면 R lapply(var_name, function) 함수를 적용해주면 됩니다. 


DT data.table 데이터셋에 대해 'grp' 그룹 별로 모든 변수에 대해 최소값(min), 최대값(max), 표준편차(standard deviation), 평균(mean) 을 계산하시오. 



> # To compute on (multiple) columns, 

> # we can then simply use the base R function lapply().

> DT[, lapply(.SD, min), by = grp]

   grp x1 x2 x3

1:   F  1 11 30

2:   B  6 21 45

3:   A  9 27 54

> DT[, lapply(.SD, max), by = grp]

   grp x1 x2 x3

1:   F  5 19 42

2:   B  8 25 51

3:   A 10 29 57

> DT[, lapply(.SD, sd), by = grp]

   grp        x1       x2       x3

1:   F 1.5811388 3.162278 4.743416

2:   B 1.0000000 2.000000 3.000000

3:   A 0.7071068 1.414214 2.121320

> DT[, lapply(.SD, mean), by = grp]

   grp  x1 x2   x3

1:   F 3.0 15 36.0

2:   B 7.0 23 48.0

3:   A 9.5 28 55.5

 



그룹별로 통계량을 계산하는 'grp' 변수를 기준으로 정렬하려면 아래의 두가지 방법을 사용합니다. 

  • DT[, lapply(.SD, mean), by = grp][order(grp)]
  • DT[, lapply(.SD, mean), keyby = grp]

> # ordering group column using 'keyby'

> DT[, lapply(.SD, mean), keyby = grp]

   grp  x1 x2   x3

1:   A 9.5 28 55.5

2:   B 7.0 23 48.0

3:   F 3.0 15 36.0




위와 동일한 결과를 얻기 위해서 dplyr 로는 아래처럼 대상이 되는 숫자형 변수를 summarise_at(vars(x1:x3), mean) 혹은 summarise_at(c("x1", "x2", "x3"), mean, na.rm = T) 처럼 변수 이름을 써줘야 합니다. 변수 개수가 많을 수록 일일이 써주기가 번거로울 수 있습니다. 반면 data.table 은 .SD 특수부호로 모든 변수를 가져올 수 있으니 편리합니다. 


> # by dplyr

library(dplyr)

> DT %>% 

+   group_by(grp) %>% 

+   summarise_at(vars(x1:x3), mean, na.rm = T)

# A tibble: 3 x 4

  grp      x1    x2    x3

  <chr> <dbl> <dbl> <dbl>

1 A       9.5    28  55.5

2 B       7      23  48  

3 F       3      15  36





  (2) data.table .SDcols 특수 부호를 사용하여 특정 다수의 칼럼을 지정하여 처리하기


data.table에서 다수의 특정 칼럼을 지정하고 싶으면 DT[i, j, by, .SDcols] 구문에서 by 다음에 .SDcols = c(var1, var2, ...) 처럼 .SDcols 매개변수를 사용합니다. 즉, .SDcols = c("x1", "x2") 로 "x1", "x2" 의 특정한 두 개 변수로만 지정을 하고, lapply(.SD, mean) 에서 .SD 로 .SDcols에서 특정한 칼럼 내에서의 모든 칼럼(즉, "x1", "x2")에 대해서 평균을 구하게 됩니다. 


DT data.table에서 ("A", "B") 그룹에 속하는 관측치에 한해, "x1", "x2" 칼럼에 대해서만 'grp' 그룹별(by = grp)로 각 변수별 평균(lapply(.SD, mean))을 구하시오.



# .SDcols

# to specify just the columns we want to compute the mean() on using .SDcols.

DT[grp %in% c("A", "B"),    # subset of ("A", "B") in grp

   lapply(.SD, mean),       # compute the mean for multiple columns except grp. col.

   by = grp,             # by 'grp' groups, orginal order

   .SDcols = c("x1", "x2")] # for just those specified in .SDcols


[Out]

   grp  x1 x2

1:   B 7.0 23

2:   A 9.5 28

 




  (3) data.table .SD, .SDcols 와 lapply 함수를 사용하여 여러개의 숫자형 변수를 표준화

      (How to standardize or transform the multiple numeric columns using .SD, .SDcols, lapply in R data.table?)


만약 여러개의 숫자형 변수 모두를 한꺼번에 z-표준화(z = (x - mean) / std_dev)를 하고 싶다면 for loop 반복문을 사용하는 대신에 R lapply(변수, 함수) 를 사용하면 쉽고 깔끔하게 코드를 짤 수 있습니다. 


DT data.table 의 숫자형 변수인 ("x1", "x2", "x3") 특정 변수(.SDcols)들 모두에(.SD) 대해서 한꺼번에 표준화(standardization)를 하시오.  


> # Standardization of all numeric columns

> DT_scaled <- DT[, lapply(.SD, scale), .SDcols = c("x1", "x2", "x3")]

> print(DT_scaled)

         x1.V1      x2.V1      x3.V1

 1: -1.4863011 -1.4863011 -1.4863011

 2: -1.1560120 -1.1560120 -1.1560120

 3: -0.8257228 -0.8257228 -0.8257228

 4: -0.4954337 -0.4954337 -0.4954337

 5: -0.1651446 -0.1651446 -0.1651446

 6:  0.1651446  0.1651446  0.1651446

 7:  0.4954337  0.4954337  0.4954337

 8:  0.8257228  0.8257228  0.8257228

 9:  1.1560120  1.1560120  1.1560120

10:  1.4863011  1.4863011  1.4863011




위의 실행 결과는 결과값의 변수 이름이 "x1.V1", "x2.V1", "x3.V1" 으로 나와서 마음에 안드네요. 이번에는  ':=' 특수 부호를 사용하여 새로운 변수 이름을 부여(set new column names)해 보겠습니다. data.table 만의 특수 부호인 '.SD', '.SDcols', ':=' 가 총 출동해서 정신이 없네요. 허허.. ^^;;;


DT data.table 의 숫자형 변수인 ("x1", "x2", "x3") 특정 변수(.SDcols)들 모두에(.SD) 대해서 한꺼번에 표준화(standardization)를 하여 DT data.table에 ("scaled_x1", "scaled_x2", "scaled_x3") 라는 새로운 이름의 변수를 추가하시오. 


> # Standardization of all numeric columns with new column names

> num_vars <- c('x1', 'x2', 'x3')

> new_num_vars <- paste0('scaled_', num_vars)

> # In data.table, the := operator and all the set* (e.g., setkey, setorder, setnames etc..) functions 

> # are the only ones which modify the input object by reference

> DT_scaled_2 <- DT[, (new_num_vars) := lapply(.SD, scale), .SDcols = num_vars]

> print(DT_scaled_2)

    grp x1 x2 x3  scaled_x1  scaled_x2  scaled_x3

 1:   F  1 11 30 -1.4863011 -1.4863011 -1.4863011

 2:   F  2 13 33 -1.1560120 -1.1560120 -1.1560120

 3:   F  3 15 36 -0.8257228 -0.8257228 -0.8257228

 4:   F  4 17 39 -0.4954337 -0.4954337 -0.4954337

 5:   F  5 19 42 -0.1651446 -0.1651446 -0.1651446

 6:   B  6 21 45  0.1651446  0.1651446  0.1651446

 7:   B  7 23 48  0.4954337  0.4954337  0.4954337

 8:   B  8 25 51  0.8257228  0.8257228  0.8257228

 9:   A  9 27 54  1.1560120  1.1560120  1.1560120

10:   A 10 29 57  1.4863011  1.4863011  1.4863011




물론, Base R에서 사용하는 apply(dataset, 2 for col, function) 의 구문으로 각 칼럼별 평균과 표준편차를 구해서 별도의 객체로 저장을 해놓고, t(apply(dataset, 1 for row, function(x) {transformation_function} 의 형태로 데이터를 변환할 수도 있습니다. 위의 예제 코드보다 조금 복잡하기는 하지만 function(x){custom_function} 부분에 custom function 도 넣을 수 있고 해서 유연하게 쓸 수 있으므로 알아두면 좋겠습니다. 아래에서 t() 는 전치(transpose) 하라는 뜻입니다. 



> # -- standardization using apply() and function()

> #-- mean by columns

> num_vars_mean <- apply(DT[, .(x1, x2, x3)], 2, mean)

> print(num_vars_mean)

  x1   x2   x3 

 5.5 20.0 43.5 

> # sd by columns

> num_vars_sd <- apply(DT[, .(x1, x2, x3)], 2, sd)

> print(num_vars_sd)

      x1       x2       x3 

3.027650 6.055301 9.082951 

> t(apply(DT[, 2:4], 1, function(x){

+   (x - num_vars_mean) / num_vars_sd})

+ )

              x1         x2         x3

 [1,] -1.4863011 -1.4863011 -1.4863011

 [2,] -1.1560120 -1.1560120 -1.1560120

 [3,] -0.8257228 -0.8257228 -0.8257228

 [4,] -0.4954337 -0.4954337 -0.4954337

 [5,] -0.1651446 -0.1651446 -0.1651446

 [6,]  0.1651446  0.1651446  0.1651446

 [7,]  0.4954337  0.4954337  0.4954337

 [8,]  0.8257228  0.8257228  0.8257228

 [9,]  1.1560120  1.1560120  1.1560120

[10,]  1.4863011  1.4863011  1.4863011




아래 예제는 여러개의 모든 숫자형 변수의 최대값과 최소값을 계산하여 객체로 저장해놓고, apply() 함수로 [0-1] 변환을 해보았습니다. 



> # [0-1] transformation using apply() and function()

> # max and min for each columns

> num_vars_max <- apply(DT[, .(x1, x2, x3)], 2, max)

> num_vars_min <- apply(DT[, .(x1, x2, x3)], 2, min)

> # [0-1] transformation

> t(apply(DT[, 2:4], 1, function(x){

+   (x - num_vars_min) / (num_vars_max - num_vars_min)})

+ )

             x1        x2        x3

 [1,] 0.0000000 0.0000000 0.0000000

 [2,] 0.1111111 0.1111111 0.1111111

 [3,] 0.2222222 0.2222222 0.2222222

 [4,] 0.3333333 0.3333333 0.3333333

 [5,] 0.4444444 0.4444444 0.4444444

 [6,] 0.5555556 0.5555556 0.5555556

 [7,] 0.6666667 0.6666667 0.6666667

 [8,] 0.7777778 0.7777778 0.7777778

 [9,] 0.8888889 0.8888889 0.8888889

[10,] 1.0000000 1.0000000 1.0000000




[Reference]

https://cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 의 기본 구문 DT[i, j, by] 에서 행과 열 선택하고 계산하는 방법에 대하여 소개하였습니다.  


이번 포스팅에서는 R data.table 의 DT[i, j, by] by 표현를 이용하여 그룹별로 집계하는 방법을 소개하겠습니다. 


(1) Base R dplyr, data.table 패키지별 그룹별 집계하기

(2) data.table에서 두 개 이상 그룹 기준별 집계하기

(3) data.table에서 keyby 로 그룹별 집계결과를 그룹 key 기준으로 정렬하기

(4) data.table에서 Chaining과 order()로 그룹별 집계결과 정렬하기

(5) data.table에서 by 에 조건절의 블리언 그룹별로 집계하기



예제로 사용할 데이터는 MASS 패키지에 내장되어 있는 Cars93 데이터프레임입니다. Base R과 dplyr에 사용할 'df' 이름의 data.frame과 data.table에서 사용할 'DT' 이름의 data.table을 만들어보겠습니다. 



library(data.table)

library(dplyr)

library(MASS)


df <- data.frame(Cars93)

DT <- data.table(df)

str(DT)


> str(DT)

Classes 'data.table' and 'data.frame': 93 obs. of  27 variables:

 $ Manufacturer      : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4 4 4 5 ...

 $ Model             : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ...

 $ Type              : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ...

 $ Min.Price         : num  12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ...

 $ Price             : num  15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ...

 $ Max.Price         : num  18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3 36.3 ...

 $ MPG.city          : int  25 18 20 19 22 22 19 16 19 16 ...

 $ MPG.highway       : int  31 25 26 26 30 31 28 25 27 25 ...

 $ AirBags           : Factor w/ 3 levels "Driver & Passenger",..: 3 1 2 1 2 2 2 2 2 2 ...

 $ DriveTrain        : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3 2 2 ...

 $ Cylinders         : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4 4 4 5 ...

 $ EngineSize        : num  1.8 3.2 2.8 2.8 3.5 2.2 3.8 5.7 3.8 4.9 ...

 $ Horsepower        : int  140 200 172 172 208 110 170 180 170 200 ...

 $ RPM               : int  6300 5500 5500 5500 5700 5200 4800 4000 4800 4100 ...

 $ Rev.per.mile      : int  2890 2335 2280 2535 2545 2565 1570 1320 1690 1510 ...

 $ Man.trans.avail   : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 1 1 ...

 $ Fuel.tank.capacity: num  13.2 18 16.9 21.1 21.1 16.4 18 23 18.8 18 ...

 $ Passengers        : int  5 5 5 6 4 6 6 6 5 6 ...

 $ Length            : int  177 195 180 193 186 189 200 216 198 206 ...

 $ Wheelbase         : int  102 115 102 106 109 105 111 116 108 114 ...

 $ Width             : int  68 71 67 70 69 69 74 78 73 73 ...

 $ Turn.circle       : int  37 38 37 37 39 41 42 45 41 43 ...

 $ Rear.seat.room    : num  26.5 30 28 31 27 28 30.5 30.5 26.5 35 ...

 $ Luggage.room      : int  11 15 14 17 13 16 17 21 14 18 ...

 $ Weight            : int  2705 3560 3375 3405 3640 2880 3470 4105 3495 3620 ...

 $ Origin            : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1 1 1 1 ...

 $ Make              : Factor w/ 93 levels "Acura Integra",..: 1 2 4 3 5 6 7 9 8 10 ...

 - attr(*, ".internal.selfref")=<externalptr>

 



  (1) Base R dplyr, data.table 패키지별 그룹별 집계하기





생산기지(Origin)이 "USA" 인 차에 대해서 차종(Type) 별로 차량 대수를 집계(aggregation by Type for Origin from "USA") 를 base R, dplyr, data.table 패키지별로 해보겠습니다. 

 

data.table 패키지는 기본 구분 DT[i, j, by] 를 사용하여, 

  (a) DT data.table 로 부터 

  (b) i 에서 Origin == "USA" 조건으로 행 subset 을 해주고, 

  (c) j 에서  .(cnt = .N) 으로 행의 개수를 세어서 'cnt' 라는 칼럼 이름으로 집계하는데 이때

  (d) by 에서 by = Type 으로 설정하여 차종(Type) 그룹별로 집계를 합니다. 이때 'by ='는 생략 가능합니다. 


다른 패키지 대비 data.table 의 그룹별 집계 코드가 매우 간결하고 또 속도도 빠릅니다! 

이때 결과가 Type 의 알파벳 순서로 정렬되어 있지 않으며, keyby 를 사용하면 그룹 기준 변수의 항목 순서대로 정렬할 수 있습니다. ( (3)번 예시 참조 )


base R 에서는 table() 함수로 행 개수 집계를 한 후에 data.frame() 으로 변환을 해주었는데요, 그룹 기준의 알파벳 순서대로 정렬이 되어 있습니다. 집계 결과 표의 변수 이름이 "Var1", "Freq" 로 되어 있어서 칼럼 이름을 바꿔주고 싶으면 colnames() 함수로 사용해야 하는 불편함이 있습니다. 


dplyr 는 연산 체인(Chaining of operations) 방식으로 구문을 표현하므로 사람이 생각하는 방식과 유사한 순서로서 코드 짜기도 쉽고 또 가독성도 매우 좋습니다. 집계 결과는 그룹 기준이 Type의 알파벳 순서대로 정렬이 되었습니다. 


"생산기지(Origin)이 "USA"인 차 중에서 차종(Type) 별로 차 대수를 집계하시오."


 패키지

 R codes (aggregation by group)

결과 (results) 

 Base R

data.frame(table(df[df$Origin == "USA", ]$Type))

     Var1 Freq

1 Compact    7

2   Large   11

3 Midsize   10

4   Small    7

5  Sporty    8

6     Van    5

 dplyr

df %>% 

  filter(Origin == "USA") %>% 

  group_by(Type) %>% 

  summarise(cnt = n())

# A tibble: 6 x 2

  Type      cnt

  <fct>   <int>

1 Compact     7

2 Large      11

3 Midsize    10

4 Small       7

5 Sporty      8

6 Van         5

 data.table

DT[Origin == "USA", .(cnt = .N), by = Type]


# equivalent : without 'by'

DT[Origin == "USA", .N, Type]

      Type cnt

1: Midsize  10

2:   Large  11

3: Compact   7

4:  Sporty   8

5:     Van   5

6:   Small   7




  (2) data.table에서 두 개 이상 그룹 기준별 집계하기


이번에는 by = .(그룹기준1, 그룹기준2, ...) 의 'by' 구분으로 두 개 이상의 그룹 기준 별로 집계하는 방법입니다. 그룹기준을 묶어놓은 괄호의 앞에 있는 작은 점('.') 은 오타가 아니라 list() 를 의미하는 것이므로 꼭 포함시키기 바랍니다. 


생산기지(Origin)가 "USA"인 차 중에서 차종(Type) 과 실린더(Cylinders) 그룹을 기준으로 고속도로연비(MPG.highway)의 평균(mean)을 구하시오.



# Counting the number of rows for each Type and Cylinders for Origin "USA".

DT[Origin == "USA", 

   .(mean_mpg_highway = mean(MPG.highway)), 

   by = .(Type, Cylinders)]


# or equivalently

DT[Origin == "USA", 

   .(mean_mpg_highway = mean(MPG.highway)), 

   by = list(Type, Cylinders)]


[Out]

       Type  Cylinders  mean_mpg_highway

 1: Midsize         4         29.50000

 2:   Large         6         27.28571

 3: Midsize         6         27.20000

 4:   Large         8         25.75000

 5: Midsize         8         25.00000

 6: Compact         4         30.57143

 7:  Sporty         6         26.66667

 8:     Van         6         21.40000

 9:  Sporty         8         25.00000

10:   Small         4         33.85714

11:  Sporty         4         28.75000

 




  (3) data.table에서 keyby 로 그룹별 집계결과를 그룹 key 기준으로 정렬하기


위의 (2)번에서 차종과 실린더 그룹별 평균 계산 결과가 정렬이 안되어 있는데요(original order 로 정렬되어 있다보니 정렬이 안된것 처럼 보임), keyby = .(Type, Cylinders) 를 사용하여 차종과 실린더 기준으로 정렬을 해서 결과를 제시해보겠습니다. 


이때 keyby 를 사용하면 단순히 정렬만 해서 제시하는 것 뿐만 아니라 'sorted'라는 attribute를 설정하여 'Key'를 설정해주게 됩니다. 


생산기지(Origin)가 "USA"인 차 중에서 차종(Type) 과 실린더(Cylinders) 그룹을 기준으로 고속도로연비(MPG.highway)의 평균(mean)을 구하시오. 이때 그룹별 평균 계산 결과를 차종(Type)과 실린더(Cylinders) 그룹 기준으로 정렬하여 제시하시오. 



# Sorted by: keyby

# Directly order by all the grouping variables using 'keyby'.

DT[Origin == "USA", 

   .(mean_mpg_highway = mean(MPG.highway)), 

   keyby = .(Type, Cylinders)]


[Out] 

       Type Cylinders mean_mpg_highway

 1: Compact         4         30.57143

 2:   Large         6         27.28571

 3:   Large         8         25.75000

 4: Midsize         4         29.50000

 5: Midsize         6         27.20000

 6: Midsize         8         25.00000

 7:   Small         4         33.85714

 8:  Sporty         4         28.75000

 9:  Sporty         6         26.66667

10:  Sporty         8         25.00000

11:     Van         6         21.40000

 




  (4) data.table에서 Chaining과 order()로 그룹별 집계결과 정렬하기


만약 그룹별 집계 결과를 제시할 때 오름차순(in ascending order) 또는 내림차순(in descending order) 으로 정렬하는 방법을 설정하고 싶을 때는 Chaining 으로 [order()] 구문을 추가해주면 됩니다. 


order() 구문에서 오름차순을 변수 이름만 써주면 되며, 내림차순일 때는 변수 앞에 '마이너스 부호('-')를 붙여줍니다. 


생산기지(Origin)이 "USA"인 차 중에서 차종(Type)과 실린더(Cylinders) 그룹 별로 고속도로 연비(MPG.highway)의 평균(mean)을 구하여, 차종 내림차순(Type in descending order)과 실린더 오름차순(Cylinders in ascending order) 으로 정렬하여 제시하시오. 



# Chaining of operations

# Ordering by Type in descending order and Cylinders in ascending order using 'order'

DT[Origin == "USA", 

   .(mean_mpg_highway = mean(MPG.highway)), 

   by = .(Type, Cylinders)][

     order(-Type,     # in descending order

           Cylinders  # in ascending order

           )]


[Out]

       Type Cylinders mean_mpg_highway

 1:     Van         6         21.40000

 2:  Sporty         4         28.75000

 3:  Sporty         6         26.66667

 4:  Sporty         8         25.00000

 5:   Small         4         33.85714

 6: Midsize         4         29.50000

 7: Midsize         6         27.20000

 8: Midsize         8         25.00000

 9:   Large         6         27.28571

10:   Large         8         25.75000

11: Compact         4         30.57143

 



dplyr 는 Chaining 으로 표기할 때 '%>%' (shift + ctrl + M) 을 사용하는데요, data.table 은 그냥 기본 구문인 DT[i, j, by] 의 뒤에 바로 [ ... ] 를 이어서 붙여서 써주면 되므로 dplyr 대비 data.table 이 Chaining 표기도 더 깔끔합니다. 


data.table 의 Chaining 시 행이 너무 길 경우에는 가독성을 높이기 위해 줄 넘김을 할 수 있습니다. (아래의 2가지 형태 참조) 



# Chaining of operations in data.table

DT[ ... ][ ... ][ ... ][ ... ]


# or equivalently

DT[ ... 

     ][ ... 

        ][ ...

          ][ ... 

            ]





  (5) data.table에서 by 에 조건절의 블리언 그룹별로 집계하기


그룹별 집계의 마지막으로, by 구문에 조건절을 넣어서 TRUE, FALSE 블리언 그룹별로 집계를 하는 방법에 대한 소개입니다. 


가격(Price)이 19.0 보다 큰 지 여부(TRUE, FALSE)와 고속도로 연비(MPG.highway)가 29.0 보다 큰지 여부(TRUE, FALSE)를 기준으로 그룹별로 차의 대수를 세어서 집계하시오.


keyby = .(Price > 19.0, MPG.highway > 29.0) 으로 해서 정렬(ordering)하여 제시하라고 했더니 R은 FALSE 를 '0'으로 인식하고, TRUE는 '1'로 인식하므로 FALSE 그룹을 먼저 제시하였군요. 




# Expressions in 'by'

# to find out how many cars price  

DT[, .N, keyby = .(Price > 19.0, MPG.highway > 29.0)]


[Out]

   Price MPG.highway  N

1: FALSE       FALSE 20

2: FALSE        TRUE 33

3:  TRUE       FALSE 35

4:  TRUE        TRUE  5

 



[Reference]

* https://cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html



다음 포스팅에서는 R data.table에서 여러개의 칼럼을 처리할 때 사용하는 .SD, .SDcols 특수 부호에 대해서 알아보겠습니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 Base R, dplyr, SQL 구문과 R data.table 의 기본구문을 비교해봄으로써 data.table 의 기본 구문이 상대적으로 간결하여 코딩을 읽고 쓰기에 좋다는 점을 소개하였습니다. 


이번 포스팅에서는 R data.table 의 기본 구문 DT[i, j, by] 에서 한걸음 더 나아가서 i 행을 선별하고 정렬하기, j 열을 선택하고 계산하기, by 그룹 별로 집계하기 등을 하는데 있어 소소한 팁을 추가로 설명하겠습니다. 


R data.table 의 기본 구문 DT[i, j, by][order]



MASS 패키지에 있는 Cars93 data.frame을 data.table로 변환하고 앞에서부터 8개 변수만 가져와서 예제로 사용하겠습니다. 



library(data.table)


# getting Cars93 dataset

library(MASS)

DT <- as.data.table(Cars93) # convert data.frame to data.table

DT <- DT[,1:8]  # use the first 8 variables


> str(DT)

Classes 'data.table' and 'data.frame': 93 obs. of  8 variables:

 $ Manufacturer: Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4 4 4 5 ...

 $ Model       : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ...

 $ Type        : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ...

 $ Min.Price   : num  12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ...

 $ Price       : num  15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ...

 $ Max.Price   : num  18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3 36.3 ...

 $ MPG.city    : int  25 18 20 19 22 22 19 16 19 16 ...

 $ MPG.highway : int  31 25 26 26 30 31 28 25 27 25 ...

 - attr(*, ".internal.selfref")=<externalptr> 

>

> head(DT)

   Manufacturer   Model    Type Min.Price Price Max.Price MPG.city MPG.highway

1:        Acura Integra   Small      12.9  15.9      18.8       25          31

2:        Acura  Legend Midsize      29.2  33.9      38.7       18          25

3:         Audi      90 Compact      25.9  29.1      32.3       20          26

4:         Audi     100 Midsize      30.8  37.7      44.6       19          26

5:          BMW    535i Midsize      23.7  30.0      36.2       22          30

6:        Buick Century Midsize      14.2  15.7      17.3       22          31

 




  (1) DT[i, j, by] : i 행 선별하기 (Subset rows in i)


DT[i, j, by] 기본 구문에서 i 행 조건문을 이용하여 [ 차종(Type)이 소형("Small")이고 & 제조사(Manufacturer)가 현대("Hyundai") 인 관측치 ] 를 선별해보겠습니다. 



> # Subset rows in i

> # Get all the cars with "Small" as the Type of "Hyundai" manufacturer

> # No need of prefix 'DT$' each time.

> small_hyundai <- DT[Type == "Small" & Manufacturer == "Hyundai"]

>

> print(small_hyundai)

   Manufacturer   Model  Type Min.Price Price Max.Price MPG.city MPG.highway

1:      Hyundai   Excel Small       6.8     8       9.2       29          33

2:      Hyundai Elantra Small       9.0    10      11.0       22          29

 



이때 Base R 대비 소소한 차이점이 있는데요, Base R에서는 dataframe_nm$variable_nm 처럼 접두사로서 원천이 되는 "dataframe_nm$" 을 명시해주는데요, data.table은 위의 예에서 처럼 DT[...] 의 [...] 안에서는 'DT$variable_nm' 처럼 'DT$' 를 명시 안해줘도 되고, 아니면 아래 예에서처럼 DT[...] 대괄호 안에서 'DT$'를 명시해줘도 됩니다. ('DT$'를 안해줘도 잘 작동하는데 굳이 'DT$' 접두사를 꼬박꼬박 붙일 이유는 없겠지요).  



> # Prefix of 'DT$' works fine. 

> DT[DT$Type == "Small" & DT$Manufacturer == "Hyundai"]

   Manufacturer   Model  Type Min.Price Price Max.Price MPG.city MPG.highway

1:      Hyundai   Excel Small       6.8     8       9.2       29          33

2:      Hyundai Elantra Small       9.0    10      11.0       22          29




Base R 과의 또 하나 차이점은 data.table의 경우 위의 예에서 처럼 i 조건절만 써주고 뒤에 '콤마(,)'를 안찍어 줘도 모든 열(all columns)을 자동으로 선택해온다는 점입니다. 물론 아래의 예와 같이 Base R 구문처럼 i 행 조건절 다음에 '콤마(,)'를 명시적으로 찍어줘서 '모든 열(all columns)을 선택'하게끔 해도 정상 작동하기는 합니다. 



> # a comma after the condition in i works fine. 

> DT[Type == "Small" & Manufacturer == "Hyundai", ]

   Manufacturer   Model  Type Min.Price Price Max.Price MPG.city MPG.highway

1:      Hyundai   Excel Small       6.8     8       9.2       29          33

2:      Hyundai Elantra Small       9.0    10      11.0       22          29

 



슬라이싱을 이용해서 [ 1번째부터 ~ 5번째 행까지의 데이터 가져오기 ] 를 할 수 있습니다. (이때 행 조건 다음에 콤마를 사용하지 않은 DT[1:5] 는 콤마(,)를 추가한 DT[1:5, ] 와 결과는 동일합니다. )



# Get the first 5 rows.

DT_5 <- DT[1:5]  # = DT[1:5, ]


> print(DT_5)

   Manufacturer   Model    Type Min.Price Price Max.Price MPG.city MPG.highway

1:        Acura Integra   Small      12.9  15.9      18.8       25          31

2:        Acura  Legend Midsize      29.2  33.9      38.7       18          25

3:         Audi      90 Compact      25.9  29.1      32.3       20          26

4:         Audi     100 Midsize      30.8  37.7      44.6       19          26

5:          BMW    535i Midsize      23.7  30.0      36.2       22          30

 




이번에는 정렬을 해볼 텐데요, [ 차종(Type) 기준 오름차순으로 정렬(sorting by Type in ascending order)한 후 가격(Price) 기준으로 내림차순으로 정렬(sorting by Price in descending order)하기 ] 를 해보겠습니다. 


data.table 의 정렬은 DT[order()] 구문을 사용하는데요, 단 data.table 은 정렬을 할 때  Base R의 'order' 함수가 아니라 data.table 의 'forder()' 함수를 이용함으로써 훨씬 빠른 속도로 정렬을 수행합니다. 


오름차순(ascending order)이 기본 설정이며, 내림차순(descending order)을 하려면 변수 앞에 '마이너스(-)'를 붙여주면 됩니다. 



# Sort DT by Type in ascending order, and then by Price in descending order

# using data.table's internal fast radix order 'forder()', not 'base::order'.

DT_ordered <- DT[order(Type, -Price)] # '-' for descending order

> head(DT_ordered)

    Manufacturer  Model    Type Min.Price Price Max.Price MPG.city MPG.highway

1: Mercedes-Benz   190E Compact      29.0  31.9      34.9       20          29

2:          Audi     90 Compact      25.9  29.1      32.3       20          26

3:          Saab    900 Compact      20.3  28.7      37.1       20          26

4:         Volvo    240 Compact      21.8  22.7      23.5       21          28

5:    Volkswagen Passat Compact      17.6  20.0      22.4       21          30

6:        Subaru Legacy Compact      16.3  19.5      22.7       23          30





  (2) DT[i, j, by] : j 열 선택하기 (Select column(s) in j)


열을 선택할 때는 DT[i, j, by] 에서 j 부분에 선택하려는 변수(칼럼) 이름을 넣어주면 됩니다. 이때 변수 이름만 넣어주면 결과로 벡터가 반환되며, 아니면 list(column_nm) 처럼 list() 로 싸주면 data.table 이 반환됩니다. 


  • Price 변수를 선택하여 벡터로 반환하기 (Select 'Price' but return it as a Vector)


# Select columns(s) in j

# Select 'Price' column with all rows but return it as a vector

price_vec <- DT[, Price]


> head(price_vec)

[1] 15.9 33.9 29.1 37.7 30.0 15.7

 



  • Price 변수를 선택하여 data.table로 반환하기 (Select 'Price' but return it as a Data.Table)
list(Price) 처럼 변수 이름을 list() 로 감싸주면 data.table 자료형태로 반환합니다. 


# Select 'Price' column with all rows, but return it as a 'data.table'

# wrap the variables within list()

# '.()' is an alias to 'list()'

price_dt <- DT[, list(Price)]


> head(price_dt)

   Price

1:  15.9

2:  33.9

3:  29.1

4:  37.7

5:  30.0

6:  15.7

 



data.table 에서 .() 는 list() 와 동일하게 계산이나 수행 결과를 data.table 로 반환합니다. 많은 경우 코딩을 더 간결하게 하기 위해 list() 보다는 .() 를 더 많이 사용하는 편입니다. (반면, .() 를 처음보는 분은 '이게 도대체 무엇일까?' 하고 궁금할 것 같긴합니다.)



# '.()' is an alias to 'list()'

price_dt2 <- DT[, .(Price)] # .() = list()


> head(price_dt2)

   Price

1:  15.9

2:  33.9

3:  29.1

4:  37.7

5:  30.0

6:  15.7

 




변수를 선택해 올 때 DT[, .(새로운 이름 = 원래 이름)] 형식의 구문을 사용하면 변수 이름을 다른 이름으로 변경 (rename) 할 수도 있습니다.



# Select 'Model' and 'Price' and rename them to 'model_2', 'price_2'

model_price_dt2 <- DT[, .(model_2 = Model, price_2 = Price)]


> head(model_price_dt2)

   model_2 price_2

1: Integra    15.9

2:  Legend    33.9

3:      90    29.1

4:     100    37.7

5:    535i    30.0

6: Century    15.7

 




data.table의 1번째~4번째 칼럼까지를 선택해올 때는 Base R과 동일하게 DT[, 1:4] 또는 직접 칼럼 이름을 입력해서 DT[, c("Manufacturer", "Model", "Type", "Min.Price")] 의 두 가지 방법 모두 가능합니다. 



> head(DT[, 1:4])

   Manufacturer   Model    Type Min.Price

1:        Acura Integra   Small      12.9

2:        Acura  Legend Midsize      29.2

3:         Audi      90 Compact      25.9

4:         Audi     100 Midsize      30.8

5:          BMW    535i Midsize      23.7

6:        Buick Century Midsize      14.2

>

> head(DT[, c("Manufacturer", "Model", "Type", "Min.Price")])

   Manufacturer   Model    Type Min.Price

1:        Acura Integra   Small      12.9

2:        Acura  Legend Midsize      29.2

3:         Audi      90 Compact      25.9

4:         Audi     100 Midsize      30.8

5:          BMW    535i Midsize      23.7

6:        Buick Century Midsize      14.2





  (3) DT[i, j, by] : by 그룹별로 j 열을 계산하거나 실행하기 

                       (Compute or do in j by the group)


DT[i, j, by] 에서 j 열에 대해 by 그룹별로 함수 연산, 계산, 처리를 수행할 수 있습니다. 아래 예에서는
[ 차종(Type) 그룹 별로 가격(Price)의 평균을 구해서 data.table로 반환하기 ] 를 해보겠습니다. 



# Compute or do in j

# calculate mean Price by Type


> DT[, .(mean_price = mean(Price)), Type]

      Type mean_price

1:   Small   10.16667

2: Midsize   27.21818

3: Compact   18.21250

4:   Large   24.30000

5:  Sporty   19.39286

6:     Van   19.10000





DT[i, j, by] 에서 i 행을 선별하고 & j 열에 대해 연산/집계를 by 그룹별로 수행하는 것을 해보겠습니다. 

이때 data.table 은 DT[i, j, by] 처럼 i, j, by 가 DT[...] 안에 모두 들어 있기 때문에 i 행 선별이나 j 열 선택이나 by 그룹별 연산을 각각 분리해서 평가하는 것이 아니라 동시에 고려하여 내부적으로 수행 최적화를 하기 때문에 속도도 빠르고 메모리 효율도 좋습니다


아래 예에서는 i 행 선별할 때 

  (a) 차종이 소형인 행 선별: Type == "Small"

  (b) 차종이 소형이 아닌 행 선별: Type != "Small"

  (c) 차종이 소형, 중형, 컴팩트형인 행 선별: Type %in% c("Small", "Midsize", "Compact")

처럼 조건 유형에 맞게 '==', '!=', '%in%' 등의 연산자(operator)를 사용하였습니다. 



> # Subset in i and do in j

> # Because the three main components of the query (i, j and by) are together inside [...], 

> # data.table can see all three and optimize the query altogether before evaluation, not each separately. 

> # We are able to therefore avoid the entire subset

> DT[Type == "Small", .(mean_price = mean(Price)), Type]

    Type mean_price

1: Small   10.16667

> DT[Type != "Small", .(mean_price = mean(Price)), Type]

      Type mean_price

1: Midsize   27.21818

2: Compact   18.21250

3:   Large   24.30000

4:  Sporty   19.39286

5:     Van   19.10000

> DT[Type %in% c("Small", "Midsize", "Compact"), .(mean_price = mean(Price)), Type]

      Type mean_price

1:   Small   10.16667

2: Midsize   27.21818

3: Compact   18.21250

 




아래 예에서는 조건을 만족하는 관측치의 개수를 세는 두 가지 방법을 소개하겠습니다. 첫번째 방법은 length(변수이름) 을 사용해서 변수 벡터의 길이를 반환하도록 하는 방법이구요, 또다른 방법은 .N 이라는 data.table의 특수 내장변수를 사용해서 관측치 개수를 세는 방법입니다. 아래의 Q1, Q2에 대해 length()와 .N 을 사용한 방법을 차례대로 소개합니다. (역시 .N 이 더 간결하므로 많이 쓰입니다.)


[ Q1: 차종별 관측치 개수 ]

[ Q2: 차종이 소형("Small")이고 가격이 11.0 미만(Price < 11.0) 인 관측치 개수 ]




  • length() 를 사용해서 관측치 개수 세기


> # How many Small cars with price less than 11.0

> DT[, .(N = length(Price)), Type]

      Type  N

1:   Small 21

2: Midsize 22

3: Compact 16

4:   Large 11

5:  Sporty 14

6:     Van  9

> DT[Type == "Small" & Price < 11.0, length(Price)]

[1] 14




  • .N 을 이용하여 관측치 개수 세기


> # .N is a special built-in variable that holds the number of observations in the current group

> DT[, .N, Type]

      Type  N

1:   Small 21

2: Midsize 22

3: Compact 16

4:   Large 11

5:  Sporty 14

6:     Van  9

> DT[Type == "Small" & Price < 11.0, .N]

[1] 14

 



[ Reference ]

* Introduction to R data.table: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html


다음번 포스팅에서는 data.table 의 DT[i, j, by] 에서 by 구문을 사용하여 그룹별로 집계하는 방법을 소개하겠습니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table이 무엇이고 또 왜 좋은지, 그리고 data.table을 생성하는 4가지 방법을 소개하였습니다. 


이번 포스팅에서는 R data.table 의 일반 구문(general syntax of data.table)을 Base R, dplyr, SQL 구문과 비교해가면서 설명해보겠습니다. 그러면 상대적으로 data.table 이 매우 간결하고 일관성있으며, 또 SQL 구문과도 비슷한 면이 있어서 익히기 쉽겠구나 하는 인상을 받으실 겁니다. 




R data.table 은 DT[i, j, by][order] 의 기본 구문 형태를 따릅니다. 여기서 DT 는 data.table을 말하여, i, j, by, order 는 

  • i : 데이터 조작/집계의 대상이 되는 행(rows)이 무엇인지를 지정합니다. Base R의 subset() 또는 which() 함수dplyr의 filter() 함수, SQL의 where 구문과 같은 역할을 합니다. 

  • j : 일반적으로 조작/집계의 실행이 일어나는 열(columns)과 실행(action, function)을 말합니다. 기존 열을 수정할 수도 있고, 새로운 조작/집계를 통해 새로운 열을 만들 수도 있습니다. 어떤 R 패키지의 어떤 함수 표현도 모두 사용할 수 있습니다. Base R, dplyr 의 function(column_name), 또는 SQL의 select 구문과 같은 역할을 합니다. 

  • by : j에서 지정한 열(columns)과 실행(action, function)을 그룹 별로(by group) 수행하라고 지정해줍니다. Base R의 tapply()나 by() 함수, dplyr의 group_by() 함수, SQL 의 group by 구문과 같은 역할을 합니다. 

  • order : 결과 테이블을 기준 변수에 따라 오름차순(ascending) 또는 내림차순(descending)으로 정렬할 때 사용합니다. Base R의 sort() 또는 order() 함수, dplyr의 arrange() 함수, SQL의 order by 구문과 같은 역할을 합니다. 


아래에는 소속 그룹을 나타내는 'g' 변수와 정수 값을 가지는 'x' 변수로 구성된 'data'라는 이름의 data.frame, data.table에서, 'x' 가 8 이하인 관측치를 대상으로, 그룹 'g' 별로 'x'의 합을 구한 후에 그룹 'g'를 기준으로 내림차순으로 그 결과를 제시하라는 예제입니다. 


Base R, dplyr, data.table, 그리고 SQL 을 각각 비교해보겠습니다. 


먼저, 예제로 사용할 간단한 data.frame 과 data.table 을 만들어보겠습니다. 변수 'g'는 (그룹 "a" 5개, 그룹 "b" 5개)의 관측치로 이루어져있으며, 변수 'x'는 1~10 까지 순차적인 정수로 이루어져 있습니다.  



# loading packages

library(data.table)

library(dplyr)


# creating data.frame with 2 variables, 10 observations

> data <- data.frame(g = c(rep('a', 5), rep('b', 5)), x = 1:10)

> data

   g  x

1  a  1

2  a  2

3  a  3

4  a  4

5  a  5

6  b  6

7  b  7

8  b  8

9  b  9

10 b 10

>

>

>

# converting data.frame to data.table

> data_dt <- as.data.table(data)

> str(data_dt)

Classes 'data.table' and 'data.frame': 10 obs. of  2 variables:

 $ g: Factor w/ 2 levels "a","b": 1 1 1 1 1 2 2 2 2 2

 $ x: int  1 2 3 4 5 6 7 8 9 10

 - attr(*, ".internal.selfref")=<externalptr> 




이제 아래 과업(task)을 Base R, dplyr, data.table, SQL 로 각각 수행해보겠습니다. 


# ===== 과업 (Task) =====

'data' 라는 데이터셋에서 

'x'가 8 이하인 관측치를 대상으로 

'x'의 합을 구하되

그룹 변수 g 별로 (그룹 'a', 그룹 'b') 구분하여 구하여 

결과를 그룹 변수 g의 내림차순으로 정렬(그룹 'b' 먼저, 그룹 'a' 나중에) 하시오. 


 Base R

dplyr 

data.table 

data2 <- subset(data, 

                subset = (x <= 8))


sort(tapply(data2$x, 

            data2$g, 

            sum), 

     decreasing=TRUE)

data %>% 

  filter(x <= 8) %>% 

  group_by(g) %>% 

  summarise(sum(x)) %>% 

  arrange(desc(g))

data_dt[x<=8, sum(x), by=g][

  order(-g)]

# Out

 b  a 

21 15

# Out

# A tibble: 2 x 2

  g     `sum(x)`

  <fct>    <int>

1 b           21

2 a           15 

# Out

   g V1

1: b 21

2: a 15 



Base R 은 data2$x, data2$g 처럼 각 칼럼 앞에 접두사로 'data2$' 가 필요했는데요, data.table은 각 칼럼별로 접두사 'DT$' 가 필요없습니다. DT[ ] 안에 각 변수 이름을 그냥 써주면 DT의 변수라고 인식을 합니다. 


Base R의 경우 x 가 8 이하인 관측치만 먼저 subset()을 하고 나서, 이후 tapply()로 그룹별로 x에 대해서 sum을 하고, sort()로 내림차순 정렬하는 식으로 하다보니 코드가 길고 복잡합니다. 반면에 data.table의 경우 DT[i, j, by][order] 구문 형식에 따라 i 부분에서 x<=8 로 조건을 만족하는 관측치만 가져온 후, j 부분에서 sum(x) 로 합계를 구하되, by 부분에서 by=g 로 그룹별로 수행하라고 설정하고, [order(-g)] 를 뒤에 설정해주어서 내림차순으로 제시하라고 하였습니다. Base R 대비 무척 간결하고 직관적입니다! 


dplyr 도 Base R 보다는 간결하기는 합니다만, data.table이 조금 더 간결합니다. 


만약 SQL 구문에 이미 익숙한 분이라면 data.table 의 구문이 좀더 수월하게 다가올 수도 있겠습니다. 아래 표의 왼쪽 칸에 SQL 구문으로 똑같은 과업을 수행했을 때의 query 인데요, 오른쪽의 data.table 구문과 비교해보면 어떠신가요? 좀 비슷하지요?!


 SQL

data.table 

SELECT 

g, 

SUM(x) AS x_sum

FROM data_dt

WHERE x <= 8

GROUP BY g 

ORDER BY g DESC;

data_dt[x<=8, 

        sum(x), 

        by=g][

          order(-g)]



이번 포스팅에서는 data.table을 Base R, dplyr, SQL과 비교해보면서 data.table의 구문이 얼마나 간결하고 깜끔한지에 대한 이해를 도모했다면, 다음 포스팅에서는 data.table에 대해서만 DT[i, j, by][order] 기본 구문의 i, j, by, order 별로 좀더 다양한 사용방법에 대해서 한번 더 소개하겠습니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,