이전 포스팅에서는 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
,

"Colab" 은 Google Colaboratory 를 줄여서 쓴 것으로, Google Research 팀에서 개발한 서비스 제품입니다. Colab을 사용하면 웹브라우저를 통해서 클라우드 상에서 CPU뿐만 아니라 GPU나 TPU도 무료로(!!!) 사용하여 Python과 Jupyter Notebook으로 데이터 분석을 할 수 있습니다. 


물론 무료로 Colab을 이용하려면 Resource 한도가 있기는 합니다만, 교육용이나 간단한 실습용으로 GPU 또는 TPU를 써서 기계학습 모델을 훈련시켜보기에는 손색이 없습니다. 


Colab 한도를 높여서 더 빠른 GPU, 더 긴 타임, 추가 메모리를 사용하고 싶으면 $9.99/월 유료 Colab Pro 를 이용하면 됩니다. (무료는 최대 12시간 vs. Colab Pro는 최대 24시간) 



이번 포스팅에서는 Google Colab에서 외부 데이터를 로딩해서 이용할 수 있는 2가지 방법을 소개하겠습니다. 


(1) Local folder에서 Colab 으로 파일 로딩하기 

    (Loading a file from the local folder to Colab)


(2) Google Drive를 Colab에 연결해서 데이터 로딩하기 

    (Mounting google drive to Colab) (추천)





  0. Google Colab 실행하여 Jupyter Notebook 론칭하기


Google Colab을 시작하려면, 먼저 Google Drive에 접속한 후에 왼쪽 상단의 '+ New' 메뉴를 선택한 후, 




'More' 메뉴의 오른쪽 꺽쇠 화살표를 눌러 나온 추가 메뉴에서 'Google Colaboratory' 를 선택해주면 됩니다. 




그러면 아래처럼 Colab Jupyter Notebook 이 론칭됩니다. (backend 에서 도커 이미지 생성되어 돌아가고 있음.)




CPU, GPU, 또는 TPU 를 사용할지 여부는 Runtime > Change runtime type 메뉴에서 선택할 수 있습니다.




  (1) Local folder에서 Colab 으로 파일 로딩하기 

       (Loading a file from the Local folder to Colab)


만약 Colab에서 로컬 머신에 있는 폴더 내 파일을 읽어오려고 하면 "FileNotFoundError: [Errno 2] No such file or directory: your_local_path_here" 라는 에러 메시지가 나올 겁니다. Google 의 클라우드에서 작동하고 있는 Colab 은 로컬 머신의 경로를 인식하지 못하기 때문에 이런 에러가 발생합니다.("우물가에서 숭늉 찾는 격"이라고 할까요...)  


이럴 때 필요한게 Local machine 의 folder에서 파일을 Colab으로 로딩해서 사용하는 방법입니다. Google Colab에서 쉽게 로컬 폴더에서 업로드할 수 있도록 해주는 메소드인 google.colab.files.upload() 를 제공합니다. 


Colab에서 아래처럼 코드를 실행하면 '파일 선택' 하는 메뉴가 나타납니다. 



from google.colab import files
file_uploaded = files.upload()

 




'파일 선택' 메뉴를 선택하면 아래와 같이 '탐색기 창'이 팝업으로 나타납니다. 원하는 위치에서 원하는 파일을 선택한 후 '열기' 메뉴를 선택해줍니다. 




선택한 'abalone.txt' 파일이 Colab 으로 업로딩이 완료되면 아래 화면캡쳐처럼 '100% done' 이라는 메시지와 함께 'Saving abalone.txt to abalone.txt' 라는 안내 메시지가 뜹니다. 



