다른 블로그에도 관련된 내용이 없거나 적고 스스로 rlang과 관련된 내용 정리가 필요한 것 같아 포스팅을 올리게 되었습니다.
mpg라는 데이터프레임에서 "model"이라는 열을 기준으로 그룹화를 한다면 아래와 같이 작성할 수 있습니다.
mpg |> group_by(model)

그렇다면 아래 코드와 같이 그룹화 변수를 따로 선언하고, 그 객체를 전달하면 어떨까요?
group_vars <- "model"
mpg |> group_by(group_vars)

오류가 발생하네요?
도대체 무슨 차이가 있어서 이런 차이를 만드는 것일까요?
group_by 함수는 인자로 받는 값을 변수명이라고 생각하고, 데이터 프레임에서 인자로 받은 값과 받은 열을 찾습니다.
그래서 첫번째 코드에서는 mpg에 존재하는 "model"이라는 열을 기준으로 그룹화를 진행하는 것이고, 두번째 코드에서는 mpg에 존재하는 "group_vars"라는 열을 찾으려는데 존재하지 않아서 오류가 발생하게 됩니다.
그러면, group_by와 같은 함수에서는 그룹을 지정하는 변수를 전달할 수 없는걸까요??
이번 포스팅에서는 이런 물음에 대답할 수 있는 기반이 되는 동적 변수 전달, 비정형 평가, 데이터 마스킹에 대해 알아보려고 합니다.
1. 데이터 마스킹
데이터 마스킹은 함수 내부에서 "열"을 일반적인 객체(변수)처럼 사용할 수 있는 기능입니다.
R이나 파이썬에서는 데이터프레임을 제일 많이 사용하고 데이터프레임에서 열을 참조하는 경우가 굉장히 많습니다.
예를 들면, iris 데이터에서 Sepal.Length와 Petal.Length의 합을 구하려면 아래와 같이 코드를 작성할 수 있습니다.
iris$Sepal.Length + iris$Petal.Length

하지만, 아래 코드와 같이 작성한다면 당연히 오류가 발생할 것입니다.
Sepal.Length + Petal.Length

tidyverse 스타일을 접한 독자분들은 (혹은 with 함수) 아래와 같이 "iris$"라는 중복되는 단어를 제거하고 작성할 수 있습니다.
iris |> transmute(sum.Length = Sepal.Length + Petal.Length)
with(iris, Sepal.Length+Petal.Length)
데이터 마스킹을 지원하는 함수 내부(환경)에서는 데이터 셋에 있는 "열"을 객체로 정의해 iris$Sepal.Length와 같이 명시적으로 지정하지 않고, Sepal.Length 만으로도 값에 접근할 수 있게 됩니다.
이러한 방식 덕분에 함수 내부의 수식이나 표현을 간결하게 표현할 수 있으며 직관적으로도 보기에도 좋습니다.
2. Embracing("{{") Opereator
데이터 마스킹은 함수 내부에서 열 이름을 단순히 변수처럼 사용할 수 있도록 코드의 즉각적인 평가를 억제합니다.
즉, 데이터 마스킹을 위해 열 이름을 참조하는 코드가 바로 실행되지 않고, 데이터프레임의 열이 정의된 환경에서 다시 실행되는데 이를 defusing이라고 합니다. (지연평가 Lazy Evaluation과는 다른 개념입니다.)
아래 코드는 두 변수를 받아 그 합을 데이터프레임에 추가하는 함수로 실행시키면 정상적으로 돌아갑니다.
sum_two_vars <- function(data, var1, var2){
data |> mutate(var1+var2)
}
하지만, 아래와 같이 데이터프레임과 열을 넣고 실행시키면 오류가 발생하죠
sum_two_vars(mpg, cty, hwy)

처음 함수를 정의할 때, mutate는 코드 defusing이 적용되기 때문에 오류가 발생하지 않으나,
두번째 코드와 같이 데이터프레임과 해당 열이 정의된 환경에서 다시 실행될때 var1과 var2라는 변수가 data(=mpg)에 없기 때문에 오류가 발생하게 되는 것입니다. (data에는 mpg가 정상적으로 할당됨)
sum_two_vars(mpg, cty, hwy)를 실행할 때,
계산되기 원하는 mpg |> mutate(cty, hwy)로 실행되기 위해서는 인젝션(Injection, 주입)을 필요로 합니다.
인젝션은 크게 "{{"의 Embracing Operator과 "!!"의 Injection Operator로 구분됩니다.
여기서는 아래와 같이 Embracing Operator를 사용하여 원하는 결과를 도출하도록 수정할 수 있습니다.
sum_two_vars_embrace <- function(data, var1, var2){
data |> mutate({{var1}} + {{var2}})
}
sum_two_vars_embrace(mpg, cty, hwy)

