plyr package

published on Dec 25, 2012

keywords : R, apply, split, data analysis

1. 소개

우리는 Data를 Split, Apply, Combine 할 때 R에서 우리는 익히 알고 있던 'apply' family(apply, sapply, lapply 등)를 사용한다. apply를 데이터 분석에서 흔한 행동과 실수들은 무엇일까요? 흔한 데이터 분석에서 나타나는 문제의 아주 작은 부분을 해결하고자 한다.

2. 사용법

아래의 표 2.는 plyr package의 핵심이 되는 12가지 function들이다. 각각의 function은 받아들이는 input의 유형과 output의 유형에 따라 이름지어졌다. a=array, d=data frame, l=list, _는 output이 버려진 것이다. 하나씩 살펴보기보다는 효율적으로 Input의 세가지 유형과 Output의 네가지 유형을 배워볼 것이다. 이러한 이유로 Input의 function은 d*ply, Output의 function은 *dply와 같이 표현하겠다.

* 2.1에서는 input type이 큰 데이터 구조를 작은 조각으로 나누는 방법을 결정하는 것에 대해 살펴본다.
* 2.2에서는 output type이 조각들이 다시 합치는 방법을 결정하는 것에 대해 살펴본다.

표 2: plyr package의 12가지 핵심 function. Arrays는 matrices와 vector를 포함.

Input\Output Array Data frame List Discarded
Array aaply adply alply a_ply
Data frame daply ddply dlply d_ply
List laply ldply llply l_ply

2.1. Input

Input: Array (a*ply)

Figure 1
그림 1: 2차원 matrix를 나누는 3가지 방법이다. 기존 matrix는 좌측 상단. 각 분할 방법에 따른 한개의 조각은 파란색으로 표시되어 있다.

Figure 2
그림 2: 3차원 array를 나누는 7가지 방법이다. 기존 array는 좌측 상단. 각 분할 방법에 따른 한개의 조각은 파란색으로 표시되어 있다.

Input: Data frme (d*ply)

또는, 분할하기 위해 아래와 같은 방법을 사용할 수 있다.

Figure 3
그림 3: variables에 의해 data frame을 나누는 두가지 예제이다.

아래의 코드 및 결과는 그림 3에 대한 것이다.

library(plyr)

name <- c("John", "Mary", "Alice", "Peter", "Roger", "Phyllis")
age <- c(13, 15, 14, 13, 14, 13)
sex <- c("Male", "Female", "Female", "Male", "Male", "Female")
ex_d <- data.frame(name, age, sex)
ex_d
##      name age    sex
## 1    John  13   Male
## 2    Mary  15 Female
## 3   Alice  14 Female
## 4   Peter  13   Male
## 5   Roger  14   Male
## 6 Phyllis  13 Female

ddply(ex_d, .(sex))  # sex로 split-up
##      name age    sex
## 1    Mary  15 Female
## 2   Alice  14 Female
## 3 Phyllis  13 Female
## 4    John  13   Male
## 5   Peter  13   Male
## 6   Roger  14   Male
ddply(ex_d, .(age))  # age로 split-up
##      name age    sex
## 1    John  13   Male
## 2   Peter  13   Male
## 3 Phyllis  13 Female
## 4   Alice  14 Female
## 5   Roger  14   Male
## 6    Mary  15 Female
ddply(ex_d, ~sex + age)  # sex로 split-up 한 후에 sex의 subset 안에서 다시 age로 split-up
##      name age    sex
## 1 Phyllis  13 Female
## 2   Alice  14 Female
## 3    Mary  15 Female
## 4    John  13   Male
## 5   Peter  13   Male
## 6   Roger  14   Male
# 같은 방법으로 .variables에 .(sex, age)라고 해도 같은 결과가 나옴.
ddply(ex_d, ~age + sex)  # age로 split-up 한 후에 age의 subset 안에서 다시 age로 split-up
##      name age    sex
## 1 Phyllis  13 Female
## 2    John  13   Male
## 3   Peter  13   Male
## 4   Alice  14 Female
## 5   Roger  14   Male
## 6    Mary  15 Female
# 같은 방법으로 .variables에 .(age, sex)라고 해도 같은 결과가 나옴.