위에서 업로딩한 'abalone.txt' 텍스트 파일을 읽어와서 Pandas DataFrame으로 만들어보겠습니다. 이때 pd.read_csv()를 바로 사용하면 안되며, Python으로 다양한 I/O 스트림을 다룰 때 사용하는 io 모듈을 사용해서 io.BytesIO(file_uploaded['abalone.txt'] 로 '_io.BytesIO object'를 먼저 만들어준 다음에 pandas.read_csv() 메소드로 Pandas DataFrame을 만들 수 있습니다. 



# io : Python's main facilities for dealing with various types of I/O.
import io
import pandas as pd

abalone_df = pd.read_csv(io.BytesIO(file_uploaded['abalone.txt']))

abalone_df.head()




그런데, 이 방법은 로컬 머신에 수동으로 경로를 찾아가서 하나씩 파일 열기를 하여 읽어와야 하는 번거로움이 있고, 구글 드라이브 클라우드 내에서 모든 걸 해결할 수 있는 더 편리하고 폴더 채 데이터에 접근할 수 있는 더 강력한 방법이 있습니다. 그게 다음에 설명할 Google Drive를 Colab에 연결(mounting)해서 사용하는 방법입니다. 




  (2) Google Drive를 Colab에 연결해서 데이터 로딩하기 

       (Mounting google drive to Colab) 


Google Drive를 Colab에 올려서(mounting) 사용하는 방법이 전에는 인증, API 연결같은게 좀 까탈스러웠는데요, 지금은 아래처럼 google.colab.drive.mount('/content/drive') 를 해주고 'authorization code' 만 복사해서 넣어주면 되게끔 깔끔하게 간소화되었습니다. 


아래 화면캡쳐에서 'Go to this URL in a browser: https://xxxx ' 부분의 URL 링크를 클릭해줍니다. 



from google.colab import drive
drive.mount('/content/drive')




그러면 아래 화면처럼 'Sign in with Google' 팝업 창의 'Choose an account to continue to Google Drive File Stream' 에서 데이터가 들어있는 자신의 구글 계정을 선택해서 클릭해줍니다. 




다음으로, 아래 화면캡쳐처럼 'Google Drive File Stream에서 내 Google 계정에 액세스하려고 합니다.' 안내창이 뜨면 하단의 '허용'을 선택해줍니다. 




이제 거의 다 왔네요. 아래 화면캡쳐처럼 'Google 로그인: 이 코드를 복사하여 애플리케이션으로 전환한 다음 붙여넣으세요' 라는 메시지가 뜨는데요, '인증 코드를 복사'합니다. 





바로 위에서 복사한 '인증코드'를 Google Colab Jupyter Notebook으로 다시 돌아와서 아래의 'Enter your authorization code:' 의 아래 빈 칸에 붙여넣기를 해주고 Jupyter notebook cell 을 실행시킵니다. 




자, 이제 Google Drive를 Colab 위에 올려놓기(mounting)를 성공했으므로 Colab의 왼쪽 상단의 '폴더' 아이콘을 클릭하여 탐색기 모드로 전환하고 폴더와 파일을 찾아봅시다.  저기에 Google Drive에 올라가 있는 'abalone.txt' 파일이 보이는군요. 이 파일의 오른쪽 끝에 ':' 을 클릭한 후 'Copy path'를 선택해서 'abalone.txt' 파일이 있는 경로를 복사해놓습니다. 




바로 위에서 'Copy path'로 복사한 'abalone.txt' 파일이 있는 경로(path)를 붙여넣기 해서 보니 '/content/abalone.txt' 입니다. 이 경로를 pd.read_csv() 에 바로 입력해서 pandas DataFrame 으로 만들어보겠습니다. 



import pandas as pd

abalone_df2 = pd.read_csv('/content/abalone.txt', sep=',')
abalone_df2.head()





참고로, 위에서 처럼 마우스로 폴더 탐색기 눌러서 하나씩 찾아가는 방법 말고, Python의 os 모듈을 이용하면 Colab의 현재 작업경로(os.getcwd(), current working directory), 폴더와 파일 리스트 (os.listdir()), 경로에 특정 파일이 존재하는지 여부(os.path.isfile()) 등을 편리하게 확인할 수 있습니다. 



import os

os.getcwd()
[Out] '/content'


os.listdir()
[Out] ['.config', 'drive', 'abalone.txt', '.ipynb_checkpoints', 'sample_data']

os.path.isfile('/content/abalone.txt')
[Out] True







만약에 여러개의 파일들을 폴더별로 계층적으로 정리하여 놓은 데이터를 이용해야 하는 경우라면 아래처럼 Google Drive에 로컬에 있는 폴더를 통째로 올려서 사용할 수도 있습니다. 




저는 로컬 머신의 다운로드 폴더에 들어있는 'dogs_cats_sample' 폴더를 통째로 Google Drive에 업로드해보겠습니다. 'dogs_cats_sample' 폴더 밑에는 'output', 'test', 'train', 'validation' 하위 폴더가 있으며, 이들 각 폴더에는 개와 고양이 이미지 jpg 파일들과 정답 라벨 텍스트 파일이 들어있습니다. 폴더를 통째로 Google Drive 에 한번에 올릴 수 있어서 정말 편리합니다. 



개와 고양이 jpg 이미지들과 라벨 데이터가 Google Drive에 업로드가 되는데 시간이 좀 걸립니다. 업로드가 완료되면 아래 화면처럼 Colab의 왼쪽 사이드바에서 '폴더' 모양 아이콘을 클릭하여 보조창을 열고, 'drive > My Drive > 0_dogs_cats_sample' 폴더를 찾아가서 선택한 후 'Copy path'를 클릭하면 '0_dogs_cats_sample' 폴더의 경로를 복사할 수 있습니다. 



위에서 복사한 경로를 붙여넣기 해서 보면 '/content/drive/My Drive/0_dogs_cats_sample' 경로에 잘 업로드되어 있음을 알 수 있습니다. 아래에는 이들 폴더 내 이미지 파일들 중에서 'train/cat/cat.1.jpg' 이미지 파일 하나를 가져와서 시각화해보았습니다. 



img_dir = '/content/drive/My Drive/0_dogs_cats_sample'
os.listdir(img_dir)
[Out] ['.DS_Store', 'train', 'output', 'validation', 'test']

os.listdir(os.path.join(img_dir, 'train/cat'))
[Out] 'cat.1.jpg'


from keras.preprocessing import image

img = image.load_img(os.path.join(img_dir, 'train/cat/cat.1.jpg'),
target_size=(250, 250))

import matplotlib.pyplot as plt
plt.imshow(img)
plt.show()




[ Reference ]

* Coalbroatory FAQs: https://research.google.com/colaboratory/faq.html

* How to mount Google drive to Colab: https://colab.research.google.com/notebooks/io.ipynb#scrollTo=u22w3BFiOveA

* Python io module: https://docs.python.org/3/library/io.html


출처: https://rfriend.tistory.com/431 [R, Python 분석과 프로그래밍의 친구 (by R Friend)]


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

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


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
,

희소행렬(Sparse matrix)은 대부분의 값이 원소 '0'인 행렬, '0'이 아닌 원소가 희소(sparse)하게, 듬성듬성 있는 행렬을 말합니다. 반대로 밀집행렬(Dense matrix)은 대부분의 원소 값이 '0'이 아닌 행렬을 말합니다. 


자연어처리 분석을 할 때 문서 내 텍스트를 컴퓨터가 이해할 수 있는 형태의 자료구조로 만들 때 텍스트 파싱을 거쳐 단어-문서 행렬(Term-Document matrix) (or 문서-단어 행렬, Document-Term matrix) 를 만드는 것부터 시작합니다. 


문서별로 많은 수의 단어가 포함되어 있고, 또 단어별로 발생 빈도가 보통은 드물기 때문에, 문서에서 단어를 파싱하여 Term-Document 행렬을 만들면 대부분은 희소행렬(Sparse matrix)을 얻게 됩니다. 


이번 포스팅에서는 


(1) 문서별 단어로 부터 CSR 행렬(Compressed Sparse Row matrix) 을 만들고, 

(2) CSR 행렬을 이용해 NumPy array의 Term-Document 행렬 만들기


를 해보겠습니다. 




단, 이번 포스팅의 주 목적은 문서로부터 문서-단어 CSR 행렬을 만들고 --> 이를 NumPy array의 Term-Document 행렬을 만드는 과정에 집중해서 소개하는 것으로서, 텍스트 파싱하는데 필요한 세부 절차(가령 문장 분리, 대문자의 소문자로 변환, Stop words 생략 등)는 생략합니다. 

(텍스트를 단어 단위로 파싱해서 one-hot encoding 하는 방법은 https://rfriend.tistory.com/444 포스팅 참조하세요.)



  (1) 문서별 단어로 부터 CSR 행렬(Compressed Sparse Row matrix) 을 만들기


먼저, NumPy와 SciPy 모듈을 importing 하겠습니다. 



import numpy as np

from scipy.sparse import csr_matrix

 



아래와 같이 리스트 [] 하나를 문서(Document) 하나로 간주했을 때, 총 3개의 문서를 가진 "docs" 로 부터 단어(Term)를 파싱해서 단어집(Vocabulary) 사전형(dictionary)을 만들고, 압축 희소 행기준 행렬(Compressed Sparse Row matrix) 을 만들기 위해 필요한 indptr, indices, data 객체를 for loop 문을 써서 만들어보겠습니다. 


참고로, CSR 행렬 소개, SciPy.sparse.csr_matrix() 메소드 소개, NumPy 희소행렬을 SciPy 압축 희소 행기준 행렬 (Compressed Sparse Row matrix) 로 변환하는 방법은 https://rfriend.tistory.com/551 포스팅을 참고하세요. 



# To construct a CSR matrix incrementally

docs = [["python", "is", "a", "programming", "language"], 

        ["programming", "is", "fun"], 

        ["python", "is", "easy"]]


indptr = [0]

indices = []

data = []

vocabulary = {}


for d in docs:

    for term in d:

        index = vocabulary.setdefault(term, len(vocabulary))

        indices.append(index)

        data.append(1)

    indptr.append(len(indices))



* reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html



위의 실행결과로 얻은 단어집(Vocabulary)을 Key : Value 쌍으로 출력을 해서 살펴보겠습니다. 3개의 문서에 총 7개의 단어가 있군요. (문서별로 중복되는 단어(term)가 존재함)


 

for k, v in vocabulary.items():

    print(k, ':', v)


[Out]
python : 0
is : 1
a : 2
programming : 3
language : 4
fun : 5
easy : 6




위에서 얻은 indptr, indices, data 를 가지고 SciPy.sparse.csr_matrix() 메소드를 이용하여 압축 희소 행기준 행렬(CSR matrix)을 만들어보겠습니다.  



term_document_csr_mat = csr_matrix((data, indices, indptr), dtype=int)

term_document_csr_mat

[Out] <3x7 sparse matrix of type '<class 'numpy.int64'>'
	with 11 stored elements in Compressed Sparse Row format>


print(term_document_csr_mat)

[Out]
  (0, 0)	1
  (0, 1)	1
  (0, 2)	1
  (0, 3)	1
  (0, 4)	1
  (1, 3)	1
  (1, 1)	1
  (1, 5)	1
  (2, 0)	1
  (2, 1)	1
  (2, 6)	1



print('-- SciPy Compressed Sparse Row matrix --')

print('indptr:', term_document_csr_mat.indptr)

print('indices:', term_document_csr_mat.indices)

print('data:', term_document_csr_mat.data)


-- SciPy Compressed Sparse Row matrix --
indptr: [ 0  5  8 11]
indices: [0 1 2 3 4 3 1 5 0 1 6]
data: [1 1 1 1 1 1 1 1 1 1 1]





  (2) CSR 행렬을 이용해 NumPy array의 Term-Document 행렬 만들기


위의 (1)번에서 만든 SciPy CSR(Compressed Sparse Row) matrix를 csr_matrix.toarray() 또는 csr_matrix.todense() 메소드를 사용해서 NumPy array 행렬로 변환해보겠습니다. 이로부터 Term-Document Matrix를 만들었습니다. 



# converting SciPy CSR matrix to NumPy array

term_document_arr = term_document_mat.toarray() # or todense()


term_document_arr

[Out]
array([[1, 1, 1, 1, 1, 0, 0],
       [0, 1, 0, 1, 0, 1, 0],
       [1, 1, 0, 0, 0, 0, 1]])




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

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



728x90
반응형
Posted by Rfriend
,
이번 포스팅에서는 Python Numpy 의 배열(array)을 특정 형상(shape)으로 변형할 때 빈 자리를 '0'이나 다른 값으로 채우는 2가지 방법을 소개하겠습니다.

1. numpy.pad() 함수를 사용하여 배열(array)을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기
2. tensorflow.keras.preprocessing.sequence.pad_sequence() 함수를 사용하여 배열의 원소 개수가 다른 Ragged array 를 특정 형상의 배열로 바꾸면서 빈자리를 '0'으로 채우기


1. numpy 배열을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기 (padding)

    : numpy.pad() 함수



먼저, numpy 라이브러리를 importing 하고, 예제로 사용할 2 by 3 의 간단한 2차원 배열(array)을 만들어보겠습니다.




import numpy as np


x = np.array([[1, 2, 3],
                 [7, 2, 5]])
print(x)

array([[1, 2, 3],
          [7, 2, 5]])




위의 2 by 3 의 2차원 배열 x 의 위, 아래에 1개씩의 행을 추가하고, 왼쪽, 오른쪽에 1개씩의 열을 추가하여 4 by 5 의 2차원 배열을 만들되, 새로 추가되는 행과 열의 자리는 '0'으로 채워넣기(padding)를 numpy.pad() 함수를 사용하여 해보겠습니다.

numpy.pad(array, pad_width, mode='constant', **kwargs)


# np.pad(x, (1, 1))

np.pad(x, (1, 1),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0]

    [0, 1, 2, 3, 0],     [0, 7, 2, 5, 0],     [0, 0, 0, 0, 0]])




