Introduction to R II

이번 주는 데이터를 효율적으로, 그리고 체계적으로 전처리하는 방법에 대해서 살펴보겠습니다. 저번 주에 이어서 전처리 및 데이터 관리를 효율적이고 간편하게 수행하기 위해 tidyverse 패키지와 그 패키지에 속하는 다른 패키지들(tidyverse familiy), 그리고 함수들을 주로 사용하도록 하겠습니다.

패키지 불러오기

# install.package("pacman") # 여러 개의 패키지를 한 번에 불러올 수
                            # 있게끔 도와주는 패키지
# pacman::p_load(here, lubridate, ezpickr, tidyverse)
library(here)       # 현재 작업디렉토리를 R-스크립트가 위치한 디렉토리로 자동설정하는 패키지
library(lubridate)  # 날짜시각 데이터를 원활하게 가공하는 데 특화된 패키지
library(ezpickr)    # 다른 확장자의 파일을 R로 불러오기 위한 패키지
library(tidyverse)  # 데이터 관리 및 전처리를 위한 주요 패키지

들어가기에 앞서서 간단한 기본 함수들을 리뷰해보도록 하겠습니다.

x <- 10
add_one <- function(x) x + 10
add_one(5) # 결과가 15일까, 20일까요?
## [1] 15

자칫하면 위의 함수에서 x <- 10으로 x라는 객체에 10을 넣었기 때문에 10 + 10이 되어 결과를 20으로 리턴할 것이라고 생각할 수 있습니다. 하지만 어디까지나 add_one 함수가 정의되고 난 이후, xadd_one()의 괄호 안에 들어가는 값으로 재정의되었습니다. 따라서 5를 add_one()에 투입한 순간, 그 함수는 5 + 10을 계산하게 되어 결과는 15가 됩니다.

new_add_one <- function(x) x + 10 # 그렇다면 이 경우는 어떨까요?
new_add_one() 
## Error in new_add_one(): argument "x" is missing, with no default

이번에는 new_add_one() 함수를 정의하고, x 값을 따로 주지 않고 빈 함수를 작동시켰습니다. 이때, new_add_one()은 주어진 투입값(input)이 없기 때문에, 함수에 요구되는 x를 찾기 위해 함수 안에서 x를 탐색하는 것을 넘어서 한 단계 위에서 x를 찾습니다. 바로 처음에 만든 x <- 10을 불러오는 것입니다. 따라서 10 + 10, 20을 출력하게 됩니다.

그럼 여기서 함수와 객체의 관계에 대해 다시 한 번 살펴보도록 하겠습니다.

print_hello_world <- function() {
  z <- "Hello, world"
  print(z)
}

print_hello_world()
## [1] "Hello, world"
print(z)
## Error in print(z): object 'z' not found

print_hello_world() 함수를 작동시키면 함수 내에서 정의된 객체 z의 값을 반환합니다. 그렇지만 그 z는 어디까지나 함수 내에서만 정의된 것이지 R의 글로벌 환경에 저장된 객체는 아닙니다. 따라서 함수 내에서 정의된 z를 출력하라고 명령하면, 오류 코드를 확인하게 됩니다.

R에서 객체를 제외하고 작동하는 모든 기능들은 ’함수’라고 부릅니다. 그리고 함수는 같은 결과를 다른 방식으로 출력할 수도 있습니다.

5 + 5     # 간단하게 말하자면 이 함수(+)는
## [1] 10
`+`(5, 5) # 이런 식으로도 쓸 수 있습니다.
## [1] 10

또한, 기존에 R에는 내장되지 않았던 함수도 별도로 특정하게 지정하여 만들 수 있습니다. 그러나 이 경우에는 별도의 패키지로 만들어서 저장해주지 않는 한, R 코드가 작성된 해당 세션에서만 지속되는 함수일 뿐입니다.

아래는 문자열과 문자열을 하나로 합치고 있는데, 일반적으로 두 객체를 합칠 때 쓰는 함수인 +는 숫자형 객체 간에만 기능합니다. 따라서 문자열끼리 합쳐주는 함수, paste()와 동일한 기능을 하는 별도의 함수 기호를 하나 만들어 보겠습니다.

print("hello" + "world") # 오류메시지를 확인할 수 있습니다. 
## Error in "hello" + "world": non-numeric argument to binary operator
                         # +는 숫자형 객체들에만 작동하기 때문입니다.
paste("hello ", "world")
## [1] "hello  world"
`%+%` <- function(lhs, rhs) paste0(lhs, rhs)
print("hello " %+% "world")
## [1] "hello world"

이제 패키지를 불러오는 작업과 간단한 함수, 그리고 객체의 특성에 대해 리뷰했으니 다음으로 넘어가 보도록 하겠습니다.

작업 디렉토리 설정하기

