magrittr - Ceci n'est pas une pipe

by Minho Lee — on

cover-image

magrittr

dplyr 이후로 Hadley Wickham의 패키지들은 %>%라는 연산자를 굉장히 많이 사용한다. 파이프 연산자를 사용하면 코드의 가독성을 향상시키면서 생산성도 높이고, 유지 보수에도 굉장히 도움이 많이 된다. 그런데 pipe operator를 모르는 사람에게 코드를 보여주면 코드자체가 암호문으로 변해버리는 단점?이 있었다. 그래서 파이프 연산자의 사용법을 간단하게 설명하면서 magrittr 패키지의 기능들을 일부 설명해보려고 한다.


기본적인 pipe의 이용

  • fun(x,y,z)x %>% fun(y,z)와 같다

  • 파이프를 기준으로 왼쪽에 있는 오브젝트(또는 연산의 결과물)는 파이프 오른쪽의 연산에 가장 첫 번째 인자로 들어간다. 만약 첫 번째가 아닌 다른 위치로 보내려고 한다면 . 으로 위치를 표시한다. 따라서 y %>% fun(x, ., z)fun(x, y, z)와 같다

  • 파이프는 연속적으로 적용할 수 있기 때문에 중첩된 함수를 쉽게 표현할 수 있다. fun3(fun2(fun1(x)))x %>% fun1 %>% fun2 %>% fun3과 같다


실제로 pipe연산이 어떻게 이용되는지 보기 위해서 다음과 같은 연산 과정을 생각해보자

  1. iris 데이터에서 Sepal.Length와 Species 열만 선택한다

  2. Species에 대해 group_by 지정한다

  3. Sepal.Length의 평균과 표준편차를 계산하여 정규화시킨다

  4. 계산과정을 위해 생성한 열을 제거한다

library(magrittr)
library(dplyr)
select(
  mutate(
    group_by(
      select(iris, Sepal.Length, Species), 
      Species),
    sp_mean = mean(Sepal.Length),
    sp_sd = sd(Sepal.Length),
    norm = (Sepal.Length - sp_mean) / sp_sd
    ), 
  -sp_mean, -sp_sd)
## Source: local data frame [150 x 3]
## Groups: Species
## 
##    Sepal.Length Species        norm
## 1           5.1  setosa  0.26667447
## 2           4.9  setosa -0.30071802
## 3           4.7  setosa -0.86811050
## 4           4.6  setosa -1.15180675
## 5           5.0  setosa -0.01702177
## 6           5.4  setosa  1.11776320
## 7           4.6  setosa -1.15180675
## 8           5.0  setosa -0.01702177
## 9           4.4  setosa -1.71919923
## 10          4.9  setosa -0.30071802
## ..          ...     ...         ...
  • 우선 문제가 되는 것은 연산과정에서 맨 먼저 적용되는, 중심 데이터의 위치를 찾기가 어렵다

  • 또, 중심이 되는 iris 데이터를 기준으로 해서 함수들이 중첩되어 있다.

  • 그래서 코드를 읽으려면 코드 덩어리에 중심에서부터 한 단계씩 밖으로 나오게 되는데 중심을 기준으로 함수명은 앞에, 옵션은 뒤에 있기 때문에 코드를 보고 내용을 이해하기가 힘들어진다

  • 위와 같은 상황에서는 가독성을 위해 코드를 중간중간 끊어서 사용하게 되는데, 그러면 불필요한 임시변수들이 많이 생길 수 있다

위 코드를 pipe를 이용해서 정리하면 다음과 같다

iris %>%
  select(Sepal.Length, Species) %>%
  group_by(Species) %>%
  mutate(sp_mean = mean(Sepal.Length),
         sp_sd = sd(Sepal.Length),
         norm = (Sepal.Length - sp_mean) / sp_sd) %>%
  select(-sp_mean, -sp_sd)
## Source: local data frame [150 x 3]
## Groups: Species
## 
##    Sepal.Length Species        norm
## 1           5.1  setosa  0.26667447
## 2           4.9  setosa -0.30071802
## 3           4.7  setosa -0.86811050
## 4           4.6  setosa -1.15180675
## 5           5.0  setosa -0.01702177
## 6           5.4  setosa  1.11776320
## 7           4.6  setosa -1.15180675
## 8           5.0  setosa -0.01702177
## 9           4.4  setosa -1.71919923
## 10          4.9  setosa -0.30071802
## ..          ...     ...         ...
  • 일단 코드가 훨씬 깔끔하게 정리되었다

  • 또 함수와 해당 함수의 옵션 및 추가적인 인수들을 같은 줄에서 관리할 수 있다

  • 시작이 되는 데이터가 iris 라는 것이 명확하게 보이고, 우리가 처음에 생각했던 연산 순서대로 함수를 볼 수 있게 되었다


dplyr 등의 패키지는 자동으로 %>% 함수를 불러오기 때문에 magrittr 패키지를 굳이 불러오지 않더라도 많이 사용하게 되는데 magrittr 패키지를 불러오면 기본 pipe 이외에도 몇 가지 추가적인 기능을 제공하는 pipe operator와 pipeline을 통한 코드 작성을 도와주는 함수들을 더 제공한다


다양한 파이프 연산자들

‘tee’ operator %T>%

pipe를 기준으로 왼쪽에 있는 연산은 값을 반환하지만 오른쪽에 있는 연산은 값을 반환하지 않는다. 따라서 tee opeator 다음에는 print, plot 등의 함수를 사용해서 연산 중간에 자연스럽게 부수적인 결과물을 출력시킬 수 있다

rnorm(5) %T>%
  print %>%
  mean
## [1]  0.4826458 -0.8295812  0.1662653  0.0854539  0.9908439
## [1] 0.1791255