만약 위의 행 1개 추가, 왼쪽 열 1개 추가, 아래쪽 행 2개 추가, 오른쪽 열 2개를 추가하고 싶다면 pad_width 매개변수에 (1, 2) 를 설정해주면 됩니다.


np.pad(x, (1, 2),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0, 0],
       [0, 1, 2, 3, 0, 0],
       [0, 7, 2, 5, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0]])

 



np.pad() 메소드를 사용하지 않고, 아래처럼 numpy.zeros() 로 원하는 형상(shape)대로 모두 '0'으로 채워진 배열을 먼저 만들어놓고, indexing을 사용하여 왼쪽, 위쪽, 오른쪽, 아래쪽에 행과 열을 1개씩 비워놓을 수 있는 위치에 기존 배열을 삽입하여 np.pad() 메소드를 사용했을 때와 동일한 결과를 얻을 수도 있습니다. 이때는 새로 만들어지는 배열 z의 형상(shape)과 기존 배열 x를 채워넣을 위치의 indexing에 신경을 써주어야 하므로 조금 신경이 쓰이는 편이기는 합니다. (위의 np.pad() 와 일처리 순서가 정 반대라고 생각하면 됩니다.)


z = np.zeros((4, 5))
print(z)

[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]


z[1:-1, 1:-1] = x
print(z)
[[0. 0. 0. 0. 0.] [0. 1. 2. 3. 0.] [0. 7. 2. 5. 0.] [0. 0. 0. 0. 0.]]

 



