이전 포스팅에서는 R data.table에서 '키와 빠른 이진 탐색 기반의 부분집합 선택 (Key and fast binary search based subset)' 에 대해서 소개하였습니다. (rfriend.tistory.com/569)

이번 포스팅에서는 R data.table에서 '2차 인덱스 (Secondary indices)'를 사용하여 빠른 이진 탐색 기반의 부분집합 가져오기 방법을 소개하겠습니다.  이번 포스팅은 R data.table vignettes 을 참조하였습니다.

 

(1) 이차 인덱스 (Secondary indices) 는 무엇이, 키(Key)와는 무엇이 다른가?

(2) 이차 인덱스를 설정하고 확인하는 방법

(3) 'on' 매개변수와 이차 인덱스를 사용해서 빠르게 부분집합 가져오기

(4) Chaining 해서 정렬하기

(5) j 에 대해 계산하기 (compute or do in j)

(6) J 에 := 를 사용해서 참조하여 부분할당하기 (sub-assign by reference using := in j)

(7) by 를 사용해서 집계하기 (Aggregation using by)

(8) mult 매개변수를 사용해 첫번째 행, 마지막행 가져오기

(9) 이차 인덱스 제거하기 (remove all secondary indices)

 

 

(1) 이차 인덱스는 무엇이고, 키와는 무엇이 다른가?
    (Key vs. Secondary indices)

이차 인덱스(Secondary indices) 는 data.table의 키(Key)와 비슷하게 빠른 이진 탐색 기반의 부분집합 가져오기를 할 때 사용합니다.

하지만 키(Key)가 (a) 순서 벡터를 계산한 다음에, (b) 물리적으로 data.table을 재정렬(physically reordering)하는데 비해, 이차 인덱스(secondary indices)는 순서 벡터를 계산해서 index 를 생성해 속성으로 저장만 하고, 물리적 재정렬은 하지 않는 차이점이 있습니다. 만약 data.table의 행과 열이 매우 많은 큰 크기의 데이터셋이라면 물리적으로 재정렬하는데 많은 처리 비용과 시간이 소요될 것입니다.

또 하나 큰 차이점은, 키(Key)는 하나의 칼럼을 키로 설정했을 때 다른 칼럼을 키로 사용하려면 키를 새로운 칼럼으로 재설정하고 data.table을 물리적으로 재정렬을 해야 하는 반면에, 이차 인덱스(secondary indices)는 복수의 이차 인덱스를 설정하고 재사용할 수 있습니다.

 

이러한 차이점을 고려했을 때, 키(Key)는 동일 칼럼을 "반복적으로" 사용해서 빠르게 부분집합 가져오기(subsetting)를 해야 할 때 유리하며, 이차 인덱스(Secondary indices)는 복수개의 칼럼을 단발성으로 사용하면서 빠르게 부분집합 가져오기를 해야 하는 경우에 유리합니다.

 

[ R data.table 키(Key) vs. 이차적인 인덱스 (Secondary indices) ]

 

 

 

(2) 이차 인덱스를 설정하고 확인하는 방법

 

R data.table 패키지를 importing 하고, 예제로 사용할 데이터로는 Lahman 패키지에 들어있는 투수의 투구 통계 데이터인 "Pitching"을 참조하여 Data.Table로 불러오겠습니다.

 

library(data.table)

## Lahman database on baseball
#install.packages("Lahman")
library(Lahman)
data("Pitching")

## coerce lists and data.frame to data.table by reference
setDT(Pitching)

