R에서 데이터 다루기(dplyr)

0. 시작하기 앞서

이 글은 Data Carpentry for data analysis을 공부하고 정리한 것입니다. 목표는 프로그래밍 경험이 없는 과학자들이 R을 가지고 작업하는데 필요한 기본적인 개념, 기술, 사용법을 가르치는데 있습니다.간략한 목차는 다음과 같습니다.

  • R 기초 문법
  • CSV 파일 불러오는 방법과 데이터프레임 구조
  • 요인 처리방법, 행과 열 추가/삭제
  • 요약 통계 계산법, 시각화에 대한 간단한 소개

1. R 소개

1.1. 객체 만들기

R에서는 다음과 같이 객체를 생성합니다.

In [3]:
mass <- 47.5            # mass?
age  <- 122             # age?
mass <- mass * 2.0      # mass?
age  <- age - 20        # age?
mass_index <- mass/age  # mass_index?
mass_index
0.931372549019608

1.2. 벡터와 자료형

벡터는 R에서 가장 기본적인 자료구조입니다. 예를 들어, 다음과 같이 벡터를 생성할 수 있습니다.

In [2]:
num_char <- c(1, 2, 3, 'a')
num_logical <- c(1, 2, 3, TRUE)
char_logical <- c('a', 'b', 'c', TRUE)
tricky <- c(1, 2, 3, '4')
num_char # 벡터값 확인하기
  1. '1'
  2. '2'
  3. '3'
  4. 'a'

1.2.1. 벡터 부분집합 뽑아내기

벡터에서 값을 하나 혹은 다수 값을 추출하려면, 꺾쇠 괄호 내부에 인덱스를 넣어 줍니다.

In [3]:
animals <- c("mouse", "rat", "dog", "cat")
animals[2]
'rat'

2. 데이터 불러오기

  • 외부 데이터(CSV 파일)를 불러와
  • R에서 데이터프레임 구조와 내용물을 탐색해 봅니다.

R함수 download.file()를 사용해서 CSV파일을 다운로드합니다. 그리고 read.csv()기능을 이용해 파일을 불러오겠습니다. 아래의 명령어를 실행해 파일을 다운로드 하세요.

download.file("https://ndownloader.figshare.com/files/2292169",
              "portal_data_joined.csv")

그런다음 파일을 읽어오도록 하겠습니다.

In [4]:
surveys <- read.csv('portal_data_joined.csv')
head(surveys) # 파일 확인하기
record_id month day year plot_id species_id sex hindfoot_length weight genus species taxa plot_type
1 7 16 1977 2 NL M 32 NA Neotoma albigula Rodent Control
72 8 19 1977 2 NL M 31 NA Neotoma albigula Rodent Control
224 9 13 1977 2 NL NA NA Neotoma albigula Rodent Control
266 10 16 1977 2 NL NA NA Neotoma albigula Rodent Control
349 11 12 1977 2 NL NA NA Neotoma albigula Rodent Control
363 11 12 1977 2 NL NA NA Neotoma albigula Rodent Control

2.1. Dataframe

Dataframe은 엑셀의 테이블을 생각하시면 됩니다. 각각의 행이 백터로 구성되고 모여서 테이블 형태로 표현한 것입니다. str() 함수를 사용하면 각각의 행이 어떠한 자료형인지 확인 할 수 있습니다.

In [8]:
str(surveys)
'data.frame':	34786 obs. of  13 variables:
 $ record_id      : int  1 72 224 266 349 363 435 506 588 661 ...
 $ month          : int  7 8 9 10 11 11 12 1 2 3 ...
 $ day            : int  16 19 13 16 12 12 10 8 18 11 ...
 $ year           : int  1977 1977 1977 1977 1977 1977 1977 1978 1978 1978 ...
 $ plot_id        : int  2 2 2 2 2 2 2 2 2 2 ...
 $ species_id     : Factor w/ 48 levels "AB","AH","AS",..: 16 16 16 16 16 16 16 16 16 16 ...
 $ sex            : Factor w/ 3 levels "","F","M": 3 3 1 1 1 1 1 1 3 1 ...
 $ hindfoot_length: int  32 31 NA NA NA NA NA NA NA NA ...
 $ weight         : int  NA NA NA NA NA NA NA NA 218 NA ...
 $ genus          : Factor w/ 26 levels "Ammodramus","Ammospermophilus",..: 13 13 13 13 13 13 13 13 13 13 ...
 $ species        : Factor w/ 40 levels "albigula","audubonii",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ taxa           : Factor w/ 4 levels "Bird","Rabbit",..: 4 4 4 4 4 4 4 4 4 4 ...
 $ plot_type      : Factor w/ 5 levels "Control","Long-term Krat Exclosure",..: 1 1 1 1 1 1 1 1 1 1 ...