np.pad() 함수에 몇가지 재미있는 옵션을 마저 소개하겠습니다.
constant_values 를 설정해주면 '0' 대신에 원하는 다른 특정 상수 값으로 빈 자리를 채워넣을 수 있습니다. '0' 대신에 '-1'을 한번 채워볼까요?


np.pad(x, (1, 1), constant_values=-1)


array([[-1, -1, -1, -1, -1],
       [-1,  1,  2,  3, -1],
       [-1,  7,  2,  5, -1],
       [-1, -1, -1, -1, -1]])




빈 자리 채워넣기하는 방법(mode)에는 항상 똑같은 상수('constant', default) 값을 채워넣는 방법 외에도 'edge', 'linear_ramp', 'maximum', 'mean', 'median', reflect', 'symmetric', 'wrap', 'empty' 등의 다양한 mode 옵션을 제공합니다. 이들 중에서 위의 예시에서 사용한 'constant' 이외에 'edge', 'maximum', 'wrap' 의 mode 옵션을 사용하여 채워넣기 padding을 해보겠습니다. (아래 결과에서 빨간색으로 표시한 부분이 padding된 부분입니다.)


  • mode = 'edge' : 가장 변두리의 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='edge')


array([[1, 1, 2, 3, 3], [1, 1, 2, 3, 3], [7, 7, 2, 5, 5], [7, 7, 2, 5, 5]])

 


  • mode = 'maximum' : 행과 열의 가장 큰 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='maximum')