str(Pitching)
# Classes 'data.table' and 'data.frame':	47628 obs. of  30 variables:
#   $ playerID: chr  "bechtge01" "brainas01" "fergubo01" "fishech01" ...
# $ yearID  : int  1871 1871 1871 1871 1871 1871 1871 1871 1871 1871 ...
# $ stint   : int  1 1 1 1 1 1 1 1 1 1 ...
# $ teamID  : Factor w/ 149 levels "ALT","ANA","ARI",..: 97 142 90 111 90 136 111 56 97 136 ...
# $ lgID    : Factor w/ 7 levels "AA","AL","FL",..: 4 4 4 4 4 4 4 4 4 4 ...
# $ W       : int  1 12 0 4 0 0 0 6 18 12 ...
# $ L       : int  2 15 0 16 1 0 1 11 5 15 ...
# $ G       : int  3 30 1 24 1 1 3 19 25 29 ...
# $ GS      : int  3 30 0 24 1 0 1 19 25 29 ...
# $ CG      : int  2 30 0 22 1 0 1 19 25 28 ...
# $ SHO     : int  0 0 0 1 0 0 0 1 0 0 ...
# $ SV      : int  0 0 0 0 0 0 0 0 0 0 ...
# $ IPouts  : int  78 792 3 639 27 3 39 507 666 747 ...
# $ H       : int  43 361 8 295 20 1 20 261 285 430 ...
# $ ER      : int  23 132 3 103 10 0 5 97 113 153 ...
# $ HR      : int  0 4 0 3 0 0 0 5 3 4 ...
# $ BB      : int  11 37 0 31 3 0 3 21 40 75 ...
# $ SO      : int  1 13 0 15 0 0 1 17 15 12 ...
# $ BAOpp   : num  NA NA NA NA NA NA NA NA NA NA ...
# $ ERA     : num  7.96 4.5 27 4.35 10 0 3.46 5.17 4.58 5.53 ...
# $ IBB     : int  NA NA NA NA NA NA NA NA NA NA ...
# $ WP      : int  7 7 2 20 0 0 1 15 3 44 ...
# $ HBP     : int  NA NA NA NA NA NA NA NA NA NA ...
# $ BK      : int  0 0 0 0 0 0 0 2 0 0 ...
# $ BFP     : int  146 1291 14 1080 57 3 70 876 1059 1334 ...
# $ GF      : int  0 0 0 1 0 1 1 0 0 0 ...
# $ R       : int  42 292 9 257 21 0 30 243 223 362 ...
# $ SH      : int  NA NA NA NA NA NA NA NA NA NA ...
# $ SF      : int  NA NA NA NA NA NA NA NA NA NA ...
# $ GIDP    : int  NA NA NA NA NA NA NA NA NA NA ...
# - attr(*, ".internal.selfref")=<externalptr>

 

 

이차 인덱스는 setindex(DT, column) 함수의 구문으로 설정할 수 있습니다. 그러면 순서 벡터를 계산해서 내부에 index 라는 속성(attribute)을 생성해서 저장하며, 물리적으로 data.table을 재정렬하는 것은 하지 않습니다(no physical reordering)

names(attributes(DT)) 으로 확인해보면 제일 마지막에 "index"라는 속성이 추가된 것을 알 수 있습니다.  indices(DT) 함수를 사용하면 모든 이차 인덱스의 리스트를 얻을 수 있습니다.  이때 만약 아무런 이차 인덱스가 설정되어 있지 않다면 NULL 을 반환합니다.

 

## (1) Secondary indices
## set the column teamID as a secondary index in teh data.table Pitching
setindex(Pitching, teamID)
head(Pitching)
#    playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp   ERA IBB WP HBP BK  BFP GF   R SH SF
# 1: bechtge01   1871     1    PH1   NA  1  2  3  3  2   0  0     78  43  23  0 11  1    NA  7.96  NA  7  NA  0  146  0  42 NA NA
# 2: brainas01   1871     1    WS3   NA 12 15 30 30 30   0  0    792 361 132  4 37 13    NA  4.50  NA  7  NA  0 1291  0 292 NA NA
# 3: fergubo01   1871     1    NY2   NA  0  0  1  0  0   0  0      3   8   3  0  0  0    NA 27.00  NA  2  NA  0   14  0   9 NA NA
# 4: fishech01   1871     1    RC1   NA  4 16 24 24 22   1  0    639 295 103  3 31 15    NA  4.35  NA 20  NA  0 1080  1 257 NA NA
# 5: fleetfr01   1871     1    NY2   NA  0  1  1  1  1   0  0     27  20  10  0  3  0    NA 10.00  NA  0  NA  0   57  0  21 NA NA
# 6: flowedi01   1871     1    TRO   NA  0  0  1  0  0   0  0      3   1   0  0  0  0    NA  0.00  NA  0  NA  0    3  1   0 NA NA
#      GIDP
# 1:   NA
# 2:   NA
# 3:   NA
# 4:   NA
# 5:   NA
# 6:   NA


## alternatively we can provide character vectors to the function 'setindexv()'
# setindexv(Pitching, "teamID") # useful to program with

## 'index' attribute added
names(attributes(Pitching))
# [1] "names"             "row.names"         "class"             ".internal.selfref" 
# [5] "index"


## get all the secondary indices set
indices(Pitching)
# [1] "teamID"

 

 

 

(3) 'on' 매개변수와 이차 인덱스를 사용해서 빠르게 부분집합 가져오기

 

'on' 매개변수를 사용하면 별도로 setindex()로 매번 이차 인덱스를 설정하는 절차 없이, 바로 실행 중에(on the fly) 이차 인덱스를 계산해서 부분집합 가져오기(subsetting)을 할 수 있습니다. 