위 결과에서 볼 수 있듯이, 다수 칼럼은 정수형 벡터로 구성되지만, species 와 sex는 요인(factor)으로 불리는 특수한 클래스로 입니다. 데이터프레임 클래스에 대해 추가적인 내용을 알아 보기 전에, 요인에 대해 먼저 살펴보겠습니다.

2.2. 요인(Factors)

범주형 데이터를 표현할때 사용됩니다. 요인은 순서가 있거나 순서가 없는 것으로 구분되는데, 이를 정확히 이해하는 것이 통계분석과 시각화에 필요합니다.

요인은 정수형으로 저장되고, 유일무이한 정수값과 연관된 표식이 붙여진다. 요인은 마치 문자형 벡터처럼 보이고 흔히 그렇게 동작하지만, 실제로 내부를 들여다 보면 정수값이다. 그래서 주의가 요구된다.

요인형 벡터를 생성시키게 되면, 요인벡터에는 수준(level)으로 미리 정의된 값이 집합으로 담겨집니다. 기본 설정으로은 알파벳 순으로 정렬이 됩니다. 예를 들어, 수준 2개를 갖는 요인이 있다고 가정하면:

In [5]:
sex <- factor(c("male", "female", "female", "male"))

R은 정수 1을 "female" 수준, 정수 2를 "male" 수준에 대입한다. 벡터 첫번째 원소로 "male"이 왔지만, 알파벳순으로 보면 f가 m보다 순서가 앞서기 때문이다. levels() 함수를 사용해서 이런 사실을 확인할 수 있고, nlevels() 함수를 사용해서 수준 갯수도 확인할 수 있습니다:

In [6]:
levels(sex)
  1. 'female'
  2. 'male'
In [7]:
nlevels(sex)
2

2.2. 요인 자료형 변경

  1. 요인 벡터를 문자형 벡터로 변경시킬 때는 as.character() 함수를 사용합니다.
  2. 요인 벡터를 숫자형 벡터로 변경시키는 것 as.numeric()을 사용하는데 두가지 방법이 있습니다.
    • 문자형으로 바꾸고나서 다시 숫자로 전환시키거나
    • 또다른 방법은 levels() 함수를 사용하는 것입니다.
In [10]:
f <- factor(c(1, 5, 10, 2))
f2 <- as.character(f)
f2
  1. '1'
  2. '5'
  3. '10'
  4. '2'
In [11]:
as.numeric(f2)
  1. 1
  2. 5
  3. 10
  4. 2
In [10]:
as.numeric(levels(f))[f]
  1. 1
  2. 5
  3. 10
  4. 2

levels() 방법을 사용하면 아래 세가지 절차가 내부적으로 실행됩니다.

  1. levels(f)를 실행시켜 요인에 대한 모든 수준을 얻어온다.
  2. as.numeric(levels(f))을 실행시켜 앞서 받아온 수준을 숫자값으로 전환시킨다.
  3. 그리고 나서, 꺾쇠 괄호 내부 f 벡터 정수값을 사용해서 숫자값에 접근한다.

3. Dataframe 설명

앞서 알아본 Dataframe에 대하여 자세히 알아 보겠습니다.

3.1. 데이터프레임은 무엇인가?

데이터프레임(dataframe)은 가장 대중적인 표형식 데이터에 대한 사실상 표준(de facto) 으로, 통계 및 시각화에 활용하는 자료구조입니다. 동일한 길이를 갖는 벡터 집합이며, 벡터 각각은 행을 표현하지만 각 벡터는 서로 다른 자료형이 될 수 있다(예를 들어, 문자형, 정수형, 요인형). 우리는 앞에서 str() 함수를 사용해서 각 칼럼별 자료형을 조사했었습니다. 데이터프레임은 일일이 생성할 수도 있지만, 일반적으로 ead.csv() 혹은 read.table() 함수로 만들어 냅니다.