array([[7, 7, 2, 5, 7],
       [3, 1, 2, 3, 3],
       [7, 7, 2, 5, 7],
       [7, 7, 2, 5, 7]])



  • mode = 'wrap' : 행과 열의 반대편 끝에 있는 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='wrap')


array([[5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1],
       [5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1]])


* Reference: https://numpy.org/doc/stable/reference/generated/numpy.pad.html



  2. 원소 개소가 다른 Ragged array를 특정 형상의 배열로 바꿀 때 빈자리를 '0'으로 채우기

     : tensorflow.keras.preprocessing.sequence.pad_sequence() 함수


위의 np.pad() 함수의 경우 변경하기 전의 원래 배열이 (m by n) 형상인 고정된 차원의 배열을 대상으로 채워넣기를 하였습니다. 두번째로 소개하려는 keras의 sequence.pad_sequence() 함수는 각 행의 원소 개수가 다른 Ragged array(?) 를 대상으로 특정 (j by k) 형상의 고정된 배열로 바꾸려고 할 때 빈 자리를 '0'으로 채워넣는데 사용할 수 있는 차이가 있습니다.


아래의 예를 보면 원소 개수가 1개, 2개, 3개, 4개로서 들쭉날쭉함을 알 수 있습니다. (list를 원소로 가지고 있고, data type 이 object 이네요.)