3. 환경(Environment)
주입 연산자(!!)는 환경과 관련있어 환경에 대해 잠깐 소개하도록 하겠습니다.
R에서는 환경 검색 순서가 있는데(Lexical Scoping Rule), 객체(Object)를 찾기 위해 주어진 환경에 따라 검색하는 순서가 다릅니다.
- 기본 검색 순서 : Global > Loaded Package(나중에 로드된 패키지부터 검색) > base 패키지
- 함수 내부 순서 : Local (함수내부) > 함수가 정의된 환경 > Global > Loaded Package
- 데이터 마스킹 함수 순서 : 데이터 프레임의 열 > Local (함수 내부) > Global > Loaded Package
x <- "global" # 전역 환경
foo <- function() {
x <- "local" # 함수 로컬 환경
print(x) # "local" 출력
}
foo() # "local" 출력
print(x) # "global" 출력
foo( ) 라는 함수를 실행하면 함수 내부에서 x를 찾는 것이므로 Local(함수 내부)에 있는 x로 검색되기 때문에 x를 출력하면 "local"이 출력되고, global 환경에 있는 print(x)를 실행시키면 Global 환경부터 x를 검색하기 때문에 "global"이 출력되는 원리입니다.
또 다른 예시로 아래 코드에서 my_mean이라는 함수 내부에서 정의된 함수 mean은 trimmed mean으로 my_mean(data)을 실행하면 내부의 mean은 렉시컬 스코핑 규칙에 의해서 함수 내부의 mean으로 계산되어 결과가 다르게 나오게 됩니다.
my_mean <- function(data){
mean <- function(data){
get("mean", envir = global_env)(x=data, trim=0.3)}
return(mean(data))
}
data = c(-1,3,5,6,9,10:15, 100)
mean(data) # base::mean 실행
my_mean(data) # 내부 정의된 mean 함수 실행
4. Injection("!!") Opereator
주입 연산자(!!)는 환경과 관련된 모호함을 제거할 수 있는 연산자 입니다.
주입 연산자는 데이터 마스킹과 관련된 규칙이 적용되기 전에 표현이나 객체를 대입(=주입)하는 방식으로 실행됩니다.
만약, 객체의 값이 주입된다면 R은 해당 값을 찾기 위해서 환경을 탐색하지 않습니다.
아래 iris 데이터 프레임에 5라는 값을 가지는 "x"열을 추가 시켰습니다.
add5_iris <- iris |> mutate(x=5)

그렇다면 아래 코드는 어떻게 실행될까요?
add5_iris |> mutate(new_feature = Sepal.Length + Sepal.Width + x)
당연하게도 add5_iris에 있는 Sepal.Length와 Sepal.Length를 더한값에 "x"열의 값인 5를 더한 값이 출력됩니다.

그렇다면 전역환경에 있는 x를 설정하고 같은 코드를 실행하면 어떻게 될까요?
x=3
add5_iris |> mutate(new_feature = Sepal.Length + Sepal.Width + x)
mutate 함수는 데이터 마스킹을 지원하는 함수이므로 검색 규칙에 따라 add5_iris에 있는 "x"열의 값을 적용하여 이전의 결과와 동일하게 됩니다. 하지만 전역함수에 있는 x=3이라는 값으로 더하고 싶은 경우에는 변수의 이름을 바꾸고, 함수에 적용되는 수식을 변경해야 할까요?
이럴때 등장하는게 주입 연산자로 아래와 같이 "!!" 연산자를 통해 x=3이라는 값을 주입해 더 이상 x라는 값을 검색하지 않도록 할 수 있습니다.
add5_iris |> mutate(new_feature = Sepal.Length + Sepal.Width + !!x)

내용이 많아 다음 포스팅에 이어서 올리도록 하겠습니다.
Reference
- Advanced R - Hadley Wickham
- What is data-masking and why do I need {{? (https://rlang.r-lib.org/reference/topic-data-mask.html)
'Data Science > Manipulation' 카테고리의 다른 글
| [R] 사용자 정의 함수 관련 잡기술 (1) | 2024.10.19 |
|---|---|
| [EDA] 상관계수 시각화 (Visualization of Correlation Coefficient) with R (0) | 2024.09.27 |
| [R] slice함수 : 위치를 이용한 행 선택 (Subset rows using position) (2) | 2023.09.06 |
| [Data Science With R] 16. 리스트열(List-column)을 이용한 모델 (0) | 2023.09.02 |
| [R] all_of와 any_of를 사용한 변수 선택 (조건을 이용한 선택 추가) (1) | 2023.07.29 |