학습목표

이번 학습단원을 마치게 되면, 학습자는 다음 작업을 수행할 수 있는 경험치를 얻게 된다.:


dplyr 팩키지를 활용한 데이터 조작

꺽쇠 연산자를 활용한 부분집합 뽑아내는 방법은 편리하기도 하지만, 복잡한 연산작업에는 가독성이 떨어진다. dplyr 세상속으로 들어가 보자. dplyr은 데이터조작을 더 쉽고 데이터 분석을 즐겁도록 개발된 팩키지다.

R에서 팩키지 역할은 더 많은 작업을 수행할 수 있도록 함수를 모아놓은 것이다. 지금까지 사용한 함수, 예를 들면, str() 혹은 data.frame()은 R에 기본으로 내장된 것이다; 처음 팩키지를 사용하려면, 본인 컴퓨터에 팩키지를 설치해야 된다. 그리고 나서 필요할 때마다 R 세션에서 가져와서 사용해야 된다.

install.packages("dplyr")

CRAN 미러(mirror)를 지정하도록 요청받는 경우가 있다 – 본질적으로 팩키지를 다운로드 받을 웹사이트를 지정하는 것과 다름없다. 어떤 것을 고르든지 그다지 차이가 나지 않는다; 굳이 선택하라고 하면 RStudio 미러를 추천한다.

library("dplyr")    ## 팩키지 적재
## Warning: package 'dplyr' was built under R version 3.2.5

dplyr은 뭘까?

dplyr 팩키지에는 흔히 데이터를 조작하는데 필요한 쉬운 도구가 포함되어 있다. dplyr 팩키지는 데이터프레임과 직접붙어 동작하도록 개발되었다. dplyr 팩키지 이면에는 그전에 한동안 폭넓게 사용된 plyr 팩키지에서 영감을 받았는데, plyr 팩키지는 일부 경우에 속도가 떨어지는 성능문제가 있었다. dplyr은 연산 상당부분을 C++로 이식해서 이 문제를 해결했다. 추가적인 기능으로 외부 데이터베이스에 저장된 데이터와 직접 붙어 작업할 수도 있다. 이런 방식으로 작업을 수행하게 되면 데이터는 자연스럽게 관계형 데이터베이스에서 관리되어 질의문은 데이터베이스 위에서 실행되고, 질의문 실행결과만 반환된다.

이런 접근법은 R에서 흔히 접하게 되는 일반적인 메모리 관련된 문제에 대한 해법이 된다. 일반적으로 R에서 작업할 수 있는 데이터 크기는 가용한 메모리 크기로 제약된다. 데이터베이스 연결이 체결되면 이러한 R 한계를 극복하게 되어 수백 GB 크기를 갖는 데이터를 데이터베이스에 넣고, 직접 질의문을 던져 분석에 필요한 데이터를 R로 끌어올 수 있다.

워크샵을 마치고 나서 dplyr에 대해 더 많이 배우고자 하는 경우, dplyr cheatsheet를 참조한다.

칼럼을 뽑아내고 행을 필터링한다.

dplyr 함수중에서 가장 활용도가 높은 함수를 학습한다: select(), filter(), mutate(), group_by(), summarize(). 데이터프레임에서 칼럼을 뽑아낼 때 select()를 사용한다. select() 함수에 넣은 첫번째 인자는 데이터프레임(surveys), 그리고 후속 인자는 뽑아낼 칼럼이 인자로 들어간다.

select(surveys, plot_id, species_id, weight)

행을 뽑아내는데 filter()를 사용한다:

filter(surveys, year == 1995) %>% head
##   record_id month day year plot_id species_id sex hindfoot_length weight
## 1     22314     6   7 1995       2         NL   M              34     NA
## 2     22728     9  23 1995       2         NL   F              32    165
## 3     22899    10  28 1995       2         NL   F              32    171
## 4     23032    12   2 1995       2         NL   F              33     NA
## 5     22003     1  11 1995       2         DM   M              37     41
## 6     22042     2   4 1995       2         DM   F              36     45
##       genus  species   taxa plot_type
## 1   Neotoma albigula Rodent   Control
## 2   Neotoma albigula Rodent   Control
## 3   Neotoma albigula Rodent   Control
## 4   Neotoma albigula Rodent   Control
## 5 Dipodomys merriami Rodent   Control
## 6 Dipodomys merriami Rodent   Control

파이프(Pipes)