x2 = np.array([[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]])
display(x2)


array([list([1]), 
      list([2, 3]),
      list([4, 5, 6]),
      list([7, 8, 9, 10])], dtype=object)

 



TensorFlow와 Keras의 tf.keras.preprocessing.sequence() 메소드를 importing 해보겠습니다.



import tensorflow as tf
from tensorflow.keras.preprocessing import sequence

print(tf.__version__)

2.3.0

 



이제 Keras의 pad_sequences() 함수를 사용하여 가장 많은 원소를 가진 행에 맞추어서 (4 by 4) 형상의 배열로 바꾸고, 왼쪽의 빈자리는 '0'으로 채워넣기(padding)를 해보겠습니다.

tf.keras.preprocessing.sequence.pad_sequences(
    sequences, maxlen=None, dtype='int32',
    padding='pre', truncating='pre',
    value=0.0
)

이렇게 (4 by 4) 형상으로 해서 빈자리는 '0'으로 채워주고 나니 각 행의 원소 개수가 모두 4개로서 배열다운 배열이 되었습니다. padding을 해주는 위치의 기본 설정값은 padding='pre' 로서 앞쪽(왼쪽)에 '0'을 채워줍니다. (value=0 이 기본 설정값으로서 '0' 값으로 채워줌)


sequence.pad_sequences(x2) # default: padding='pre', value=0


