행(row) 단위로 그룹을 지을 수 있는 자료가 데이터 프레임으로 주어졌다고 하자. 각 그룹 안에서 특정 컬럼의 최대 혹은 최솟값을 찾아내는 방법은 잘 알려져 있다. 예전에 작성한 [하루에 한 R] BLAST tabular output에서 best hit 뽑아내기에서는 base R을 이용한 원초적인 방법을 소개한 적이 있다. 학습 목적을 위하여 좀 더 단순한 데이터 프레임을 만들어 놓자.
> data = data.frame( letter=sample(LETTERS,1000,replace=TRUE), serial=1:1000, number=sample(1:20,1000,replace=TRUE) )
A 그룹에 속하는 raw만 보고 싶다면 data[which(data$group=='A'),]라고 입력하는 것이 정석이다. which() 함수를 쓰지 않고 data[data$group=='A',]라고 쳐도 결과는 같지만 전자의 방법이 더욱 바람직하다고 생각한다.
각 그룹에 대하여 numer의 최댓값은 무엇일까? base R을 쓰려면 다음과 같이 입력하면 된다. 명령어가 끝나는 곳에서 닫아야 할 괄호가 많으니 빼먹지 말도록 하자.
> do.call(rbind,lapply(split(data,data$letter),function(x){return(x[which.max(x$number),])})) letter serial number A A 77 20 B B 251 20 C C 473 20 ... X X 861 20 Y Y 371 20 Z Z 264 20
반환되는 serial column을 이용하여 max값을 갖는 row의 정보를 알 수 있다. 하지만 완벽하지는 않다. 왜냐하면 공동 1위에 대한 배려가 없기 때문이다. 각 그룹에 대하여 최댓값을 갖는 row가 여럿인 경우, 어느 하나만을 반환한다.
aggregate() 함수를 쓰면 각 그룹에 대한 number 컬럼의 최댓값을 출력할 수 있다.
> aggregate(data$number,by=list(data$letter),max) Group.1 x 1 A 20 2 B 20 .. 25 Y 20 26 Z 19 > aggregate(number ~ letter,data,max) letter number 1 A 20 2 B 20 .. 25 Y 20 26 Z 19
Hardley Wickham이 만든 dplyr 패키지(공식 문서)를 이용하면 좀 더 현명한 데이터 탐색이 가능하다. 다음의 웹사이트에 훌륭한 예제가 많다.
R: dplyr - Maximum value row in each group
Dplyr Introduction
[R] 데이터 처리의 새로운 강자, dplyr 패키지 - 강력 추천!
공구 그림을 내세운 것을 보니 '디(D)플라이어'라고 발음하는 것이 맞을 것이다. |
그러면 최초의 질문으로 돌아오자. 각 그룹별로 최대의 number 값을 갖는 row를 찾아내자. serial 컬럼을 같이 출력하게 되면 문제가 해결된다. 웹을 뒤지다가 정말 단순하고 강력한 방법을 찾아내어 여기에 소개하고자 한다. 공동 1위가 나타나서 한 그룹에 대해 복수의 row를 출력하게 되어도 상관이 없다. 위에서 do.call() 함수를 쓴 것과 serial의 값이 다르게 나온 것은 data 데이터 프레임을 중간에 새로 만들었기 때문이다. merge() 함수를 이런 곳에서 사용하다니 정말 창의적이다.
Basic R: rows that contain the maximum value of a variable
> data.agg = aggregate(number ~ letter,data,max)
> data.max = merge(data.agg,data) > data.max letter number serial 1 A 20 617 2 A 20 33 3 B 20 807 4 B 20 219 5 C 19 759 6 D 20 546 ... 51 Z 19 539 52 Z 19 448 53 Z 19 542 54 Z 19 95
최종적으로 얻어지는 컬럼의 순서는 별로 마음에 들지는 않는다. dplyr 패키지를 사용하려면 다음과 같이 하라.
> library(dplyr) > data_df = tbl_df(data) > data_df %>% group_by(letter) %>% filter(number==max(number)) %>% arrange(letter) # A tibble: 52 x 3 # Groups: letter [26] letter serial number <fct> <int> <int> 1 A 38 18 2 A 364 18 3 B 135 20 4 C 33 20 5 C 650 20 6 C 661 20 7 D 471 20 8 E 647 20 9 F 292 20 10 F 537 20 # ... with 42 more rows
dplyr은 데이터 프레임을 다루기에 매우 유용한 패키지이다. 기초 수준의 사용법을 익혀서 나중에 별도로 글을 써야 되겠다. 실습을 위한 장난감용 데이터와 코드 몇 줄을 만들어 보았다.
> library(dplyr) > data = data.frame( letter=sample(LETTERS,1000,replace=TRUE), serial=1:1000, num1=sample(1:20,1000,replace=TRUE), num2=round(rnorm(1000,10,2),digit=0), num3=sample(0:50,1000,replace=TRUE) ) > data_df = tbl_df(data) > cols = c("num1","num2","num3") > data_df %>% group_by(letter) %>% summarise_each(funs(mean,sd),cols) # A tibble: 26 x 7 letter num1_mean num2_mean num3_mean num1_sd num2_sd num3_sd <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 A 10.7 10.2 27.2 6.37 1.72 14.8 2 B 10.7 9.93 24.3 5.33 1.91 15.0 3 C 11.9 10 21.8 5.86 1.90 14.0 4 D 10.2 10.3 24.8 5.82 1.80 16.0 5 E 9.78 10.8 27.9 5.38 1.77 16.3 6 F 10.5 9.97 25.9 5.67 1.97 14.8 7 G 10.3 9.76 24.2 5.82 2.20 15.1 8 H 11.5 10.3 23.2 5.75 2.09 16.0 9 I 10.7 10.4 24.9 6.53 2.17 14.5 10 J 10.6 10.0 27.7 5.67 2.15 15.2 # … with 16 more rows
dplyr과 사랑에 빠진 어느 블로거의 글을 소개한다. dplyr을 쓰는 것에 익숙해지면, apply() 계열의 함수나 aggregate() 함수를 쓸 일이 없어질 것 같다.
[R-bloggers] I fell out with tapply and in love with dplyr
댓글 1개:
댓글 쓰기