지난 포스팅에서는 R data.table 패키지에서 선형회귀 모델의 오른쪽 부분의 변수 조합을 .SD, .SDcols, lapply(), sapply()를 사용해서 간단하게 단 몇줄의 코드로 처리하는 방법(rfriend.tistory.com/609)을 소개하였습니다.

 

이번 포스팅에서는 R data.table 패키지에서 '조건이 있는 상태에서 Key를 기준으로 데이터셋을 Left Join 하는 방법 (Conditional Joins)'을 소개하겠습니다.  그리고 base R이나 dplyr 대비 data.table의 조건이 있는 경우의 데이터셋끼리 병합이 얼마나 간단한지 비교를 해보겠습니다.  이번 포스팅은 R data.table의 Vignettes 을 참조하였습니다.

 

(1) data.table을 이용한 조건이 있는 경우의 Left Join

(2) dplyr을 이용한 조건이 있는 경우의 Left Join 비교

 

 

 

먼저, data.table 패키지를 불러오고, 예제로 사용할 데이터로 Lahman 패키지에 들어있는 야구 투구 통계 데이터인 'Pitching' 데이터셋과 야구 팀 성적 통계 데이터인 'Teams' 데이터셋을 참조해서 data.table과 data.frame으로 만들어보겠습니다.

 

library(data.table)

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

data("Pitching")

## data.frame
Pitching_df <- data.frame(Pitching)


## data.table
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> 


data("Teams")
setDT(Teams)
str(Teams)
# Classes 'data.table' and 'data.frame':	2925 obs. of  48 variables:
#   $ yearID        : int  1871 1871 1871 1871 1871 1871 1871 1871 1871 1872 ...
# $ lgID          : Factor w/ 7 levels "AA","AL","FL",..: 4 4 4 4 4 4 4 4 4 4 ...
# $ teamID        : Factor w/ 149 levels "ALT","ANA","ARI",..: 24 31 39 56 90 97 111 136 142 8 ...
# $ franchID      : Factor w/ 120 levels "ALT","ANA","ARI",..: 13 36 25 56 70 85 91 109 77 9 ...
# $ divID         : chr  NA NA NA NA ...
# $ Rank          : int  3 2 8 7 5 1 9 6 4 2 ...
# $ G             : int  31 28 29 19 33 28 25 29 32 58 ...
# $ Ghome         : int  NA NA NA NA NA NA NA NA NA NA ...
# $ W             : int  20 19 10 7 16 21 4 13 15 35 ...
# $ L             : int  10 9 19 12 17 7 21 15 15 19 ...
# $ DivWin        : chr  NA NA NA NA ...
# $ WCWin         : chr  NA NA NA NA ...
# $ LgWin         : chr  "N" "N" "N" "N" ...
# $ WSWin         : chr  NA NA NA NA ...
# $ R             : int  401 302 249 137 302 376 231 351 310 617 ...
# $ AB            : int  1372 1196 1186 746 1404 1281 1036 1248 1353 2571 ...
# $ H             : int  426 323 328 178 403 410 274 384 375 753 ...
# $ X2B           : int  70 52 35 19 43 66 44 51 54 106 ...
# $ X3B           : int  37 21 40 8 21 27 25 34 26 31 ...
# $ HR            : int  3 10 7 2 1 9 3 6 6 14 ...
# $ BB            : int  60 60 26 33 33 46 38 49 48 29 ...
# $ SO            : int  19 22 25 9 15 23 30 19 13 28 ...
# $ SB            : int  73 69 18 16 46 56 53 62 48 53 ...
# $ CS            : int  16 21 8 4 15 12 10 24 13 18 ...
# $ HBP           : int  NA NA NA NA NA NA NA NA NA NA ...
# $ SF            : int  NA NA NA NA NA NA NA NA NA NA ...
# $ RA            : int  303 241 341 243 313 266 287 362 303 434 ...
# $ ER            : int  109 77 116 97 121 137 108 153 137 166 ...
# $ ERA           : num  3.55 2.76 4.11 5.17 3.72 4.95 4.3 5.51 4.37 2.9 ...
# $ CG            : int  22 25 23 19 32 27 23 28 32 48 ...
# $ SHO           : int  1 0 0 1 1 0 1 0 0 1 ...
# $ SV            : int  3 1 0 0 0 0 0 0 0 1 ...
# $ IPouts        : int  828 753 762 507 879 747 678 750 846 1548 ...
# $ HA            : int  367 308 346 261 373 329 315 431 371 573 ...
# $ HRA           : int  2 6 13 5 7 3 3 4 4 3 ...
# $ BBA           : int  42 28 53 21 42 53 34 75 45 63 ...
# $ SOA           : int  23 22 34 17 22 16 16 12 13 77 ...
# $ E             : int  243 229 234 163 235 194 220 198 218 432 ...
# $ DP            : int  24 16 15 8 14 13 14 22 20 22 ...
# $ FP            : num  0.834 0.829 0.818 0.803 0.84 0.845 0.821 0.845 0.85 0.83 ...
# $ name          : chr  "Boston Red Stockings" "Chicago White Stockings" "Cleveland Forest Citys" "Fort Wayne Kekiongas" ...
# $ park          : chr  "South End Grounds I" "Union Base-Ball Grounds" "National Association Grounds" "Hamilton Field" ...
# $ attendance    : int  NA NA NA NA NA NA NA NA NA NA ...
# $ BPF           : int  103 104 96 101 90 102 97 101 94 106 ...
# $ PPF           : int  98 102 100 107 88 98 99 100 98 102 ...
# $ teamIDBR      : chr  "BOS" "CHI" "CLE" "KEK" ...
# $ teamIDlahman45: chr  "BS1" "CH1" "CL1" "FW1" ...
# $ teamIDretro   : chr  "BS1" "CH1" "CL1" "FW1" ...
# - attr(*, ".internal.selfref")=<externalptr>

 

 

(1) data.table을 이용한 조건이 있는 경우의 Left Join

 

(1-1) 참여 경기 수 조건 하에 팀별 연도별 평균자책점 순위 (rank_in_team)

 

(조건) G > 5 : 6개 이상의 경기(G, game)에 참여하여 공을 던진 투수에 한하여,

(연산) ERA (평균 자책점, Earned Run Average)의 순위를 구해서 rank_in_team 변수를 만들되,

(그룹 기준) 'teamID' & 'yearID' 그룹 별로 ERA 순위(frank(ERA))를 구하여라.

 

조건, 연산, 그룹 기준이 모두 Pitching[조건, 연산, 그룹 기준] 의 구문으로 해서 단 한줄로 모두 처리가 가능합니다. 간결함의 끝판왕이라고나 할까요!

 

## -- rank in team using frank() function by teamID & yearID groups
## to exclude pitchers with exceptional performance in a few games,
##   subset first; then define rank of pitchers within their team each year
##   (in general, we should put more care into the 'ties.method' of frank)
Pitching[G > 5, rank_in_team := frank(ERA), by = .(teamID, yearID)]

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

 

 

(1-2) 팀내 순위 조건하에 다른 데이터셋에 앴는 팀 성적 (team_performance) Left join

 

(조건) Pitching 데이터셋에서 팀/연도별로 순위가 1등인 투수에 한해 (rank_in_team == 1)

(연산) Teams 데이터셋의 순위(Rank)를 가져다가 Pitching 데이터셋에 team_performance 이름의 변수로 만들되,

(병합 기준) Pitching 과 Teams 데이터셋의 'teamID'와 'yearID'를 기준으로 매칭하시오.

 

 

Pitching[rank_in_team == 1, # condition for rows
         team_performance := Teams[.SD, Rank, 
                                   on = c('teamID', 'yearID')] # left join by keys
         ]


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

 

 

 

(2) dplyr을 이용한 조건이 있는 경우의 Left Join 비교

 

(2-1) 참여 경기 수 조건하에 팀별 연도별 평균자책점 순위 (rank_in_team)

이제 위의 (1-1)에서 data.table로 수행했던 것과 동일한 과업을 dplyr 패키지로 수행해보겠습니다.

 

(조건) G > 5 : 6개 이상의 경기(G, game)에 참여하여 공을 던진 투수에 한하여,

(연산) ERA (평균 자책점, Earned Run Average)의 순위를 구해서 rank_in_team 변수를 만들되,

(그룹 기준) 'teamID' & 'yearID' 그룹 별로 ERA 순위(frank(ERA))를 구하여라.

 

1단계에서 G > 5 라는 조건으로 filter() 를 하여 그룹별로 rank_in_team 을 구해서 별도의 'rank_in_team_df' data.frame을 만든 후에, --> 2단계에서 이를 원본 'Pitching_df'에 left_join() 을 해서 left join 병합을 해주었습니다. G > 5 라는 조건(condition)이 들어감으로써 2단계로 나누어서 진행이 되다보니 코드가 길어졌습니다.

 

 

## -- doing the same operation using 'dplyr'
library(dplyr)

## -- rank in team by teamID & yearID group using dense_rank() window function
rank_in_team_df <- Pitching_df %>% 
  filter(G > 5) %>% 
  group_by(teamID, yearID) %>% 
  mutate(rank_in_team = dense_rank(ERA)) %>% 
  select(playerID, lgID, teamID, yearID, stint, rank_in_team)


## left outer join {dplyr}
Pitching_df <- left_join(Pitching_df, 
                         rank_in_team_df, 
                         by = c('playerID', 'lgID', 'teamID', 'yearID', 'stint'))

# ## left outer join {base}
# Pitching_df <- merge(x = Pitching_df, 
#                      y = rank_in_team_df, 
#                      by = c('playerID', 'lgID', 'teamID', 'yearID', 'stint'), 
#                      all.x = TRUE, all.y = FALSE)


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

 

 

 

(2-2) 팀내 순위 조건하에 다른 데이터셋에 앴는 팀 성적 (team_performance) Left join

 

이제 위의 (1-2)에서 data.table로 했던 것과 똑같은 과업을 dplyr로 해보겠습니다.

 

(조건) Pitching 데이터셋에서 팀/연도별로 순위가 1등인 투수에 한해 (rank_in_team == 1) (연산) Teams 데이터셋의 순위(Rank)를 가져다가 Pitching 데이터셋에 team_performance 이름의 변수로 만들되,
(병합 기준) Pitching 과 Teams 데이터셋의 'teamID'와 'yearID'를 기준으로 매칭하시오.

 

아래처럼 dplyr로 하게 되면 '팀/연도별로 순위가 1등인 투수에 한해(rank_in_team==1)' 라는 조건을 충족시키기 위해서 조건절을 포함한 ifelse() 절을 한번 더 수행해줘야 합니다.

 

## -- merging team performance
Pitching_df <- left_join(Pitching_df, 
                         Teams[, c('teamID', 'yearID', 'Rank')], 
                         by = c('teamID', 'yearID'))