그리고 만약 기존이 이미 이차 인덱스가 설정이 되어 있다면 속성을 확인하여 존재하는 이차 인덱스를 재활용해서 부분집합 가져오기를 빠르게 할 수 있습니다 (on 매개변수는 Key에 대해서도 동일하게 작동합니다).

또 'on' 매개변수는 무슨 칼럼을 기준으로 subsetting 이 실행될지에 대해서 명확하게 코드 구문으로 확인할 수 있게 해주어 코드 가독성을 높여줍니다.

 

아래 예제는 Pitching data.table에서 이차 인덱스(secondary indices)를 설정한 'teamID' 칼럼의 값이 "NY2" 인 팀을 subsetting 해서 가져온 것입니다. (칼럼 개수가 너무 많아서 1~10번까지 칼럼만 가져왔습니다. [, 1:10]) 

Pirthcing["NY2", on = "teamID"], Pitching[.("NY2"), on = "teamID"], Pitching[list("NY2"), on = "teamID"] 모두 동일한 결과를 반환합니다.

 

## subset all rows where the teamID matches "NY2" using 'on'
Pitching["NY2", on = "teamID"][,1:10]
#    playerID yearID stint teamID lgID  W  L  G GS CG
# 1: fergubo01   1871     1    NY2   NA  0  0  1  0  0
# 2: fleetfr01   1871     1    NY2   NA  0  1  1  1  1
# 3: woltery01   1871     1    NY2   NA 16 16 32 32 31
# 4: cummica01   1872     1    NY2   NA 33 20 55 55 53
# 5: mcmuljo01   1872     1    NY2   NA  1  0  3  1  1
# 6: martiph01   1873     1    NY2   NA  0  1  6  1  1
# 7: mathebo01   1873     1    NY2   NA 29 23 52 52 47
# 8: hatfijo01   1874     1    NY2   NA  0  1  3  0  0
# 9: mathebo01   1874     1    NY2   NA 42 22 65 65 62
# 10: gedneco01   1875     1    NY2   NA  1  0  2  1  1
# 11: mathebo01   1875     1    NY2   NA 29 38 70 70 69


## or alternatively
# Pitching[.("NY2"), on = "teamID"]
# Pitching[list("NY2"), on = "teamID"]

 

 

복수개의 이차 인덱스 (multiple secondary indices)를 setindex(DT, col_1, col_2, ...) 구문 형식으로 설정할 수도 있습니다.

아래 예에서는 Pitching data.table에 "teamID", "yearID"의 2개 칼럼을 이차 인덱스로 설정하고, teamID가 "NY2", yearID가 1873 인 행을 subsetting 해본 것입니다.

 

## set multiple secondary indices
setindex(Pitching, teamID, yearID)
indices(Pitching)
# [1] "teamID"         "teamID__yearID"


## subset based on teamID and yearID columns.
Pitching[.("NY2", 1873), # i
         on = c("teamID", "yearID")]
#    playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp  ERA IBB WP HBP BK  BFP GF   R SH SF
# 1: martiph01   1873     1    NY2   NA  0  1  6  1  1   0  0    102  50  13  0  6  1    NA 3.44  NA  1  NA  0  177  5  37 NA NA
# 2: mathebo01   1873     1    NY2   NA 29 23 52 52 47   2  0   1329 489 127  5 62 79    NA 2.58  NA 23  NA  0 2008  0 348 NA NA
# GIDP
# 1:   NA
# 2:   NA

 

 

이차 인덱스도 DT[i, j, by] 의 구문 형식을 그대로 따르므로 이차 인덱스로 i 에 대해 행을 subsetting 하고, j 에 대해서 특정 칼럼들을 선택해서 가져올 수 있습니다.

아래 예에서는 이차 인덱스인 teamID가 "NY2", yearID가 1873인 행을 subsetting하고, j 부분에 .(teamID, yearID, playerID, W, L) 로 지정해줘서 칼럼은 teamID, yearID, playerID, W, L 만 선별적으로 선택해서 가져온 것입니다.

## -- select in j
## return palyerID, W, L columns as a data.table corresponding to teamID = "NY2" and yearID = 1873
Pitching[.("NY2", 1873), # i
         .(teamID, yearID, playerID, W, L), # j
         on = c("teamID", "yearID")] # secondary indices
#    teamID yearID  playerID  W  L
# 1:    NY2   1873 martiph01  0  1
# 2:    NY2   1873 mathebo01 29 23

 

 

 

(4) Chaining 해서 정렬하기

 

이차 인덱스를 사용해서 subsetting 한 후의 결과에 DT[i, j, by][order()] 처럼 chaining을 해서 특정 칼럼을 기준으로 정렬을 할 수 있습니다.