Input: List (l*ply)

List는 이미 조각(List의 elements)으로 나누어져 있으므로 가장 다루기 쉬운 input type이다. 이러한 이유로 l*ply function은 data structure를 분해하는데 argument가 필요없게 된다. l*ply는 1d array의 a*ply와 동일하다.

2.2. Output

Output: Array (*aply)

input의 분할과 각 결과의 차원수에 의해 array output의 모양이 결정된다.

Figure 4
그림 4: Columns는 input: (left) 길이가 2인 vector (right) 3X2 matrix. Rows는 하나의 처리된 조각의 모양: (top) 길이가 3인 vector (bottom) 2X2 matrix. 우측 하단의 3가지 cube들은 Column(input)과 Row(shape of processed piece)에 의해 나온 결과이다. 나머지 1가지는 4차원이라 표시할 수 없다.

Figure 5
그림 5: 1차원 vector. Columns는 input: (left) 2X3 matrix를 행 분할 (right) 3X2 matrix를 열 분할. Rows는 하나의 처리된 조각의 모양: (top) 단일값 (middle) 길이가 3인 vector (bottom) 2X2 matrix

1차원과 2차원의 경우, 아래의 코드를 보고 생각해보자.

x <- array(1:24, 2:4)
shape <- function(x) if (is.vector(x)) length(x) else dim(x)
shape(x)
## [1] 2 3 4
shape(aaply(x, 2, function(y) 0))
## [1] 3
shape(aaply(x, 2, function(y) rep(1, 5)))
## [1] 3 5
shape(aaply(x, 2, function(y) matrix(0, nrow = 5, ncol = 6)))
## [1] 3 5 6
shape(aaply(x, 1, function(y) matrix(0, nrow = 5, ncol = 6)))
## [1] 2 5 6

Output: Data frame (*dply)

Figure 6
그림 6: 그림 3의 예제에서 ddply()를 사용해서 나온 결과이다.

아래의 코드 및 결과는 그림 6에 대한 것이다.

ex_sex <- ddply(ex_d, .(sex))  # sex로 split-up
ex_age <- ddply(ex_d, .(age))  # age로 split-up
ex_sex_age <- ddply(ex_d, .(sex, age))  # sex로 split-up후에 sex의 subset에서 age로 split-up

ddply(ex_sex, .(sex), "nrow")  # sex별 row개수
##      sex nrow
## 1 Female    3
## 2   Male    3
ddply(ex_age, .(age), "nrow")  # age별 row개수
##   age nrow
## 1  13    3
## 2  14    2
## 3  15    1
ddply(ex_sex_age, .(sex, age), "nrow")  # sex, age별 row개수
##      sex age nrow
## 1 Female  13    1
## 2 Female  14    1
## 3 Female  15    1
## 4   Male  13    2
## 5   Male  14    1

Output: List (*lply)

가장 간단한 output format이다. 각각의 처리된 조각들이 list로 합쳐지기 때문이다.

4. 전략

5.1. Case Study: Baseball

이 Data frame은 http://www.baseball-databank.org/에서 모은 일부 선수의 타격에 관한 통계치이다. 1871~2007년까지 총 21,699 records, 1,228명의 선수들에 관한 내용이다.

더 자세한 정보는 ?baseball.

먼저 career year를 계산하자. 예를 들면 1년차, 2년차, …, 10년차와 같이 표시하기 위함이다. 1명의 선수에 대해서 이것을 계산하는 것은 어려운 일이 아니다. 아래의 코드는 1명의 선수의 career year를 계산한 것이다.

