재현가능한 과학적 분석을 위한 R

함수 생성하기

학습 목표

  • 인자를 받는 함수를 정의한다.
  • 함수에서 값을 반환한다.
  • 함수를 테스트한다.
  • 함수 인자에 기본디폴트 값을 설정한다.
  • 프로그램을 작게, 단일-목적을 갖는 함수로 쪼개는 이유를 설명한다.

분석할 데이터셋이 하나라면, 파일을 엑셀같은 스프레드쉬트로 불러와서 간단한 통계량을 도식화하는 것이 아마도 훨씬 빠를 것이다. 하지만, gapminder 데이터는 주기적으로 갱신되서, 나중에 새로운 정보를 뽑아와서 분석작업을 다시 실행할 수도 있다. 또한, 미래 특정 시점에 다양한 곳에서 유사한 데이터를 얻어올 수도 있다.

이번 수업에서, 단일 명령어로 여러가지 연산작업을 반복하는 함수 작성방법을 학습할 것이다.

함수 정의하기

새로운 R 스크립트 파일을 functions 디렉토리에 functions-lesson.R 이름으로 저장하고 편집기에서 연다.

my_sum <- function(a, b) {
  the_sum <- a + b
  return(the_sum)
}

화씨온도에서 캘빈온도로 변환하는 fahr_to_kelvin 함수를 정의한다:

fahr_to_kelvin <- function(temp) {
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

function 출력을 대입하는 fahr_to_kelvin 함수를 정의한다. 인자 명칭 리스트는 괄호 안에 담겨진다. 다음으로 함수 몸통부문 (함수를 실행할 때 실행되는 문장)이 괄호({}) 내부에 담겨진다. 몸통부문 문장을 공백 2 개로 들여쓰기한다. 들여쓰면, 코드 가독성이 높아지지만, 코드 동작에는 영향을 끼치지 않는다.

함수를 호출할 때, 함수에 전달되는 값이 변수에 대입된다. 그래서, 함수 내부에서 전달되는 값을 사용할 수 있다. 함수 내부에서 반환문(return)을 사용해서 호출한 쪽에 결과를 되돌려준다.

함수를 실행해보자. 본인이 작성한 사용자 정의 함수 호출은 여타 다른 함수 호출과 차이가 없다:

# 빙점
fahr_to_kelvin(32)
[1] 273.15
# 끓는 점
fahr_to_kelvin(212)
[1] 373.15

도전과제 1

kelvin_to_celsius 함수를 작성해서 캘빈온도를 받아 섭씨온도를 반환한다.

힌트: 캘빈온도를 섭씨온도로 전환하려면, 273.15 을 뺀다.

함수 결합

함수의 진정한 힘은 원하는 효과를 얻어 내는데 함수를 섞고, 매칭하고, 결합해서 더 큰 덩어리로 구현함에 있다.

화씨 온도에서 캘빈 온도로, 갤빈 온도에서 섭씨 온도로 온도를 전환하는 함수 두개를 정의한다:

fahr_to_kelvin <- function(temp) {
  kelvin <- ((temp - 32) * (5 / 9)) + 273.15
  return(kelvin)
}

kelvin_to_celsius <- function(temp) {
  celsius <- temp - 273.15
  return(celsius)
}

도전과제 2

바로 위에 정의된 함수 두개를 재사용해서 화씨온도에서 바로 섭씨온도로 변환하는 함수를 정의한다. (혹은, 본인이 원하면 자신이 직접 작성한 함수를 사용한다.)

데이터셋에서 한 국가에 대한 국내 총 생산(GDP)를 계산하는 함수를 작성한다:

# 데이터를 인자로 받아, 1인당 GDP 칼럼과 인구수 칼럼을 곱한다.
calcGDP <- function(dat) {
  gdp <- dat$pop * dat$gdpPercap
  return(gdp)
}

function 출력을 대입하는 calcGDP 함수를 정의한다. 인자 명칭 리스트는 괄호 안에 담겨진다. 다음으로 함수 몸통부문 (함수를 실행할 때 실행되는 문장)이 괄호({}) 내부에 담겨진다.

몸통부문 문장을 공백 2 개로 들여쓰기한다. 들여쓰면, 코드 가독성이 높아지지만, 코드 동작에는 영향을 끼치지 않는다.

함수를 호출할 때, 함수에 전달되는 값이 인자에 대입된다. 전달되는 값은 함수 몸통 내부에서 변수가 된다.

함수 내부에서, return 함수를 사용해서 결과를 함수밖으로 배출한다. 반환 return 함수는 선택옵션이다: R은 자동으로 함수 마지막 줄에 실행되는 어떤 명령어든 결과를 반환한다.

calcGDP(head(gapminder))
[1]  6567086330  7585448670  8758855797  9648014150  9678553274 11697659231

그다지 유용한 정보는 아니다. 인자를 일부 추가해서 연도별, 국가별 GDP를 추출한다.

# 데이터를 인자로 받아, 1인당 GDP 칼럼과 인구수 칼럼을 곱한다.
calcGDP <- function(dat, year=NULL, country=NULL) {
  if(!is.null(year)) {
    dat <- dat[dat$year %in% year, ]
  }
  if (!is.null(country)) {
    dat <- dat[dat$country %in% country,]
  }
  gdp <- dat$pop * dat$gdpPercap

  new <- cbind(dat, gdp=gdp)
  return(new)
}

별도 R 스크립트 파일에 상기 함수를 작성했다면(좋은 아이디어!), source 함수를 사용해서 R 세션에 함수를 올려 적재할 수 있다:

source("functions/functions-lesson.R")

이제 함수에 상당히 많은 일을 하고 있네요. 쉬운 말로 풀어쓰면, 연도 year 인자가 비어있지 않는다면, 함수가 전달된 연도 정보로 부분집합 데이터를 생성하고 나서, 국가 country 인자가 비어있지 않는다면, 함수가 전달된 국가 정보로 부분집합 데이터를 생성한다. 그리고 나서, 연도와 국가 인자에서 추출된 부분집합에 대해 GDP를 계산한다. 그리고 나서, 함수가 GDP를 신규 칼럼으로 추가해서 부분집합으로 뽑아낸 데이터셋에서 추가하고 최종 결과로 반환한다. 숫자 벡터로 쭉 뽑혀진 것보다 출력결과가 훨씬 더 유용한 정보를 제공함을 알 수 있다.

연도를 인자로 명세할 때 어떤 일이 발생하는지 살펴보자:

head(calcGDP(gapminder, year=2007))
       country year      pop continent lifeExp  gdpPercap          gdp
12 Afghanistan 2007 31889923      Asia  43.828   974.5803  31079291949
24     Albania 2007  3600523    Europe  76.423  5937.0295  21376411360
36     Algeria 2007 33333216    Africa  72.301  6223.3675 207444851958
48      Angola 2007 12420476    Africa  42.731  4797.2313  59583895818
60   Argentina 2007 40301927  Americas  75.320 12779.3796 515033625357
72   Australia 2007 20434176   Oceania  81.235 34435.3674 703658358894

혹은 특정 국가를 인자로 전달하면 어떻게 되는지 살펴보자:

calcGDP(gapminder, country="Australia")
     country year      pop continent lifeExp gdpPercap          gdp
61 Australia 1952  8691212   Oceania  69.120  10039.60  87256254102
62 Australia 1957  9712569   Oceania  70.330  10949.65 106349227169
63 Australia 1962 10794968   Oceania  70.930  12217.23 131884573002
64 Australia 1967 11872264   Oceania  71.100  14526.12 172457986742
65 Australia 1972 13177000   Oceania  71.930  16788.63 221223770658
66 Australia 1977 14074100   Oceania  73.490  18334.20 258037329175
67 Australia 1982 15184200   Oceania  74.740  19477.01 295742804309
68 Australia 1987 16257249   Oceania  76.320  21888.89 355853119294
69 Australia 1992 17481977   Oceania  77.560  23424.77 409511234952
70 Australia 1997 18565243   Oceania  78.830  26997.94 501223252921
71 Australia 2002 19546792   Oceania  80.370  30687.75 599847158654
72 Australia 2007 20434176   Oceania  81.235  34435.37 703658358894

혹은, 연도와 국가를 모두 인자로 전달하면 어떻게 되는지 살펴보자:

calcGDP(gapminder, year=2007, country="Australia")
     country year      pop continent lifeExp gdpPercap          gdp
72 Australia 2007 20434176   Oceania  81.235  34435.37 703658358894

함수 몸통부문을 살펴본다:

calcGDP <- function(dat, year=NULL, country=NULL) {

위에서 인자 두개 year, country를 추가했다. 함수 정의부에서 기본디폴트 인자를 모두 = 연산자를 사용해서 NULL로 설정했다. 이것이 의미하는 바는 사용자가 달리 인자를 설정하지 않는다면, 각 인자에 NULL 값을 인자로 넘긴다는 것이다.

  if(!is.null(year)) {
    dat <- dat[dat$year %in% year, ]
  }
  if (!is.null(country)) {
    dat <- dat[dat$country %in% country,]
  }

여기서, 각기 추가되는 인자가 null 로 설정되었는지 검사한다. null 이 아닌 경우, null이 아닌 인자로 부분집합 데이터로 뽑아내려고 dat에 저장된 데이터셋을 덮어쓴다.

추후에 좀더 유연하게 함수를 작성할 예정이다. 따라서, GDP를 다음 질문에 대해 답할 수 있다:

  • 전체 데이터셋;
  • 단일 연도;
  • 단일 국가;
  • 단일 연도와 단일국가 조합.

대신에 %in% 연산자를 사용해서, 여러 연도와 국가를 인자에 넘길 수 있다.

  gdp <- dat$pop * dat$gdpPercap
  new <- cbind(dat, gdp=gdp)
  return(new)
}

마지막으로, 신규 부분집합 데이터셋에 GDP를 계산하고, 신규 데이터프레임을 생성하고 동일한 명칭을 갖는 칼럼을 추가한다. 즉, 나중에 함수를 호출할 때, 반환된 GDP 값에 대한 맥락을 찾아볼 수 있다는 것이다. 앞서 숫자 벡터보다 이런 점에서 훨씬 더 낫다.

도전과제 3

paste 함수를 사용해서 텍스트를 결합할 수 있다, 즉:

best_practice <- c("Write", "programs", "for", "people", "not", "computers")
paste(best_practice, collapse=" ")
[1] "Write programs for people not computers"

textwrapper라는 벡터 인자 두개를 받는 fence라는 함수를 작성한다. 함수는 wrapper로 둘러싼 텍스트를 출력한다:

fence(text=best_practice, wrapper="***")

주목: paste 함수는 sep 라는 인자를 갖는데, sep 인자는 텍스트 사이 구분자를 명세한다. 기본디폴트 설정은 공백이다: " “. paste0 함수에 대한 기본디폴트 설정은 공백이 없음이다:”“.

도전과제 해답

도전과제 1 에 대한 해답

kelvin_to_celsius 함수를 작성해서 캘빈온도를 받아 섭씨온도를 반환한다.

힌트: 캘빈온도를 섭씨온도로 전환하려면, 273.15 을 뺀다.

kelvin_to_celsius <- function(temp) {
 celsius <- temp - 273.15
 return(celsius)
}

도전과제 2 에 대한 해답

바로 위에 정의된 함수 두개를 재사용해서 화씨온도에서 바로 섭씨온도로 변환하는 함수를 정의한다. (혹은, 본인이 원하면 자신이 직접 작성한 함수를 사용한다.)

fahr_to_celsius <- function(temp) {
  temp_k <- fahr_to_kelvin(temp)
  result <- kelvin_to_celsius(temp_k)
  return(result)
}

도전과제 3 에 대한 해답

textwrapper라는 벡터 인자 두개를 받는 fence라는 함수를 작성한다. 함수는 wrapper로 둘러싼 텍스트를 출력한다:

fence <- function(text, wrapper){
  text <- c(wrapper, text, wrapper)
  result <- paste(text, collapse = " ")
  return(result)
}
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
fence(text=best_practice, wrapper="***")
[1] "*** Write programs for people not computers ***"