## condition: rank_in_team == 1
Pitching_df$team_performance <- ifelse(Pitching_df$rank_in_team == 1, 
                                       Pitching_df$Rank, # if TRUE
                                       NA) # if FALSE

head(Pitching_df)
# bechtge01   1871     1    PH1   NA  1  2  3  3  2   0  0     78  43  23  0 11  1    NA  7.96  NA
# 2 brainas01   1871     1    WS3   NA 12 15 30 30 30   0  0    792 361 132  4 37 13    NA  4.50  NA
# 3 fergubo01   1871     1    NY2   NA  0  0  1  0  0   0  0      3   8   3  0  0  0    NA 27.00  NA
# 4 fishech01   1871     1    RC1   NA  4 16 24 24 22   1  0    639 295 103  3 31 15    NA  4.35  NA
# 5 fleetfr01   1871     1    NY2   NA  0  1  1  1  1   0  0     27  20  10  0  3  0    NA 10.00  NA
# 6 flowedi01   1871     1    TRO   NA  0  0  1  0  0   0  0      3   1   0  0  0  0    NA  0.00  NA
# WP HBP BK  BFP GF   R SH SF GIDP rank_in_team Rank team_performance
# 1  7  NA  0  146  0  42 NA NA   NA           NA    1               NA
# 2  7  NA  0 1291  0 292 NA NA   NA            1    4                4
# 3  2  NA  0   14  0   9 NA NA   NA           NA    5               NA
# 4 20  NA  0 1080  1 257 NA NA   NA            1    9                9
# 5  0  NA  0   57  0  21 NA NA   NA           NA    5               NA
# 6  0  NA  0    3  1   0 NA NA   NA           NA    6               NA

 

 

 

위에서 소개했던 똑같은 과업을 수행하는 data.table과 dplyr의 조건있는 데이터셋 병합(conditional left join) 예제 코드를 나란히 제시해서 비교해보면 아래와 같습니다. data.table이 dplyr (혹은 base R) 대비 조건있는 데이터셋 병합의 경우 비교할 수 없을 정도로 훨~씬 코드가 간결합니다!

 

구분

data.table

dplyr

(1)

conditional

left join

by key

w/ a list

library(data.table)

Pitching[G > 5, # condition

         rank_in_team := frank(ERA), 

         by = .(teamID, yearID)]

library(dplyr)

rank_in_team_df <- Pitching_df %>% 

  filter(G > 5) %>% 

  group_by(teamID, yearID) %>% 

  mutate(rank_in_team = dense_rank(ERA)) %>% 

  select(playerID, lgID, teamID, yearID, stint, rank_in_team)

## left outer join {dplyr}

Pitching_df <- left_join(

  Pitching_df, 

  rank_in_team_df, 

  by = c('playerID', 'lgID', 'teamID', 'yearID', 'stint'))

(2)

conditional

left join

by key

w/ a data.table

Pitching[rank_in_team == 1,          team_performance := Teams[           .SD, Rank, 
         on = c('teamID', 'yearID')]
         ]

## -- merging team performance

Pitching_df <- left_join(

  Pitching_df, 

  Teams[, c('teamID', 'yearID', 'Rank')], 

  by = c('teamID', 'yearID'))

## condition: rank_in_team == 1

Pitching_df$team_performance <- ifelse(

  Pitching_df$rank_in_team == 1, 

  Pitching_df$Rank, # if TRUE

  NA) # if FALSE

 

 

[ Reference ]

* R data.table vignettes 'Using .SD for Data Analysis'
  : cran.r-project.org/web/packages/data.table/vignettes/datatable-sd-usage.html

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 패키지에서 .SD 가 무엇이고, .SD와 .SDcols 를 사용해서 특정 칼럼만을 가져오는 방법(을 소개하였습니다. 그리고 lapply()도 같이 사용해서 여러개의 칼럼의 데이터 유형을 변환하는 방법도 같이 설명하였습니다. (rfriend.tistory.com/608)

 

이번 포스팅에서는 지난 포스팅에 이어서, R data.table의 .SD, .SDcols와 lapply(), combn() 함수를 같이 사용하여 '선형회귀 모델의 오른쪽 부분에 여러 변수의 모든 가능한 조합을 적용하여 다루는 방법'을 소개하겠습니다.

 

(1) 설명변수 간 모든 가능한 조합 만들기: combn(x, m, simplify = TRUE, ...)

(2) 설명변수 간 모든 가능한 조합으로 선형회귀모델 적합하여 회귀계수 가져오기

(3) 설명변수 조합별 회귀계수로 막대그래프 그리기: barplot()

 

만약 모델에서 사용하는 칼럼의 개수가 몇 개 안된다면 수작업으로 일일이 변수 이름을 나열해서 적어주면 됩니다. 하지만, 모델에서 사용하는 칼럼 개수가 수십개인 경우(** 몇 개의 칼럼 간의 조합을 사용하는 경우 기하급수적으로 칼럼 개수가 늘어남)에는 키보드를 쳐가면서 일일이 변수 이름(조합)을 나열하기가 시간도 오래 걸리고, 오타 에러를 유발할 위험도 높아집니다. 이럴 경우에 이번에 소개하는 내용이 매우 유용하고 간편하게 사용될 수 있습니다.

 

먼저, data.table 패키지를 불러오고, 예제로 사용할 데이터로 Lahman 패키지에 들어있는 야규 투구 통계 데이터인 'Pitching' 데이터셋을 Data.Table로 만들어보겠습니다.

 

library(data.table)

## Lahman database on baseball
#install.packages("Lahman")
library(Lahman)
data("Pitching")
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>

 

(1) 설명변수 간 모든 가능한 조합 만들기: combn(x, m, simplify = TRUE, ...)

 

먼저, 코드 이해를 돕기 위해서 utils 패키지의 combn(x, m, simplify = TRUE, ...) 함수에 대해서 설명하겠습니다.

 

combn() 함수는 투입되는 객체 x 내 원소들 간의 m 개로 이루어진 모든 가능한 조합을 만들어줍니다. 이때 simplify = TRUE (default 설정) 이면 바로 아래의 예처럼 행렬(matrix), 배열(array) 형태로 간소화해서 조합의 결과를 나타내주며, simplify = FALSE 로 설정해주면 두번째 예처럼 원소 간 조합의 결과를 리스트(list) 형태로 해서 위에서 아래로 길게 나타내줍니다.

 

## combn {utils}
## : combn(x, m, FUN = NULL, simplify = TRUE, ...)
## : Generate all combinations of the elements of 'x' taken 'm' at a time
combn(x = letters[1:4], m = 2)
# [,1] [,2] [,3] [,4] [,5] [,6]
# [1,] "a"  "a"  "a"  "b"  "b"  "c" 
# [2,] "b"  "c"  "d"  "c"  "d"  "d"


## simplify = FALSE : returns the combinations in a list format
combn(x = letters[0:4], m = 2, simplify = FALSE)
# [[1]]
# [1] "a" "b"
# 
# [[2]]
# [1] "a" "c"
# 
# [[3]]
# [1] "a" "d"
# 
# [[4]]
# [1] "b" "c"
# 
# [[5]]
# [1] "b" "d"
# 
# [[6]]
# [1] "c" "d"

 

 

앞에서 combn() 함수로 인풋 x 의 모든 원소 간 조합을 만들 수 있다는 것을 알았으므로, 이제 c('yearID', 'teamID', 'G', 'L') 의 4개 설명변수 간 모든 조합을 만들어보겠습니다. 이때 m=0~4 (설명변수 개수) 까지 바꾸어가면서 lapply() 로 combn() 함수에 x = c('yearID', 'teamID', 'G', 'L') 의 4개 원소를 가진 인풋을 적용해서 가능한 모든 조합을 만들어보겠습니다. 그리고 조합의 결과를 unlist() 해서 아래의 models 결과처럼 각 조합이 문자형 벡터(character vector)로 해서 차곡 차곡 리스트에 묶여있도록 하겠습니다.

 

## this generates a list of the 2^k possible extra variables
## for models of the form ERA ~ G + (...)
extra_var <- c('yearID', 'teamID', 'G', 'L')
models <- unlist(
  lapply(0L:length(extra_var), combn, x = extra_var, simplify = FALSE), 
  recursive = FALSE # the function will not recurse beyond the first level items in x
)

models
# [[1]]
# character(0)
# 
# [[2]]
# [1] "yearID"
# 
# [[3]]
# [1] "teamID"
# 
# [[4]]
# [1] "G"
# 
# [[5]]
# [1] "L"
# 
# [[6]]
# [1] "yearID" "teamID"
# 
# [[7]]
# [1] "yearID" "G"     
# 
# [[8]]
# [1] "yearID" "L"     
# 
# [[9]]
# [1] "teamID" "G"     
# 
# [[10]]
# [1] "teamID" "L"     
# 
# [[11]]
# [1] "G" "L"
# 
# [[12]]
# [1] "yearID" "teamID" "G"     
# 
# [[13]]
# [1] "yearID" "teamID" "L"     
# 
# [[14]]
# [1] "yearID" "G"      "L"     
# 
# [[15]]
# [1] "teamID" "G"      "L"     
# 
# [[16]]
# [1] "yearID" "teamID" "G"      "L"

 

 

위의 lapply() 와 combn() 함수를 같이 쓴 코드가 잘 이해가 안가신다면, 바로 아래처럼 m=0~4 까지 일일이 반복적으로 길게 쓴 코드를 lapply() 사용해서 좀더 간결하게 쓴 것이 위의 코드라고 생각하면 되겠습니다.

 

## or, equivalently by manual
unlist(
  list(
    combn(x=extra_var, m=0, simplify=F), 
    combn(x=extra_var, m=1, simplify=F), 
    combn(x=extra_var, m=2, simplify=F), 
    combn(x=extra_var, m=3, simplify=F), 
    combn(x=extra_var, m=4, simplify=F)
  ), 
  recursive = FALSE
)

 

 

 

(2) 설명변수 간 모든 가능한 조합으로 선형회귀모델 적합하여 회귀계수 가져오기

 

R에서 선형회귀모형은 lm(y ~ x, data) 형태의 구문을 사용합니다. 그리고 coef(lm(y ~ x, data)) 로 적합된 회귀모형의 회귀계수에 접근할 수 있습니다.

아래의 Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)] 코드가 무척 어려워보일 수 있는데요, 하나씩 설명해보자면요, data = .SD, .SDcols = c('W', rhs) 는 Pitching 데이터셋을 참조해서 .SDcols 에 있는 칼럼(즉, 'W' 변수와 (1)번에서 만든 models 설명변수 조합에 해당하는 rhs 변수들)만 선별해서 가져오라는 뜻입니다. 이들 설명변수 조합을 사용해 lm(ERA ~ .) 로 회귀모형을 적합하며, coef(lm(ERA ~ ., data=.SD))['W'] 는 변수 'W'의 적합된 추정 회귀계수를 가져오라는 뜻입니다.