아래 예에서는 이차 인덱스 'teamID' 의 값이 "NY2" 인 행을 subsetting 하고, 칼럼은 .(teamID, yearID, playerID, W, L) 만 선별해서 가져오는데, 이 결과에 chaining을 해서 [order(-W)] 로 W (승리 회수) 를 기준으로 내림차순 정렬 (sorting in descending order) 을 해본 것입니다.  order(-W) 에서 마이너스 부호('-')는 내림차순 정렬을 하라는 의미입니다. (order()의 기본설정은 오름차순 정렬임)

 

## -- Chaining
## use chaining to order the W column in descending order
Pitching[.("NY2"), # i
         .(teamID, yearID, playerID, W, L), # j
         on = c("teamID")][ # secondary indices
           order(-W)] # order by W in decreasing order
#    teamID yearID  playerID  W  L
# 1:    NY2   1874 mathebo01 42 22
# 2:    NY2   1872 cummica01 33 20
# 3:    NY2   1873 mathebo01 29 23
# 4:    NY2   1875 mathebo01 29 38
# 5:    NY2   1871 woltery01 16 16
# 6:    NY2   1872 mcmuljo01  1  0
# 7:    NY2   1875 gedneco01  1  0
# 8:    NY2   1871 fergubo01  0  0
# 9:    NY2   1871 fleetfr01  0  1
# 10:    NY2   1873 martiph01  0  1
# 11:    NY2   1874 hatfijo01  0  1

 

 


(5) j 에 대해 계산하기 (compute or do in j)

 

이차 인덱스로 i 행을 Subsetting 한 다음에 j 열에 대해서 연산을 할 수 있습니다.

아래 예에서는 (a) 이차 인덱스 'teamID' 의 값이 "NY2"인 행을 subsetting 한 후에, 그 결과 안에서 W (승리회수) 의 최대값을 계산, (b) 복수의 이차 인덱스 'teamID', 'yearID'의 값이 각각 "NY2", 1873인 값을 subsetting 해서 W의 값의 최대값을 계산(max(W))한 것입니다.

 

## -- Compute or do in j

## Find the maximum W corresponding to teamID="NY2"
Pitching[.("NY2"), max(W), on = c("teamID")]
# [1] 42


Pitching[.("NY2", 1873), max(W), on = c("teamID", "yearID")]
# [1] 29

 

 

 

(6) j 에 := 를 사용해서 참조하여 부분할당하기
    (sub-assign by reference using := in j)

 

DT[i, j, by] 에서 j 부분에 := 사용해 'on'으로 이차 인덱스를 참조하여 부분 할당(sub-assign) 하면 매우 빠르게 특정 일부분의 행의 값만을 대체할 수 있습니다. 

만약 행의 개수가 매우 많은 데이터셋에서 Key() 를 사용해서 참조하여 부분할당을 하려고 한다면 data.table에 대한 물리적인 재정렬(physical reordering)이 발생하여 연산비용과 시간이 많이 소요될텐데요, 이를 이차 인덱스(secondary indices)를 사용하면 data.table에 대한 재정렬 없이 일부 행의 값을 다른 값으로 대체하는 일을 빠르게 할 수 있는 장점이 있습니다.

 

아래의 예는 이차 인덱스인 yearID 의 값이 '2019' 인 행의 값을 '2020' 으로 대체하는 부분할당을 해본 것입니다. (2019년을 2020년으로 바꾼 것은 별 의미는 없구요, 그냥 이차 인덱스 참조에 의한 부분할당 기능 예시를 들어본 것입니다.)

 

## -- sub-assign by reference using := in j
## get all yearID in Pitching
Pitching[, sort(unique(yearID))]
# [1] 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895
# [26] 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920
# [51] 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945
# [76] 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970
# [101] 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995
# [126] 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019

## replace 2019 with 2020 using on instead of setting keys
Pitching[.(2019L), yearID := 2020L, on = "yearID"] # no reordering
Pitching[, sort(unique(yearID))]
# [1] 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895
# [26] 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920
# [51] 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945
# [76] 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970
# [101] 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995
# [126] 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2020

 

 

 

(7) by 를 사용해서 집계하기 (Aggregation using by)

 

만약 'on' 매개변수로 이차 인덱스를 사용해 "그룹별로 집계나 연산"을 하고 싶다면 by 를 추가해주면 됩니다.

아래 예에서는 이차 인덱스 'teamID'의 값이 "NY2"인 팀을 subsetting 해서, keyby = yearID를 사용해 연도(yearID) 그룹 별로 나누어서 승리회수(W)의 최대값을 계산한 것입니다.

