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

지난번 포스팅에서는 R data.table 이 무엇이고, 왜 data.table 이 좋은지, 어떻게 패키지를 설치하는지에 대해서 알아보았습니다. 


그러면 이번 포스팅에서는 샘플 데이터를 가지고 data.table 을 만들어보는 몇 가지 방법을 소개하겠습니다. 


-- 로컬에서 csv 파일, txt 파일을 읽어와서 data.table 만들기

(1) fread() 함수로 csv 파일을 빠르게 읽어와서 R data.table 로 만들기

(2) fwrite() 함수로 R data.table을 csv 파일로 빠르게 쓰기


-- 메모리 상에서 data.table 만들기

(3) data.table() 함수로 R data.table 만들기

(4) data.table() 함수로 기존의 data.frame을 data.table로 변환하기

(5) rbindlist() 함수로 여러개로 쪼개져 있는 파일들을 하나의 data.table로 합치



먼저, data.table의 fread() 함수를 사용해서 로컬 머신에 있는 csv file 을 빠르게(Fast!!) 읽어와서 R data.table로 만들거나, 혹은 반대로 fwrite() 함수를 사용해서 data.table을 빠르게(Fast!!!) csv file로 쓰는 방법을 소개하겠습니다. 




  (1) fread() 함수로 csv 파일을 빠르게 읽어와서 R data.table 로 만들기


data.table의 fread() 함수는 Base R의 read.csv() 함수 또는 read.table() 함수와 유사한 역할을 합니다만, 대신에 fread() 라는 이름에서 짐작할 수 있듯이 매우 빠릅니다(Fast Read)!  벤치마킹 테스트를 해보면 fread() 가 read.table() 보다 40배 이상 빠릅니다!!


파일을 빠르게 읽어와서 data.table 자료로 만들 때 로컬 file path (directory/file_name)를 입력해줘도 되고, https:// 로 시작하는 url 을 입력해줘도 됩니다. 아래 예에서는 UCI Machine Learning Repository에 오픈되어 있는 abalone.data 데이터셋을 url을 통해 읽어와서 data.table로 만들어보았습니다. 


Base R 의 data.frame에서는 문자형 칼럼의 경우 디폴트가 요인형(factor)으로 인식하는데요, data.table은 그렇지 않습니다. 만약 data.table에서 문자형 칼럼을 요인형(factor)로 인식하게 하려면 stringsAsFactors = TRUE 라고 명시적으로 설정을 해줘야 합니다. 



#install.packages("data.table")

library(data.table)


# (1) Fast reading csv file using fread()

url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data'


abalone_dt <- fread(url, 

                    sep=",", 

                    stringsAsFactors=TRUE, 

                    header = FALSE, 

                    col.names=c("sex", "length", "diameter", "height", "whole_weight", 

                                "shucked_weight", "viscera_weight", "shell_weight", "rings"))


> head(abalone_dt)

   sex length diameter height whole_weight shucked_weight viscera_weight shell_weight rings

1:   M  0.455    0.365  0.095       0.5140         0.2245         0.1010        0.150    15

2:   M  0.350    0.265  0.090       0.2255         0.0995         0.0485        0.070     7

3:   F  0.530    0.420  0.135       0.6770         0.2565         0.1415        0.210     9

4:   M  0.440    0.365  0.125       0.5160         0.2155         0.1140        0.155    10

5:   I  0.330    0.255  0.080       0.2050         0.0895         0.0395        0.055     7

6:   I  0.425    0.300  0.095       0.3515         0.1410         0.0775        0.120     8

 


* 참고: https://www.rdocumentation.org/packages/data.table/versions/1.13.0/topics/fread




  (2) fwrite() 함수로 R data.table을 csv 파일로 빠르게 쓰기


data.table의 fwrite() 함수는 Base R의 write.csv() 와 유사한 역할을 합니다만, 역시 fwrite()의 이름에서 짐작할 수 있듯이 매우 매우 빠릅니다!  Base R의 write.csv()보다 data.table의 fwrite()가 약 30~40배 더 빠릅니다!!!  여러개의 CPU가 있을 경우 fwrite()는 이들 CPU를 모두 이용해서 쓰기를 하기 때문입니다. 



# (2) fwrite(): Fast CSV Writer

fwrite(x = abalone_dt, 

       file = "/Users/ihongdon/Downloads/abalone.csv", 

       append = FALSE, 

       sep = ",",

       na = "",

       row.names = FALSE, 

       col.names = TRUE

       )

 


-- [Terminal] command line

(base) ihongdon@lhongdon-a01 ~ % cd Downloads

(base) ihongdon@lhongdon-a01 Downloads % 

(base) ihongdon@lhongdon-a01 Downloads % ls

abalone.csv

(base) ihongdon@lhongdon-a01 Downloads % 

(base) ihongdon@lhongdon-a01 Downloads % 

(base) ihongdon@lhongdon-a01 Downloads % cat abalone.csv 

sex,length,diameter,height,whole_weight,shucked_weight,viscera_weight,shell_weight,rings

M,0.455,0.365,0.095,0.514,0.2245,0.101,0.15,15

M,0.35,0.265,0.09,0.2255,0.0995,0.0485,0.07,7

F,0.53,0.42,0.135,0.677,0.2565,0.1415,0.21,9

...



* 참고: https://www.rdocumentation.org/packages/data.table/versions/1.13.0/topics/fwrite





  (3) data.table() 함수로 R data.table 만들기


data.table 자료구조는 data.frame 의 특성을 그대로 이어받아서 확장한 데이터 구조입니다. 따라서 Base R 에서 사용하는 함수를 모두 사용할 수 있습니다. 


아래 예에서는 2개의 변수와 10개의 관측치를 가진 데이터셋에 대해 data.table() 함수를 사용해서 data.table 클래스 객체를 만들어보았습니다. 



# (3) constructing data.table

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


>

> str(data_dt)

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

 $ g: chr  "a" "a" "a" "a" ...

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

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

>

> head(data_dt)

   g x

1: a 1

2: a 2

3: a 3

4: a 4

5: a 5

6: b 6



* 참고: https://www.rdocumentation.org/packages/data.table/versions/1.13.0/topics/data.table-package




  (4) data.table() 함수로 기존의 data.frame을 data.table로 변환하기