동시에 칼럼을 뽑아내고 행을 필터링한다면 어떨까? 세가지 방식이 존재한다: 중간 과정을 놓는 방법, 중첩함수를 사용하는 방법, 파이프를 사용하는 방법. 중간 과정을 두는 방법은 임시 데이터프레임을 생성해서, 다음 함수에 입력값으로 사용하는 방법이다. 이 방법을 사용하게 되면 작업공간이 수많은 객체로 난잡하게 되는 단점이 있다. 중첨함수(함수 내부에 또다른 함수)를 사용하는 방법도 있다. 편리한 방법이기도 하지만, 처리 과정이 내부에서 외부로 나가는 방향으로 진행되어 너무 많은 함수가 중첩되는 경우 가독성이 상당히 떨어진다. 마지막 선택지가 파이프로 가장 최근에 추가된 기능이다. 파이프는 함수 출력값을 받아 다음번 함수에 곧바로 전송한다. 동일한 데이터셋에 수많은 작업을 수행할 때 매우 유용하다. R에서 파이프는 %>% 모양인데, dplyr 일부로서 magrittr 팩키지를 경유해서 이용가능하게 된다.

surveys %>%
  filter(weight < 5) %>%
  select(species_id, sex, weight) %>% head
##   species_id sex weight
## 1         PF   F      4
## 2         PF   F      4
## 3         PF   M      4
## 4         RM   F      4
## 5         RM   M      4
## 6         PF          4

상기 예제에서 파이프를 사용해서 survyes 데이터셋을 filter로 먼저 보내서 체중 weight이 5보다 작은 행만 뽑아내고 나서, selectspecies, sex, weight 칼럼을 뽑아낸다. 데이터프레임을 파이프로 filter(), select() 함수로 전달할 때, 인자로 데이터프레임을 더이상 포함시킬 필요가 없다.

더 작은 크기를 갖는 데이터프레임 객체를 새로 생성하고자 한다면, 일련의 파이프 작업결과를 새로운 이름으로 대입시킨다:

surveys_sml <- surveys %>%
  filter(weight < 5) %>%
  select(species_id, sex, weight)

surveys_sml
##    species_id sex weight
## 1          PF   F      4
## 2          PF   F      4
## 3          PF   M      4
## 4          RM   F      4
## 5          RM   M      4
## 6          PF          4
## 7          PP   M      4
## 8          RM   M      4
## 9          RM   M      4
## 10         RM   M      4
## 11         PF   M      4
## 12         PF   F      4
## 13         RM   M      4
## 14         RM   M      4
## 15         RM   F      4
## 16         RM   M      4
## 17         RM   M      4

최종 작업 결과각 표현식 가장 왼쪽에 위치했다.

도전과제

파이프를 사용해서, 데이터의 부분집합을 만들어내는데, 1995년 이전 포획된 개체로 year, sex, weight 칼럼만 포함되도록 한다.

mutate

종종, 기존 칼럼값을 활용하여 새로운 칼럼을 생성하고자 한다. 예를 들어, 단위를 전환하거나, 두 칼럼을 활용하여 비율을 계산하거나 한다. 이 작업을 위해서 mutate() 동사를 사용한다.

킬로그램 단위를 갖는 새로운 칼럼을 생성한다:

surveys %>%
  mutate(weight_kg = weight / 1000) %>% head
##   record_id month day year plot_id species_id sex hindfoot_length weight
## 1         1     7  16 1977       2         NL   M              32     NA
## 2        72     8  19 1977       2         NL   M              31     NA
## 3       224     9  13 1977       2         NL                  NA     NA
## 4       266    10  16 1977       2         NL                  NA     NA
## 5       349    11  12 1977       2         NL                  NA     NA
## 6       363    11  12 1977       2         NL                  NA     NA
##     genus  species   taxa plot_type weight_kg
## 1 Neotoma albigula Rodent   Control        NA
## 2 Neotoma albigula Rodent   Control        NA
## 3 Neotoma albigula Rodent   Control        NA
## 4 Neotoma albigula Rodent   Control        NA
## 5 Neotoma albigula Rodent   Control        NA
## 6 Neotoma albigula Rodent   Control        NA

실행결과 화면을 뒤덮게 되서, 첫 몇줄만 화면에 보려면 파이프를 사용해서 데이터 head() 함수를 적용시키면 된다. dplyr 혹은 magrittr 팩키지가 적재되기만 하면 non-dplyr 함수도 파이프와 연결시켜 실행시킬 수 있다