array([[ 0,  0,  0,  1],
       [ 0,  0,  2,  3],
       [ 0,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



만약 padding을 해주는 위치를 뒤쪽(오른쪽)으로 하고 싶다면 padding='post' 로 매개변수 설정을 바꿔주면 됩니다.


sequence.pad_sequences(x2, padding='post')


array([[ 1,  0,  0,  0],
       [ 2,  3,  0,  0],
       [ 4,  5,  6,  0],
       [ 7,  8,  9, 10]], dtype=int32)

 



물론 빈 곳 채워넣기(padding)하는 값을 '0'이 아니라 다른 값으로 할 수도 있습니다. '-1'을 사용(value=-1)해서 앞쪽에 빈 곳을 채워넣기해보겠습니다.



sequence.pad_sequences(x2, padding='pre', value=-1)


array([[-1, -1, -1,  1],
       [-1, -1,  2,  3],
       [-1,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



maxlen 매개변수값을 별도로 설정해주지 않으면 배열 내 행 중에서 가장 많은 원소를 가진 행을 기준으로 maxlen 이 자동으로 정해지는데요, 이를 사용자가 직접 설정해줄 수도 있습니다. 아래의 예에서 maxlen=5 로 설정해주면 (4 by 5) 의 padding 된 배열이 생성됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=5)


array([[ 0,  0,  0,  0,  1],
       [ 0,  0,  0,  2,  3],
       [ 0,  0,  4,  5,  6],
       [ 0,  7,  8,  9, 10]], dtype=int32)




아래 예에서처럼 원래의 배열 x2 의 최대길이는 4인데 maxlen=3 으로 값을 설정하게 되면 4-3=1 개의 길이만큼의 원소 값들을 잘라내기(truncating) 해주어야 합니다. 이때 truncating='post' 라고 설정해주면 뒤쪽(오른쪽)을 기준으로 '1'개의 값들을 잘라내주고, 앞쪽을 기준으로 비어있는 곳에는 '0'의 값을 채워주게 됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=3, truncating='post')


array([[0, 0, 1],
       [0, 2, 3],
       [4, 5, 6],
       [7, 8, 9]], dtype=int32)



* Reference: https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/sequence/pad_sequences

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

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 (1) 파이썬의 자료형 중에서도 사전형(Dictionary)을 키와 값 (Key and Value) 의 쌍으로 인쇄를 할 때 좀더 가독성을 좋게 하도록 키(Key) 에서 일정 간격을 띄우고 나서 값(Value)을 인쇄하는 옵션을 소개하겠습니다.  


그리고 소소한 팁으로 (2) 파이썬 인쇄 escape 옵션, (3) 파이썬 인쇄 sep, end 옵션을 추가로 소개하겠습니다. 



  (1) 파이썬 사전형의 키와 값을 일정 간격을 두고 인쇄하기

      (Print Key, Value of Python Dictionary with fixed space)




먼저, 예제로 사용할 간단한 파이썬 사전형(Dictionary) 객체를 키와 값의 쌍(pair of key and value)으로 만들어보고 print() 로 인쇄를 해보겠습니다


사전형 객체에 대해서 그냥 print() 로 인쇄를 하면 옆으로 길게 쭈욱~ 늘어서 인쇄가 되기 때문에 아무래도 한눈에 보기가 쉽지 않습니다. 



my_dict = {'first name': 'KilDong.', 

           'last name': 'Hong.',

           'age': '30.', 

           'address': 'Seoul, Korea.'}



print(my_dict)

[Out]

{'first name': 'KilDong.', 'last name': 'Hong.', 'age': '30.', 'address': 'Seoul, Korea.'}





그러면, 좀더 보기에 좋도록 이번에는 dictionary.items() 로 키와 값을 분해해서 각각 가져오고, for loop 순환문으로 반복을 하면서 각 키와 값을 한줄에 하나씩 인쇄를 해보겠습니다. 이때 format() 메소드를 사용해서 Key, Value 값을 인쇄할 때 각각 치환해주었습니다. 

바로 앞의 예에서 그냥 print() 한 것보다는 한결 보기에 좋습니다만, Key 문자열의 길이가 들쭉날쭉 하다보니 Key : Value 로 쌍을 이루어서 인쇄를 할 때 Value 인쇄가 시작하는 위치도 역시 들쭉날쭉해서 좀 정신이 없고 눈에 잘 안들어오는 한계가 있습니다. 



for k, v in my_dict.items():

    print("{} : {}".format(k, v))


[Out]
first name : KilDong.
last name : Hong.
age : 30.
address : Seoul, Korea.





이럴 때 {!r:15s} 으로 특정 숫자만큼의 string 간격을 두고 인쇄하라는 옵션을 추가해주면 아래와 같이 Key 문자열의 시작위치부터 15 string 만큼 각격을 두고, 이어서 다음의 문자열(여기서는 ': {value}')을 인쇄하게 됩니다. 위의 예보다는 아래의 예가 한결 Key : Value 쌍을 한눈에 보기에 좋아졌습니다.   



for k, v in my_dict.items():

    print("{!r:15s} : {}".format(k, v))


[Out]

'first name' : KilDong. 'last name' : Hong. 'age' : 30. 'address' : Seoul, Korea.

 





  (2) 파이썬 인쇄 escape 옵션 (Python Print escape options)

파이썬 문법을 탈출(escape)하여 인쇄할 수 있는 소소한 옵션들이 몇 개 있습니다. 

(아래에서 \ 는 역슬래쉬  , back-slash 입니다)


  • \n : 새로운 줄로 바꾸어서 인쇄하기
  • \t : 탭(tab)으로 들여쓰기해서 인쇄하기 (오른쪽으로 탭한 만큼 밀어서 인쇄)
  • \b : 뒤에서 한칸 back-space 하여 인쇄하기 (제일 뒤에 문자가 있으면 삭제되어서 인쇄)
  • \" : 큰 따옴표(") 인쇄하기
  • \' : 작은 따옴표(') 인쇄하기
  • \\ : 역슬래쉬('\') 인쇄하기


  • \n : 새로운 줄로 바꾸어서 인쇄하기


for k, v in my_dict.items():

    print("\n{} : {}".format(k, v))


[Out]

first name : KilDong.

last name : Hong.

age : 30.

address : Seoul, Korea.





  • \t : 탭(tab)으로 들여쓰기해서 인쇄하기 (오른쪽으로 탭한 만큼 밀어서 인쇄)


for k, v in my_dict.items():

    print("\t{} : {}".format(k, v))


[Out]
	first name : KilDong.
	last name : Hong.
	age : 30.
	address : Seoul, Korea.





  • \b : 뒤에서 한칸 back-space 하여 인쇄하기 (제일 뒤에 문자가 있으면 삭제되어서 인쇄)



for k, v in my_dict.items():

    print("{} : {}\b".format(k, v))


[Out]
first name : KilDong
last name : Hong
age : 30
address : Seoul, Korea

 




  • \" : 큰 따옴표(") 인쇄하기


for k, v in my_dict.items():

    print("{} : \"{}\"".format(k, v))


[Out]
first name : "KilDong."
last name : "Hong."
age : "30."
address : "Seoul, Korea."

 




  • \\ : 역슬래쉬('\') 인쇄하기


for k, v in my_dict.items():

    print("{} : \\{}\\".format(k, v))


[Out]
first name : \KilDong.\
last name : \Hong.\
age : \30.\
address : \Seoul, Korea.\

 





  (3) 파이썬 인쇄 sep, end 옵션 (Python Print sep, end options)


  • sep="separator" 옵션 : 두 개의 문자열 사이에 구분자(separator) 문자로 구분하여 붙여서 인쇄
  • end="end_string" 옵션 : 앞의 문자열에 바로 이어서 end_string을 붙여서 인쇄


앞의 (1)번에서 예로 들었던 Key : Value 쌍으로 item 한줄씩 인쇄하는 것을 sep, end 옵션을 사용해서 똑같이 재현해보겠습니다. 


  • sep="separator" 옵션 : 두 개의 문자열 사이에 구분자(separator) 문자로 구분하여 붙여서 인쇄


for k, v in my_dict.items():

    print(k, v, sep=" : ")


[Out]

first name : KilDong. last name : Hong. age : 30. address : Seoul, Korea.

 




  • end="end_string" 옵션 : 앞의 문자열에 바로 이어서 end_string을 붙여서 인쇄


for k, v in my_dict.items():

    print(k + " : ", end=v+"\n")


[Out]
first name : KilDong.
last name : Hong.
age : 30. 

address : Seoul, Korea.




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

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



728x90
반응형
Posted by Rfriend
,