[R data.table] data.table 의 구문 DT[i, j, by] 에서 행 subset 과 열 select 하고 계산하기
R 분석과 프로그래밍/R 데이터 전처리 2020. 10. 3. 23:49지난번 포스팅에서는 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)
# 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 구문을 사용하여 그룹별로 집계하는 방법을 소개하겠습니다.
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요.