## -- coefficient of 'W' from linear regression model
## using ERA ~ . and data = .SD, then varying which
##   columns are included in .SD allows us to perform this
##   iteration over 16 models succinctly.
##   coef(.)['W'] extracts the W coefficient from each model fit
lm_coef = sapply(models, function(rhs) {
  Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)]
})



## coefficients of 'W' from the linear regression models above
lm_coef
# W           W           W           W           W           W           W           W 
# -0.20643825 -0.20877606 -0.20763604 -0.11362170 -0.18675857 -0.20689003 -0.09819310 -0.18763789 
# W           W           W           W           W           W           W           W 
# -0.10765791 -0.18214290 -0.12478923 -0.09595359 -0.18197728 -0.11646014 -0.11793832 -0.11143365

 

 

위의 sapply() 함수와 .SD, .SDcols 를 사용하지 않고 각 설명변수 16개 조합별로 일일이 수작업으로 회귀모형을 나열해서 적으려면 아래처럼 단순 작업을 여러번 해야 합니다. 시간도 오래걸리고, 또 오타 에러를 낼 위험이 높습니다.

 

## all combinations of linear regression models by manually
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'teamID')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'G')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'teamID')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'G')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'teamID', 'G')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'teamID', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'G', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'teamID', 'G')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'teamID', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'G', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'teamID', 'G', 'L')]
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', 'yearID', 'teamID', 'G', 'L')]

 

 

(3) 설명변수 조합별 회귀계수로 막대그래프 그리기: barplot()

 

위의 (2)번에서 구한 각 설명변수 16개 조합별 변수 'W'의 회귀계수를 가지고 barplot() 함수를 사용해서 막대그래프(bar plot)를 그려보겠습니다. 

여기서도 'W' 회귀계수가 적합되었을 때 각 모델별로 사용되었던 설명변수 조합을 names.arg = sapply(models, paste, collapse = '/') 로 해서 넣어주었습니다.

 

## -- barplot
# here are 16 visually distinct colors, taken from the list of 20 here:
#   https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/
col16 = c('#e6194b', '#3cb44b', '#ffe119', '#0082c8',
          '#f58231', '#911eb4', '#46f0f0', '#f032e6',
          '#d2f53c', '#fabebe', '#008080', '#e6beff',
          '#aa6e28', '#fffac8', '#800000', '#aaffc3')

par(oma = c(2, 0, 0, 0))

barplot(lm_coef, names.arg = sapply(models, paste, collapse = '/'), 
        main = "Wins coefficient\n With various covariates", 
        col = col16, las = 2L, cex.names = .8)
        

 

 

참고로, paste() 함수로 'models' 리스트에 들어있는 변수 이름들의 조합을 하나의 합칠 때, collpase = '/' 를 설정해주면 아래처럼 각 리스트 원소 내 여러개의 변수 이름을 하나로 합치면서 사이에 '/'를 넣어줍니다. (paste 함수에서 sep와 collapse 의 차이점은 여기를 참고하세요.)

sapply(models, paste, collapse = '/')
# [1] ""                  "yearID"            "teamID"            "G"                 "L"                
# [6] "yearID/teamID"     "yearID/G"          "yearID/L"          "teamID/G"          "teamID/L"         
# [11] "G/L"               "yearID/teamID/G"   "yearID/teamID/L"   "yearID/G/L"        "teamID/G/L"       
# [16] "yearID/teamID/G/L"

 

 

[ Reference ]

* R data.table vignettes 'Using .SD for Data Analysis'

  : cran.r-project.org/web/packages/data.table/vignettes/datatable-sd-usage.html

 

 

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

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

 

 

 

728x90
반응형
Posted by Rfriend
,

R data.table 패키지를 사용하다 보면 base R이나 tydeverse 에서는 사용하지 않는, data.table에서만 사용하는 .SD, .SDcols, .N 등의 매개변수를 볼 수 있는데요, 이게 data.table 패키지를 굉장히 이질적이고 코드가 이해가 되지 않는 어려운 프로그래밍 언어라는 첫인상을 주는 것 같습니다.  사실 이 첫번째 관문만 무사히 넘으면 data.table 의 강력함과 간결함에 매료될만도 한데 말이지요.

 

이번 포스팅에서는 data.table의 vignettes을 참조하여 R의 data.table 패키지에서

 

(1) .SD는 무엇인가? (What is .SD in data.table?)

(2) .SDcols 로 일부 칼럼만 가져오기 (Column Subsetting using .SDcols)

(3) lapply()와 .SDcols로 칼럼 유형 변환하기 (Column Type Conversion)

(4) 패턴이 일치하는 특정 칼럼만 가져오기 (Column subsetting using pattern-based matching)

 

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

 

 

(1) .SD는 무엇인가? (What is .SD in data.table package?)

 

data.table 패키지에서 .SD 는 "데이터의 부분집합, 데이터 자기 자신, 데이터의 자기 참조" ( 'Subset, Selfsame, or Self-reference of the Data')를 의미한다고 이해할 수 있습니다.  다시 말하자면, .SD는 data.table 자기 자신에 대한 재귀적인 참조 (a reflexive reference to the data.table), data.table 그 자체 (data.table itself) 를 의미합니다.

 

 

R의 Lahman database에 있는 야구 팀과 투수의 통계 데이터를 사용해서 예를 들어보겠습니다.  setDT(Pitching) 은 Lists와 DataFrame을 참조해서 Data.Table로 만들어줍니다.

 

먼저, 아래의 예는 .SD 를 사용해서 'Pitching' data.table 자체를 재귀적으로 참조해오는 예입니다. (복사가 아니라 참조임. not copy, but referece to the Pitching data.table)

 

library(data.table)

## .SD stands for Subset, Selfsame, or Self-reference of the Data.
## .SD is a reflexive reference to the data.table itself. 
## .SD is helpful for chaining together "queries" (extractions/subsets/etc using [). 


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

data("Pitching")
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>



## .SD on Ungrouped Data
## In terms of subsetting, .SD is a subset of the data, the set itself. 

Pitching[, .SD]
# playerID yearID stint teamID lgID  W  L  G GS CG SHO SV IPouts   H  ER HR BB SO BAOpp
# 1: bechtge01   1871     1    PH1   NA  1  2  3  3  2   0  0     78  43  23  0 11  1    NA
# 2: brainas01   1871     1    WS3   NA 12 15 30 30 30   0  0    792 361 132  4 37 13    NA
# 3: fergubo01   1871     1    NY2   NA  0  0  1  0  0   0  0      3   8   3  0  0  0    NA
# 4: fishech01   1871     1    RC1   NA  4 16 24 24 22   1  0    639 295 103  3 31 15    NA
# 5: fleetfr01   1871     1    NY2   NA  0  1  1  1  1   0  0     27  20  10  0  3  0    NA
# ---                                                                                       
#   47624: zamorda01   2019     1    NYN   NL  0  1 17  0  0   0  0     26  10   5  1  5  8 0.294
# 47625: zeuchtj01   2019     1    TOR   AL  1  2  5  3  0   0  0     68  22  12  2 11 20 0.250
# 47626: zimmejo02   2019     1    DET   AL  1 13 23 23  0   0  0    336 145  86 19 25 82 0.311
# 47627: zimmeky01   2019     1    KCA   AL  0  1 15  0  0   0  0     55  28  22  2 19 18 0.337
# 47628: zobribe01   2019     1    CHN   NL  0  0  1  0  0   0  0      3   0   0  0  2  1 0.000
# ERA IBB WP HBP BK  BFP GF   R SH SF GIDP
# 1:  7.96  NA  7  NA  0  146  0  42 NA NA   NA
# 2:  4.50  NA  7  NA  0 1291  0 292 NA NA   NA
# 3: 27.00  NA  2  NA  0   14  0   9 NA NA   NA
# 4:  4.35  NA 20  NA  0 1080  1 257 NA NA   NA
# 5: 10.00  NA  0  NA  0   57  0  21 NA NA   NA
# ---                                           
#   47624:  5.19   1  0   1  0   41  3   5  0  1    0
# 47625:  4.76   0  2   0  0   99  0  13  0  0    1
# 47626:  6.91   2  3   6  0  504  0  89  3  4    5
# 47627: 10.80   0  2   0  0  102  3  22  0  0    1
# 47628:  0.00   0  0   0  0    5  1   0  0  0    0

 

 

Pitching[, .SD] 는 단순히 Pitching data.table 자체를 그대로 반환하는데요, identical() 로 두 data.table이 동일한지 여부를 확인해보면 TRUE 입니다.

 

identical(Pitching, Pitching[ , .SD])
# [1] TRUE

 

 

(2) .SDcols 로 일부 칼럼만 가져오기 (Column Subsetting using .SDcols)

 

.SD 를 사용하는 가장 일반적인 예는 .SDcols와 함께 일부 칼럼의 부분집합을 선택해서 가져오는 것입니다. 

 

아래 예에서는 Pitching[, .SD] 로 Pitching data.table 자체를 재귀적으로 참조한 후에 --> Pitching[, .SD, .SDcols = c("playerID", "W", "L", "G")] 처럼 .SDcols 에 칼럼 이름을 넣어줘서 일부 칼럼만 선택적으로 가져와 보겠습니다.

 

library(data.table)

library(Lahman)
data("Pitching")

## Coerce lists and Data.Frames to Data.Table by Reference
setDT(Pitching)


## ** Column Subsetting: .SDcols **
Pitching[, .SD, .SDcols = c("playerID", "W", "L", "G")]

# playerID  W  L  G
# 1: bechtge01  1  2  3
# 2: brainas01 12 15 30
# 3: fergubo01  0  0  1
# 4: fishech01  4 16 24
# 5: fleetfr01  0  1  1
# ---                   
#   47624: zamorda01  0  1 17
# 47625: zeuchtj01  1  2  5
# 47626: zimmejo02  1 13 23
# 47627: zimmeky01  0  1 15
# 47628: zobribe01  0  0  1



 

(3) 칼럼 유형 변환하기 (Column Type Conversion)

 

이번에는 lapply()와 .SDcols를 사용해서 data.table에서 여러개 칼럼의 유형을 한꺼번에 변환해보겠습니다.

 

먼저, setDT(Teams) 로 'Teams' DataFrame을 참조하여 Data.Table로 만들고, c('teamIDBR', 'teamIDlahman45', 'teamIDretro') 의 3개 칼럼이 '문자형 인지 여부(is.character)'를 확인해보겠습니다. 3개 칼럼 모두 is.character()가 TRUE 이므로 문자형 맞군요.

 

library(data.table)

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

## Coerce lists and Data.Frames to Data.Table by Reference
setDT(Teams)