surveys %>%
  mutate(weight_kg = weight / 1000) %>%
  head
##   record_id month day year plot_id species_id sex hindfoot_length weight
## 1         1     7  16 1977       2         NL   M              32     NA
## 2        72     8  19 1977       2         NL   M              31     NA
## 3       224     9  13 1977       2         NL                  NA     NA
## 4       266    10  16 1977       2         NL                  NA     NA
## 5       349    11  12 1977       2         NL                  NA     NA
## 6       363    11  12 1977       2         NL                  NA     NA
##     genus  species   taxa plot_type weight_kg
## 1 Neotoma albigula Rodent   Control        NA
## 2 Neotoma albigula Rodent   Control        NA
## 3 Neotoma albigula Rodent   Control        NA
## 4 Neotoma albigula Rodent   Control        NA
## 5 Neotoma albigula Rodent   Control        NA
## 6 Neotoma albigula Rodent   Control        NA

첫번째 몇 줄은 NA 값으로 가득찬다. NA 값을 제거하려면, 파이프 체인에 filter() 함수를 다음과 같이 삽입시킬 수 있다:

surveys %>%
  filter(!is.na(weight)) %>%
  mutate(weight_kg = weight / 1000) %>%
  head
##   record_id month day year plot_id species_id sex hindfoot_length weight
## 1       588     2  18 1978       2         NL   M              NA    218
## 2       845     5   6 1978       2         NL   M              32    204
## 3       990     6   9 1978       2         NL   M              NA    200
## 4      1164     8   5 1978       2         NL   M              34    199
## 5      1261     9   4 1978       2         NL   M              32    197
## 6      1453    11   5 1978       2         NL   M              NA    218
##     genus  species   taxa plot_type weight_kg
## 1 Neotoma albigula Rodent   Control     0.218
## 2 Neotoma albigula Rodent   Control     0.204
## 3 Neotoma albigula Rodent   Control     0.200
## 4 Neotoma albigula Rodent   Control     0.199
## 5 Neotoma albigula Rodent   Control     0.197
## 6 Neotoma albigula Rodent   Control     0.218

is.na() 함수는 NA가 있는지 없는지 판단하는 함수다. ! 기호는 부정하는 기호로 NA가 아닌 모든 것을 찾아낸다.

도전과제

다음 기준을 만족하는 survey 데이터에서 데이터프레임을 새로 생성시킨다: species_id 칼럼과 hindfoot_length을 반으로 나누는 값을 포함하는 칼럼만 포함시킨다. 새로운 칼럼명은 hindfoot_half이다. hindfoot_half 칼럼에는 NA 값이 없고 모든 값은 30 보다 작아야 한다.

힌트: 해법 데이터프레임을 생성하려면 명령어가 어떤 순서로 정렬되어야 하는지 생각해본다!

분할-적용-병합 데이터분석과 summarize() 함수

데이터분석 상당수 작업은 “split-apply-combine(분할-적용-병합)” 패러다임으로 해결된다: 데이터를 집단으로 쪼개고, 각 집단별로 특정 분석을 적용시키고 나서, 결과를 병합한다. dplyr 팩키지는 group_by() 함수를 사용해서 이런 유형의 작업을 매우 쉽게 구현한다.

summarize() 요약 함수

group_by() 함수는 summarize() 함수와 함께 사용되는데 각 집단을 한줄로 요약해서 축약하게 된다. group_by() 함수는 인자로 범주형 변수가 포함된 칼럼명을 인자로 받아 요약 통계량을 계산하게 된다. 성별로 weight 체중을 계산하려면 다음과 같이 R코드를 작성한다:

surveys %>%
  group_by(sex) %>%
  summarize(mean_weight = mean(weight, na.rm = TRUE))
## # A tibble: 3 × 2
##      sex mean_weight
##   <fctr>       <dbl>
## 1           64.74257
## 2      F    42.17055
## 3      M    42.99538

칼럼 다수를 group_by()에 넣어 사용하는 것도 가능하다:

surveys %>%
  group_by(sex, species_id) %>%
  summarize(mean_weight = mean(weight, na.rm = TRUE))
## Source: local data frame [92 x 3]
## Groups: sex [?]
## 
##       sex species_id mean_weight
##    <fctr>     <fctr>       <dbl>
## 1                 AB         NaN
## 2                 AH         NaN
## 3                 AS         NaN
## 4                 BA         NaN
## 5                 CB         NaN
## 6                 CM         NaN
## 7                 CQ         NaN
## 8                 CS         NaN
## 9                 CT         NaN
## 10                CU         NaN
## # ... with 82 more rows