아까 불러온 here 패키지의 here() 함수를 사용해보도록 하겠습니다. here()를 사용하면 자동으로 현재 R-스크립트가 저장된 경로를 확인, 복사합니다. %>%는 파이프 왼쪽의 기능 이후에 오른쪽의 기능을 적용시키는 지정된 함수로 tidyverse 패키지가 가지고 있는 강점 중 하나라고 할 수 있습니다.

이 파이프 함수를 이용하여 우리는 코드를 보다 논리적으로, 그리고 정연하게 작성하여 가독성을 높일 수 있습니다.

here() %>% setwd() # here()로 R-스크립트의 디렉토리를 확인하고 난 다음에
                   # 그 디렉토리로 작업 디렉토리를 설정(set working directory, setwd)하라는
                   # 함수를 작동시킨 것입니다.

이렇게 작업 디렉토리를 설정하였다면, 이제 데이터를 불러와 보겠습니다.: ggplot 패키지에 내장된 diamonds 데이터를 사용해보도록 하겠습니다. ggplot2 패키지가 로드 되어 있다면 해당 데이터는 쉽게 불러올 수 있습니다.

## diamonds data를 df 라는 이름으로 저장
df <- diamonds %>% slice(1:5000)
class(df)
## [1] "tbl_df"     "tbl"        "data.frame"

데이터 전처리 하기(Data Cleaning)

dplyr 패키지를 이용한 데이터 전처리

데이터를 들여다보고, 결측치를 확인하기

## 데이터의 행의 개수를 확인하는 함수입니다.
nrow(df)   
## [1] 5000
## 데이터의 열의 개수를 확인하는 함수입니다.
ncol(df)   
## [1] 10
## 데이터에 속한 관측치의 개수를 확인하는 함수이다.
length(df) # 데이터프레임 자체를 지정하면 열의 개수
## [1] 10
length(df$carat) # 데이터프레임 내의 변수를 지정하면 행의 개수
## [1] 5000
## 기본적으로 R에 내장된 함수들을 이용하여 변수를 만들고 목록화하기(indexing)

## index라는 새로운 변수를 만들고 행의 수로 연번 매기기
df$index <- 1:nrow(df) 

## 맨 위의 몇 개 행을 보여줍니다.
head(df)               
## 데이터의 구조를 깔끔하게 보여주는 함수입니다.
glimpse(df)            
## Rows: 5,000
## Columns: 11
## $ carat   <dbl> 0.23, 0.21, 0.23, 0.29, 0.31, 0.24, 0.24, 0.26, 0.22, 0.23,...
## $ cut     <ord> Ideal, Premium, Good, Premium, Good, Very Good, Very Good, ...
## $ color   <ord> E, E, E, I, J, J, I, H, E, H, J, J, F, J, E, E, I, J, J, J,...
## $ clarity <ord> SI2, SI1, VS1, VS2, SI2, VVS2, VVS1, SI1, VS2, VS1, SI1, VS...
## $ depth   <dbl> 61.5, 59.8, 56.9, 62.4, 63.3, 62.8, 62.3, 61.9, 65.1, 59.4,...
## $ table   <dbl> 55, 61, 65, 58, 58, 57, 57, 55, 61, 61, 55, 56, 61, 54, 62,...
## $ price   <int> 326, 326, 327, 334, 335, 336, 336, 337, 337, 338, 339, 340,...
## $ x       <dbl> 3.95, 3.89, 4.05, 4.20, 4.34, 3.94, 3.95, 4.07, 3.87, 4.00,...
## $ y       <dbl> 3.98, 3.84, 4.07, 4.23, 4.35, 3.96, 3.98, 4.11, 3.78, 4.05,...
## $ z       <dbl> 2.43, 2.31, 2.31, 2.63, 2.75, 2.48, 2.47, 2.53, 2.49, 2.39,...
## $ index   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, ...

데이터를 좀 더 자세하게 들여다보기

## tibble 유형의 데이터 df의 첫 12개 행을 보여줍니다.
print(df, n = 12) 
## # A tibble: 5,000 x 11
##    carat cut       color clarity depth table price     x     y     z index
##    <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl> <int>
##  1 0.23  Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43     1
##  2 0.21  Premium   E     SI1      59.8    61   326  3.89  3.84  2.31     2
##  3 0.23  Good      E     VS1      56.9    65   327  4.05  4.07  2.31     3
##  4 0.290 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63     4
##  5 0.31  Good      J     SI2      63.3    58   335  4.34  4.35  2.75     5
##  6 0.24  Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48     6
##  7 0.24  Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47     7
##  8 0.26  Very Good H     SI1      61.9    55   337  4.07  4.11  2.53     8
##  9 0.22  Fair      E     VS2      65.1    61   337  3.87  3.78  2.49     9
## 10 0.23  Very Good H     VS1      59.4    61   338  4     4.05  2.39    10
## 11 0.3   Good      J     SI1      64      55   339  4.25  4.28  2.73    11
## 12 0.23  Ideal     J     VS1      62.8    56   340  3.93  3.9   2.46    12
## # ... with 4,988 more rows
## 마지막 5개 행을 제외한 행을 보여줍니다.
df %>% slice(nrow(df) - 5:nrow(df))  
## 뒤에서부터 12개 행을 보여줍니다.
df %>% slice(nrow(df):12)            
## 원하는 행만을 보여줍니다.
df %>% slice(c(1, 7, 54))            

