2019년 10월 15일 화요일

[하루에 한 R] 그룹 내의 최댓값/최솟값 구하기, 그리고 그 값을 갖는 행을 찾아내기 - dplyr 패키지

최대값·최소값이 아니라 최댓값·최솟값이 올바른 표기라 한다. 뒤에 오는 말의 첫소리가 된소리이면 사이시옷을 넣어야 한다는 것이다(링크). '북엇국'이라고 쓰기는 정말 어색한데...

행(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개:

Anneli Jäätteenmäki :
블로그 관리자가 댓글을 삭제했습니다.