str(Teams)
# Classes 'data.table' and 'data.frame':	2925 obs. of  48 variables:
#   $ yearID        : int  1871 1871 1871 1871 1871 1871 1871 1871 1871 1872 ...
# $ lgID          : Factor w/ 7 levels "AA","AL","FL",..: 4 4 4 4 4 4 4 4 4 4 ...
# $ teamID        : Factor w/ 149 levels "ALT","ANA","ARI",..: 24 31 39 56 90 97 111 136 142 8 ...
# $ franchID      : Factor w/ 120 levels "ALT","ANA","ARI",..: 13 36 25 56 70 85 91 109 77 9 ...
# $ divID         : chr  NA NA NA NA ...
# $ Rank          : int  3 2 8 7 5 1 9 6 4 2 ...
# $ G             : int  31 28 29 19 33 28 25 29 32 58 ...
# $ Ghome         : int  NA NA NA NA NA NA NA NA NA NA ...
# $ W             : int  20 19 10 7 16 21 4 13 15 35 ...
# $ L             : int  10 9 19 12 17 7 21 15 15 19 ...
# $ DivWin        : chr  NA NA NA NA ...
# $ WCWin         : chr  NA NA NA NA ...
# $ LgWin         : chr  "N" "N" "N" "N" ...
# $ WSWin         : chr  NA NA NA NA ...
# $ R             : int  401 302 249 137 302 376 231 351 310 617 ...
# $ AB            : int  1372 1196 1186 746 1404 1281 1036 1248 1353 2571 ...
# $ H             : int  426 323 328 178 403 410 274 384 375 753 ...
# $ X2B           : int  70 52 35 19 43 66 44 51 54 106 ...
# $ X3B           : int  37 21 40 8 21 27 25 34 26 31 ...
# $ HR            : int  3 10 7 2 1 9 3 6 6 14 ...
# $ BB            : int  60 60 26 33 33 46 38 49 48 29 ...
# $ SO            : int  19 22 25 9 15 23 30 19 13 28 ...
# $ SB            : int  73 69 18 16 46 56 53 62 48 53 ...
# $ CS            : int  16 21 8 4 15 12 10 24 13 18 ...
# $ HBP           : int  NA NA NA NA NA NA NA NA NA NA ...
# $ SF            : int  NA NA NA NA NA NA NA NA NA NA ...
# $ RA            : int  303 241 341 243 313 266 287 362 303 434 ...
# $ ER            : int  109 77 116 97 121 137 108 153 137 166 ...
# $ ERA           : num  3.55 2.76 4.11 5.17 3.72 4.95 4.3 5.51 4.37 2.9 ...
# $ CG            : int  22 25 23 19 32 27 23 28 32 48 ...
# $ SHO           : int  1 0 0 1 1 0 1 0 0 1 ...
# $ SV            : int  3 1 0 0 0 0 0 0 0 1 ...
# $ IPouts        : int  828 753 762 507 879 747 678 750 846 1548 ...
# $ HA            : int  367 308 346 261 373 329 315 431 371 573 ...
# $ HRA           : int  2 6 13 5 7 3 3 4 4 3 ...
# $ BBA           : int  42 28 53 21 42 53 34 75 45 63 ...
# $ SOA           : int  23 22 34 17 22 16 16 12 13 77 ...
# $ E             : int  243 229 234 163 235 194 220 198 218 432 ...
# $ DP            : int  24 16 15 8 14 13 14 22 20 22 ...
# $ FP            : num  0.834 0.829 0.818 0.803 0.84 0.845 0.821 0.845 0.85 0.83 ...
# $ name          : chr  "Boston Red Stockings" "Chicago White Stockings" "Cleveland Forest Citys" "Fort Wayne Kekiongas" ...
# $ park          : chr  "South End Grounds I" "Union Base-Ball Grounds" "National Association Grounds" "Hamilton Field" ...
# $ attendance    : int  NA NA NA NA NA NA NA NA NA NA ...
# $ BPF           : int  103 104 96 101 90 102 97 101 94 106 ...
# $ PPF           : int  98 102 100 107 88 98 99 100 98 102 ...
# $ teamIDBR      : chr  "BOS" "CHI" "CLE" "KEK" ...
# $ teamIDlahman45: chr  "BS1" "CH1" "CL1" "FW1" ...
# $ teamIDretro   : chr  "BS1" "CH1" "CL1" "FW1" ...
# - attr(*, ".internal.selfref")=<externalptr> 


## check whether columns are character type or not
col_lists = c('teamIDBR', 'teamIDlahman45', 'teamIDretro')
Teams[, sapply(.SD, is.character), .SDcols = col_lists]
# teamIDBR teamIDlahman45    teamIDretro 
# TRUE           TRUE           TRUE


head(unique(Teams[[col_lists[1L]]]))
# [1] "BOS" "CHI" "CLE" "KEK" "NYU" "ATH"

 

 

이제 위의 3개 문자형 칼럼들을 요인형(factor type)으로 lapply()와 .SDcols 를 사용해서 한꺼번에 데이터 유형을 변환해보겠습니다.

 

이때 주의를 해야 할 것이 있는데요, 아래에 색깔을 칠해 놓은 것처럼, (col_lists) 처럼 괄호 () 로 싸주어야 col_lists 안의 3개 칼럼 이름을 인식해서 할당을 해줍니다. (괄호를 안쳐주면 'col_lists' 라는 이름으로 할당해버립니다.)

 

Teams[ , (col_lists) := lapply(.SD, factor), .SDcols = col_lists]

 

## Converting columns to factor by adding the := assignment operator.
## we must wrap fkt in parentheses () to force data.table to interprete this as column names.
Teams[ , (col_lists) := lapply(.SD, factor), .SDcols = col_lists]

 

col_lists의 3개 칼럼의 데이터 유형이 무엇인지 확인해보면, 요인형(factor)로 잘 변환이 되었네요.

sapply(Teams[, .SD, .SDcols = col_lists], class)
# teamIDBR teamIDlahman45    teamIDretro 
# "factor"       "factor"       "factor"



head(unique(Teams[[col_lists[1L]]]))
# [1] BOS CHI CLE KEK NYU ATH
# 101 Levels: ALT ANA ARI ATH ATL BAL BLA BLN BLU BOS BRA BRG BRO BSN BTT BUF BWW CAL CEN CHC ... WSN

 

 

(4) 패턴이 일치하는 특정 칼럼만 가져오기 (Column subsetting using pattern-based matching)

 

data.table의 .SDcols는 패턴 매칭을 지원합니다. 아래의 예에서는 'Teams' Data.Table에서 'team' 이라는 단어가 들어가 있는 칼럼 이름(.SDcols = patterns('team'))을 선별해서 가져와 보겠습니다.

 

names(Teams)

# [1] "yearID"         "lgID"           "teamID"         "franchID"       "divID"         
# [6] "Rank"           "G"              "Ghome"          "W"              "L"             
# [11] "DivWin"         "WCWin"          "LgWin"          "WSWin"          "R"             
# [16] "AB"             "H"              "X2B"            "X3B"            "HR"            
# [21] "BB"             "SO"             "SB"             "CS"             "HBP"           
# [26] "SF"             "RA"             "ER"             "ERA"            "CG"            
# [31] "SHO"            "SV"             "IPouts"         "HA"             "HRA"           
# [36] "BBA"            "SOA"            "E"              "DP"             "FP"            
# [41] "name"           "park"           "attendance"     "BPF"            "PPF"           
# [46] "teamIDBR"       "teamIDlahman45" "teamIDretro" 



## pattern-based matching of columns in .SDcols to select all columns 
## which contain team back to factor. 
Teams[ , .SD, .SDcols = patterns('team')]

# teamID teamIDBR teamIDlahman45 teamIDretro
# 1:    BS1      BOS            BS1         BS1
# 2:    CH1      CHI            CH1         CH1
# 3:    CL1      CLE            CL1         CL1
# 4:    FW1      KEK            FW1         FW1
# 5:    NY2      NYU            NY2         NY2
# ---                                           
#   2921:    SLN      STL            SLN         SLN
# 2922:    TBA      TBR            TBA         TBA
# 2923:    TEX      TEX            TEX         TEX
# 2924:    TOR      TOR            TOR         TOR
# 2925:    WAS      WSN            MON         WAS

 

아래의 예는 team_idx = grep('team', names(Teams), value=TRUE)로, 먼저 names(Teams)를 통해 얻은 전체 칼럼 이름들 중에서 'team'이 들어가 있는 칼럼 이름을 찾아서 team_idx 에 저장을 해주었습니다.  다음으로, 위의 (3)번에서 했던 lapply()와 .SD, .SDcols를 이용하여 칼럼 이름에 'team'이 들어간 모든 칼럼의 데이터 유형을 요인형(factor type)으로 일괄 변환해주었습니다.

 

team_idx = grep('team', names(Teams), value = TRUE)
team_idx

# [1] "teamID"         "teamIDBR"       "teamIDlahman45" "teamIDretro"



Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
sapply(Teams[, .SD, .SDcols = team_idx], class)

# teamID       teamIDBR teamIDlahman45    teamIDretro 
# "factor"       "factor"       "factor"       "factor"

 

[ Reference ]

* R data.table vignettes 'Using .SD for Data Analysis'

  : cran.r-project.org/web/packages/data.table/vignettes/datatable-sd-usage.html

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R dplyr 패키지의 group_by(), mutate(), filter(), select() 함수를 사용해서 단위 그룹별 관측치 개수별로 DataFrame을 구분해서 생성하는 방법을 소개(https://rfriend.tistory.com/606)하였습니다.


이번 포스팅에서는 R data.table 패키지를 사용해서 동일하게 단위 그룹별 관측치 개수별로 DataTable을 구분해서 생성하는 방법을 소개하겠습니다.


(1) R data.table을 사용해서 그룹 별 관측치 개수 세기

(2) R data.table을 사용해서 그룹 별 관측치 개수 별로 data.table 구분해서 생성하기





만약 데이터셋이 대용량이라면 R dplyr 패키지를 사용하는 것보다 R data.table 패키지를 사용하는 것이 속도 면이나 메모리 효율성 면에서 유리합니다.



먼저 data.table 패키지를 불러오고, 예제로 사용할 'id' 변수를 관측치 개수를 세는 단위 기준으로 삼는 간단한 data.table을 만들어보겠습니다.



library(data.table)


id <- c("A", "B", "C", "C", "D", "E", "E", "E")
x1 <- c(3, 2, 1, 1, 3, 3, 2, 4)
x2 <- c(60, 20, 30, 10, 70, 10, 20, 30)

DT <- data.table(id, x1, x2)
DT
# id x1 x2
# 1:  A  3 60
# 2:  B  2 20
# 3:  C  1 30
# 4:  C  1 10
# 5:  D  3 70
# 6:  E  3 10
# 7:  E  2 20
# 8:  E  4 30





  (1) R data.table을 사용해서 그룹 별 관측치 개수 세기


data.table 패키지에서 관측치 개수를 세는 것은 '.N' 매개변수를 사용하며, 그룹별 연산은 'by' 매개변수를 사용합니다. 그리고 새로운 변수를 만들어서 이름을 부여할 때는  'new_col_name := operation_method' 처럼 ':=' 로 할당을 해주면 됩니다.  


'id' 기준 그룹별로 관측치 개수를 세어서 'n' 이라는 이름으로 새로운 칼럼을 생성해보았습니다. id가 'A', 'B',  D' 는 관측치가 각 1개씩이며, 'C'는 관측치가 2개, 'E'는 관측치가 3개임을 알 수 있습니다.



## number of rows by 'id'
DT[, n := .N, by = id]
DT
# id x1 x2 n
# 1:  A  3 60 1
# 2:  B  2 20 1
# 3:  C  1 30 2
# 4:  C  1 10 2
# 5:  D  3 70 1
# 6:  E  3 10 3
# 7:  E  2 20 3
# 8:  E  4 30 3





  (2) R data.table을 사용해서 그룹 별 관측치 개수 별로 data.table 구분해서 생성하기


data.table 패키지도 dplyr 처럼 코드를 연속해서 이어서 쓸 수 있습니다. 위의 (1)번에서 구한 그룹별 관측치 개수를 조건으로 해서, 관측치 개수가 1개인 데이터셋([n == 1])과, 그룹별 관측치 개수가 2개 이상인 데이터셋(n >=2])을 조건절을 이어써줘서 선별해보겠습니다.