기존 Base R의 data.frame을 data.table() 함수를 사용하면 간단하게 data.table 자료구조로 변환할 수 있습니다. 



> # DataFrame of base R

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

> str(data_df)

'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

> # (4) converting data.frame to data.table

> data_dt2 <- data.table(data_df)

> str(data_dt2)

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> 

> head(data_dt2)

   g x

1: a 1

2: a 2

3: a 3

4: a 4

5: a 5

6: b 6





  (5) rbindlist() 함수로 여러개로 쪼개져 있는 파일들을 하나의 data.table로 합치


여러개의 파일을 폴더에서 차례대로 읽어들여서 하나의 data.table로 합치는 방법은 rbindlist() 함수를 사용하면 무척 간단하게 할 수 있습니다. 


예를 들어, 아래처럼 'file_1.txt', 'file_2.txt', 'file_3.txt' 의 3개의 텍스트 파일이 있다고 해보겠습니다. 이럴 경우 먼저 list.files() 함수로 특정 폴더에 들어있는 이들 3개의 파일 이름을 리스트로 만들어 놓습니다. 다음으로 읽어온 파일을 하나씩 리스트로 쌓아놓을 빈 templist 를 만들어놓고, for loop 반복문을 사용하여 텍스트 파일 이름이 들어있는 리스트 f_list 로부터 하나씩 차례대로 파일이름을 읽어와서 paste0(base_dir, f_list[i]) 로 전체의 파일 경로(디렉토리/파일이름.txt)를 만들고, fread() 함수로 하나씩 읽어와서 templist 에 차곡차곡 쌓아놓습니다. 마지막에 rbindlist(templist) 로 모든 리스트를 합쳐서 하나의 data.table 을 만듭니다. 





> # (5) combining multiple files into a data.table using rbindlist()

> base_dir <- '/Users/ihongdon/Downloads/files/'

> f_list <- list.files(base_dir)

> print(f_list)

[1] "file_1.txt" "file_2.txt" "file_3.txt"

> paste0(base_dir, f_list[1])

[1] "/Users/ihongdon/Downloads/files/file_1.txt"

> templist <- list() # black list to store multiple data.tables

> for(i in 1:length(f_list)) {

+   f_path <- paste0(base_dir, f_list[i])

+   templist[[i]] <- fread(f_path)

+ }

> all_dt <- rbindlist(templist)

> str(all_dt)

Classes 'data.table' and 'data.frame': 9 obs. of  3 variables:

 $ x1: int  1 4 7 10 13 16 19 22 25

 $ x2: int  2 5 8 11 14 17 20 23 26

 $ x3: int  3 6 9 12 15 18 21 24 27

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

> print(all_dt)

   x1 x2 x3

1:  1  2  3

2:  4  5  6

3:  7  8  9

4: 10 11 12

5: 13 14 15

6: 16 17 18

7: 19 20 21

8: 22 23 24

9: 25 26 27




https://rfriend.tistory.com/225 포스팅에 보면 (a) do.call(rbind, data), (b) ldply(data, rbind), (c) rbind.fill(data), (d) rbindlist(data) 의 4가지 패키지/함수별로 비교를 했을 때 data.table의 rbindlist() 가 월등히 빠르다고 소개를 하였습니다.(비교가 안되게 빠름!!!) 


다음번 포스팅에서는 data.table의 기본 구문에 대해서 소개하겠습니다. 



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

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



728x90
반응형
Posted by Rfriend
,

R 의 가장 대표적인 데이터 자료 구조 중의 하나를 꼽으라고 하면 행(row)과 열(column)로 구성된 2차원 배열 형태의 테이블인 Data Frame 일 것입니다. 

 

그리고 Data Frame 자료 구조를 조작, 관리, 전처리 할 수 있는 패키지로는 Base R과 dplyr 패키지가 많이 사용이 되는데요, 이번 포스팅에서는 Data Frame의 확장 데이터 구조인 data.table 데이터 구조와 이를 조작, 관리, 처리하는데 사용하는 data.table 패키지에 대해서 소개하려고 합니다. 

 

(1) R data.table은 무엇인가? (What is R data.table?)

(2) 왜 R data.table 인가? (Why R data.table?)

(3) R data.table 설치하기 (How to install R data.table)

(4) R data.table 참고할 수 있는 사이트 (Reference sites of R data.table)

(5) R data.table 구문 소개 포스팅 링크

 

 

  (1) R data.table 은 무엇인가? (What is R data.table?)

 

R data.table 은 (a) 행과 열로 구성된 2차원의 테이블 형태를 가지는 'data.frame'의 확장 데이터 구조(data.table: Extension of 'data.frame')이면서, (b) data.table을 생성, 조작, 처리, 집계할 수 있는 패키지(package) 입니다. 

 

아래에 data.table 패키지를 사용하여 문자열을 가진 'g' 변수와 숫자형 데이터를 가진 'x' 변수에 대해 10개의 관측치를 가지고 있는 간단한 data.table 자료구조를 하나 만들어 봤는데요, str() 로 자료 구조에 대해 확인해보면 'Classes 'data.table' and 'data.frame': 10 obs. of 2 variables:' 라는 설명이 나옵니다. 'data.table'이면서 'data.frame'이라고 나옵니다. 

 

View(data_dt) 로 자료를 확인해봐도 기존에 우리가 알고 있던 data.frame 과 data.table 이 구조가 같다는 것을 눈으로 확인할 수 있습니다. 

 

 

library(data.table)

 

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

> str(data)

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>

 

 

 

> View(data)

 

 

 

R CRAN에는 data.table 패키지에 대해서, 

  • 대용량 데이터(예: 100GB)에 대한 빠른 집계
    (Fast aggregation of large data (e.g. 100GB in RAM))
  • 빠른 정렬된 조인
    (Fast ordered joins)
  • 복사를 사용하지 않고 그룹별로 칼럼에 대한 빠른 합치기/수정/삭제
    (Fast add/modify/delete of columns by group using no copies at all)
  • 사용자 친화적이고 빠른 문자 구분 값의 읽기와 쓰기
    (Friendly and fast character-separated-value read/write)
  • 빠른 개발을 위한 자연스럽고 유연한 구문을 제공
    (Offers a natural and flexible syntax, for faster development.)