“sex” 와 “species_id” 모두를 집단으로 묶을 때, 첫번째 행은 성별과 체중이 확인되기 전에 이탈해야 되는 개체다. 마지막 칼럼은 NA가 아니라 NaN(“Not a Number”)가 포함된 것에 주목한다. 이런 문제를 피해가기 위해서, 체중에 대한 요약 통계량을 산출하기 전에 체중에 대한 결측값을 제거한다. 결측값이 파이프 앞단에서 제거되었기 때문에, 평균을 계산할 때 na.rm=TRUE 인자를 생략해도 된다:

surveys %>%
  filter(!is.na(weight)) %>%
  group_by(sex, species_id) %>%
  summarize(mean_weight = mean(weight))
## Source: local data frame [64 x 3]
## Groups: sex [?]
## 
##       sex species_id mean_weight
##    <fctr>     <fctr>       <dbl>
## 1                 DM    38.28571
## 2                 DO    50.66667
## 3                 DS   120.00000
## 4                 NL   167.68750
## 5                 OL    29.00000
## 6                 OT    21.20000
## 7                 PB    30.60000
## 8                 PE    17.66667
## 9                 PF     6.00000
## 10                PI    18.00000
## # ... with 54 more rows

상기 R코드를 실행시키게 되면 출력 결과가 더이상 화면을 뒤덮지 않게 되는 것에 주목한다. dplyr 팩키지가 data.frametbl_df로 전환시켰기 때문이다. tbl_df 데이터프레임은 데이터프레임과 매우 유사하다; 유용한 점이 여럿 있지만, 지금으로서는 자동으로 출력결과를 화면을 뒤덮지 않고, 각 칼럼명 아래 자료형을 표현한다는 점이다. 화면에 더 많은 데이터를 출력하고자 한다면, print() 함수에 n 인자에 출력하고자 하는 행갯수를 지정하면 된다:

surveys %>%
  filter(!is.na(weight)) %>%
  group_by(sex, species_id) %>%
  summarize(mean_weight = mean(weight)) %>%
  print(n=15)
## Source: local data frame [64 x 3]
## Groups: sex [?]
## 
##       sex species_id mean_weight
##    <fctr>     <fctr>       <dbl>
## 1                 DM    38.28571
## 2                 DO    50.66667
## 3                 DS   120.00000
## 4                 NL   167.68750
## 5                 OL    29.00000
## 6                 OT    21.20000
## 7                 PB    30.60000
## 8                 PE    17.66667
## 9                 PF     6.00000
## 10                PI    18.00000
## 11                PL    25.00000
## 12                PM    20.25000
## 13                PP    14.60000
## 14                RM    11.08333
## 15                SF    40.50000
## # ... with 49 more rows

데이터가 집단으로 묶이게 되면, 한번에 다수 변수(꼭 변수가 하나일 필요는 없음)를 요약할 수도 있다. 예를 들어, 성별로 각 종별로 최소 체중을 나타내는 칼럼을 추가할 수도 있다:

surveys %>%
  filter(!is.na(weight)) %>%
  group_by(sex, species_id) %>%
  summarize(mean_weight = mean(weight),
            min_weight = min(weight))
## Source: local data frame [64 x 4]
## Groups: sex [?]
## 
##       sex species_id mean_weight min_weight
##    <fctr>     <fctr>       <dbl>      <int>
## 1                 DM    38.28571         24
## 2                 DO    50.66667         44
## 3                 DS   120.00000         78
## 4                 NL   167.68750         83
## 5                 OL    29.00000         21
## 6                 OT    21.20000         18
## 7                 PB    30.60000         20
## 8                 PE    17.66667         17
## 9                 PF     6.00000          4
## 10                PI    18.00000         18
## # ... with 54 more rows

총계 기록하기, 갯수 세기(Tallying)

데이터로 작업할 때, 각 요인별(혹은 다수 요인을 조합)로 관측점 갯수가 몇개가 되는지 확인하는 작업이 흔하다. 이런 목적을 달성하는데, dplyr 팩키지에 tally() 함수가 제공된다. 예를 들어, 성별로 집단을 묶어 각 성별로 행의 갯수를 세어 총계를 기록하고자 하는 경우, R 코드를 다음과 같이 작성한다:

surveys %>%
  group_by(sex) %>%
  tally()