baberuth <- subset(baseball, id == "ruthba01")
baberuth <- transform(baberuth, cyear = year - min(year) + 1)
baberuth
##             id year stint team lg   g  ab   r   h X2b X3b hr rbi sb cs  bb
## 14646 ruthba01 1914     1  BOS AL   5  10   1   2   1   0  0   2  0 NA   0
## 15457 ruthba01 1915     1  BOS AL  42  92  16  29  10   1  4  21  0 NA   9
## 16238 ruthba01 1916     1  BOS AL  67 136  18  37   5   3  3  15  0 NA  10
## 16776 ruthba01 1917     1  BOS AL  52 123  14  40   6   3  2  12  0 NA  12
## 17286 ruthba01 1918     1  BOS AL  95 317  50  95  26  11 11  66  6 NA  58
## 17790 ruthba01 1919     1  BOS AL 130 432 103 139  34  12 29 114  7 NA 101
## 18329 ruthba01 1920     1  NYA AL 142 457 158 172  36   9 54 137 14 14 150
## 18834 ruthba01 1921     1  NYA AL 152 540 177 204  44  16 59 171 17 13 145
## 19363 ruthba01 1922     1  NYA AL 110 406  94 128  24   8 35  99  2  5  84
## 19883 ruthba01 1923     1  NYA AL 152 522 151 205  45  13 41 131 17 21 170
## 20420 ruthba01 1924     1  NYA AL 153 529 143 200  39   7 46 121  9 13 142
## 20967 ruthba01 1925     1  NYA AL  98 359  61 104  12   2 25  66  2  4  59
## 21507 ruthba01 1926     1  NYA AL 152 495 139 184  30   5 47 150 11  9 144
## 22038 ruthba01 1927     1  NYA AL 151 540 158 192  29   8 60 164  7  6 137
## 22572 ruthba01 1928     1  NYA AL 154 536 163 173  29   8 54 142  4  5 137
## 23110 ruthba01 1929     1  NYA AL 135 499 121 172  26   6 46 154  5  3  72
## 23656 ruthba01 1930     1  NYA AL 145 518 150 186  28   9 49 153 10 10 136
## 24167 ruthba01 1931     1  NYA AL 145 534 149 199  31   3 46 163  5  4 128
## 24694 ruthba01 1932     1  NYA AL 133 457 120 156  13   5 41 137  2  2 130
## 25199 ruthba01 1933     1  NYA AL 137 459  97 138  21   3 34 103  4  5 114
## 25702 ruthba01 1934     1  NYA AL 125 365  78 105  17   4 22  84  1  3 104
## 26477 ruthba01 1935     1  BSN NL  28  72  13  13   0   0  6  12  0 NA  20
##       so ibb hbp sh sf gidp cyear
## 14646  4  NA   0  0 NA   NA     1
## 15457 23  NA   0  2 NA   NA     2
## 16238 23  NA   0  4 NA   NA     3
## 16776 18  NA   0  7 NA   NA     4
## 17286 58  NA   2  3 NA   NA     5
## 17790 58  NA   6  3 NA   NA     6
## 18329 80  NA   3  5 NA   NA     7
## 18834 81  NA   4  4 NA   NA     8
## 19363 80  NA   1  4 NA   NA     9
## 19883 93  NA   4  3 NA   NA    10
## 20420 81  NA   4  6 NA   NA    11
## 20967 68  NA   2  6 NA   NA    12
## 21507 76  NA   3 10 NA   NA    13
## 22038 89  NA   0 14 NA   NA    14
## 22572 87  NA   3  8 NA   NA    15
## 23110 60  NA   3 13 NA   NA    16
## 23656 61  NA   1 21 NA   NA    17
## 24167 51  NA   1  0 NA   NA    18
## 24694 62  NA   2  0 NA   NA    19
## 25199 90  NA   2  0 NA   NA    20
## 25702 63  NA   2  0 NA   NA    21
## 26477 24  NA   0  0 NA    2    22

이 일을 여러 선수에 대해서 하기 위해서 우리는 function을 만들거나 for문을 사용할 필요가 없다. 왜냐하면 transform()이 각 조각에 적용되기 때문이다. 즉, .fun에 transform을 적용시킨다.