결측치 확인하기

데이터 안에 결측치(NA, missing data)가 몇 개나 있는지를 확인하는 것은 중요합니다. 실제로 통계분석을 수행할 때에는 변수에 결측치가 하나라도 존재하면 최종 모형에서 그 결측치가 속한 행 전체는 분석에서 제외됩니다. 따라서 분석 모형에 있어서 표본의 수(sample size)라는 측면에서 생각해볼 때, 전체 관측치의 개수만큼이나 결측치가 얼마나 되는 것을 파악하는 것도 중요합니다.

## 결측치의 수를 계산하는 함수
sum(is.na(df)) 
## [1] 0
## 위와 동일한 함수
df %>% is.na() %>% sum() 
## [1] 0
## 만약 결측치가 있었다면?
## 모든 값이 NA인 행 missing을 만들어보겠습니다.
df %>% mutate(missing = NA) %>% is.na() %>% sum()
## [1] 5000
## 결과적으로 이전 df에 비해 missing column의 관측치 5000개를
## NA라고 세고 있는 것을 확인할 수 있습니다.

df라는 tibble 을 가지고 is.na(), 즉 dfNA인 것들만 골라서 sum()으로 더하라는 코드입니다.

  • 파이프 함수에서 ()의 빈괄호는 앞의 데이터를 그대로 받아넘기는 기능입니다.

  • 그리고 is.na() 함수는 논리형으로 “()에 들어간 객체에 결측치가 있는가?”를 묻습니다.

  • 결측치가 있으면 TRUE, 없으면 FALSE로 나올 것이고, R에서 TRUE = 1, FALSE = 0 입니다.

  • 그렇게 나온 TRUE들의 총합을 구하면 df라는 데이터 안의 결측치의 총 개수를 확인할 수 있습니다.

    • 그렇다면 왜 총합을 구하는 함수 내에 na.rm = T라는 옵션을 사용하지 않은 것일까요?
    • 보통 R에서 sum() 함수는 그 객체에 결측치가 하나라도 있으면 전체 계산을 NA로 반환합니다.
    • 그러나 이 경우에서는 파이프를 통해서 df에서 결측치/관측치를 각각 1, 0으로 변환시켰기 때문에
    • 더 이상 결측치도 missing data, NA가 아닌 숫자형으로 간주되기 때문에 바로 더할 수 있습니다.

그렇다면 이제는 각 열마다 (변수마다) 관측치가 몇이나 있는지를 확인해보겠습니다.

df %>% map_int( function(x) is.na(x) %>% sum() %>% as.integer() )
##   carat     cut   color clarity   depth   table   price       x       y       z 
##       0       0       0       0       0       0       0       0       0       0 
##   index 
##       0

위의 함수는 dfmap_int 함수로 넘기되, 이 함수는 만약 x라는 객체가 관측치이거든 총합을 구해 그 결과를 정수형(integer)로 반환하라는 코드입니다. 즉, 이 경우 df에 관측치가 있으면 그 관측치의 총합을 더하여 숫자로 바꾼 결과를 출력할 것입니다. 변수별로 관측치의 개수를 확인할 수 있는 함수입니다.

## 위와 동일하지만 double 유형의 데이터로 반환하는 함수입니다.
df %>% map_dbl( function(x) is.na(x) %>% sum() ) 
##   carat     cut   color clarity   depth   table   price       x       y       z 
##       0       0       0       0       0       0       0       0       0       0 
##   index 
##       0
## 자료유형 integer
typeof(df %>% map_int( function(x) is.na(x) %>% sum() %>% as.integer() )) 
## [1] "integer"
## 자료유형 double
typeof(df %>% map_dbl( function(x) is.na(x) %>% sum() ))                  
## [1] "double"
## 만약 변수명을 따로 보기 싫다면? unname() 함수 추가
df %>% map_int( function(x) is.na(x) %>% 
                  sum() %>% as.integer() ) %>% unname()
##  [1] 0 0 0 0 0 0 0 0 0 0 0

unname() 함수에 대해 조금 더 알아보겠습니다.