기본 설정으로 데이터프레임이 생성될때 문자열이 담긴 행은 요인(factor) 자료형으로 변환됩니다. 따라서 작업하려는 의도에 따라 문자형(character)으로 칼럼을 그대로 두고자 하는 경우에는 stringsAsFactors = FALSE로 설정하면 요인형이 아닌 문자형 그대로 사용할 수 있습니다.

In [13]:
example_data <- data.frame(animal=c("dog", "cat", "sea cucumber", "sea urchin"),
                           feel=c("furry", "furry", "squishy", "spiny"),
                           weight=c(45, 8, 1.1, 0.8),
                           stringsAsFactors = FALSE)
str(example_data)
'data.frame':	4 obs. of  3 variables:
 $ animal: chr  "dog" "cat" "sea cucumber" "sea urchin"
 $ feel  : chr  "furry" "furry" "squishy" "spiny"
 $ weight: num  45 8 1.1 0.8

3.2. Dataframe 살펴보기

head()str() 함수를 사용해서 Dataframe 구조와 내용물을 살펴볼수 있습니다. 그 외에도 아래와 같이 유용한 명령어가 존재 합니다.

  • 데이터프레임 크기:
    • dim() - 첫번째 원소는 행의 갯수, 두번째 원소는 열의 갯수를 갖는 벡터를 반환 (객체에 대한 차원, dimensions)
    • nrow() - 행 갯수를 반환
    • ncol() - 열 갯수를 반환
  • 콘텐츠/내용:
    • head() - 첫번째 5 행
    • tail() - 마지막 5 행
  • 명칭:
    • names() - 데이터프레임의 칼럼명을 반환(data.frame 객체에 대한 colnames()과 동의어)
    • rownames() - 행명칭을 반환
  • 요약:
    • str() - 객체의 구조와 클래스, 길이, 각 칼럼별 내용에 대한 정보를 제공 structure of the object and information about the class, length and content of each column
    • summary() - 각 칼럼별 요약 통계량
In [15]:
summary(example_data)
          animal       feel       weight      
 cat         :1   furry  :2   Min.   : 0.800  
 dog         :1   spiny  :1   1st Qu.: 1.025  
 sea cucumber:1   squishy:1   Median : 4.550  
 sea urchin  :1               Mean   :13.725  
                              3rd Qu.:17.250  
                              Max.   :45.000  

4. dplyr로 데이터 편집과 분석하기

4.1. dplyr 설명

dplyr 패키지에는 흔히 데이터를 조작하는데 필요한 도구가 포함되어 있습니다. dplyr 패키지는 그전에 폭넓게 사용된 plyr 패키지에서 영감을 받았는데, plyr 팩키지는 속도가 떨어지는 성능문제가 있었습니다. 하지만 dplyr은 연산의 상당부분을 C++ 을 이용해 연산 속도가 매우 빠릅니다. dplyr에 대해 더 많이 배우고자 하는 경우, dplyr cheatsheet를 참조하세요.

4.2. dplyr 패키지를 활용한 데이터 편집

dplyr은 데이터조작을 더 쉽고 데이터 분석을 즐겁도록 개발되었습니다. 아래의 방법으로 설치하고 불러옵니다.

install.packages("dplyr")
library("dplyr")    ## 팩키지 불러오기

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

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

In [19]:
library(dplyr) # dplyr을 불러옵니다.
surveys <- read.csv('surveys.csv') # 데이터를 불러옵니다.
select_df <- select(surveys, plot_id, species_id, weight) # plot_id, species_id, weight 행만 추출합니다.
tail(select_df) # 테이블 확인
plot_id species_id weight
35544 15 US NA
35545 15 AH NA
35546 15 AH NA
35547 10 RM 14
35548 7 DO 51
35549 5 NA
In [18]:
filter(surveys, year == 1995) %>% head # 1995년 데이터만 필터합니다.
record_id month day year plot_id species_id sex hindfoot_length weight
21993 1 11 1995 18 PF F 16 7
21994 1 11 1995 12 DO M 36 47
21995 1 11 1995 2 DO M 36 51
21996 1 11 1995 21 PF F 14 7
21997 1 11 1995 24 RM M 15 10
21998 1 11 1995 1 DM M 38 46