한다고 소개하고 있습니다. 

 

 

 

  (2) 왜 R data.table 인가? (Why R data.table?)

 

data.table 의 GitHub 페이지에서 '왜 data.table 인가 (Why data.table?)' 에 대해서 6가지 이유를 말하고 있습니다. (위의 data.table 소개 내용과 유사합니다만, 이유 항목별로 순서가 조금 다르고, 항목도 조금 다르긴 합니다.)

  • 빠르게 쓰고 읽을 수 있는 간결한 구문 (concise syntax: fast to type, fast to read)
  • 빠른 속도 (fast speed)
  • 효율적인 메모리 사용 (memory efficient)
  • 세심한 API 라이프사이클 관리 (careful API lifecycle management)
  • 커뮤니티 (community)
  • 풍부한 기능 (feature rich)

 

첫번째 data.frame 이 좋은 이유로 "빠르게 쓰고 읽을 수 있는 간결한 구문 (concise systax: fast to type, fast to read)"을 들었는데요, 이에 대해서는 Base R, dplyr, data.table 의 세 개 패키지별로 위의 data 에 대해서 그룹 'g' 별로 변수 'x'의 평균(mean) 을 구해보는 구문을 비교해보면 이해하기 쉬울 것 같습니다. 

 

자, 아래 구문을 보면 어떠신가요? 제 눈에는 data.table 이 제일 간결해보이네요!

 

 Base R

dplyr 

data.table 

 tapply(

    data$x, 

    data$y, 

    mean)

 data %>%

    group_by(g) %>%

    summarise(mean(x))

 data[, mean(x), by = 'g']

 

 

두번째 data.frame 의 장점으로 "빠른 속도 (fast speed)" 를 들었는데요, H2Oai 에서 R data.table과 R dplyr 뿐만 아니라 Python pandas, Spark, Julia 등 다른 Database-like 언어의 패키지까지 벤치마킹 테스트를 해놓은 자료가 있어서 아래에 소개합니다. 

 

질문은 5GB 데이터셋에 대해 'id1' 그룹별로 'v1' 칼럼에 대해 합(sum)을 구하라는 집계를 수행하는 것인데요, R data.table 이 12초, R dplyr은 156초, Python pandas는 100초가 걸렸네요. data.table이 겁나게 빠른 것을 알 수 있습니다. 

 

 

* 출처: https://h2oai.github.io/db-benchmark/

 

data.table이 이처럼 빠른데는 여러가지 이유가 있는데요, 그중에서도 핵심은 data.table은 테이블을 수정할 때 레퍼런스(Reference)를 이용할 수 있으므로, 새로운 객체를 다시 만들 필요없이, 원래의 객체를 바로 수정할 수 있다는 점입니다.  

 

 

세번째 data.table의 장점으로 "효율적인 메모리 사용 (memory efficient)"을 꼽았는데요, 역시 H2O ai 에서 위와 동일 질문에 대해 이번에는 '50GB' 의 대용량 데이터에 대해서 수행해보았습니다. 

 

특기할 점은 R data.table은 122초, Spark은 374초가 걸려서 에러없이 수행이 된 반면, R dplyr, Python pandas는 "Out of Memory"가 났다는 점입니다. 

 

* 출처: https://h2oai.github.io/db-benchmark/

 

 

 

네번째로 data.table의 장점으로 "세심한 API 라이프사이클 관리(careful API lifecycle management)"를 들고 있습니다. 

 

data.table 패키지는 Base R에만 의존성(dependency)을 가지고 있습니다. 폐쇄망 환경에서 R 패키지 설치하거나 버전 업그레이드 할 때 의존성 확인하면서 하나 하나 설치하려면 보통 일이 아닌데요, data.table 패키지는 Base R에만 의존성이 있으므로 패키지 설치 및 관리가 무척 수월하겠네요. 

 

 

다섯째 data.table의 장점으로 "커뮤니티 (Community)"를 들고 있습니다. data.table 패키지는 Matt Dowle 외 100여명의 contributor 가 제작 및 관리하고 있는 패키지로서, 소스코드는 https://github.com/Rdatatable/data.table 에서 확인하거나 소스 코드에 기여할 수 있습니다. 

 

 

여섯째 data.table의 장점으로 "풍부한 기능 (feature rich)"을 들고 있는데요, 앞으로 하나씩 같이 알아가보시지요. 

 

 

이렇게 data.table 은 굉장히 매력적인 장점을 가지고 있는데요, 한가지 단점이 있다면 기존에 익숙한 Base R, dplyr 을 놔두고 새로 data.table 패키지의 구문을 배워야 한다는 점입니다. 

 

Base R 쓰다가 dplyr 을 처음 봤을 때의 당혹스러움만큼이나 data.table 구문을 처음 보면 아마도 당황스러울 것입니다. '어, 이게 R 맞나? 내가 지금 무슨 프로그래밍 언어를 보고 있는 거지?' 하고 말이지요. 

 

그래도 다행이라면 구문이 간결해서 배우기 (상대적으로) 쉽다는 점이겠습니다. R data.table은 충분히 시간과 노력을 투자해서 배워볼 만한 값어치가 있다고 생각합니다. 

 

 

 

  (3) R data.table 설치하기 (How to install R data.table)

 

패키지 설치 및 로딩은 다른 패키지와 동일합니다. 

 

 

# install data.table package

install.packages("data.table")

 

# latest development version:

data.table::update.dev.pkg()

 

# loading data.table package

library(data.table)

 

 

 

 

  (4) R data.table 참고할 수 있는 사이트 (Reference sites of R data.table)

 

- data.table 패키지 소소코드: https://github.com/Rdatatable/data.table

- R package 다운로드, 매뉴얼, 튜토리얼: https://cran.r-project.org/web/packages/data.table/ 

 

 

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