## -- aggregation using by
## get the maximum W for each yearID corresponding to teamID="NY2". order the result by yearID
Pitching[.("NY2"), # i
         max(W),   # j
         keyby = yearID, # order by  
         on = "teamID"]  # secondary indices
#    yearID V1
# 1:   1871 16
# 2:   1872 33
# 3:   1873 29
# 4:   1874 42
# 5:   1875 29

 

 

 

(8) mult 매개변수를 사용해 첫번째 행, 마지막행 가져오기

 

이차 인덱스(secondary indices)로 빠르게 탐색하여 참조해 행을 subsetting을 해 온 다음에, mult = "first" 매개변수를 사용해서 첫번째 행, 또는 mult = "last"로 마지막 행만을 반환할 수 있습니다.

## -- melt argument
## subset only the first matching row where teamID matches "NY2" and "WS3"
Pitching[c("NY2", "WS3"), on = "teamID", 
         mult = "first"] # subset the first matching row
#   playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp  ERA IBB WP HBP BK  BFP GF   R SH SF
# 1: fergubo01   1871     1    NY2   NA  0  0  1  0  0   0  0      3   8   3  0  0  0    NA 27.0  NA  2  NA  0   14  0   9 NA NA
# 2: brainas01   1871     1    WS3   NA 12 15 30 30 30   0  0    792 361 132  4 37 13    NA  4.5  NA  7  NA  0 1291  0 292 NA NA
# GIDP
# 1:   NA
# 2:   NA

 

 

이차 인덱스로 참조할 기준이 많아지다 보면 그 조건들에 해당하는 행의 값이 존재하지 않을 때도 있습니다.  아래 예의 경우 이차 인덱스 teamID 가 "WS3" 이고 yearID가 '1873'인 행이 존재하지 않아서 mult = "last"로 마지막 을 반환하라고 했을 때 NA 가 반환되었습니다.(두번째 행)

## subset only the last matching row where teamID matches "NY2", "WS3" and yearID matches 1873
Pitching[.(c("NY2", "WS3"), 1873), on = c("teamID", "yearID"), 
         mult = "last"] # subset the last matching row
#    playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp  ERA IBB WP HBP BK  BFP GF   R SH SF
# 1: mathebo01   1873     1    NY2   NA 29 23 52 52 47   2  0   1329 489 127  5 62 79    NA 2.58  NA 23  NA  0 2008  0 348 NA NA
# 2:      <NA>   1873    NA    WS3 <NA> NA NA NA NA NA  NA NA     NA  NA  NA NA NA NA    NA   NA  NA NA  NA NA   NA NA  NA NA NA
# GIDP
# 1:   NA
# 2:   NA

 

 

이처럼 참조할 값이 존재하지 않을 경우 nomatch = NULL 매개변수를 추가해주면 매칭이 되는 행의 값만을 가져올 수 있습니다. (아래 예에서는 teamID "WS3" & yearID 1873 과 매칭되는 행이 존재하지 않으므로 nomatch = NULL 옵션이 추가되니 결과값에서 없어졌습니다.)

 

## -- the nomatch argument
## From the previous example, setset all rows only if there is a match
Pitching[.(c("NY2", "WS3"), 1873), on = c("teamID", "yearID"), 
         mult = "last", 
         nomatch = NULL] # subset only if there's a match
# playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp  ERA IBB WP HBP BK  BFP GF   R SH SF
# 1: mathebo01   1873     1    NY2   NA 29 23 52 52 47   2  0   1329 489 127  5 62 79    NA 2.58  NA 23  NA  0 2008  0 348 NA NA
# GIDP
# 1:   NA

 

 

 

(9) 이차 인덱스 제거하기 (remove all secondary indices)

 

이차 인덱스를 제거할 때는 setindex(DT, NULL) 처럼 해주면 기존의 모든 이차 인데스들이 모두 한꺼번에 NULL로 할당되어 제거됩니다.

 

## remove all secondary indices
setindex(Pitching, NULL)

indices(Pitching)
# NULL

 

 

참고로, Key를 설정, 확인, 제거하는 함수는 setkey(DT, col), key(DT), setkey(DT, NULL) 입니다.

 

## set Key
setkey(Pitching, teamID)

## check Key
key(Pitching)
# [1] "teamID"


## remove Key
setkey(Pitching, NULL)
key(Pitching)
# NULL

 

[ Reference ]

* R data.table vignettes 'Secondary indices and Auto indexing'
  : cran.r-project.org/web/packages/data.table/vignettes/datatable-secondary-indices-and-auto-indexing.html

 

 

 

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

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

 

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
,