## 벡터 객체는 값에 라벨링을 할 수 있습니다.
c(one = 1, two = 2, three = 3)         
##   one   two three 
##     1     2     3
# unname()을 사용하면 그 라벨링을 제외한 순수한 요소의  값만을 확인할 수 있습니다.
unname(c(one = 1, two = 2, three = 3)) 
## [1] 1 2 3

특정 벡터 내에 결측치의 개수가 몇 개인지를 구하는 코드를 함수로 만들어서 함수 객체의 형태로 저장해보도록 하겠습니다.

get_number_of_missings_in_vector <- function(some_vector) {
  result <- some_vector %>%
    is.na() %>%
    sum() %>%
    as.integer()
  return(result)
}
get_number_of_missings_in_vector(df)
## [1] 0

get_number_of_missings_in_vector() 함수는 아까 위의 함수(주어진 객체의 결측치 확인, 총합 계산, 정수형 반환)를 result라는 객체에 저장하고 반환하라는 코드를 내장하고 있습니다. 따라서 우리는 앞서와 마찬가지로 df에 결측치가 없다는 0을 반환하게 됩니다.

앞서 말했다시피 데이터프레임과 같은 형식의 자료에 NA가 있으면, R은 결측치를 제외할 때, 측치가 속한 행을 아예 삭제해버립니다.

df %>% na.omit()

중요한 점은 R은 단지 함수적인 프로그래밍 언어이기 때문에 그 결측치를 제거한 이후에 별도로 저장해주지 않으면 다시 불러오는 객체 df는 결측치가 제거되지 않은 원래의 형태로 다시 불려오게 됩니다. 즉, new_df <- df %>% na.omit()와 같은 식으로 재저장 해주어야만 결측치가 제거된 데이터를 가지게 됩니다.

데이터에 결측치가 있을 때, 그 결측치들을 제외하게 되면 어떻게 되는지 한 번 아래의 예제 코드로 확인해보도록 하겠습니다.

example <- matrix(c(1, 2, 3, NA), nrow = 2, byrow = T) %>%
  as.data.frame()
example %>% na.omit()
## 결측치를 제거한 example 데이터를 재저장 하지 않았기 때문에 결측치가 그대로 남아있습니다.
example  
example <- example %>% na.omit()

## 결측치가 제거되어 있는 것을 확인할 수 있습니다.
example  

한 가지 강조할 것은 모든 dplyr 함수는 (아마 거의 모든 tidyverse 함수들은) 첫 번째로 데이터를 지정하여 파이프로 넘기면 그 이후의 파이프들은 맨 처음의 데이터를 그대로 가지고 후속 함수들을 그 데이터에 순차적으로 적용하는 과정을 거치게 된다는 것입니다.

dplyr패키지의 주요 함수들

dplyr::filter(): 필터 함수

dplyr::filter() 함수는 () 안에 설정하는 조건문에 따라서 관측치를 필터링합니다. 이때, 조건문은 논리형 연산자로 기능하는데, 조건에 따라 투입값이 참(TRUE)인지 거짓(FALSE)인지 반환합니다. R에는 dplyr 패키지 말고도 다른 filter() 함수를 가지고 있기 때문에 ::의 로딩 함수를 가지고 dplyr 패키지의 filter() 함수를 직접 불러오는 것이 오류를 줄일 수 있는 방법입니다.

## df 데이터의 cut이라는 변수가 Ideal이라는 값을 가질 경우만 보여주면,
df %>% dplyr::filter(cut == "Ideal")  
## 조건을 여러 개 걸 수도 있습니다.
df %>% dplyr::filter(cut %in% c("Ideal", "Premium")) 
## %in% 함수는 우측에 지정한 객체가 좌측에 포함되어 있냐는 것을 묻는 논리형의 기능을 수행합니다.

## %in% 함수를 자세히 알아보겠습니다.
## names_라는 객체에 세 이름이 있을 때, 
names_ <- c("Sara", "Robert", "James") 

## names_안에 James라는 이름이 있으면?
"James" %in% names_                    
## [1] TRUE

dplyr::select(): 선택 함수

dplyr::select() 함수는 데이터 안에서 특정한 변수만을 선택하고자 할 때 사용할 수 있습니다. 데이터를 관리하고 전처리를 할 때 굉장히 유용하게 사용할 수 있는 함수이다. 예를 들어, World Development Indicators에서 전체 변수 중 필요한 변수만을 선택하여 새로운 데이터로 재저장할 수 있는 것입니다.

## df 데이터 중 carat ,color, x 변수만 선택합니다.
df %>% select(carat, color, x)            
## carat과 color를 제외한 나머지 변수들만 선택합니다.
df %>% select(-carat, -color)          
## 변수의 순서 정리: depth, price, 나머지는 그대로 둡니다.
df %>% select(depth, price, everything()) 
## 숫자형형 변수들만 남깁니다.
df %>% select_if(is.numeric)            