다음번 포스팅에서는 R data.table 의 데이터셋을 만드는 3가지 방법을 소개하겠습니다. 

 

 

 

  (5) R data.table 구문 소개 포스팅 링크

 

 

  • 패턴이 있는 여러개의 칼럼을 동시에 길게 녹이고(melt), 옆으로 넓게 주조(cast) 하기
    :
    rfriend.tistory.com/576

  • 그룹별 관측치 개수 별로 Data.Table을 구분해서 생성하기
    :
    rfriend.tistory.com/607

 

 

  • 회귀 모델의 오른쪽 부분(model's right-hand side)의 변수 조합을 일괄 다루기
    : rfriend.tistory.com/609

 

  • R data.table의 조건이 있는 상태에서 Key를 기준으로 데이터셋 합치기(Conditional Joins)
    :
    rfriend.tistory.com/610

 

  • R data.table의 .SD[], by 를 사용해서 그룹별로 부분집합 가져오기 (Group Subsetting)

    : rfriend.tistory.com/611

 

  • R data.table 그룹별 선형회귀모형 적합하고 회귀계수 구하기 (Grouped Regression)
    :
    rfriend.tistory.com/614

 

  • R data.table 이차 인덱스(Secondary indices)를 활용한 빠른 탐색 기반 subsetting
    : rfriend.tistory.com/615

 

 

 

이번 포스팅이 도움이 되었다면 아래의 '공감~

'를 꾹 눌러주세요.

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

 

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 R dplyr 패키지의 case_when() 함수를 이용해서 연속형 변수를 여러개의 범주로 구분하여 범주형 변수를 만들어보겠습니다. dplyr 패키지의 case_when() 함수를 사용하면 여러개의 if, else if 조건절을 사용하지 않고도 벡터화해서 쉽고 빠르게 처리를 할 수 있습니다. R dplyr 의 case_when() 함수는 SQL의 case when 절과 유사하다고 보면 되겠습니다. 




간단한 예제로 1~10 까지의 양의 정수를 "2 이하", "3~5", "6~8", "9 이상" 의 4개 범주로 구분을 해보겠습니다. 

(dplyr::case_when()에서 dplyr:: 는 생략해도 되며, dplyr 패키지의 함수를 이용하다는 의미입니다)


case_when(

조건 ~ 할당값, 

조건 ~ 할당값, 

TRUE ~ 할당값)

의 형식으로 작성합니다. 


아래의 예에서는 조건절이 총 4개 사용되었는데요, if, else if, else if, else 등의 조건절문 없이 case_when() 함수의 괄호안에 바로 조건을 나열했고, 마지막에는 앞의 조건절에 모두 해당 안되는 나머지(else)에 대해서 TRUE ~ "9~" 로 지정을 해주었습니다. 



library(dplyr)


x <- 1:10

x


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



dplyr::case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ "9~"

)


 [1] "~2"  "~2"  "3~5" "3~5" "3~5" "6~8" "6~8" "6~8" "9~"  "9~" 





이때 조건절의 순서가 중요합니다. 복수의 조건절을 나열하면 앞에서 부터 순서대로(in order) 조건에 해당하는 관측치에 대해 값을 할당하게 됩니다. 따라서 만약 TRUE ~ "9~"를 case_when(() 조건절의 제일 앞에 사용하게 되면 1~10까지의 모든 값에 대해 "9~" 를 할당하게 됩니다. 따라서 조건절의 처리 순서를 반드시 고려해서 조건절을 작성해줘야 합니다. 



# order matters!!!

case_when(

  TRUE ~ "9~",

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

)


[1] "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~"

 




case_when() 조건절의 오른쪽(right hand side)의 데이터 유형이 모두 동일해야 합니다. 만약 데이터 유형이 다를 경우 error를 발생합니다. 가령, 아래 예에서는 오른쪽에 character를 반환하게끔 되어있는데 logical 인 NA 가 포함되는 경우 Error가 발생합니다. 이때는 'NA_character_' 를 사용해서 NA가 character로 반환되게끔 해주면 됩니다. 


  • 오른쪽에 문자형(character) 반환하는 경우 NA 값으로는 NA_character_ 사용

 잘못된 사용 예 (오른쪽 데이터 유형 다름)

 올바른 사용 예 (오른쪽 데이터 유형 같음)


# error as NA is logical not character

case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ NA

)


Error: must be a character vector, not a logical vector

Call `rlang::last_error()` to see a backtrace


# use NA_character_

case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ NA_character_

)


[1] "~2"  "~2"  "3~5" "3~5" "3~5" "6~8" "6~8" "6~8" NA    NA 



  • 오른쪽에 숫자형(numeric)을 반환하는 경우 NA 값으로는 NA_real_ 사용

  잘못된 사용 예 (오른쪽 데이터 유형 다름)

 올바른 사용 예 (오른쪽 데이터 유형 같음)


# error as NA is logical not numeric

case_when(

  x <= 2 ~ 2,

  x <= 5 ~ 5,

  x <= 8 ~ 8,

  TRUE ~ NA

)


Error: must be a double vector, not a logical vector

Call `rlang::last_error()` to see a backtrace


# use NA_real_

case_when(

  x <= 2 ~ 2,

  x <= 5 ~ 5,

  x <= 8 ~ 8,

  TRUE ~ NA_real_

)


[1]  2  2  5  5  5  8  8  8 NA NA




dplyr의 case_when() 함수는 mutate() 함수와 함께 사용하면 매우 강력하고 편리하게 여러개의 조건절을 사용해서 새로운 변수를 만들 수 있습니다. 아래는 mtcars 데이터셋의 cyl (실린더 개수)  와 hp (자동차 마력) 의 두 개 변수를 사용해  첫번째 "or" 조건절로 "big" 유형으로 찾고, 두번째 "and" 조건절로 "medium" 유형을 찾으며, 마지막으로 나머지에 대해서는 "small" 유형을 명명해본 예입니다. 



mtcars$name <- row.names(mtcars)


mtcars %>% 

  select(name, mpg, cyl, hp) %>% 

  mutate(

    type = case_when(

      cyl >= 8 | hp >= 180 ~ "big",          # or

      cyl >= 4 & hp >= 120 ~ "medium", # and

      TRUE ~ "small"

    )

  )


                 name  mpg cyl  hp   type

1            Mazda RX4 21.0   6 110  small

2        Mazda RX4 Wag 21.0   6 110  small

