published on Dec 25, 2012
keywords : R, apply, split, data analysis
우리는 Data를 Split, Apply, Combine 할 때 R에서 우리는 익히 알고 있던 'apply' family(apply, sapply, lapply 등)를 사용한다. apply를 데이터 분석에서 흔한 행동과 실수들은 무엇일까요? 흔한 데이터 분석에서 나타나는 문제의 아주 작은 부분을 해결하고자 한다.
아래의 표 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 |
Input: Array (a*ply)
그림 1: 2차원 matrix를 나누는 3가지 방법이다. 기존 matrix는 좌측 상단. 각 분할 방법에 따른 한개의 조각은 파란색으로 표시되어 있다.
그림 2: 3차원 array를 나누는 7가지 방법이다. 기존 array는 좌측 상단. 각 분할 방법에 따른 한개의 조각은 파란색으로 표시되어 있다.
Input: Data frme (d*ply)
.(var1)은 var1 variable의 value에 의해 data frame을 group으로 분할한다. 만약에 여러개의 variable을 사용하면 (.(a, b, c)와 같이) group은 variable간의 interaction에 의해 group이 구성된다. 그리고 output은 3개의 변수로 표시된다. array output에 대해서는 3-dimension으로 표시된다(a,b,c로).
variable의 function 또한 사용할 수 있다: .(round(a)), .(a*b).
또는, 분할하기 위해 아래와 같은 방법을 사용할 수 있다.
그림 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와 동일하다.
Output: Array (*aply)
input의 분할과 각 결과의 차원수에 의해 array output의 모양이 결정된다.
그림 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차원이라 표시할 수 없다.
그림 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)
그림 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로 합쳐지기 때문이다.
이 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()
아래는 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)
결정계수가 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"
그림 7: intercept와 slope의 산점도. 점의 크기는 모형의 결정계수에 비례한다. 그림 7을 보면 slope와 intercept 사이에는 음의 상관계를 나타냄을 알 수 있다. baseball player case study에서 우리는 ddply, d_ply, dlply, ldply를 사용했다. 통계적 분석은 비록 매우 정교하지 못했지만 plyr package를 사용하여 우리는 손쉽게 작업을 할 수 있었다.
'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