위의 예제에서 눈여겨볼 만한 것은 바로 세 번째 select() 함수 내에서 작동하는 everything()함수와 select_if()라는 변형 함수입니다.

  • 만약 everything() 함수가 없었다면 변수들의 이름을 줄줄이 나열해야 해서 select() 함수의 효용이 많이 떨어졌을 것입니다.
  • 그리고 select_if()는 조건문을 반영할 수 있습니다.
  • -를 통해서 변수를 제외하는 여집합적 구성도 가능합니다(caratcolor만 제외하는 것처럼).

select() 함수를 조금 더 자세하게 알아보겠습니다.

## 인덱싱 기능을 이용하여 열번(number of columns)을 이용해 select()를 활용해보겠습니다.
## 첫 번째부터 세 번째 변수만을 선택하겠습니다.
df %>% select(1:3)         
## 첫 번째, 두 번째, 여섯 번째 변수를 제외하고 나머지를 선택하겠습니다.
df %>% select(-c(1, 2, 6)) 
## select() 함수는 범용성이 높다. 변수명이 어떤 글자로 시작하는지, 끝나는지, 
## 혹은 어떤 글자를 포함하는지에 따라서도 select()를 적용하여 변수를 선택할수 있습니다.
df %>% select(starts_with("c"))
df %>% select(ends_with("y"))
df %>% select(contains("olo"))
## 마찬가지로 제외하는 함수(-)도 적용됩니다.
df %>% select(-contains("olo"))

select() 함수를 이용해서 변수를 선택-추출해내는 것 외에도 변수 이름을 변경하는 것도 가능합니다. 단, 이때 everything() 함수를 지정해주는 것을 잊어서는 안 됩니다. 왜냐하면 everything() 없이 변수명만 바꿔버리면 select()는 바꾼 그 변수들만을 출력하고 나머지 변수들은 제외해버리기 때문입니다.

물론 그렇게 select() 함수를 적용하고 별도로 저장하지 않으면 df 자체에는 변화가 없기 때문에 다시 everything()을 추가해서 코드를 작동시키고 다른 객체로 저장하면 됩니다.

  • 그러면 바뀐 변수 + 바꾸지 않은 다른 변수들이 new_df 라던지 다른 객체 이름으로 저장될 것입니다.
  • 그리고 이때, 바뀐 함수들이 먼저 오고 그 다음으로 다른 변수들이 순서대로 이어지게 됩니다.
## 새 변수 + 기존 변수
df %>% select(new_depth = depth, new_color = color, everything())  
## 기존 변수들의 목록에서 지정한 변수들은 새 이름으로 변경됩니다.
## 다만 순서는 그대로입니다.
## 논리상: everything() 모든 변수들을 선택한 상태에서
##         그 중 depth와 color의 이름을 변경하라는 의미입니다.
df %>% select(everything(), new_depth = depth, new_color = color) 
## select()로도 변수명을 바꿀 수 있지만, rename()을 이용하면 굳이 everything() 안쓰고도
## 간단하게 할 수 있습니다. 역시 편법은 쓰는 게 아닙니다.
df %>% rename(new_depth = depth, new_color = color)
## 한 번에 모든 변수들의 이름을 일괄적으로 변경할 수도 있습니다.
df %>% rename_all(function(x) str_c(x, "_new"))
df %>% rename_all(function(x) str_to_upper(x))
## 특정한 조건을 가진 변수들만 이름을 변경할 수 있습니다.
df %>% rename_at(             # rename_at()으로 조건을 특정합니다.
  vars( starts_with("c") ),   # "c"로 이름이 시작하는 변수들을 대상으로
  function(x) str_to_upper(x) # 어떻게? 모두 변수명을 대문자로 바꿉니다.
)
## rename_if() 함수를 이용하면 특정 조건을 충족하는 변수들의 이름을 변경할 수 있습니다.
## 숫자형인 변수들의 이름을 대문자로 바꿔라.
df %>% rename_if( is.numeric, str_to_upper ) 
## 이 경우 문자형 값을 가지는 color, cut 등은 변수명이 바뀌지 않는 것을 확인할 수 있습니다.

dplyr::mutate(): 변수 조작 함수

dplyr::mutate()는 데이터 전처리 및 관리에서 가장 요긴하게 쓰일 함수입니다. 이 함수는 새로운 변수를 만들거나 기존 변수에 조작을 가할 때 사용합니다.

## 기존 carat 변수에 10배가 된 값을 가진 새로운 변수 carat_multiplied를 만들도록 하겠습니다.
df %>% mutate(carat_multiplied = carat * 10) 
## 기존 carat 변수의 값에 10배를 곱하겠습니다.
df %>% mutate(carat = carat * 10)            
## 첫 번째 코딩과 두 번째 코딩의 차이점은 첫 번째 코딩은 기존 변수를 이용해
## 새 변수를 만든 것이고, 두 번째 코딩은 기존 변수 자체의 값을 바꾸어 버린 것입니다.