DT[, n:=.N, by = id][n == 1,]
# id x1 x2 n
# 1:  A  3 60 1
# 2:  B  2 20 1
# 3:  D  3 70 1

DT[, n:=.N, by = id][n >= 2,]
# id x1 x2 n
# 1:  C  1 30 2
# 2:  C  1 10 2
# 3:  E  3 10 3
# 4:  E  2 20 3
# 5:  E  4 30 3




그룹별 관측치 개수를 조건절로 사용해서 원하는 그룹별 관측치별 개수 1개, 2개 이상 관측치 데이터셋을 구분하고 나면, 이제 원래 데이터셋에 있었던 칼럼인 c("id", "x1", "x2") 만 칼럼을 선택해서 가져오고 나머지는 버리도록 하겠습니다.



DT_1 = DT[, n := .N, by = id][n == 1,][, c("id", "x1", "x2")]
DT_1
# id x1 x2
# 1:  A  3 60
# 2:  B  2 20
# 3:  D  3 70

DT_2 = DT[, n := .N, by = id][n >= 2,][,c("id", "x1", "x2")]
DT_2
# id x1 x2
# 1:  C  1 30
# 2:  C  1 10
# 3:  E  3 10
# 4:  E  2 20
# 5:  E  4 30




이전의 dplyr 보다 data.table 패키지의 코드가 처음에는 낮설어 보일 수 있기는 합니다만, 코드의 길이나 표현이 좀더 간결하고 속도/성능도 우수하므로 사용해보시길 권해드립니다.



[Reference]

* R data.table 의 DT[i, j, by]에서 행 subset, 열 select 하고 계산하기: https://rfriend.tistory.com/566

* R data.table의 by 구문으로 그룹별 집계하기: https://rfriend.tistory.com/567

* R data.table의 Reference Semantics: https://rfriend.tistory.com/573



많은 도움이 되었기를 바랍니다.
행복한 데이터 과학자 되세요!  :-)



728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 데이터 전처리 과정 중에서


(1) 기준이 되는 단위나 그룹 별로 관측치의 개수를 구해서

(2) 그룹 별 관측치 개수가 1개인 그룹의 DataFrame 과 2개 이상인 그룹의 DataFrame을 구분


해서 생성하는 방법을 소개하겠습니다.


dplyr 패키지를 이용해서 chain operator (%>%) 로 한꺼번에 코드를 짜면 편리합니다. dplyr 패키지의 group_by(), mutate(), filter(), select() 등의 여러개의 함수를 사용하였습니다.


(그룹 별 관측치 개수를 구해서 먼저 DataFrame으로 만들어놓고, 그 다음에 merge 함수를 이용해서 그룹별로 관측치 개수를 원본 DataFrame에 합쳐준 후에, 조건문으로 그룹별 관측치 개수별로 DataFrame을 구분해서 생성해주는 방식으로 step by step 진행해도 됩니다.)





먼저, dplyr 패키지를 불러오고, 간단한 예제 DataFrame을 만들어보겠습니다. 이때 칼럼 'id'가 관측치 개수를 세는 기준이 되는 그룹이 되겠습니다.



library(dplyr)

id <- c("A", "B", "C", "C", "D", "E", "E", "E")
x1 <- c(3, 2, 1, 1, 3, 3, 2, 4)
x2 <- c(60, 20, 30, 10, 70, 10, 20, 30)

df <- data.frame(id, x1 x2)
df
# id x1 x2
# 1  A  3 60
# 2  B  2 20
# 3  C  1 30
# 4  C  1 10
# 5  D  3 70
# 6  E  3 10
# 7  E  2 20
# 8  E  4 30

 




  (1) 단위 그룹별로 관측치 개수 구하기


(a) dplyr 패키지의 group_by() 함수에 관측치 개수를 세는 단위 기준이 되는 그룹변수 'id' 를 넣어주고,


(b) mutate() 함수의 n() 매개변수로 관측치 개수(number of rows)를 세어서 'n'이라는 이름의 새로운 변수를 만들어줍니다.


(c) 그리고 filter() 함수를 사용해서 위의 (b)에서 새로 만든 그룹별 관측치 개수 'n'에 대해 조건 '관측치 개수가 1개인 조건 (n == 1)' 을 만족하는 행만 걸러내는 원리입니다.



df %>%
  group_by(id) %>%
  mutate(n = n()) %>%
  filter(n == 1)


# # A tibble: 3 x 4
# # Groups:   id [3]
# id       x1    x2     n
# <chr> <dbl> <dbl> <int>
# 1 A         3    60     1
# 2 B         2    20     1
# 3 D         3    70     1






  (2) 단위 그룹별로 관측치 개수가 1개 vs. 2개 이상인 그룹 구분해서 DataFrame 만들기


우리가 원하는 최종 산출물은 단위 그룹별 관측치 개수는 필요하지 않으므로 (d) select(id, x1, x2) 함수를 사용해서 원본 데이터에 있는 변수들만 선택해서 가져오겠습니다.


그리고 그룹별 관측치 개수가 1개인 관측치만으로 이루어진 'df_1' 라는 이름의 DataFrame과, 그룹별 관측치 개수가 2개 이상인 관측치들로 이루어진 'df_2' 라는 이름의 DataFrame을 각각 구분해서 생성해보겠습니다.


dplyr 패키지의 체인 연산(%>%) 으로 한꺼번에 코드를 짤 수 있으므로 코드가 단순하고 가독성이 좋습니다.



## data.frame which has only 1 observation by x1 category
df_1 <- df %>%
  group_by(id) %>%
  mutate(n = n()) %>%
  filter(n == 1) %>%
  select(id, x1, x2)

df_1
# id       x1    x2
# <chr> <dbl> <dbl>
# 1 A         3    60
# 2 B         2    20
# 3 D         3    70


# data.frame which has more than 2 observations by x1 categories
df_2 <- df %>%
  group_by(id) %>%
  mutate(n = n()) %>%
  filter(n >= 2) %>%
  select(id, x1, x2)

df_2
# id       x1    x2
# <chr> <dbl> <dbl>
# 1 C         1    30
# 2 C         1    10
# 3 E         3    10
# 4 E         2    20
# 5 E         4    30



[ Reference ]

* R dplyr 패키지 기본함수 소개: https://rfriend.tistory.com/234

* R dplyr 패키지의 새로운 변수만들기 소개: https://rfriend.tistory.com/235

* R dplyr 패키지의 chain operator (or pipe operator) 소개: https://rfriend.tistory.com/236

* R dplyr 패키지의 그룹별 행의 개수 세기 소개: https://rfriend.tistory.com/240