위 코드는 표준정규분포를 따르는 난수 5개를 발생시키고

해당 난수를 print함수를 이용해 출력한 다음에 5개 난수의 평균을 내보낸다.

rnorm(5)에서 나온 값이 print와 mean에 반영된다

iris %>%
  head(n=3) %T>%
  plot %>%
  select(Sepal.Length)

##   Sepal.Length
## 1          5.1
## 2          4.9
## 3          4.7

위 코드에서는 iris 데이터의 1~3행에 대해서 plot을 그리고 Sepal.Length 열만 선택한다

plot 함수는 기본적으로 데이터를 반환하지 않기 때문에 pipeline의 중간에 plot을 넣기 위해 tee operator를 사용했다

‘exposition’ operator %$%

with 함수의 축약버전이다. pipe 기준으로 왼쪽에 있는 오브젝트 내의 name을 pipe 오른쪽의 연산에서 오브젝트 처럼 사용할 수 있게 해준다

다음 세 가지 코드는 같은 결과물을 출력한다

cor(iris$Petal.Length, iris$Petal.Width)
## [1] 0.9628654
with(iris,
     cor(Petal.Length, Petal.Width))
## [1] 0.9628654
iris %$%
  cor(Petal.Length, Petal.Width)
## [1] 0.9628654

compound assignment operator %<>%

chain의 맨 처음에서 사용될 수 있다. pipeline의 결과물을 operator 왼쪽의 오브젝트에 저장한다

data <- data %>% func1 %>% func2data %<>% func1 %>% func2 와 같다

c(1:10)의 값을 모두 로그변환을 하고 소수점 둘째 자리까지만 남도록 반올림한다면

x <- 1:10
x <- round(log(x),2)
print(x)
##  [1] 0.00 0.69 1.10 1.39 1.61 1.79 1.95 2.08 2.20 2.30
x <- 1:10
x %<>% log %>%
  round(2)
print(x)
##  [1] 0.00 0.69 1.10 1.39 1.61 1.79 1.95 2.08 2.20 2.30

기본연산을 대체하는 함수들

magrittr 패키지에서는 다양한 기본 연산을 pipeline에 쉽게 적용할 수 있도록 다양한 기본 연산에 대한 대체 함수를 제공한다

예를 들면, iris %>% select(Species) 는 데이터프레임의 형태로 Species 를 반환하는데 iris$Species 처럼 단순하게 벡터 형태로 반환해야 한다면 '$' 함수를 직접 호출하여 이용할 수도 있지만 magrittr의 use_series 함수를 이용할 수 있다

따라서 아래 두 연산은 동일하다

mtcars %>%
  '$'('mpg')
##  [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2
## [15] 10.4 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4
## [29] 15.8 19.7 15.0 21.4
mtcars %>%
  use_series(mpg)
##  [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2
## [15] 10.4 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4
## [29] 15.8 19.7 15.0 21.4

'==' 연산에 대한 함수로 equals 함수를 제공한다 결과물이 TRUE, FALSE로 반환되기 때문에 dplyr::filter 등의 함수에서도 적용할 수 있다

letters %>%
  equals('a')
##  [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [12] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [23] FALSE FALSE FALSE FALSE
mtcars %>%
  filter(cyl %>% equals(4))
##     mpg cyl  disp  hp drat    wt  qsec vs am gear carb
## 1  22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
## 2  24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
## 3  22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2
## 4  32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
## 5  30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
## 6  33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
## 7  21.5   4 120.1  97 3.70 2.465 20.01  1  0    3    1
## 8  27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
## 9  26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
## 10 30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
## 11 21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2

gcookbook 패키지의 aapl데이터에서 2012년의 자료만 필터링하려면 다음과 같이 해볼 수 있다.

아래 두 가지 방법은 동일한 결과물을 출력한다

library(gcookbook)
aapl %>% 
  filter(strftime(date, '%Y') == '2012') %>%
  head
##         date adj_price
## 1 2012-01-06    420.59
## 2 2012-01-13    418.02
## 3 2012-01-20    418.50
## 4 2012-01-27    445.37
## 5 2012-02-03    457.71
## 6 2012-02-10    491.31
aapl %>%
  filter(date %>% strftime('%Y') %>% equals(2012)) %>%
  head
##         date adj_price
## 1 2012-01-06    420.59
## 2 2012-01-13    418.02
## 3 2012-01-20    418.50
## 4 2012-01-27    445.37
## 5 2012-02-03    457.71
## 6 2012-02-10    491.31

이처럼 다양한 연산에 대한 대체 함수를 이용하면 pipeline 위주로 코드를 작성할 때 도움이 될 수 있다

함수들의 목록은 다음과 같다


함수를 pipe의 형태로 적용하면 중복되는 pipe 연산들에 동시에 적용할 수도 있다

iris %>%
  group_by(Species) %>%
  summarise(cnt = n())
## Source: local data frame [3 x 2]
## 
##      Species cnt
## 1     setosa  50
## 2 versicolor  50
## 3  virginica  50
count_group <- . %>% group_by(Species) %>% summarise(cnt = n())

iris %>%
  count_group
## Source: local data frame [3 x 2]
## 
##      Species cnt
## 1     setosa  50
## 2 versicolor  50
## 3  virginica  50

동일한 연산 과정들이 반복되거나, 구조가 같은 여러 자료에 공통적으로 적용할 수 있는 연산들의 경우에는 이러한 방법으로 함수를 작성하여 반복되는 연산을 간단하게 표현할 수 있다.

magrittr패키지에 대해 더 알아보려면 패키지 내에 존재하는 vignette 문서를 살펴보거나 다음 문서 등을 읽어보면 보움이 될 것이다

Comments