하나의 mutate() 함수 내부에 여러 줄의 코드를 통해서 순서대로 변수를 조작할 수도 있습니다.

df %>% select(carat) %>%
  mutate(
    caret_times_2 = carat * 2,
    caret_times_2_times_2 = caret_times_2 * 2,
    caret_times_2_times_2_times_3 = caret_times_2_times_2 * 3
  )
## mutate() 함수 역시 _at, _all, _if의 세부함수를 가집니다.
## 변수가 요인형이거든 문자형으로 바꿉니다.
df %>% mutate_if(is.factor, as.character)  
## color, clarity 변수를 문자형으로 바꿉니다.
df %>% mutate_at(vars( color, clarity ), as.character) 
## 모든 변수들을 문자형으로 바꿉니다.
df %>% mutate_all(as.character) 

다른 dplyr 패키지의 유용한 함수들

arrange(): 변수의 값을 정렬할 때 쓰는 함수

df 데이터에서 price라는 함수 + 나머지 다른 함수로 순서를 재정리하고, 그 다음에 price를 기준으로 변수를 정렬해보도록 하겠습니다. arrange()의 디폴트 값은 오름차순입니다. 내림차순으로 바꾸고 싶으면 desc() 함수를 사용하면 됩니다.

## price 기준으로 오름차순 정렬
df %>% select(price, everything()) %>% arrange(price) 
## price 기준 내림차순 정렬
df %>% select(price, everything()) %>% arrange(desc(price)) 
## color, cut을 맨 앞으로 빼고 전체 변수는 price 기준으로 내림차순 정렬
df %>% arrange(color, cut, desc(price))  

바로 위의 코딩은 관심있는 주요 변수를 맨 앞으로 빼고 주요 변수들이 다른 변수(price)의 크기에 따라 어떻게 나타나는지를 파악할 수 있게 해주는 코드입니다. select()를 사용하지 않고 바로 arrange()를 적용하였습니다. 예를 들어, 정치체제(민주주의/비민주주의) 변수를 앞으로 빼고 정렬 기준을 GDPpc로 하는 등 응용이 가능핳ㅂ니다.

group_by(): 집단별 묶음

group_by()를 쓰면 함수 내의 같은 변수값별로 묶인 결과를 보여줍니다. 숫자형, 문자형 모두 적용됩니다. 즉, 만약 group_by(price)로 하면 변수들이 같은 가격별로 묶여서 보일 것이고, 아래와 같이 group_by(cut)을 한다면 다이아몬드 컷팅 유형별로 분류해서 보여줄 것입니다.

유의할 점은 먼저 group_by()를 지정해주고 그 이후에 다른 함수를 사용하면 집단별로 묶인 상태에서 그 함수들이 순차적으로 적용된다는 점입니다. group_by()를 사용했을 때와 그렇지 않을 때를 구분해보도록 하겠습니다. 내림차순된 가격 변수를 기준으로 첫째 행과 둘째 행, 즉 가장 비싼 가격과 두 번째로 비싼 가격만을 잘라서(slice(1 : 2)) 보여주라는 명령어입니다.

df %>% group_by(cut) %>%     # df의 cut 변수 유형별로 묶었습니다.
  ## 그렇게 묶인 데이터가 파이프로 넘어가고, 가격 기준 내림차순 정렬
  arrange(desc(price)) %>%   
  ## 컷팅 유형별 + 가격 기준 내림차순 중 첫 두 행만 보여주라는 코드
  slice(1 : 2)               
## group_by()를 사용하지 않았을 때와 비교해보도록 하겠습니다.
df %>% arrange(desc(price)) %>% slice(1:2)
## 이 경우는 전체 df 데이터에서 가격 순으로 1, 2위의 값만 갖게 됩니다.
## cut 변수는 반영되지 않습니다.

한 가지 유의해야할 점은 티블 유형에 group_by()를 적용할 경우 그 결과가 일반 티블과는 다른 특성을 가지게 된다는 것입니다.

df_group <- df %>% group_by(cut) %>%
  arrange(desc(price)) %>%
  slice(1:2)
class(df_group)
## [1] "grouped_df" "tbl_df"     "tbl"        "data.frame"
df_group