다음 포스팅에서는 이번 포스팅과 동일한 결과를 얻기위해 R data.table 패키지를 사용해서 그룹별 관측치 개수별로 data.table을 구분해서 생성하는 방법(https://rfriend.tistory.com/607)을 소개하겠습니다.


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

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



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table의 melt(), dcast() 함수를 사용하여 data.table 자료를 녹여서 넓은 자료를 길게(wide to long) 재구조화 해주는 melt() 함수, 반대로 data.table 자료를 주조하여 긴 자료를 넓게 재구조화 해주는 dcast() 함수를 소개하였습니다. (https://rfriend.tistory.com/575)


이번 포스팅에서는 복수의 칼럼 이름이 특정 패턴을 따르는 여러개의 칼럼을 대상으로 

(1) melt() 함수로 data.table 자료의 칼럼 이름에서 패턴을 찾아 녹여서(melt) 넓은 자료를 길게(wide to long) 재구조화

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

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




 (1) melt() 함수로 data.table 자료의 칼럼 이름에서 패턴을 찾아 녹여서(melt)

     넓은 자료를 길게 (wide to long) 재구조화 하기


먼저 예제로 사용할 'DT' 라는 이름의 data.table 을 fread() 로 데이터를 빠르게 읽어들여서 만들어보겠습니다. 이번 포스팅의 주제에 맞게 자녀 첫째/둘째/셋째의 출생일(date of birth, dob_child1, dob_child2, dob_child3), 자녀 첫째/둘째/셋째의 성별(gender_child1, gender_child2, gender_child3) 칼럼이 특정 패턴을 가진 복수개의 칼럼으로 만들었습니다.



library(data.table)


## -- melting and casting multiple columns
s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
1         30 1998-11-26 2000-01-29         NA             1             2            NA
2         27 1996-06-22         NA         NA             2            NA            NA
3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
5         29 2000-12-05 2005-02-28         NA             2             1            NA"


DT <- fread(s2)
DT
# family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# 1:         1         30 1998-11-26 2000-01-29       <NA>             1             2            NA
# 2:         2         27 1996-06-22       <NA>       <NA>             2            NA            NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
# 5:         5         29 2000-12-05 2005-02-28       <NA>             2             1            NA

str(DT)
# Classes 'data.table' and 'data.frame':    5 obs. of  8 variables:
#   $ family_id    : int  1 2 3 4 5
# $ age_mother   : int  30 27 26 32 29
# $ dob_child1   : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
# $ dob_child2   : IDate, format: "2000-01-29" NA "2004-04-05" "2009-08-27" ...
# $ dob_child3   : IDate, format: NA NA "2007-09-02" "2012-07-21" ...
# $ gender_child1: int  1 2 2 1 2
# $ gender_child2: int  2 NA 2 1 1
# $ gender_child3: int  NA NA 1 1 NA
# - attr(*, ".internal.selfref")=<externalptr>




특정 패턴을 따르는 칼럼 이름을 가진 복수의 칼럼을 녹이는(melt) 방법에는 (a) list에 복수의 칼럼 이름을 직접 입력해주는 방법과, (b) 특정 패턴을 정규 표현식(regular expression)으로 매칭하는 2가지가 있습니다.


이때 data.table의 melt(), dcast() 함수는 전부 C 언어로 구현이 되어 있으므로 매우 빠르고 메모리 효율적으로 작동합니다.


(1-a) melt() 함수의 measure 매개변수에 복수의 칼럼 이름을 list()로 넣고, 공통의 value.name 을 지정해서 data.table 넓은 자료를 길게(wide to long) 녹이기 (melt)



## a) Enhanced melt to multiple columns simultaneously.
## The functionality is implemented entirely in C,
## and is therefore both fast and memory efficient in addition to being straightforward.
colA <- paste("dob_child", 1:3, sep="")
colB <- paste("gender_child", 1:3, sep="")


colA
# [1] "dob_child1" "dob_child2" "dob_child3"

colB
# [1] "gender_child1" "gender_child2" "gender_child3"

## We pass a list of columns to 'measure.vars', where each element
## of the list contains the columns that should be combined together.
DT.m2 <- melt(DT,
              measure = list(colA, colB),
              value.name = c("dob", "gender"))
DT.m2
# family_id age_mother variable        dob gender
# 1:         1         30        1 1998-11-26      1
# 2:         2         27        1 1996-06-22      2
# 3:         3         26        1 2002-07-11      2
# 4:         4         32        1 2004-10-10      1
# 5:         5         29        1 2000-12-05      2
# 6:         1         30        2 2000-01-29      2
# 7:         2         27        2       <NA>     NA
# 8:         3         26        2 2004-04-05      2
# 9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3       <NA>     NA
# 12:         2         27        3       <NA>     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3       <NA>     NA

str(DT.m2)
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#   $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
# $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
# $ variable  : Factor w/ 3 levels "1","2","3": 1 1 1 1 1 2 2 2 2 2 ...
# $ dob       : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
# $ gender    : int  1 2 2 1 2 2 NA 2 1 1 ...
# - attr(*, ".internal.selfref")=<externalptr>



(1-b) melt() 함수의 measure 매개변수에 특정 패턴을 정규 표현식(regular expression)으로 매칭하여 여러개의 칼럼을 녹여서 data.table 넓은 자료를 길게 녹이기 (melt)



## Using patterns()
DT.m2 <- melt(DT,
              # using regular expression
              measure = patterns("^dob", "^gender"),
              value.name = c("dob", "gender"))

DT.m2

# family_id age_mother variable        dob gender
# 1:         1         30        1 1998-11-26      1
# 2:         2         27        1 1996-06-22      2
# 3:         3         26        1 2002-07-11      2
# 4:         4         32        1 2004-10-10      1
# 5:         5         29        1 2000-12-05      2
# 6:         1         30        2 2000-01-29      2
# 7:         2         27        2       <NA>     NA
# 8:         3         26        2 2004-04-05      2
# 9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3       <NA>     NA
# 12:         2         27        3       <NA>     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3       <NA>     NA

 




(2) dcast() 함수로 data.table 자료의 여러개의 칼럼 이름을 주조(cast)하여

     긴 자료를 넓게 (long to wide) 재구조화 하기



## b) Enhanced dcast : casting multiple value.vars simultaneously
## we can provide multiple 'value.var' columns to dcast for data.tables directly
## Everything is taken care of internally, and efficiently. fast and memory efficient.
DT.c2 <- dcast(DT.m2,
               family_id + age_mother ~ variable,
               value.var = c("dob", "gender"))


DT.c2

# family_id age_mother      dob_1      dob_2      dob_3 gender_1 gender_2 gender_3
# 1:         1         30 1998-11-26 2000-01-29       <NA>        1        2       NA
# 2:         2         27 1996-06-22       <NA>       <NA>        2       NA       NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02        2        2        1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21        1        1        1
# 5:         5         29 2000-12-05 2005-02-28       <NA>        2        1       NA

 



[Reference]

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



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

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



728x90
반응형
Posted by Rfriend
,

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


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


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


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

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





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


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



library(data.table)

library(MASS)


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

print(DT)

# Model    Type DriveTrain Length Width Weight

# 1: Integra   Small      Front    177    68   2705

# 2:  Legend Midsize      Front    195    71   3560

# 3:      90 Compact      Front    180    67   3375

# 4:     100 Midsize      Front    193    70   3405

# 5:    535i Midsize       Rear    186    69   3640




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



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

DT_melt_1 <- melt(DT, 

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

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


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

print(DT_melt_1)

# Model    Type DriveTrain variable value

# 1: Integra   Small      Front   Length   177

# 2:  Legend Midsize      Front   Length   195

# 3:      90 Compact      Front   Length   180

# 4:     100 Midsize      Front   Length   193

# 5:    535i Midsize       Rear   Length   186

# 6: Integra   Small      Front    Width    68

# 7:  Legend Midsize      Front    Width    71

# 8:      90 Compact      Front    Width    67

# 9:     100 Midsize      Front    Width    70

# 10:    535i Midsize       Rear    Width    69

# 11: Integra   Small      Front   Weight  2705

# 12:  Legend Midsize      Front   Weight  3560

# 13:      90 Compact      Front   Weight  3375

# 14:     100 Midsize      Front   Weight  3405

# 15:    535i Midsize       Rear   Weight  3640


str(DT_melt_1)

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

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

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

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

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

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

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




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



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

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

DT_melt_2 <- melt(DT, 

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

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

                  variable.factor = FALSE)


str(DT_melt_2)

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

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

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

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

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

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

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




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



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

DT_melt_3 <- melt(DT, 

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

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

                  variable.name = "measure"

                  value.name = "val")


head(DT_melt_3)

# Model    Type DriveTrain measure val

# 1: Integra   Small      Front  Length 177

# 2:  Legend Midsize      Front  Length 195

# 3:      90 Compact      Front  Length 180

# 4:     100 Midsize      Front  Length 193

# 5:    535i Midsize       Rear  Length 186

# 6: Integra   Small      Front   Width  68





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


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


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



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

## reverse operation of melting

## dcast uses formula interface.

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

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560




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


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

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

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



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

dcast(DT_melt_3, Model + Type + DriveTrain ~ measure

      value.var = "val")

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560

 



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


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



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

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

# Type   Length Width Weight

# 1: Compact 180.0000    67   3375

# 2: Midsize 191.3333    70   3535

# 3:   Small 177.0000    68   2705




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


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



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

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

dcast(DT_melt_1, Type ~ variable, 

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

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

# Type Width Weight

# 1: Compact     1      1

# 2: Midsize     3      3

# 3:   Small     1      1



[Reference]

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



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



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

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



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 을 참조하여 := 연산자로 data.table의 칼럼 추가, 갱신, 삭제하는 방법을 소개하였습니다. 그리고 본문 중간에 얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해서도 소개를 하였습니다. 


다시 한번 얕은 복사와 깊은 복사의 정의를 보면요, 


얕은 사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터(위치)의 벡터만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하는 것은 아니라고 했습니다. 


반면, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적을 복사고 했습니다. 


이번 포스팅에서는 지난번 포스팅에서 한발 더 나아가서,

 

(1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용

(2) copy() 함수를 사용해 깊은 복사하기


하는 방법에 대해 알아보겠습니다. 

이번 포스팅은 R data.table vignette "Reference semantics" 를 번역하여 작성하였습니다.





  (1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용


먼저, 예제 데이터로 사용하기 위해 MASS 패키지에 내장되어 있는 Cars93 데이터프레임에서 변수 몇 개만 가져와서 DT 라는 이름의 data.table을 만들어보겠습니다.



library(data.table)
library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69




이제 := 연산자로 data.table을 참조하여 얕은 복사한 객체에 대해 새로운 칼럼을 추가하고, 이 새 칼럼에 대해 차종(Type) 그룹별 최대값(maximum)을 구하는 shallow_copy_func() 라는 이름의 사용자 정의함수를 정의하여 실행시켜 보겠습니다.



## (1) Shallow copy by reference
shallow_copy_func <- function(data_tbl){
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_2 <- shallow_copy_func(DT)



위에서 정의한 shallow_copy_func()에 DT data.table을 input으로 넣어서 실행시켜 DT_2 라는 이름의 새로운 객체를 반환하였더니, 원래의 객체(original object)인 DT에 'area'라는 이름의 새로운 칼럼이 추가되었습니다. 이것은 := 연산자가 원래의 객체인 DT를 참조하여 바라보고 있다가, 얕은 복사한 객체에 수정사항이 생겼을 경우 원래의 DT에도 이를 반영하여 갱신하기 때문입니다.


새로 얻은 DT_2 에는 차종(Type) 그룹별로 최대 면적(max_area)이 계산되어 있습니다.


만약 DT_2의 차종 그룹별 최대값 계산 결과만을 원하였고, 원래의 객체인 DT 는 아무런 변경도 원하지 않았다면 지금 := 를 사용해서 발생한 원본 data.table의 수정은 := 를 사용했을 때의 참조에 의한 얕은 복사의 부작용(side effect of reference, shalloe copy)이라고 할 수 있습니다.



## Note that the new column Area has been added to DT data.table.
## This is because := performs operations by reference. Side effect of := reference symantics.
head(DT)
# Model    Type Price Length Width  area
# 1: Integra   Small  15.9    177    68 12036
# 2:  Legend Midsize  33.9    195    71 13845
# 3:      90 Compact  29.1    180    67 12060
# 4:     100 Midsize  37.7    193    70 13510
# 5:    535i Midsize  30.0    186    69 12834
# 6: Century Midsize  15.7    189    69 13041


## DT_2 contains the maximum Price for each Type.
head(DT_2)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





  (2) copy() 함수를 사용해 깊은 복사하기


얕은 복사(shallow copy)가 효율적인 메모리 사용 측면에서는 장점이 있지만, 만약 복사한 객체에 수정한 내용이 원본 객체에는 반영되는 것을 원하지 않는다면 깊은 복사(deep copy)를 명시적으로 해줘야 합니다. R data.table 에서 깊은 복사(deep copy)를 하려면 copy() 함수를 사용하면 됩니다.


아래의 예에서는 위 (1)번에서 작성했던 함수에다가 data_tbl <- copy(data_tbl) 코드를 한 줄 더 추가함으로써 원본 data.table 객체를 깊은 복사한 하도록 하고, 깊은 복사된 새로운 객체에 대해 := 로 참조하여 새로운 칼럼을 추가하고, 차종(Type) 그룹별로 최대값을 계산하는 사용자정의함수를 정의하고 실행해 보겠습니다.


## (2) Deep copy
## When we don't want to update the original object, we use copy() function.
DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])

deep_copy_func <- function(data_tbl){
  data_tbl <- copy(data_tbl)
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_3 <- deep_copy_func(DT)




deep_copy_func() 사용자정의함수를 실행시키고 나서 원래의 객체인 DT를 다시 확인해보니, 위의 (1)번 얕은 복사 예에서와는 달리 이번 (2)번 copy()에 의한 깊은 복사된 객체의 수정사항이 원본 DT data.table에는 아무런 영향을 끼치지 못했음을 알 수 있습니다.

(즉, 이번에는 원본 DT data.table에 'area'라는 칼럼이 안생겼음)



## Using copy() function did not update DT data.table by reference.
## It doesn't contain the column 'Area'.
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69


## DT_3 contains the maximum Price for each Type.
head(DT_3)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





data.table의 칼럼 이름(names(DT))을 변수로 저장할 경우 칼럼 이름이 얕은 복사(shallow copy)되며, 만약 원본 data.table에 칼럼을 추가/ 갱신/ 삭제할 경우, 이를 참조하여 생성된 칼럼 이름을 저장한 변수도 연동하여 변경이 발생게 됩니다.


아래의 예에서는 원본 DT data.table의 칼럼 이름을 DT_names 라는 변수에 저장하여 만들었습니다.(얕은 복사가 됨).  그런 다음에 원본 DT data.table에 "z"라는 이름의 새로운 칼럼을 ':=' 연산자로 참조하여 추가하였습니다. 그랬더니 직전에 생성한 칼럼 이름을 저장해놓은 DT_names 변수에 자동으로 "z" 도 새로 추가되었습니다. DT_names 가 DT를 참조하면서 바라보고 있다가, DT에 수정이 발생하자 DT_names이 DT를 참조하여 같이 수정이 발생한 것입니다.



## When we store the column names on to a variable, e.g. DT_names = names(DT),
## and then add/ update/ delete columns(s) by referene,
## it would also modify DT_names, unless we do copy(names(DT)).
DT = data.table(x = 1L, y = 2L)
DT_names = names(DT)
print(DT_names)
# [1] "x" "y"

## add a new column by reference.
DT[, z := 3L]

## DT_names also gets updated.
print(DT_names)
# [1] "x" "y" "z"

 



만약 DT의 칼럼 이름을 저장해 놓은 DT_names 변수가 원본 객체 DT에 변경사항이 발생하더라도 영향을 받지않기를 원한다면 copy() 함수를 사용하여 명시적으로 DT 칼럼 이름(names(DT))을 깊은 복사(deep copy) 해놓으면 됩니다.



## use 'copy()' function, deep copy.
DT_names = copy(names(DT))
DT[, w := 4L]

## DT_names doesn't get updated. No "w" column in DT_names.
print(DT_names)
# [1] "x" "y" "z"

 


[Reference]

R data.table vignette, "Reference Semantics"
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html


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

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


728x90
반응형
Posted by Rfriend
,
지난 포스팅에서는 R data.table에서 mult와 nomatch 매개변수를 사용하여 Key 값의 매칭되는 모든 행, 첫번째 행, 마지막 행, 값이 존재하는 행만 가져오는 방법(https://rfriend.tistory.com/571)을 소개하였습니다.

이번 포스팅에서는 R data.table의 칼럼을 참조하여 := 연산자를 사용하여 칼럼을 추가, 갱신, 삭제하는 참조 구문(Reference semantics)에 대해서 알아보겠습니다.

(1) 참조 구문(reference semantics) 배경과 := 연산자
(2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy)
(3) 참조에 의한 칼럼 추가/ 갱신/ 삭제
(4) 그룹 by와 참조 := 연산자 함께 사용하기
(5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기

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




  (1) data.table의 참조 의미론 배경과 := 연산자


data.table을 다루기 이전에, 먼저 data.frame의 칼럼 생성/변경/삭제에 대해서 살펴보겠습니다. R 버전이 3.1 미만(R version < 3.1) 일 경우, 아래의 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경하거나(칼럼 x1을 101:106 값으로 재할당), 혹은 (b) 처럼 칼럼의 일부 값만을 새로 할당(grp 가 "a"인 행의 x1 칼럼 값을 500으로 할당) 하면, 전체 data.frame 의 깊은 복사(deep copy)가 발생합니다.


R version >= 3.1 부터는 data.frame 에서 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경 시 깊은 복사(deep copy) 대신 얕은 복사(shallow copy)로 변경하여 성능을 대폭적으로 향상시켰습니다. 하지만 (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 R version >= 3.1 에서도 여전히 전체 칼럼에 대한 깊은 복사(deep copy)가 발생합니다.


data.table 은 (a)처럼 특정 칼럼 전체를 다른 값으로 변경하거나, (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 모두 얕은 복사(shallow copy)를 함으로써 메모리 효율과 성능을 높였습니다.



## data.frame
## replacing entire column or subassigning in a column by deep coly (R versions < 3.1)
DF <- data.frame(grp = c("a", "a", "b", "b", "c", "c"),
                 x1 = 1:6,
                 x2 = 7:12)
DF
# grp x1 x2
# 1   a  1  7
# 2   a  2  8
# 3   b  3  9
# 4   b  4 10
# 5   c  5 11
# 6   c  6 12


## (a) replace entire column
DF$x1 <- 101:106
print(DF)
# grp  x1 x2
# 1   a 101  7
# 2   a 102  8
# 3   b 103  9
# 4   b 104 10
# 5   c 105 11
# 6   c 106 12

## (b) subassign in column 'x1'
DF$x1[DF$grp == "a"] <- 500
print(DF)
# grp  x1 x2
# 1   a 500  7
# 2   a 500  8
# 3   b 103  9
# 4   b 104 10
# 5   c 105 11
# 6   c 106 12
 





  (2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy)


그러면 위에서 말한 얕은 복사(shallow copy)와 깊은 복사(deep copy)가 무엇인지 알아보겠습니다.

얕은 복사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터의 벡터(the vector of column pointers)만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하지는 않습니다. 따라서 칼럼 값을 중복해서 물리적으로 복사하지 않기 때문에 메모리를 효율적으로 사용할 수 있습니다.

반면에, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적으로 복사를 합니다. 따라서 똑같은 값이 물리적으로 복사되기 때문에 메모리가 중복 사용됩니다.

해들리 위크햄은 "Advanced R book"에서 말하길, 복사는 비싼 연산작업이므로 가능한한 게으르게 얕은 복사만 해놓고 놀고 있다가, 새로운 객체에 실제 수정이 가해질 때에 가서야 마지못해 깊은 복사를 하면서 일을 한다고 설명해주고 있습니다. R이 메모리를 가장 효율적으로 사용하기 위한 방안이며, Python도 R과 마찬가지로 이런 게으른 실행 전략을 취하고 있습니다.
"R 의미론에서, 객체는 값으로 복사됩니다. 이것은 복사된 객체를 수정하더라도 원래의 값은 그대로 동일하게 남아있다는 것을 의미합니다. 메모리에 데이터를 복사하는 것은 비싼 연산작업이기 때문에, R에서 복사는 가능한 게으르게(lazy) 수행합니다. 데이터 복사는 새로운 객체가 실제로 변경이 될 때에만 발생합니다."
("In R semantics, objects are copied by value. This means that modifying the copy leaves the original object intact. Since copying data in memory is an expensive operation, copies in R are as lazy as possible. They only happen when the new object is actually modified.")
- "Advanced R book", Hadley Wickham


아래의 예에서 보면 x에 c(1, 2, 3) 의 벡터 값이 할당되어 있으며, 이 x를 y에 할당하였습니다. lobstr 패키지로 x와 y가 저장된 메모리 상의 위치(memory address)를 확인해보면 x와 y의 저장 주소가 동일함을 알 수 있습니다. 즉, y는 x를 얕게 복사(shallow copy)하여 동일한 위치를 가리키고(pointer)만 있습니다.

반면에 y <- c(y, -3)과 같이 y에 새로운 값을 추가하여 기존 벡터를 변형해 새로운 벡터를 만들면 이때서야 비로서 y 는 새로운 물리적인 메모리 주소에 할당이 되어 깊게 복사(deep copy)가 됩니다.


## shallow vs. deep copy

install.packages("lobstr")
library(lobstr)

## shallow copy
x <- c(1, 2, 3)
lobstr::obj_addr(x)
# [1] "0x10ccef808"

## The location ("memory address") of x and y in memory are the same.

## ie., "shallow copy" until now.
y <- x
lobstr::obj_addr(y)
# [1] "0x10ccef808"


## When new object is actually modified, then "deep copy" occurs.

## ie., new physicial copy in memory.
y <- c(y, -3)
print(lobstr::obj_addr(y))
# [1] "0x10d6dacf8"




아래에 x와 y를 인쇄해서 비교해보니 x는 아무런 영향을 받지 않아서 그대로 이고, y는 '-3' 값이 추가되어 새로운 벡터로 변경되었습니다. (즉, y에 깊은 복사(deep copy)가 일어났음.)



## no change in x

print(x)

# [1] 1 2 3


## only change in y

print(y)

# [1]  1  2  3 -3

 



R data.table은 참조하여 새로운 칼럼을 추가하거나 갱신하는 경우 얕은 복사(shallow copy)를 합니다.




  (3) 참조에 의한 칼럼 추가/ 갱신/ 삭제

      (Add/ update/ delete columns by refreence)


data.table의 기본 구문 DT[i, j, by]에서 칼럼 j 를 추가/ 갱신/ 삭제할 때는 특수부호 := 연산자를 사용하며, 아래처럼 왼쪽처럼 (방법 1) 'LHS (Left Hand Side) := RHS (Right Hand Side)' 구문이나, 또는 오른쪽에 있는 (방법 2) ':='(colA = valA, colB = vlaB, ...) 의 함수 형식(functional form)으로 구문을 작성합니다.

(방법 1) 로는 왼쪽(LHS)에 문자 벡터(Charater vector)로 칼럼 이름을 부여하고, 오른쪽(RHS)에는 리스트 값(list of values)을 받습니다. 오른쪽(RHS)은 lapply(), list(), mget(), mapply() 등 어떤 방식으로 생성되었든지간에 단지 리스트이기만 하면 되고 왼쪽은 문자 벡터를 칼럼 이름으로 받으므로, 특히 미리 칼럼에 무슨 값을 할당할지 모르는 경우에 프로그래밍하는데 사용하기에 편리합니다.

(방법 2)의 경우 칼럼과 값을 1:1로 나란히 짝을 지어 코딩하기 때문에 칼럼 개수가 적을 경우 사람이 읽기에 좀더 가독성이 좋고, 칼럼 = 값의 오른쪽 여백에 '#' 코멘트 기호로 부연설명을 추가해 넣기에 유용합니다.

만약, 대상 칼럼이 1개 뿐이라면 왼쪽 아래부분처럼 DT[, colA := valA] 처럼 구문을 간편하게 작성할 수도 있습니다.

## way 1. The LHS := RHS form
 DT[, c("colA", "colB", ...) := list(valA, valB, ...)]


## in case only for 1 column

DT[, colA := valA]

## way 2. The functional form
 Dt[, ':='(colA = valA, # valA is assigned to colA
          colB = valB, # valB is assigned to colB
          ...)]




  • data.table 참조하여 칼럼 추가 (Add column by Reference in data.table)
MASS 패키지에 내장되어 있는 Cars93 데이터셋으로 부터 칼럼 몇개만 선택해 가져와서 DT 라는 이름의 예제 data.table을 만들어보겠습니다. 그리고 위의 두 가지 방법을 이용하여 "Range.Price", "Area"라는 이름의 새로운 칼럼 2개를 추가해보겠습니다.

'최대 가격(Max.Price)'에서 '최소 가격(Min.Price)'의 차이를 계산해서 '가격 범위(Range.Price)' 칼럼을 새로 생성하여 추가하고, '길이(Length)'와 '폭(Width)'을 곱하여 '넓이(Area)' 칼럼을 새로 생성하여 추가하시오.


## a) Add columns by reference
library(data.table)
library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Min.Price", "Max.Price", "Length", "Width")])
head(DT)
# Model    Type Min.Price Max.Price Length Width
# 1: Integra   Small      12.9      18.8    177    68
# 2:  Legend Midsize      29.2      38.7    195    71
# 3:      90 Compact      25.9      32.3    180    67
# 4:     100 Midsize      30.8      44.6    193    70
# 5:    535i Midsize      23.7      36.2    186    69
# 6: Century Midsize      14.2      17.3    189    69



## way 1: LHS := RHS form
DT[, c("Range.Price", "Area") := list((Max.Price - Min.Price), Length * Width)]


## (equivalently) way 2: The functional form
DT[, ':='(Range.Price = (Max.Price - Min.Price),
           Area = Length * Width)]


head(DT)
# Model    Type Min.Price Max.Price Length Width Range.Price  Area
# 1: Integra   Small      12.9      18.8    177    68         5.9 12036
# 2:  Legend Midsize      29.2      38.7    195    71         9.5 13845
# 3:      90 Compact      25.9      32.3    180    67         6.4 12060
# 4:     100 Midsize      30.8      44.6    193    70        13.8 13510
# 5:    535i Midsize      23.7      36.2    186    69        12.5 12834
# 6: Century Midsize      14.2      17.3    189    69         3.1 13041



  • data.table 참조하여 칼럼 갱신 (Update column(s) by Reference in data.table)
이번에는 DT[i, j, by] 구문에서 i 로 조건을 만족하는 일부 행을 선별하고, colA := valB 구문으로 특정 칼럼의 값을 갱신(update) 하여 보겠습니다.

차종(Type)이 'Large'인 행의 차종의 값을 'Big'으로 변경하시오.


## b) Update some rows of columns by reference - sub-assign by reference
DT[, .N, by = Type]
# Type  N
# 1:   Small 21
# 2: Midsize 22
# 3: Compact 16
# 4:   Large 11   <---
# 5:  Sporty 14
# 6:     Van  9


## replace those rows where Type == 'Large' with the value 'Big'
DT[Type == 'Large', Type := 'Big']

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

 



위처럼 DT[Type == 'Large', Type := 'Big'] 을 실행시키면 칼럼 값 갱신이 눈에 보이지 않는(invisibly) 상태로 실행됩니다. 만약 갱신 결과를 눈에 보이도록 출력하려면 제일 뒤에 [] 를 붙여주면 됩니다.



## We can see the result by adding an empty [] at the end of the query
DT[Type == 'Large', Type := 'Big'][]
# Model    Type Min.Price Max.Price Length Width Range.Price  Area
# 1:        Integra   Small      12.9      18.8    177    68         5.9 12036
# 2:         Legend Midsize      29.2      38.7    195    71         9.5 13845
# 3:             90 Compact      25.9      32.3    180    67         6.4 12060
# 4:            100 Midsize      30.8      44.6    193    70        13.8 13510
# 5:           535i Midsize      23.7      36.2    186    69        12.5 12834
# ---
# 89:        Eurovan     Van      16.6      22.7    187    72         6.1 13464
# 90:         Passat Compact      17.6      22.4    180    67         4.8 12060
# 91:        Corrado  Sporty      22.9      23.7    159    66         0.8 10494
# 92:            240 Compact      21.8      23.5    190    67         1.7 12730
# 93:            850 Midsize      24.8      28.5    184    69         3.7 12696
 



  • data.table 참조하여 칼럼 삭제 (Delete column(s) by Reference in data.table)
data.table에서 칼럼을 삭제하려면 colA := NULL 처럼 NULL 값을 할당해주면 됩니다. rm() 이나 del() 같은 함수가 아니라 '없음(NULL)'을 할당하는 방식이라 좀 생소하기는 합니다. ^^;

삭제하려는 칼럼이 여러개라면 c("colA", "colB", ...) 처럼 c() 로 칼럼 문자 벡터를 묶어서 써줍니다.

DT data.table에서 "Range.Price"와 "Area" 칼럼을 삭제하시오.


## c) Delete column by reference
## Assigning NULL to a column deletes that column. And it happens instantly.
DT[, c("Range.Price", "Area") := NULL]
head(DT, 3)
# Model    Type Min.Price Max.Price Length Width
# 1: Integra   Small      12.9      18.8    177    68
# 2:  Legend Midsize      29.2      38.7    195    71
# 3:      90 Compact      25.9      32.3    180    67




만약 삭제하려는 칼럼이 단 1개라면 c() 나 문자형 칼럼 이름을 쓸 필요없이, 그냥 colA := NULL 처럼 바로 써주면 됩니다.


## When there is just one column to delete, we can drop the c() and bouble quotes
DT[, Width := NULL]
head(DT, 3)
# Model    Type Min.Price Max.Price Length
# 1: Integra   Small      12.9      18.8    177
# 2:  Legend Midsize      29.2      38.7    195
# 3:      90 Compact      25.9      32.3    180

 





  (4) 그룹 by와 참조 := 연산자 함께 사용하기


data.table의 기본구문인 DT[i, j, by] 에서 그룹 by 별로 연산을 한 결과를 참조하여 DT[, colA := calculation(colB), by = Group] 형식으로 새로운 칼럼을 추가할 수 있습니다.

data.table DT에서 차종(Type) 그룹별로 길이(Length)의 최대값을 구하여, 각 행의 차종(Type)에 맞게 "Max.Length"라는 이름의 칼럼에 값을 추가하시오.


## d) := along with grouping using 'by'
DT[, Max.Length := max(Length), by = Type]

## or alternatively, providing with a character vector
DT[, Max.Length := max(Length), by = c("Type")]

head(DT)
# Model    Type Min.Price Max.Price Length Max.Length
# 1: Integra   Small      12.9      18.8    177        177
# 2:  Legend Midsize      29.2      38.7    195        205 <--
# 3:      90 Compact      25.9      32.3    180        190
# 4:     100 Midsize      30.8      44.6    193        205 <--
# 5:    535i Midsize      23.7      36.2    186        205 <--
# 6: Century Midsize      14.2      17.3    189        205 <--

## check the max value of Length by Type
DT[, max(Length), by = Type]
# Type  V1
# 1:   Small 177
# 2: Midsize 205  <--
# 3: Compact 190
# 4:     Big 219
# 5:  Sporty 196
# 6:     Van 194

 





  (5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기


data.table을 참조하여 여러개의 칼럼을 참조하여 여러개의 새로운 칼럼을 한꺼번에 추가하거나 갱신, 삭제하고 싶으면 (a) .SDcols = c("colA", "colB", ...) 에서 지정한 복수의 칼럼에 대하여, (b) lapply(.SD, function) 으로 (a)에서 지정한 모든 칼럼(.SD)에 함수를 적용하여, (c) := 연산자로 (b)의 결과를 참조하여 새로운 칼럼을 추가합니다.


참고로, R data.table에서 여러개의 변수를 처리하는 특수부호 .SD, .SDcols 에 대한 자세한 소개는 https://rfriend.tistory.com/568 를 참조하세요.


DT data.table에서 차종(Type) 그룹별로 '가격(Price)'과 '길이(Length)' 변수의 평균을 구하여 '평균 가격(mean_price)', '평균 길이(mean_length)' 칼럼을 추가하시오.



## e) Multiple columns and :=

## new data.table as an sample
DT2 <- data.table(Cars93[, c("Model", "Type", "Price", "Length")])
head(DT2)
# Model    Type Price Length
# 1: Integra   Small  15.9    177
# 2:  Legend Midsize  33.9    195
# 3:      90 Compact  29.1    180
# 4:     100 Midsize  37.7    193
# 5:    535i Midsize  30.0    186
# 6: Century Midsize  15.7    189

## LHS := RHS form
in_cols = c("Price", "Length")
out_cols = c("mean_price", "mean_length")
DT2[, c(out_cols) := lapply(.SD, mean), # lapply() for multiple columns
    by = Type,         # by Type groups
    .SDcols = in_cols] # compute the mean on columns specified in .SDcols

head(DT2)
# Model    Type Price Length mean_price mean_length
# 1: Integra   Small  15.9    177   10.16667    167.1905
# 2:  Legend Midsize  33.9    195   27.21818    192.5455
# 3:      90 Compact  29.1    180   18.21250    182.1250
# 4:     100 Midsize  37.7    193   27.21818    192.5455
# 5:    535i Midsize  30.0    186   27.21818    192.5455
# 6: Century Midsize  15.7    189   27.21818    192.5455

 



[ Reference ]
* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html


다음번 포스팅에서는 R data.table에서 copy() 함수를 사용해서 깊은 복사(deep copy)를 하는 방법을 소개하겠습니다.


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

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



728x90
반응형
Posted by Rfriend
,

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


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


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

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

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

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





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


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



library(data.table)

library(MASS)


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


> str(DT)

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

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

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

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

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

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

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

 



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



> # Set the key with Type and DriveTrain columns

> setkey(DT, Type, DriveTrain)

> key(DT)

[1] "Type"       "DriveTrain"

 



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


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


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



# Subset 'all' rows where  

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

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


# or equivalently

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


      Model    Type Price DriveTrain

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

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


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

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


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

6: Corvette  Sporty  38.0       Rear

7:  Mustang  Sporty  15.9       Rear

8:     RX-7  Sporty  32.5       Rear

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





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


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



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

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

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

    Model    Type Price DriveTrain

1:   190E Compact  31.9       Rear

2:   <NA>     Van    NA       Rear

3: Camaro  Sporty  15.1       Rear






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


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



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

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

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

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2:     <NA>     Van    NA       Rear

3: Firebird  Sporty  17.7       Rear

 





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

      : mult = "last", nomatch = NULL


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



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

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

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

+    mult = "last"

+    nomatch = NULL]

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2: Firebird  Sporty  17.7       Rear

 



[Reference]

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


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

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



728x90
반응형
Posted by Rfriend
,