baseball <- subset(baseball, ab >= 25)
baseball <- ddply(baseball, .(id), transform, cyear = year - min(year) + 1)
head(baseball, 30)
##           id year stint team lg   g  ab   r   h X2b X3b hr rbi sb cs bb so
## 1  aaronha01 1954     1  ML1 NL 122 468  58 131  27   6 13  69  2  2 28 39
## 2  aaronha01 1955     1  ML1 NL 153 602 105 189  37   9 27 106  3  1 49 61
## 3  aaronha01 1956     1  ML1 NL 153 609 106 200  34  14 26  92  2  4 37 54
## 4  aaronha01 1957     1  ML1 NL 151 615 118 198  27   6 44 132  1  1 57 58
## 5  aaronha01 1958     1  ML1 NL 153 601 109 196  34   4 30  95  4  1 59 49
## 6  aaronha01 1959     1  ML1 NL 154 629 116 223  46   7 39 123  8  0 51 54
## 7  aaronha01 1960     1  ML1 NL 153 590 102 172  20  11 40 126 16  7 60 63
## 8  aaronha01 1961     1  ML1 NL 155 603 115 197  39  10 34 120 21  9 56 64
## 9  aaronha01 1962     1  ML1 NL 156 592 127 191  28   6 45 128 15  7 66 73
## 10 aaronha01 1963     1  ML1 NL 161 631 121 201  29   4 44 130 31  5 78 94
## 11 aaronha01 1964     1  ML1 NL 145 570 103 187  30   2 24  95 22  4 62 46
## 12 aaronha01 1965     1  ML1 NL 150 570 109 181  40   1 32  89 24  4 60 81
## 13 aaronha01 1966     1  ATL NL 158 603 117 168  23   1 44 127 21  3 76 96
## 14 aaronha01 1967     1  ATL NL 155 600 113 184  37   3 39 109 17  6 63 97
## 15 aaronha01 1968     1  ATL NL 160 606  84 174  33   4 29  86 28  5 64 62
## 16 aaronha01 1969     1  ATL NL 147 547 100 164  30   3 44  97  9 10 87 47
## 17 aaronha01 1970     1  ATL NL 150 516 103 154  26   1 38 118  9  0 74 63
## 18 aaronha01 1971     1  ATL NL 139 495  95 162  22   3 47 118  1  1 71 58
## 19 aaronha01 1972     1  ATL NL 129 449  75 119  10   0 34  77  4  0 92 55
## 20 aaronha01 1973     1  ATL NL 120 392  84 118  12   1 40  96  1  1 68 51
## 21 aaronha01 1974     1  ATL NL 112 340  47  91  16   0 20  69  1  0 39 29
## 22 aaronha01 1975     1  ML4 AL 137 465  45 109  16   2 12  60  0  1 70 51
## 23 aaronha01 1976     1  ML4 AL  85 271  22  62   8   0 10  35  0  1 35 38
## 24 abernte02 1955     1  WS1 AL  40  26   1   4   0   0  0   0  0  0  0  6
## 25 adairje01 1959     1  BAL AL  12  35   3  11   0   1  0   2  0  0  1  5
## 26 adairje01 1961     1  BAL AL 133 386  41 102  21   1  9  37  5  2 35 51
## 27 adairje01 1962     1  BAL AL 139 538  67 153  29   4 11  48  7  7 27 77
## 28 adairje01 1963     1  BAL AL 109 382  34  87  21   3  6  30  3  3  9 51
## 29 adairje01 1964     1  BAL AL 155 569  56 141  20   3  9  47  3  2 28 72
## 30 adairje01 1965     1  BAL AL 157 582  51 151  26   3  7  66  6  4 35 65
##    ibb hbp sh sf gidp cyear
## 1   NA   3  6  4   13     1
## 2    5   3  7  4   20     2
## 3    6   2  5  7   21     3
## 4   15   0  0  3   13     4
## 5   16   1  0  3   21     5
## 6   17   4  0  9   19     6
## 7   13   2  0 12    8     7
## 8   20   2  1  9   16     8
## 9   14   3  0  6   14     9
## 10  18   0  0  5   11    10
## 11   9   0  0  2   22    11
## 12  10   1  0  8   15    12
## 13  15   1  0  8   14    13
## 14  19   0  0  6   11    14
## 15  23   1  0  5   21    15
## 16  19   2  0  3   14    16
## 17  15   2  0  6   13    17
## 18  21   2  0  5    9    18
## 19  15   1  0  2   17    19
## 20  13   1  0  4    7    20
## 21   6   0  1  2    6    21
## 22   3   1  1  6   15    22
## 23   1   0  0  2    8    23
## 24   0   0  4  0    1     1
## 25   0   0  0  0    0     1
## 26   4   2  1  4    6     3
## 27   1   2  4  3   15     4
## 28   2   2  3  5   17     5
## 29  10   1  4  3   20     6
## 30   7   2  4  2   26     7