보면 “grouped_df”라는 특성이 추가된 것을 확인할 수 있습니다. 티블과 group_by()를 함께 쓸 때는 ungroup() 함수를 같이 사용할 것을 추천하는데, 이는 다음과 같은 이유에서입니다.

  1. 미리 언급한 바와 같이 grouped_라는 속성이 생김으로써, group_by()가 야기할 수 있는 잠재적인 오류를 피하기 위해서 입니다.

  2. ungroup() 함수를 이용하여 파이프 함수로 구성된 코드가 group_by() 함수가 적용된 것임을 명시적으로 줄 수 있습니다. 따라서 우리는 ungroup() 함수가 코드에 포함되어 있다면 해당 티블이 그룹핑된 결과일 수 있다고 바로 알 수 있습니다. 즉, 코드의 가독성과 명확성이 좋아집니다.

  3. 글로벌 환경을 .Rdata 객체로 저장하여 불러오거나 할 때, group_by() 해놓고 ungroup() 안하면 기록은 남아있지 않는데 해당 티블에 grouped_ 속성이 남아 추후 분석에 어려움이 있을 수 있습니다.

## 따라서 ungroup()을 이용하여 일반적인 티블로 다시 바꿔줍니다.
df_group <- df %>% group_by(cut) %>%
  arrange(desc(price)) %>%
  slice(1:2) %>%
  ungroup() # 원래의 티블로 돌아와!
class(df_group)
## [1] "tbl_df"     "tbl"        "data.frame"

또, dplyr 패키지는 count() 함수도 제공합니다. 이 함수는 데이터의 특정 변수값에 기초해 그 집단 수를 세어 줍니다. 보통 분류형 변수에 많이 사용되지만 숫자형도 적용됩니다. 얘를 들어 1부터 2만에 이르는 범주를 가지는 변수가 총 50만개의 관측치를 가지고 있다고 할 때, 1의 값은 몇 개, 15는 몇 개, 2만은 몇 개와 같은 식으로 범주화를 시켜주는 것입니다.

df %>% count(cut)
## count()함수를 group_by() 함수로 바꾸어서 표현하면 아래와 같습니다.
df %>% group_by(cut) %>% summarise(n = n())
## group_by() 함수는 summarise() 함수와 결합될 경우 다양한 응용이 가능합니다.
## 여기서 summarise는 총계를 구하라는 것이 아니라 데이터를 요약정리해서 보여줄 수 있는
## 여러 함수들을 통칭하는 것입니다.
df %>%
  group_by(cut) %>%
  ## 컷팅 유형별로 평균 가격을 계산하라.
  summarise(price_mean = mean(price)) 

마찬가지로 summarise() 함수도 _if, _at, _all과 같은 세부유형으로 분류하여 사용할 수 있습니다.

df %>%
  select(cut, x, y, z) %>%       # cut, x, y, z 변수만 df 티블에서 뽑아내어
  group_by(cut) %>%              # cut 유형별로 그룹화.
  ## 그리고 각 컷팅유형 별로 x, y, z의 평균을 구하도록 하겠습니다.
  summarise_all(mean, na.rm = T)
## 평균에 더하여 중앙값, 최소값, 최대값도 구해보도록 하겠습니다. 
df %>% group_by(cut) %>%
  summarise_at(            # 특정한 변수인 x, y, z를 대상으로
    vars(x, y, z),         # list 뒤의 함수들을 적용합니다.
    list(mean, median, min, max),
    na.rm = T)   # mean 등은 데이터에 결측치가 있으면 결측치를 반환하므로
                 # 결측치 제거(remove na)가 TRUE이도록 설정합니다.

## 컷팅 유형별로 그룹화한 다음에 숫자형 변수들일 경우에만 평균을 계산해 보겠습니다.
df %>% group_by(cut) %>%
  summarise_if(is.numeric, mean, na.rm = T)
## 동일한 코드이지만 표현식이 조금 다릅니다.
df %>% group_by(cut) %>%
  summarise_if(is.numeric, function(x) mean(x, na.rm = T))
df %>% group_by(cut) %>%
  summarise_if(is.numeric, ~ mean(., na.rm = T))

데이터 결합하기(Join data)

연구를 진행하다보면 하나로 분석에 필요한 모든 변수가 포함된 데이터를 만나기란 하늘에 별 따기라는 것을 알 수 있습니다. 따라서 서로 다른 소스에서 필요한 변수들을 추출해 하나의 데이터로 구성하는, 데이터 결합 작업이 중요합니다. 보통은 머징(merging)이라고도 많이 합니다.

일단 미국의 주 이름 객체 반복추출(replacement)가 가능하도록 설정하고 총 250개의 관측치를 가지는 표본을 만들어 보겠습니다. 변수명이 state_name인 티블을 하나 만들겁니다. 250의 관측치들은 미국의 각 주 이름이 중복되어 존재하게 됩니다.

  • sample() 함수의 replace = T 옵션은 상자 안에서 공을 꺼낼 때, 한 번 꺼낸 공을 다시 집어넣고 다시 꺼낼 수 있다는 것을 의미합니다.
  • 이렇게 반복추출된 state_name 변수는 미국 각 주의 이름이 무작위로 반복추출되어 총 250개의 관측치를 가지게 됩니다.
