함수 작성 기초

Author

Mike K Smith

Published

March 2, 2023

R에서 함수를 작성하는 방법과 문제가 발생했을 때 디버깅하는 방법을 배웁니다. 경험상 동일한 코드를 두 번 이상 복사해서 사용한다면, 함수로 만드는 것이 좋습니다. R 패키지 또한 특정 목적을 위해 만들어진 함수, 문서, 테스트의 모음입니다.

1. 소개

함수는 SAS의 매크로(Macro)와 유사한 역할을 합니다. 처음에는 단순한 코드로 시작하여 입력을 식별하고 작업을 결정한 뒤, 점차 인수를 추가하여 사용자가 설정을 바꿀 수 있도록 고도화해 나갑니다.

미니 프로젝트 2에서 사용했던 치료군별 성별 빈도 산출 코드를 예로 들어보겠습니다.

library(rio)
library(tidyverse)
Warning: package 'ggplot2' was built under R version 4.4.3
Warning: package 'tibble' was built under R version 4.4.3
Warning: package 'purrr' was built under R version 4.4.2
Warning: package 'lubridate' was built under R version 4.4.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.2     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
adsl_saf <- import("./data/adsl.xpt") %>%
    filter(SAFFL == "Y")
  
# 치료군별 총계(N)
Big_N_cnt <- adsl_saf %>%
  group_by(TRT01AN, TRT01A) %>%
  count(name = "N")

# 성별 빈도(n) 및 백분율 산출
adsl_mrg_cnt <- adsl_saf %>%
  group_by(TRT01AN, TRT01A, SEX) %>%
  count(name = "n") %>%
  left_join(Big_N_cnt, by = c("TRT01A", "TRT01AN")) %>%
  mutate(perc = round((n/N)*100, 1)) %>%
  mutate(npct = paste0(n, " (", format(perc, nsmall = 1), ")")) %>%
  mutate(SEX = recode(SEX, "M" = "Male", "F" = "Female")) %>%
  ungroup() %>%
  select(TRT01A, SEX, npct) %>%
  pivot_wider(names_from = TRT01A, values_from = npct)

adsl_mrg_cnt
# A tibble: 2 × 4
  SEX    Placebo   `Xanomeline Low Dose` `Xanomeline High Dose`
  <chr>  <chr>     <chr>                 <chr>                 
1 Female 53 (61.6) 50 (59.5)             40 (47.6)             
2 Male   33 (38.4) 34 (40.5)             44 (52.4)             

2. 함수의 구조

함수는 다음의 핵심 요소를 가집니다.

  • 함수 이름: 수행하는 작업을 잘 설명하는 동사(예: calculate_counts)를 권장합니다.
  • 인수(Arguments): 함수에 전달되는 입력값입니다.
  • 함수 본문(Body): { } 내부에 작성되는 실제 실행 코드입니다.
  • 반환 값(Return value): 함수의 실행 결과로 돌려주는 객체입니다.

참고: 함수 내부에서 생성된 변수나 객체는 함수의 로컬 환경에만 존재하며, 명시적으로 반환하지 않으면 함수 밖에서 사용할 수 없습니다.

3. 함수 생성 실습

RStudio IDE의 스니펫(Snippet) 기능을 활용하면 편리합니다. 콘솔이나 스크립트 창에서 fun을 입력하고 TAB 키를 눌러보세요.

위의 코드를 기반으로 adsl_counts라는 함수를 만들어 보겠습니다. 이 함수는 데이터 파일 경로를 인수로 받습니다.

adsl_counts <- function(dataFile) {
  # 데이터 읽기 및 필터링
  adsl_saf <- haven::read_xpt(dataFile) %>%
    filter(SAFFL == "Y")
  
  # 치료군별 총계
  Big_N_cnt <- adsl_saf %>%
    group_by(TRT01AN, TRT01A) %>%
    count(name = "N")
  
  # 요약 데이터 생성
  adsl_mrg_cnt <- adsl_saf %>%
    group_by(TRT01AN, TRT01A, SEX) %>%
    count(name = "n") %>%
    left_join(Big_N_cnt, by = c("TRT01A", "TRT01AN")) %>%
    mutate(perc = round((n/N)*100, 1)) %>%
    mutate(npct = paste0(n, " (", format(perc, nsmall = 1), ")")) %>%
    mutate(SEX = recode(SEX, "M" = "Male", "F" = "Female")) %>%
    ungroup() %>%
    select(TRT01A, SEX, npct) %>%
    pivot_wider(names_from = TRT01A, values_from = npct)
  
  return(adsl_mrg_cnt)
}

