함수로 코드 반복 줄이기

함수로 코드 반복 줄이기

같은 코드를 두 번 이상 복사-붙여넣기 했다면, 그때가 함수를 만들 좋은 신호예요. 함수는 반복 작업을 더 강력하고 일반적인 방식으로 자동화해 주고, 코드를 이해하기 쉽게 만들어요. 이 페이지에서는 R에서 함수를 쓸 때 필요한 이름·인자·본문 세 가지 구조와, 벡터·데이터프레임·그래프를 만드는 함수를 하나씩 살펴볼게요.

출처: R for Data Science(2e) — Functions

본문

함수는 언제 만들까

복사-붙여넣기보다 함수가 나은 이유는 네 가지예요. 함수에 의미 있는 이름을 붙여 코드를 이해하기 쉽게 하고, 요구가 바뀌면 한 곳만 고치면 되고, 복붙할 때 실수로 변수 이름을 한 군데만 고치는 실수를 막아주고, 프로젝트 사이에서 재사용하기 쉬워 생산성이 늘어나요.

좋은 기준 하나를 기억해 두세요. 코드 블록을 두 번 이상 복사-붙여넣기 했다면(즉 같은 코드가 세 벌이 됐다면) 함수를 고려해 볼 때예요.

함수의 세 가지 구성 요소

각 열을 0에서 1 사이로 조정(rescale)하는 코드를 예로 들어볼게요. mutate() 안에서 반복되던 코드를 밖으로 꺼내면, 매번 달라지는 부분은 열 이름 하나뿐이라는 게 보여요. 이를 함수로 만들려면 세 가지가 필요해요.

  1. 이름: 여기선 벡터를 0~1 사이로 조정하니까 rescale01이라고 지어볼게요.
  2. 인자(arguments): 호출마다 달라지는 부분. 여기선 숫자 벡터 하나라서 관례대로 x라고 해요.
  3. 본문(body): 모든 호출에서 반복되는 코드.

함수는 아래 템플릿으로 만들어요.

name <- function(arguments) {
  body
}

이 경우엔 이렇게 돼요.

rescale01 <- function(x) {
  (x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
}

만든 함수는 간단한 입력으로 먼저 테스트해 보는 습관이 좋아요.

rescale01(c(-10, 0, 10))
#> [1] 0.0 0.5 1.0

rescale01(c(1, 2, 3, NA, 5))
#> [1] 0.00 0.25 0.50   NA 1.00

그리고 나서 mutate() 호출을 함수 버전으로 바꿔요.

df |>
  mutate(
    a = rescale01(a),
    b = rescale01(b),
    c = rescale01(c),
    d = rescale01(d)
  )

함수 개선하기

rescale01()min()을 두 번, max()를 한 번 계산하는데, range()를 쓰면 최솟값과 최댓값을 한 번에 구할 수 있어요.

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

또한 벡터에 무한대 값이 있으면 결과가 쓸모없어지는데, range()finite = TRUE를 주면 무한대를 무시할 수 있어요. 이렇게 반복 코드를 함수로 옮겨 두면 고칠 부분이 한 곳뿐이라는 게 함수의 큰 장점이에요.

벡터 함수와 요약 함수

벡터 함수는 입력과 같은 길이의 출력을 내는 함수라 mutate()·filter() 안에서 잘 어울려요. 예를 들어 Z-점수(평균 0, 표준편차 1)를 계산하는 함수를 만들 수 있어요.

z_score <- function(x) {
  (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}

case_when()을 감싸 특정 구간 밖 값을 잘라내는 함수도 흔히 써요.

clamp <- function(x, min, max) {
  case_when(
    x < min ~ min,
    x > max ~ max,
    .default = x
  )
}

clamp(1:10, min = 3, max = 7)
#> [1] 3 3 3 4 5 6 7 7 7 7

요약 함수는 단일 값을 내는 함수라 summarize()에서 써요. 예를 들어 변동 계수(표준편차/평균)를 계산하는 함수를 만들면 인자 기본값도 함께 지정할 수 있어요.

cv <- function(x, na.rm = FALSE) {
  sd(x, na.rm = na.rm) / mean(x, na.rm = na.rm)
}

흔한 패턴에 기억하기 쉬운 이름을 붙여주는 것도 좋아요. n_missing()처럼 결측값 개수를 세는 함수가 그 예예요.

n_missing <- function(x) {
  sum(is.na(x))
}

함수는 인자를 여러 개 받을 수도 있어요. 예측값과 실제값을 비교하는 평균 절대 백분율 오차(MAPE) 함수를 예로 들어볼게요.

mape <- function(actual, predicted) {
  sum(abs((actual - predicted) / actual)) / length(actual)
}

RStudio를 쓰면 함수 정의를 찾을 때 커서를 함수 이름에 두고 F2를 누르거나, Ctrl + .로 파일·함수 검색 창을 열어 빠르게 이동할 수 있어요.

데이터프레임과 그래프 함수

데이터프레임 함수는 데이터프레임을 넣으면 데이터프레임을 내놓는 함수예요. summarize()를 감싸 특정 요약값 여러 개를 한 번에 만드는 식으로 활용해요. 그래프 함수는 데이터프레임을 넣고 ggplot2 패키지로 그래프를 그리는 함수인데, 복잡한 플롯 코드의 반복을 줄일 때 유용해요.

작명 스타일

R은 함수나 인자의 이름을 가리지 않지만, 사람이 읽기에는 이름이 매우 중요해요. 함수 이름은 동사, 인자 이름은 명사로 짓는 게 기본이에요. 잘 알려진 명사를 계산하는 함수(mean()compute_mean()보다 나은)나 객체의 속성을 꺼내는 함수(coef()get_coefficients()보다 나은)는 명사여도 괜찮아요.

function() 다음에는 항상 중괄호 {}를 붙이고, 그 안의 내용은 추가로 두 칸 들여 쓰는 걸 권장해요. 왼쪽 여백만 훑어도 코드의 계층이 보이거든요.

더 알아보기