4.4. 파이프(Pipes) 사용하기

한번에 행을 뽑아내고 필터링을 해야 한다면, 파이프를 사용해야 합니다. 파이프는 함수 출력값을 받아 다음번 함수에 곧바로 전송하는 것으로 Shell script의 그것과 동일합니다. 이는 수많은 작업을 수행할 때 매우 유용합니다. R에서 파이프는 %>% 모양입니다.

In [19]:
surveys %>%
  filter(weight < 5) %>%
  select(species_id, sex, weight) %>% head
species_id sex weight
PF M 4
PF F 4
PF 4
PF F 4
PF F 4
RM M 4

상기 예제에서 파이프를 사용해서 survyes 데이터셋을 filter로 먼저 보내서 체중이 5보다 작은 행만 뽑아내고 나서, select로 species, sex, weight 칼럼을 뽑아냈습니다. 파이프를 사용하면 이처럼 간결하게 코딩을 할 수 있습니다.

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

In [21]:
small_df <- surveys %>% filter(year < 1995) %>% select(year, sex, weight)
tail(small_df)
year sex weight
21987 1994 M 46
21988 1994 F 38
21989 1994 F 40
21990 1994 NA
21991 1994 NA
21992 1994 NA

4.5. mutate() 함수

종종, 기존 칼럼값을 활용하여 새로운 칼럼을 생성할때가 있습니다. 예를 들어, 단위를 전환하거나, 두 칼럼을 활용하여 비율을 계산할 때 말이죠.이런 작업을 위해서 mutate() 함수를 사용합니다. 예시로 무게를 킬로그램 단위로 표시된 새로운 칼럼을 생성해보겠습니다.

In [25]:
surveys %>%
  mutate(weight_kg = weight / 1000) %>% head
record_id month day year plot_id species_id sex hindfoot_length weight weight_kg
1 7 16 1977 2 NL M 32 NA NA
2 7 16 1977 3 NL M 33 NA NA
3 7 16 1977 2 DM F 37 NA NA
4 7 16 1977 7 DM M 36 NA NA
5 7 16 1977 3 DM M 35 NA NA
6 7 16 1977 1 PF M 14 NA NA

NA 값으로 가득차 있기 때문에 파이프 체인에 filter() 함수를 이용해 없는 값을 제거해 봅니다.

In [26]:
surveys %>%
  filter(!is.na(weight)) %>%
  mutate(weight_kg = weight / 1000) %>%
  head
record_id month day year plot_id species_id sex hindfoot_length weight weight_kg
63 8 19 1977 3 DM M 35 40 0.040
64 8 19 1977 7 DM M 37 48 0.048
65 8 19 1977 4 DM F 34 29 0.029
66 8 19 1977 4 DM F 35 46 0.046
67 8 19 1977 7 DM M 35 36 0.036
68 8 19 1977 8 DO F 32 52 0.052

is.na() 함수는 NA가 있는지 없는지 판단하는 함수입니다. ! 기호는 부정하는 기호로 NA가 아닌것을 의미합니다.

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

In [29]:
new_df <- surveys %>% filter(!is.na(hindfoot_length)) %>%
        mutate(hindfoot_half = hindfoot_length/2) %>%
        select(species_id, hindfoot_half) %>%
        filter(hindfoot_half < 30)
head(new_df)
species_id hindfoot_half
NL 16.0
NL 16.5
DM 18.5
DM 18.0
DM 17.5
PF 7.0

4.6. 분할-적용-병합과 summarize() 함수

데이터분석 상당수 작업은 “split-apply-combine(분할-적용-병합)” 으로 해결됩니다. 데이터를 쪼개고, 각 집단별로 분석을 적용시키고 나서, 결과를 병합하는 것이죠. dplyr 패키지는 이런 유형의 작업을 매우 쉽게 구현합니다. group_by(), summarize() 함수로 각 집단을 한줄로 요약해서 축약해 보겠습니다.

예시로 성별로 구별되는 체중을 계산해보겠습니다.