3           Datsun 710 22.8   4  93  small

4       Hornet 4 Drive 21.4   6 110  small

5    Hornet Sportabout 18.7   8 175    big

6              Valiant 18.1   6 105  small

7           Duster 360 14.3   8 245    big

8            Merc 240D 24.4   4  62  small

9             Merc 230 22.8   4  95  small

10            Merc 280 19.2   6 123 medium

---- 이하 생략 ----

 




위에서 R dplyr의 case_when() 함수로 진행했던 내용을 PostgreSQL, Greenplum DB에서 하려면 SQL CASE WHEN 문을 아래처럼 사용하면 됩니다. 참고하세요. 



-- PostgreSQL CASE WEHN


SELECT 

   name, 

   mpg, 

   cyl, 

   hp, 

   CASE 

      WHEN (cyl >= 8) OR (hp >= 180) THEN "big"

      WHEN (cyl >= 4) AND (hp >= 120) THEN "median"

      ELSE "small"

   END AS type

FROM mtcars

 



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

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



728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 R을 사용하여 예측이나 분류 모델링을 할 때 기본적으로 필요한 두가지 작업인 


(1) DataFrame을 Train set, Test set 으로 분할하기 

     (Split a DataFrame into Train and Test set)

   - (1-1) 무작위 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Random Sampling)

   - (1-2) 순차 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Sequential Sampling)

   - (1-3) 층화 무작위 샘플링에 의한 Train, Test set 분할 

             (Split of Train, Test set by Stratified Random Sampling)


(2) 여러개의 숫자형 변수를 가진 DataFrame을 표준화하기 

      (Standardization of Numeric Data)

   - (2-1) z-변환 (z-transformation, standardization)

   - (2-2) [0-1] 변환 ([0-1] transformation, normalization)


(3) 여러개의 범주형 변수를 가진 DataFrame에서 가변수 만들기 

      (Getting Dummy Variables)


에 대해서 소개하겠습니다. 



예제로 사용할 Cars93 DataFrame을 MASS 패키지로 부터 불러오겠습니다. 변수가 무척 많으므로 예제를 간단하게 하기 위해 설명변수 X로 'Price', 'Horsepower', 'RPM', 'Length', 'Type', 'Origin' 만을 subset 하여 가져오고, 반응변수 y 로는 'MPG.highway' 변수를 사용하겠습니다. 



# get Cars93 DataFrame from MASS package

library(MASS)

data(Cars93)

str(Cars93)

'data.frame': 93 obs. of 27 variables: $ Manufacturer : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4... $ Model : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1... $ Type : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3... $ 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... $ 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... $ DriveTrain : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3... $ Cylinders : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4... $ 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... $ Rev.per.mile : int 2890 2335 2280 2535 2545 2565 1570 1320 1690... $ Man.trans.avail : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 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... $ Origin : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1... 

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



X <- subset(Cars93, select=c('Price', 'Horsepower', 'RPM', 'Length', 'Type', 'Origin'))

head(X)

A data.frame: 6 × 6
PriceHorsepowerRPMLengthTypeOrigin
<dbl><int><int><int><fct><fct>
15.91406300177Smallnon-USA
33.92005500195Midsizenon-USA
29.11725500180Compactnon-USA
37.71725500193Midsizenon-USA
30.02085700186Midsizenon-USA
15.71105200189MidsizeUSA



table(X$Origin)

USA non-USA 48 45



y <- Cars93$MPG.highway

y

  1. 31
  2.  
  3. 25
  4.  
  5. 26
  6.  
  7. 26
  8.  
  9. 30
  10.  
  11. 31
  12.  
  13. 28
  14.  
  15. 25
  16.  
  17. 27
  18.  
  19. 25
  20.  
  21. 25
  22.  
  23. 36
  24.  
  25. 34
  26.  
  27. 28
  28.  
  29. 29
  30.  
  31. 23
  32.  
  33. 20
  34.  
  35. 26
  36.  
  37. 25
  38.  
  39. 28
  40.  
  41. 28
  42.  
  43. 26
  44.  
  45. 33
  46.  
  47. 29
  48.  
  49. 27
  50.  
  51. 21
  52.  
  53. 27
  54.  
  55. 24
  56.  
  57. 33
  58.  
  59. 28
  60.  
  61. 33
  62.  
  63. 30
  64.  
  65. 27
  66.  
  67. 29
  68.  
  69. 30
  70.  
  71. 20
  72.  
  73. 30
  74.  
  75. 26
  76.  
  77. 50
  78.  
  79. 36
  80.  
  81. 31
  82.  
  83. 46
  84.  
  85. 31
  86.  
  87. 33
  88.  
  89. 29
  90.  
  91. 34
  92.  
  93. 27
  94.  
  95. 22
  96.  
  97. 24
  98.  
  99. 23
  100.  
  101. 26
  102.  
  103. 26
  104.  
  105. 37
  106.  
  107. 36
  108.  
  109. 34
  110.  
  111. 24
  112.  
  113. 25
  114.  
  115. 29
  116.  
  117. 25
  118.  
  119. 26
  120.  
  121. 26
  122.  
  123. 33
  124.  
  125. 24
  126.  
  127. 33
  128.  
  129. 30
  130.  
  131. 23
  132.  
  133. 26
  134.  
  135. 31
  136.  
  137. 31
  138.  
  139. 23
  140.  
  141. 28
  142.  
  143. 30
  144.  
  145. 41
  146.  
  147. 31
  148.  
  149. 28
  150.  
  151. 27
  152.  
  153. 28
  154.  
  155. 26
  156.  
  157. 38
  158.  
  159. 37
  160.  
  161. 30
  162.  
  163. 30
  164.  
  165. 43
  166.  
  167. 37
  168.  
  169. 32
  170.  
  171. 29
  172.  
  173. 22
  174.  
  175. 33
  176.  
  177. 21
  178.  
  179. 30
  180.  
  181. 25
  182.  
  183. 28
  184.  
  185. 28




  (1) DataFrame을 Train set, Test set 으로 분할하기 (Split a DataFrame into Train and Test set)


(1-1) 무작위 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Random Sampling)


간단하게 일회성으로 무작위 샘플링 하는 것이면 sample() 함수로 난수를 생성해서 indexing을 해오면 됩니다. 