이제 함수를 호출하여 결과를 확인해 봅니다.

inFile <- "./data/adsl.xpt"
adsl_counts(inFile)
# A tibble: 2 × 4
  SEX    Placebo   `Xanomeline Low Dose` `Xanomeline High Dose`
  <chr>  <chr>     <chr>                 <chr>                 
1 Female 53 (61.6) 50 (59.5)             40 (47.6)             
2 Male   33 (38.4) 34 (40.5)             44 (52.4)             

4. 방어적 프로그래밍 (Defensive Programming)

사용자가 엉뚱한 데이터를 넣었을 때 에러를 발생시키거나 안내 메시지를 띄우는 것이 좋습니다. 예를 들어 파일명에 “adsl”이 포함되어 있는지 확인하는 코드를 추가할 수 있습니다.

# try()와 stop()을 이용한 에러 제어
# if(!stringr::str_detect(inFile, "adsl")) stop("ADSL 데이터셋이 아닙니다.")

5. 코드 리팩토링 (Refactoring)

더 효율적이고 읽기 쉬운 코드로 개선하는 과정을 리팩토링이라고 합니다. 현재 함수는 파일을 매번 새로 읽어오는데, 이는 대용량 데이터나 네트워크 환경에서 비효율적입니다. 데이터를 먼저 읽어온 뒤, 데이터 프레임을 인수로 받도록 함수를 수정해 보겠습니다.

# 데이터 프레임을 입력으로 받는 개선된 함수
calculate_adsl_counts <- function(.data) {
  Big_N_cnt <- .data %>%
    group_by(TRT01AN, TRT01A) %>%
    count(name = "N", .groups = 'drop')

  res <- .data %>%
    group_by(TRT01AN, TRT01A, SEX) %>%
    count(name = "n") %>%
    left_join(Big_N_cnt, by = c("TRT01A", "TRT01AN")) %>%
    mutate(npct = paste0(n, " (", format(round((n/N)*100, 1), nsmall = 1), ")")) %>%
    ungroup() %>%
    select(TRT01A, SEX, npct) %>%
    pivot_wider(names_from = TRT01A, values_from = npct)

  return(res)
}

# 외부에서 데이터 로드 및 전처리 후 함수 호출
adsl_prepared <- haven::read_xpt("./data/adsl.xpt") %>%
  filter(SAFFL == "Y") %>%
  mutate(SEX = recode(SEX, "M" = "Male", "F" = "Female"))

calculate_adsl_counts(adsl_prepared)
# A tibble: 2 × 4
  SEX    Placebo   `Xanomeline Low Dose` `Xanomeline High Dose`
  <chr>  <chr>     <chr>                 <chr>                 
1 Female 53 (61.6) 50 (59.5)             40 (47.6)             
2 Male   33 (38.4) 34 (40.5)             44 (52.4)             

6. 생략 부호 (Ellipses, ...) 활용

하위 함수에 여러 인수를 유연하게 전달하고 싶을 때 ...을 사용합니다. 예를 들어 반올림 자릿수를 사용자가 결정하게 할 수 있습니다.

mySummary <- function(myData, ...){
  myData %>%
    group_by(TRT01AN, TRT01A) %>%
    summarise(mean = round(mean(AGE), ...), .groups = 'drop')
}

# digits 인수를 ...을 통해 round 함수로 전달
mySummary(adsl_prepared, digits = 2)
# A tibble: 3 × 3
  TRT01AN TRT01A                mean
    <dbl> <chr>                <dbl>
1       0 Placebo               75.2
2      54 Xanomeline Low Dose   75.7
3      81 Xanomeline High Dose  74.4

챌린지

미니 프로젝트 6에서 작성한 ALT 시각화 코드를 함수로 만들어 보세요. 인수로 데이터셋을 받고, 사용자가 축 레이블을 직접 지정할 수 있도록 구성해 보세요.