In [46]:
surveys %>%
    filter(sex == "M" | sex == "F") %>%
    group_by(sex) %>%
    summarize(mean_weight = mean(weight, na.rm = TRUE)) # na.rm는 NA 값을 제거합니다
sex mean_weight
F 42.17055
M 42.99538

다수의 행을 group_by()에 넣어 사용하는 것도 가능합니다. sexspecies_id 모두를 집단으로 묶고 계산 해보도록 하겠습니다. 체중에 대한 요약 통계량을 산출하기 전에 체중에 대한 결측값(NA)을 제거했기 때문에 평균을 계산할 때 na.rm=TRUE를 생략해도 됩니다.

In [47]:
surveys %>%
    filter(sex == "M" | sex == "F") %>%
    filter(!is.na(weight)) %>% 
    group_by(sex, year, species_id ) %>%
    summarize(mean_weight = mean(weight)) %>% head
sex year species_id mean_weight
F 1977 DM 40.250000
F 1977 DO 41.900000
F 1977 DS 117.285714
F 1977 OL 22.000000
F 1977 OX 21.000000
F 1977 PF 7.266667

4.7. 총 합계 구하기와 갯수 세기(Tallying)

각 요인별로 관측점 갯수가 몇개나 되는지 확인하는 작업할 때는 tally() 함수를 사용합니다. 예를 들어, 성별로 집단을 묶어 각 행의 갯수를 세어 총계를 기록하고자 하는 경우, 다음과 같이 작성합니다.

In [48]:
surveys %>%
    filter(sex == "M" | sex == "F") %>%
    group_by(sex) %>%
    tally()
sex n
F 15690
M 17348

group_by()summarize() 함수를 사용해서, hindfoot_length에 대한 평균, 최소값, 최대값을 각 종별(species_id)로 묶어 구하도록 하겠습니다.

In [69]:
surveys %>% group_by(species_id) %>% filter(!is.na(hindfoot_length)) %>%
        summarize(mean_hindfoot_length = mean(hindfoot_length),
                 min_hindfoot_length = min(hindfoot_length),
                 max_hindfoot_length = max(hindfoot_length)) %>% head
species_id mean_hindfoot_length min_hindfoot_length max_hindfoot_length
AH 33.00000 31 35
BA 13.00000 6 16
DM 35.98235 16 50
DO 35.60755 26 64
DS 49.94887 39 58
NL 32.29423 21 70

4.8. 데이터 내보내기

지금까지 dplyr 패키지를 사용해서 데이터에서 필요한 정보를 추출하고, 요약하는 방법을 학습했습니다. 이제 데이터를 내보내는 방법을 알아 보겠습니다. CSV 파일을 R로 불러오는데 사용된 read.csv() 함수와 비슷하게, 데이터프레임에서 CSV 파일을 생성시키는데 write.csv() 함수가 사용됩니다.

원본 데이터(surveys)에 결측값이 없이 정제된 데이터셋(survey_complete)를 만들어 보겠습니다.

In [70]:
surveys_complete <- surveys %>%
  filter(species_id != "",         # 빈문자열을 갖는 species_id 제거
         !is.na(weight),           # weight 결측값 제거
             !is.na(hindfoot_length),  # hindfoot_length 결측값 제거
             sex != "")                # 빈문자열을 갖는 sex 제거
dim(surveys_complete)
  1. 30676
  2. 9

dim(surveys_complete)을 통해 확인 해보니 surveys_complete 데이터프레임은 행갯수가 30676, 열갯수가 9입니다. 이제 CSV 파일로 저장합니다. 기본 설정으로, write.csv() 함수는 행명칭(row names)이 포함된 칼럼을 포함하여 저장합니다. 이번 경우에는 행명칭이 행번호임으로 row.names = FALSE로 설정합니다.

In [35]:
write.csv(surveys_complete, file="surveys_complete.csv",
          row.names=FALSE)

5. 마무리하며,

R에서 데이터를 불러오고 전처리하는 과정을 dplyr 패키지를 이용해 해보았습니다. 이러한 과정은 필수적이면서도 노동집약적이기 때문에 평소에 잘 기억해 두는것이 좋습니다.

파이썬에서는 pandas, R에는 dplyr