(* 참고 : https://rfriend.tistory.com/58)



# (1) index for splitting data into Train and Test set

set.seed(1004) # for reprodicibility

train_idx <- sample(1:nrow(X), size=0.8*nrow(X), replace=F) # train-set 0.8, test-set 0.2

test_idx <- (-train_idx)


X_train <- X[train_idx,]

y_train <- y[train_idx]

X_test <- X[test_idx,]

y_test <- y[test_idx]


print(paste0('X_train: ', nrow(X_train)))

print(paste0('y_train: ', length(y_train)))

print(paste0('X_test: ', nrow(X_test)))

print(paste0('y_test: ', length(y_test)))

[Out]:

[1] "X_train: 74" [1] "y_train: 74" [1] "X_test: 19" [1] "y_test: 19"





(1-2) 순차 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Sequential Sampling)


시계열 분석을 할 경우 시간 순서(timestamp order)를 유지하는 것이 필요하므로 (1-1)의 무작위 샘플링을 하면 안되며, 시간 순서를 유지한 상태에서 앞서 발생한 시간 구간을 training set, 뒤의(미래의) 시간 구간을 test set 으로 분할합니다. 



# sequential sampling

test_size <- 0.2

test_num <- ceiling(nrow(X) * test_size)

train_num <- nrow(X) - test_num


X_train <- X[1:train_num,]

X_test <- X[(train_num+1):nrow(X),]

y_train <- y[1:train_num]

y_test <- y[(train_num+1):length(y)]





(1-3)  층화 무작위 샘플링에 의한 Train, Test set 분할 (Split of Train, Test set by Stratified Random Sampling)


위의 (1-1)과 (1-2)에서 소개한 무작위 샘플링, 순차 샘플링을 사용한 train, test set split 을 random_split() 이라는 사용자 정의함수(user-defined function)으로 정의하였으며, 층화 무작위 샘플링(stratified random sampling)을 사용한 train_test_split() 사용자 정의 함수도 이어서 정의해 보았습니다. (python sklearn의 train_test_split() 함수의 인자, 반환값이 유사하도록  정의해보았습니다) (* 참고 : https://rfriend.tistory.com/58)



# --- user-defined function of train_test split with random sampling

random_split <- function(X, y

                         , test_size

                         , shuffle

                         , random_state) {

    

    test_num <- ceiling(nrow(X) * test_size)

    train_num <- nrow(X) - test_num

    

    if (shuffle == TRUE) {

        # shuffle == True

        set.seed(random_state) # for reprodicibility

        test_idx <- sample(1:nrow(X), size=test_num, replace=F)

        train_idx <- (-test_idx)

            

        X_train <- X[train_idx,]

        X_test <- X[test_idx,]

        y_train <- y[train_idx]

        y_test <- y[test_idx]

    } else {

        # shuffle == False

        X_train <- X[1:train_num,]

        X_test <- X[(train_num+1):nrow(X),]

        y_train <- y[1:train_num]

        y_test <- y[(train_num+1):length(y)]

    }

    

    return (list(X_train, X_test, y_train, y_test))

}



# --- user defined function of train_test_split() with statified random sampling

train_test_split <- function(X, y

                             , test_size=0.2

                             , shuffle=TRUE

                             , random_state=2004

                             , stratify=FALSE, strat_col=NULL){

                        

    if (stratify == FALSE){ # simple random sampling

        split <- random_split(X, y, test_size, shuffle, random_state)

        X_train <- split[1]

        X_test  <- split[2]

        y_train <- split[3]

        y_test  <- split[4]

    } else { # --- stratified random sampling

        strata <- unique(as.character(X[,strat_col]))

        X_train <- data.frame()

        X_test  <- data.frame()

        y_train <- vector()

        y_test  <- vector()

        for (stratum in strata){

            X_stratum <- X[X[strat_col] == stratum, ]

            y_stratum <- y[X[strat_col] == stratum]

            split_stratum <- random_split(X_stratum, y_stratum, test_size, shuffle, random_state)

            X_train <- rbind(X_train, data.frame(split_stratum[1]))

            X_test  <- rbind(X_test,  data.frame(split_stratum[2]))

            y_train <- c(y_train, unlist(split_stratum[3]))

            y_test  <- c(y_test,  unlist(split_stratum[4]))

        }

    }

    return (list(X_train, X_test, y_train, y_test))

}

 



위에서 정의한 train_test_splie() 사용자 정의 함수를 사용하여 'Origin' ('USA', 'non-USA' 의 두 개 수준을 가진 요인형 변수) 변수를 사용하여 층화 무작위 샘플링을 통한 train, test set 분할 (split of train and test set using stratified random sampling in R) 을 해보겠습니다, 



split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=TRUE

                               , random_state=2004

                               , stratify=TRUE, strat_col='Origin')


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])



print(paste0('Dim of X_train: ', nrow(X_train), ', ', ncol(X_train)))

print(paste0('Dim of X_test:  ', nrow(X_test), ', ', ncol(X_test)))

print(paste0('Length of y_train: ', length(y_train)))

print(paste0('Length of y_test:  ', length(y_test)))

[Out]:
[1] "Dim of X_train: 74, 6"
[1] "Dim of X_test:  19, 6"
[1] "Length of y_train: 74"
[1] "Length of y_test:  19"



X_test

A data.frame: 19 × 6
PriceHorsepowerRPMLengthTypeOrigin
<dbl><int><int><int><fct><fct>
448.0815500168Smallnon-USA
233.92005500195Midsizenon-USA
398.4555700151Smallnon-USA
4012.5905400164Sportynon-USA
329.11725500180Compactnon-USA
538.3825000164Smallnon-USA
4510.01246000172Smallnon-USA
9020.01345800180Compactnon-USA
4212.11025900173Smallnon-USA
1616.31704800178VanUSA
720.81704800200LargeUSA
1140.12956000204MidsizeUSA
739.0745600177SmallUSA
1213.41105200182CompactUSA
823.71804000216LargeUSA
239.2926000174SmallUSA
1716.61654000194VanUSA
7411.11105200181CompactUSA
1415.11604600193SportyUSA