모든 선수들의 패턴을 요약하기 위해, 우선 Babe Ruth에 대해 rbi/ab(runs per bat)의 time series plot을 그려본다.

library(ggplot2)
ggplot(baberuth, aes(x = cyear, y = rbi/ab)) + geom_line()

plot of chunk unnamed-chunk-6

아래는 Babe Ruth의 회귀분석 결과 적합된 모형이다.

model <- function(df) {
    lm(rbi/ab ~ cyear, data = df)
}
model(baberuth)
## 
## Call:
## lm(formula = rbi/ab ~ cyear, data = df)
## 
## Coefficients:
## (Intercept)        cyear  
##     0.20320      0.00341

이제 모든 선수에 대해 적용해서 list 형태로 만든다.

bmodels <- dlply(baseball, .(id), model)

그 결과 1152개 모형의 list를 갖게 되었다.그리고 몇가지 요약 통계량을 구할 필요가 있다. 모형의 회귀계수(slope, intercept)와 모형 적합 검정을 위한 결정계수를 가져올 것이다.

rsq <- function(x) summary(x)$r.squared
bcoefs <- ldply(bmodels, function(x) c(coef(x), rsquare = rsq(x)))
names(bcoefs)[2:3] <- c("intercept", "slope")

아래의 Plot은 결정계수에 대한 분포를 보여준다. 전체적으로 결정계수가 낮은 쪽의 모형이 많으므로 모형의 적합이 좋지 않다.

ggplot(bcoefs, aes(rsquare)) + geom_bar(binwidth = 0.05)

plot of chunk unnamed-chunk-10

결정계수가 1에 가깝게 적합된 모형의 선수 이름을 알아보자. 36명의 선수 이름이 나온다.

baseballcoef <- merge(baseball, bcoefs, by = "id")
subset(baseballcoef, rsquare > 0.99)$id
##  [1] "bannifl01" "bannifl01" "bedrost01" "bedrost01" "burbada01"
##  [6] "burbada01" "carrocl02" "carrocl02" "cookde01"  "cookde01" 
## [11] "davisma01" "davisma01" "jacksgr01" "jacksgr01" "lindbpa01"
## [16] "lindbpa01" "oliveda02" "oliveda02" "penaal01"  "penaal01" 
## [21] "powerte01" "powerte01" "splitpa01" "splitpa01" "violafr01"
## [26] "violafr01" "wakefti01" "wakefti01" "weathda01" "weathda01"
## [31] "woodwi01"  "woodwi01"  "worthal01" "worthal01" "worthal01"
## [36] "worthal01"

Figure 7
그림 7: intercept와 slope의 산점도. 점의 크기는 모형의 결정계수에 비례한다. 그림 7을 보면 slope와 intercept 사이에는 음의 상관계를 나타냄을 알 수 있다. baseball player case study에서 우리는 ddply, d_ply, dlply, ldply를 사용했다. 통계적 분석은 비록 매우 정교하지 못했지만 plyr package를 사용하여 우리는 손쉽게 작업을 할 수 있었다.

Reference

'The Split-Apply-Combine Strategy for Data Analysis', Hadley Wickham Journal of Statistical Software
'cran manual - package {plyr}' (http://http://cran.r-project.org/web/packages/plyr/plyr.pdf)

Hankuk University of Foreign Studies. Dept of Statistics. Daewoo Choi Lab. Yong Cha.
한국외국어대학교 통계학과 최대우 교수 연구실 차용
e-mail : yong.stat@gmail.com