dplyr advanced - Standard Evaluation

by Minho Lee — on

cover-image

Non-standard Evaluation

dplyr은 기본적으로 non-standard evaluation (NSE)을 사용하여 연산을 처리한다

사실 이 글을 쓰는 지금도 NSE가 정확히 어떤 것인지는 잘 모르겠다…

모든 값을 계산하지 않고 필요할 때만 계산하는? 것이라고 알아들었기는 하지만 정확한 동작 방식은 정말 모르겠다..

내부적으로 어떻게 다른지는 알 수 없지만 한 가지 눈에 보이는 확실한 차이점은 존재한다


NSE는 아래 코드처럼 column name을 하나의 object 처럼 사용한다

library(dplyr)
iris %>% 
  head() %>% 
  mutate(sl2 = 2 * Sepal.Length)
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  sl2
## 1          5.1         3.5          1.4         0.2  setosa 10.2
## 2          4.9         3.0          1.4         0.2  setosa  9.8
## 3          4.7         3.2          1.3         0.2  setosa  9.4
## 4          4.6         3.1          1.5         0.2  setosa  9.2
## 5          5.0         3.6          1.4         0.2  setosa 10.0
## 6          5.4         3.9          1.7         0.4  setosa 10.8
iris %>% 
  group_by(Species) %>% 
  summarise(mean = mean(Sepal.Length))
## Source: local data frame [3 x 2]
## 
##      Species  mean
##       (fctr) (dbl)
## 1     setosa 5.006
## 2 versicolor 5.936
## 3  virginica 6.588

반면에 r의 기본 함수([, $ 등)들은 column name을 문자열의 형태로 받는다

iris %>% 
  head() %>% 
  '['('Sepal.Length') 
##   Sepal.Length
## 1          5.1
## 2          4.9
## 3          4.7
## 4          4.6
## 5          5.0
## 6          5.4

dplyr에서는 filter, mutate, summarise, arrange, select, group_by 등의 함수에서 nse를 사용한다

이들 함수는 각각 se버전을 가지고 있는데, 함수 이름 뒤에 _ 를 붙이면 된다

filter_, mutate_, summarise_, arrange_, select_, group_by_ 등이 각 함수의 se 버전이다


<br / > <br / >

Standard Evaluation

se버전의 함수를 사용할 때는 함수의 input값을 따옴표로 감싸진 문자열로 넣게 된다

se버전에서 함수에 인자를 넘기는 방법은 크게 세 가지가 있다

# String
iris %>% 
  head %>% 
  mutate_('mean(Sepal.Length)')
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
##   mean(Sepal.Length)
## 1               4.95
## 2               4.95
## 3               4.95
## 4               4.95
## 5               4.95
## 6               4.95
# quote()
iris %>% 
  head %>% 
  mutate_(quote(mean(Sepal.Length)))
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
##   mean(Sepal.Length)
## 1               4.95
## 2               4.95
## 3               4.95
## 4               4.95
## 5               4.95
## 6               4.95
# formula
iris %>% 
  head %>% 
  mutate_(~mean(Sepal.Length))
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
##   mean(Sepal.Length)
## 1               4.95
## 2               4.95
## 3               4.95
## 4               4.95
## 5               4.95
## 6               4.95

이 중에서 가장 좋은 방법은 formula를 쓰는 것이다

따옴표나 quote를 사용할 때는 해당 입력값이 어느 environment에 존재하는지 못찾게 되는 경우도 있다

divide_by_two = function(num) return(num/2)

# 제대로 실행된다
iris %>% 
  head %>% 
  mutate_(~divide_by_two(Sepal.Length))
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
##   divide_by_two(Sepal.Length)
## 1                        2.55
## 2                        2.45
## 3                        2.35
## 4                        2.30
## 5                        2.50
## 6                        2.70
# 오류 발생
iris %>% 
  head %>% 
  mutate_('divide_by_two(Sepal.Length)')
Error: could not find function "divide_by_two"
# 오류 발생
iris %>% 
  head %>% 
  mutate_(quote(divide_by_two(Sepal.Length)))
Error: could not find function "divide_by_two"



여러 가지 input을 한꺼번에 입력하거나, 변수명을 지정해야 할 때에는 .dots argument에 값을 넘기면 된다

setNames 함수를 사용해서 함수의 내용과 변수명을 함께 지정해주면 된다

iris %>% 
  summarise_(.dots = setNames(c('mean(Sepal.Length)', 'sd(Sepal.Length)'), c('mean', 'sd')))
##       mean        sd
## 1 5.843333 0.8280661
iris %>% 
  summarise_(.dots = setNames(c(~mean(Sepal.Length), ~sd(Sepal.Length)), c('mean', 'sd')))
##       mean        sd
## 1 5.843333 0.8280661

이러한 se버전의 함수를 어떻게 활용할까?

변수를 만들 때 고정된 문자열과 변하는 숫자, 문자열을 이용해 많은 수의 변수를 생성해야 한다고 생각해보자

예를 들면

iris %>%
  select(Sepal.Length) %>% 
  head %>% 
  mutate(sl10 = Sepal.Length + 10,
         sl20 = Sepal.Length + 20,
         sl30 = Sepal.Length + 30)
##   Sepal.Length sl10 sl20 sl30
## 1          5.1 15.1 25.1 35.1
## 2          4.9 14.9 24.9 34.9
## 3          4.7 14.7 24.7 34.7
## 4          4.6 14.6 24.6 34.6
## 5          5.0 15.0 25.0 35.0
## 6          5.4 15.4 25.4 35.4

위와 같은 형태로 변수를 만들어야 한다고 생각해보자

위 예제는 3개만 만드니까 다 타이핑할 수 있겠지만

저렇게 변수를 10개 이상? 100개를 만들어야 한다면?


이러한 상황에서 se버전의 함수를 이용하면 해결할 수 있다

iris %>% 
  select(Sepal.Length) %>% 
  head %>% 
  mutate_(
    .dots = setNames(
              sapply(c(10,20,30), function(x) paste0('Sepal.Length + ', x, collapse = ' ')),
              sapply(c(10,20,30), function(x) paste0('sl', x, collapse = ' '))
            )
    )
##   Sepal.Length sl10 sl20 sl30
## 1          5.1 15.1 25.1 35.1
## 2          4.9 14.9 24.9 34.9
## 3          4.7 14.7 24.7 34.7
## 4          4.6 14.6 24.6 34.6
## 5          5.0 15.0 25.0 35.0
## 6          5.4 15.4 25.4 35.4

paste0()는 둘 이상의 문자열을 하나로 묶어주는 역할을 한다

sapply 는 첫 번째의 인자의 요소들이 차례대로 두 번째 인자의 함수에 parameter로 들어가서 값이 계산되고

그 결과물을 벡터 등으로 묶어서 반환해준다

for문을 짧은 함수 형태로 사용하는 것과 비슷한 효과를 낼 수 있다



일반적인 상황에서 데이터를 정제하고 분석할 때는 크게 필요를 느끼지 못할 수도 있지만

엄청나게 반복적인 작업을 해야할 때에는 이러한 방식으로 연산하면 시간을 크게 절약할 수도 있다

Comments