table(X$Origin)

[Out]: USA non-USA 48 45



table(X_test$Origin)

[Out]: USA non-USA 10 9


y_test

  1. [Out]: 33
  2.  
  3. 25
  4.  
  5. 50
  6.  
  7. 36
  8.  
  9. 26
  10.  
  11. 37
  12.  
  13. 29
  14.  
  15. 30
  16.  
  17. 46
  18.  
  19. 23
  20.  
  21. 28
  22.  
  23. 25
  24.  
  25. 41
  26.  
  27. 36
  28.  
  29. 25
  30.  
  31. 33
  32.  
  33. 20
  34.  
  35. 31
  36.  
  37. 28





참고로 (1-1) 무작위 샘플링에 의한 Train, Test set 분할을 위의 (1-3)에서 정의한 train_test_split() 사용자 정의 함수를 사용해서 하면 아래와 같습니다. (shuffle=TRUE)



# split of train, test set by random sampling using train_test_split() function

split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=TRUE

                               , random_state=2004

                               , stratify=FALSE)


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])




참고로 (1-2) 순차 샘플링에 의한 Train, Test set 분할을 위의 (1-3)에서 정의한 train_test_split() 사용자 정의 함수를 사용해서 하면 아래와 같습니다. (shuffle=FALSE)



# split of train, test set by sequential sampling using train_test_split() function

split_list <- train_test_split(X, y

                               , test_size=0.2

                               , shuffle=FALSE

                               , random_state=2004

                               , stratify=FALSE)


X_train <- data.frame(split_list[1])

X_test  <- data.frame(split_list[2])

y_train <- unlist(split_list[3])

y_test  <- unlist(split_list[4])

 




  (2) 여러개의 숫자형 변수를 가진 DataFrame을 표준화하기 (Standardization of Nuemric Data)


(2-1) z-변환 (z-transformation, standardization)