states_df <- tibble(state_name = sample(state.name, 250, replace = T))
states_df %>% count(state_name)

이번에는 state_name와 미국 주 이름의 약자를 의미하는 state_abb 변수를 만들어보겠습니다. 즉, states_table은 미국의 50개 주의 이름과 약자의 두 변수를 가지고 있는 티블입니다.

states_table <- tibble(
  state_name = state.name, state_abb = state.abb
)
head(states_table)

자, 이제 250개의 관측치를 갖는 state_df 티블과 50개의 관측치 값을 갖는 states_table 티블을 결합해보겠습니다. 기준은 left_join()이므로 states_df가 됩니다. 따라서 우리는 states_df의 모든 관측치를 유지한 채로 states_table의 관측치를 옮겨 붙일 것입니다.

left_join(states_df, states_table) %>% print( n = 10 )
## # A tibble: 250 x 2
##    state_name   state_abb
##    <chr>        <chr>    
##  1 New Mexico   NM       
##  2 Louisiana    LA       
##  3 North Dakota ND       
##  4 Arizona      AZ       
##  5 Indiana      IN       
##  6 Illinois     IL       
##  7 North Dakota ND       
##  8 New Mexico   NM       
##  9 Louisiana    LA       
## 10 Indiana      IN       
## # ... with 240 more rows

이게 가능한 이유는 두 티블 사이에 공통의 변수, state_name이 존재하기 때문입니다. 이 경우는 자동으로 묶였지만 어떤 변수를 기준으로 그룹화할 것인지 지정해줄 수도 있습니다.

left_join(states_df, states_table, by = 'state_name') %>% 
  print( n = 10 )
## # A tibble: 250 x 2
##    state_name   state_abb
##    <chr>        <chr>    
##  1 New Mexico   NM       
##  2 Louisiana    LA       
##  3 North Dakota ND       
##  4 Arizona      AZ       
##  5 Indiana      IN       
##  6 Illinois     IL       
##  7 North Dakota ND       
##  8 New Mexico   NM       
##  9 Louisiana    LA       
## 10 Indiana      IN       
## # ... with 240 more rows

이외에도 right_join(), inner_join(), full_join(), 그리고 anti_join()과 같은 함수로 결합할 수도 있습니다. 자세한 내용은 tidyverse 패키지 중 결합(join)에 관한 내용에서 살펴볼 수 있습니다. 결합, 머징에 관한 내용은 추후 더 구체적으로 다루어 보도록 하겠습니다.

일단 예시로 anti_join() 함수가 어떻게 쓰이는지 보겠습니다. anti_join()은 대개 텍스트 분석에서 사용됩니다.

text_df <- tibble(
  text = c('the fox is brown and the dog is black and the rabbit is white')
)
library(tidytext)
text_df <- text_df %>%
  unnest_tokens(word, text) # text를 어절로 분해
text_df
## 특정 어절은 제외하고 단어만.
text_df %>% anti_join(tidytext::stop_words, by = 'word') 

변수 재코딩(recoding)

먼저 18세부터 90세 사이의 연령 중 25개를 표본으로 뽑아보겠습니다. 이때, 반복추출이 가능하게 설정합니다.

  1. if else()
test <- tibble(age = sample(c(18:90, NA), 25, T))
test <- test %>%
  mutate(
    age_bins <- if_else(
      is.na(age), NA_character_, # NA_character를 써주는 이유는
      if_else( # 한 벡터는 하나의 자료유형만 가질 수 있기 때문입니다.
        age < 25, "Young", # 여기서 다른 값들은 모두 문자열인데, NA만 논리형입니다.
        ifelse(  # 따라서 문자형 NA를 가지라고 지정해주는 것입니다.
          age > 75, "Old", "Middle Aged" # dplyr의 if_else()의 특징입니다.
        )                                # R에 내장된 기본함수 ifelse()는 자료유형을
                                         # 구분하지 못합니다.
      ) # if_else()는 다른 자료유형이 섞이면 error를 보여줍니다.
    )
  )
test

원하는 결과를 얻지만 만약 변수가 많아지면 조건이 많아지면서 조건을 설정하는 것과 코드를 읽는 것이 불편하다는 단점이 있습니다.

  1. case_when()

좀 더 코드가 읽기 편한 case_when()을 소개하겠습니다.

test <- test %>%
  mutate(
    age_bins = case_when(
      age < 25 ~ "Young",
      age >= 25 & age < 75 ~ "Middle Aged",
      age >= 75 ~ "Old", 
      TRUE ~ NA_character_ # 위의 조건에 위배되는 경우가 맞다면, 
                           # NA_character_를 부여합니다.
    )
  )