## # A tibble: 3 × 2
##      sex     n
##   <fctr> <int>
## 1         1748
## 2      F 15690
## 3      M 17348

이번에, tally() 함수는 group_by() 함수로 생성된 집단에만 적용된다. 각 범주별로 전체 레코드 갯수를 세어 총계를 기록한다.

도전과제

조사기간 동안 plot_type 별로 얼마나 많은 개체가 포획되었나?

도전과제

group_by()summarize() 함수를 사용해서, hindfoot_length 후족부 길이에 대한 평균, 최소값, 최대값을 각 종별(species _id)로 묶어 산출하시오.

도전과제

매년 측정된 동물 중 가장 무게가 많이 나가는 종은 어떤 것인가? year, genus, species_id, weight 칼럼만 뽑아낸다.

데이터 내보내기

dplyr 팩키지를 사용해서 원데이터에서 필요한 정보를 추출하고, 원데이터를 요약하는 방법을 학습했기 때문에, 이제 동료와 공유하거나 기록보관 목적으로 새로 작업된 데이터셋을 내보내자.

CSV 파일을 R로 불러오는데 사용된 read.csv() 함수와 유사하게, 데이터프레임에서 CSV 파일을 생성시키는데 write.csv() 함수가 존재한다.

write.csv() 함수를 사용하기 앞서, 현재 작업디렉토리에 생성된 데이터셋을 저장할 디렉토리를 data_output 이름으로 생성한다. 원데이터와 동일한 디렉토리에 생성시킨 데이터셋이 겹쳐쓰는 것은 바람직하지 못하다. 원데이터와 작업결과 데이터를 구분하여 분리시키는 것은 좋은 습관이 된다. data 디렉토리에는 원데이터, 변경되지 않는 데이터만 보관해야 한다. 이를 통해서 데이터가 삭제되거나 변경되지 않도록 별도 관리한다; 이와는 달리 data_output 디렉토리에는 스크립트로 생성된 데이터를 보관해서 필요에 따라 삭제해도 되는데 이유는 다시 데이터를 생성시킬 수 있는 스크립트 R코드가 있기 때문이다.

다음 학습, 시각화를 위해서, 결측값이 없이 정제된 데이터셋을 준비한다.

species_id 칼럼에 결측값을 갖는 관측점은 모두 제거한다. 데이터셋에서 결측된 종족은 빈 문자열 혹은 NA로 표현되어 있다. weighthindfoot_length에도 결측값이 있는 것을 제거한다. 데이터셋에는 성별이 파악된 동물에 대한 관측점만이 담겨지게 된다:

surveys_complete <- surveys %>%
  filter(species_id != "",         # 빈문자열을 갖는 species_id 제거
         !is.na(weight),           # weight 결측값 제거
             !is.na(hindfoot_length),  # hindfoot_length 결측값 제거
             sex != "")                # 빈문자열을 갖는 sex 제거

시간에 따른 종종별 번성을 시각화하는데 관심이 있기 때문에, 희귀종에 대한 관측점을 제거한다(즉, 관측값이 50보다 적은 종족) 두단계에 걸쳐 이 작업을 수행한다: 첫번째로 각 종별로 얼마나 많이 관측되었는지 횟수 정보가 담긴 데이터셋을 생성하고 희귀종을 제거하고 나서 일반적으로 흔한 종에 해당되는 관측점만 추출한다:

## 일반적으로 흔한 species_id만 추출
species_counts <- surveys_complete %>%
                  group_by(species_id) %>%
                  tally %>%
                  filter(n >= 50) %>%
                  select(species_id)

## 가장 많이 관측되는 종만 추출
surveys_complete <- surveys_complete %>%
                 filter(species_id %in% species_counts$species_id)

학습참여자 모두 동일한 데이터셋인지 점검한다. surveys_complete 데이터프레임은 행갯수가 30463, 열갯수가 13이면 된다. dim(surveys_complete) 타이핑하면 확인이 가능하다.

이제 데이터셋이 준비되어, data_output 디렉토리에 CSV 파일로 저장한다. 기본디폴트 설정으로, write.csv() 함수는 행명칭(row names)이 포함된 칼럼을 포함한다. (이번 경우에는 행명칭이 행번호). 따라서, 그다지 유용한 정보가 아니라 행명은 row.names = FALSE로 포함시키지 않는다:

write.csv(surveys_complete, file="data_output/surveys_complete.csv",
          row.names=FALSE)