X_train, X_test 데이터셋에서 숫자형 변수(numeric variable)와 범주형 변수(categorical varialble)를 구분한 후에, 숫자형 변수로 이루어진 DataFrame 에 대해서 z-표준화 변환 (z-standardization transformation)을 해보겠습니다. (* 참고 : https://rfriend.tistory.com/52)


여러개의 변수를 가진 DataFrame이므로 X_mean <- apply(X_train_num, 2, mean) 로 Train set의 각 숫자형 변수별 평균을 구하고, X_stddev <- apply(X_train_num, 2, sd) 로 Train set의 각 숫자형 변수별 표준편차를 구했습니다. 


그리고 scale(X_train_num, center=X_mean, scale=X_stddev) 로 Train set의 각 숫자형 변수를 z-표준화 변환을 하였으며, scale(X_test_num, center=X_mean, scale=X_stddev) 로 Test set의 각 숫자형 변수를 z-표준화 변환을 하였습니다. 


이때 조심해야 할 것이 있는데요, z-표준화 변환 시 사용하는 평균(mean)과 표준편차(standard deviation)는 Train set으로 부터 구해서 --> Train set, Test set 에 적용해서 z-표준화를 한다는 점입니다. 왜냐하면 Test set는 미래 데이터(future data), 볼 수 없는 데이터(unseen data) 이므로, 우리가 알 수 있는 집단의 평균과 표준편차는 Train set으로 부터만 얻을 수 있기 때문입니다.  (많은 분석가가 그냥 Train, Test set 구분하기 전에 통채로 scale() 함수 사용해서 표준화를 한 후에 Train, Test set으로 분할을 하는데요, 이는 엄밀하게 말하면 잘못된 순서입니다)



# split numeric, categorical variables

X_train_num <- X_train[, c('Price', 'Horsepower', 'RPM', 'Length')]

X_train_cat <- X_train[, c('Type', 'Origin')]

X_test_num  <- X_test[ , c('Price', 'Horsepower', 'RPM', 'Length')]

X_test_cat  <- X_test[ , c('Type', 'Origin')]


# (1) Z Standardization

# (1-1) using scale() function

X_mean   <- apply(X_train_num, 2, mean)

X_stddev <- apply(X_train_num, 2, sd)


print('---- Mean ----')

print(X_mean)

print('---- Standard Deviation ----')

print(X_stddev)

[Out]:
[1] "---- Mean ----"
     Price Horsepower        RPM     Length 
  20.22703  146.08108 5278.37838  183.67568 
[1] "---- Standard Deviation ----"
     Price Horsepower        RPM     Length 
  9.697073  51.171149 594.730345  14.356620 



X_train_scaled <- scale(X_train_num, center=X_mean, scale = X_stddev)

head(X_train_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
1-0.44621989-0.11883811.7177896-0.46498935
41.801881070.50651430.37264220.64947906
51.007827061.21003570.70892910.16189913
41-0.044036690.27200720.8770725-0.60429791
43-0.28122166-0.11883810.54078560.09224485
46-1.05465089-1.05686670.4567139-1.23118639


# note that 'mean' and 'stddev' are calculated using X_train_num dataset (NOT using X_test_num)

X_test_scaled <- scale(X_test_num, center=X_mean, scale = X_stddev)

head(X_test_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
44-1.2608987-1.27183150.3726422-1.0918778
21.41001031.05369760.37264220.7887876
39-1.2196491-1.77993030.7089291-2.2760005
40-0.7968411-1.09595120.2044988-1.3704949
30.91501560.50651430.3726422-0.2560265
53-1.2299615-1.2522893-0.4680750-1.3704949



# combine X_train_scaled, X_train_cat

X_train_scaled <- cbind(X_train_num_scaled, X_train_cat)


# combine X_trest_scaled, X_test_cat

X_test_scaled <- cbind(X_test_num_scaled, X_test_cat)





(2-2) [0-1] 변환 ([0-1] transformation, normalization)


각 숫자형 변수별 최소값(min)과 최대값(max)을 구해서 [0-1] 사이의 값으로 변환해보겠습니다. 

(* 참고 : https://rfriend.tistory.com/52)



# (2) [0-1] Normalization

# 0-1 transformation

X_max <- apply(X_train_num, 2, max)

X_min <- apply(X_train_num, 2, min)

X_train_num_scaled <- scale(X_train_num, center = X_min, scale = (X_max - X_min))

X_test_num_scaled <- scale(X_test_num, center = X_min, scale = (X_max - X_min))


head(X_train_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
10.155963300.32489450.92592590.4615385
40.555963300.45991560.62962960.6666667
50.414678900.61181430.70370370.5769231
410.227522940.40928270.74074070.4358974
430.185321100.32489450.66666670.5641026
460.047706420.12236290.64814810.3205128



head(X_test_num_scaled)

A matrix: 6 × 4 of type dbl
PriceHorsepowerRPMLength
440.011009170.075949370.62962960.3461538
20.486238530.578059070.62962960.6923077
390.01834862-0.033755270.70370370.1282051
400.093577980.113924050.59259260.2948718
30.398165140.459915610.62962960.5000000
530.016513760.080168780.44444440.2948718


# combine X_train_scaled, X_train_cat

X_train_scaled <- cbind(X_train_num_scaled, X_train_cat)


# combine X_trest_scaled, X_test_cat

X_test_scaled <- cbind(X_test_num_scaled, X_test_cat)





 (3) 여러개의 범주형 변수를 가진 DataFrame에서 가변수 만들기 (Getting Dummy Variables) 


(3-1) caret 패키지의 dummyVars() 함수를 이용하여 DataFrame 내 범주형 변수로부터 가변수 만들기



library(caret)


# fit dummyVars()

dummy <- dummyVars(~ ., data = X_train_cat, fullRank = TRUE)


# predict (transform) dummy variables

X_train_cat_dummy <- predict(dummy, X_train_cat)

X_test_cat_dummy <- predict(dummy, X_test_cat)


head(X_train_cat_dummy)

A matrix: 6 × 6 of type dbl
Type.LargeType.MidsizeType.SmallType.SportyType.VanOrigin.non-USA
001001
010001
000001
010001
010001
010000


head(X_test_cat_dummy)

A matrix: 6 × 6 of type dbl
Type.LargeType.MidsizeType.SmallType.SportyType.VanOrigin.non-USA
75000100
76010000
77100000
78000001
79001000
80001001





(3-2) 조건문 ifelse() 함수를 이용하여 수작업으로 가변수 만들기 

        (creating dummy variables manually using ifelse())


아무래도 (3-1)의 caret 패키지를 이용하는 것 대비 수작업으로 할 경우 범주형 변수의 개수와 범주형 변수 내 class 의 종류 수가 늘어날 수록 코딩을 해야하는 수고가 기하급수적으로 늘어납니다. 그리고 범주형 변수나 class가 가변적인 경우 데이터 전처리 workflow를 자동화하는데 있어서도 수작업의 하드코딩의 경우 에러를 야기하는 문제가 되거나 추가적인 비용이 될 수 있다는 단점이 있습니다. 


범주형 변수 내 범주(category) 혹은 계급(class)이 k 개가 있으면 --> 가변수는 앞에서 부터 k-1 개 까지만 만들었습니다. (회귀모형의 경우 dummy trap 을 피하기 위해)



# check level (class) of categorical variables

unique(X_train_cat$Type)

  1. [Out]: Small
  2.  
  3. Midsize
  4.  
  5. Compact
  6.  
  7. Large
  8.  
  9. Sporty
  10.  
  11. Van

unique(X_train_cat$Origin)

  1. [Out]: non-USA
  2.  
  3. USA


# get dummy variables from train set

X_train_cat_dummy <- data.frame(

    type_small = ifelse(X_train_cat$Type == "Small", 1, 0)

    , type_midsize = ifelse(X_train_cat$Type == "Midsize", 1, 0)

    , type_compact = ifelse(X_train_cat$Type == "Compact", 1, 0)

    , type_large = ifelse(X_train_cat$Type == "Large", 1, 0)

    , type_sporty = ifelse(X_train_cat$Type == "Sporty", 1, 0)

    , origin_nonusa = ifelse(X_train_cat$Origin == "non-USA", 1, 0)

)


head(X_train_cat_dummy)

A data.frame: 6 × 6
type_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl>
100001
010001
001001
010001
010001
010000


# get dummy variables from test set

X_test_cat_dummy <- data.frame(

    type_small = ifelse(X_test_cat$Type == "Small", 1, 0)

    , type_midsize = ifelse(X_test_cat$Type == "Midsize", 1, 0)

    , type_compact = ifelse(X_test_cat$Type == "Compact", 1, 0)

    , type_large = ifelse(X_test_cat$Type == "Large", 1, 0)

    , type_sporty = ifelse(X_test_cat$Type == "Sporty", 1, 0)

    , origin_nonusa = ifelse(X_test_cat$Origin == "non-USA", 1, 0)

)


head(X_test_cat_dummy)

A data.frame: 6 × 6
type_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl>
000010
010000
000100
001001
100000
100001





  (4) 숫자형 변수와 범주형 변수 전처리한 데이터셋을 합쳐서 Train, Test set 완성하기



# combine X_train_scaled, X_train_cat

X_train_preprocessed <- cbind(X_train_num_scaled, X_train_cat_dummy)

head(X_train_preprocessed)

A data.frame: 6 × 10
PriceHorsepowerRPMLengthtype_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
0.15596330.34693880.92592590.4615385100001
0.48623850.59183670.62962960.6923077010001
0.39816510.47755100.62962960.5000000001001
0.55596330.47755100.62962960.6666667010001
0.41467890.62448980.70370370.5769231010001
0.15229360.22448980.51851850.6153846010000


 

# combine X_trest_scaled, X_test_cat

X_test_preprocessed <- cbind(X_test_num_scaled, X_test_cat_dummy)

head(X_test_preprocessed)

A data.frame: 6 × 10
PriceHorsepowerRPMLengthtype_smalltype_midsizetype_compacttype_largetype_sportyorigin_nonusa
<dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
750.188990830.428571430.29629630.70512821000010
760.203669720.591836730.44444440.69230769010000
770.311926610.469387760.37037040.46153846000100
780.390825690.346938780.81481480.55128205001001
790.067889910.122448980.44444440.44871795100000
800.018348620.073469390.66666670.06410256100001




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

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



728x90
반응형
Posted by Rfriend
,