콘텐츠로 이동

08. 반복·함수형 함수

8.1 apply 계열

학생 5명의 과목별 점수에서 학생별 평균을 구하는 상황을 for() 문과 apply()로 각각 작성해 비교하겠습니다.

set.seed(1)
scores <- matrix(sample(50:100, 20), nrow = 5,
                  dimnames = list(paste0("학생", 1:5), paste0("과목", 1:4)))
scores
#>       과목1 과목2 과목3 과목4
#> 학생1    53    92    91    90
#> 학생2    88    63    59    74
#> 학생3    50    67    56    97
#> 학생4    83    82    58    84
#> 학생5    72    70    64    61

# for 문 방식: 결과 저장용 빈 벡터를 미리 만들어야 함
result <- numeric(nrow(scores))
for (i in seq_len(nrow(scores))) {
  result[i] <- mean(scores[i, ])
}
result
#> [1] 81.50 71.00 67.50 76.75 66.75

# apply() 방식: 사전할당 없이 한 줄로 동일한 결과
apply(scores, 1, mean)
#> 학생1 학생2 학생3 학생4 학생5 
#> 81.50 71.00 67.50 76.75 66.75 

apply 계열 함수는 "데이터를 어떤 단위로 쪼개서 함수에 넘기는가"와 "결과를 어떤 형태로 돌려받는가"에 따라 다음과 같이 구분됩니다.

함수 입력(X) 함수에 전달되는 단위 그룹 기준 반환 형태
apply() 행렬·배열(또는 모든 열이 같은 타입인 데이터프레임) 행 전체 또는 열 전체 없음(방향만 지정) 벡터 또는 행렬
lapply() 벡터, 리스트, 데이터프레임 원소(열) 하나씩 없음 리스트
sapply() 위와 동일 원소(열) 하나씩 없음 벡터·행렬(가능하면 단순화, 실패 시 리스트)
vapply() 위와 동일 원소(열) 하나씩 없음 지정한 서식의 벡터·행렬(서식이 안 맞으면 즉시 오류)
tapply() 벡터 원소 하나씩 팩터(1개 이상) 배열(팩터 조합별 결과)
mapply() 여러 개의 벡터·리스트 여러 인자를 짝지어 하나씩 없음 벡터·행렬(가능하면 단순화) 또는 리스트
eapply() 환경(environment) 환경 안의 변수 하나씩 없음 리스트
by() 데이터프레임·행렬 그룹 전체(부분 데이터프레임) 팩터(1개 이상) by 객체(그룹별 결과를 담은 리스트)

이 함수들은 공통적으로 "데이터(X) + 적용할 함수(FUN) + FUN에 추가로 넘길 인자(...)"를 받아, X를 정해진 단위로 나누어 FUN에 반복 적용한 뒤 그 결과를 모아 반환합니다. 함수마다 다른 점은 위 표의 "전달 단위"와 "반환 형태" 두 가지로 요약됩니다.

apply()

apply(X, MARGIN, FUN, ...)는 행렬·배열 X에 대해 지정한 방향으로 FUN을 적용합니다.

  • X : 행렬·배열(또는 모든 열이 같은 타입인 데이터프레임). 벡터는 바로 넣을 수 없고, dim()으로 배열로 바꾸어야 합니다.
  • MARGIN : 함수를 적용할 방향. 1이면 행 단위, 2이면 열 단위, c(1, 2)이면 행·열 구분 없이 원소 하나하나에 적용합니다.
  • FUN : 각 행 또는 열에 적용할 함수.
  • ... : FUN에 추가로 넘길 인자(예: na.rm = TRUE).
(x <- matrix(1:12, nrow = 3, ncol = 4))
#>      [,1] [,2] [,3] [,4]
#> [1,]    1    4    7   10
#> [2,]    2    5    8   11
#> [3,]    3    6    9   12

apply(x, 1, mean)   # 행 방향 평균
#> [1] 5.5 6.5 7.5

apply(x, 2, sum)    # 열 방향 합계
#> [1]  6 15 24 33

FUN 자리에는 사용자 정의 함수(익명 함수 포함)도 들어갈 수 있습니다. R 4.1부터는 function(x) {...} 대신 \(x) {...} 축약 문법을 쓸 수 있습니다.

apply(x, 1, \(row) max(row) - min(row))   # 행별 (최댓값 - 최솟값)
#> [1] 9 9 9

MARGIN에 c(1, 2)를 지정하면 행·열 방향으로 값을 묶어서 넘기는 대신 원소 하나하나에 FUN을 적용합니다.

apply(x, c(1, 2), \(v) v * 10)   # 원소 단위로 적용(사실상 x * 10과 같은 결과)
#>      [,1] [,2] [,3] [,4]
#> [1,]   10   40   70  100
#> [2,]   20   50   80  110
#> [3,]   30   60   90  120

데이터프레임도 모든 열이 같은 자료형(숫자형 등)이면 apply()에 바로 사용할 수 있습니다. R 내장 데이터셋 iris에서 팩터형인 Species를 제외하면 나머지 네 열은 모두 숫자형이므로 적용 가능합니다.

y <- iris[, -5]   # Species(팩터) 제외
apply(y, 2, mean, na.rm = TRUE)
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>     5.843333     3.057333     3.758000     1.199333 

lapply()

lapply(X, FUN, ...)는 X(벡터·리스트·데이터프레임)의 원소를 하나씩 FUN에 전달하고, 결과를 리스트로 반환합니다.

  • X : 벡터, 리스트, 데이터프레임 등 원소 단위로 순회할 수 있는 객체.
  • FUN : 각 원소에 적용할 함수.
  • ... : FUN을 호출할 때마다 똑같이 함께 넘길 추가 인자(예: na.rm = TRUE).

apply()가 행·열 전체를 한 번에 넘기는 것과 달리, lapply()는 행렬을 넣어도 원소를 하나씩(벡터처럼) 풀어서 전달한다는 점에 주의해야 합니다.

x <- 1:3
lapply(x, \(v) v^2 + 1)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 5
#> 
#> [[3]]
#> [1] 10

리스트 형태의 결과가 불편하면 unlist()로 벡터로 바꿀 수 있습니다(4.6.5절).

unlist(lapply(x, \(v) v^2 + 1))
#> [1]  2  5 10

데이터프레임을 lapply()에 넣으면 열(변수) 단위로 함수가 적용됩니다. 단, 팩터형처럼 mean()을 적용할 수 없는 열이 섞여 있으면 그 열은 결측치(NA)로 처리되고 경고가 함께 발생합니다.

lapply(iris, mean, na.rm = TRUE)
#> $Sepal.Length
#> [1] 5.843333
#> 
#> $Sepal.Width
#> [1] 3.057333
#> 
#> $Petal.Length
#> [1] 3.758
#> 
#> $Petal.Width
#> [1] 1.199333
#> 
#> $Species
#> [1] NA
#> 
#> 경고메시지(들):
#> mean.default(X[[i]], ...)에서:
#>   인자가 수치형 또는 논리형이 아니므로 NA를 반환합니다

이런 경고를 피하려면 apply() 예제에서처럼 iris[-5]같이 숫자형 열만 미리 골라서 넣어야 합니다.

sapply()

sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)는 lapply()의 결과를 가능하면 벡터나 행렬로 단순화(simplify) 해서 반환하는 함수입니다. 매번 결과에 unlist()를 붙이는 lapply() + unlist() 조합을 한 번에 처리해 주는 셈입니다.

  • X, FUN, ... : lapply()와 동일합니다.
  • simplify : TRUE(기본값)이면 각 결과의 길이가 모두 같을 때 벡터·행렬로 단순화하고, FALSE이면 lapply()처럼 항상 리스트로 반환합니다.
  • USE.NAMES : X가 이름 없는 문자형 벡터일 때, 그 문자열 값 자체를 결과의 이름(names)으로 쓸지 정합니다(기본값 TRUE).
sapply(c("a", "bb", "ccc"), nchar)                  # 문자열 자체가 이름이 됨
#>   a  bb ccc 
#>   1   2   3 

sapply(c("a", "bb", "ccc"), nchar, USE.NAMES = FALSE)
#> [1] 1 2 3
x <- 1:3
sapply(x, \(v) v^2 + 1)
#> [1]  2  5 10
sapply(iris[-5], mean)
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>     5.843333     3.057333     3.758000     1.199333 

각 원소마다 길이가 1보다 큰 값을 반환하면 sapply()는 이를 행렬로 묶어 줍니다.

lst <- list(a = 1:10, b = exp(-2:2))
sapply(lst, mean)
#>        a        b 
#> 5.500000 2.322111 

sapply(lst, quantile, probs = c(0.25, 0.5, 0.75))
#>        a         b
#> 25% 3.25 0.3678794
#> 50% 5.50 1.0000000
#> 75% 7.75 2.7182818

주의: sapply()는 "가능하면" 단순화할 뿐이므로, 반환값의 길이가 원소마다 다르면 단순화에 실패하고 조용히 리스트를 반환합니다. 아래 예제처럼 결측치를 제거한 뒤 남는 값의 개수가 그룹마다 다른 경우가 대표적입니다.

lst <- list(a = c(1, 2, 3), b = c(5, NA), c = c(7, 8, 9))
sapply(lst, \(v) v[!is.na(v)])   # 길이가 요소마다 달라 행렬이 아니라 리스트로 반환됨
#> $a
#> [1] 1 2 3
#> 
#> $b
#> [1] 5
#> 
#> $c
#> [1] 7 8 9

이처럼 반환 형태가 코드 실행 전에는 예측하기 어렵다는 점이 sapply()의 대표적인 약점이며, 이를 보완한 것이 다음의 vapply()입니다.

vapply()

vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)는 sapply()와 쓰임새가 같지만, FUN이 반환해야 할 값의 형식(길이와 자료형) 을 FUN.VALUE 인자에 미리 지정해야 합니다.

  • X, FUN, ..., USE.NAMES : sapply()와 동일합니다.
  • FUN.VALUE : FUN을 한 번 호출했을 때 나와야 할 값의 "본보기(template)"입니다. 예를 들어 numeric(1)은 "길이 1인 숫자형 값", character(3)은 "길이 3인 문자형 벡터"가 나와야 한다는 뜻입니다. 이름 붙은 벡터(c(최솟값 = 0, 최댓값 = 0))를 넣으면 그 이름이 결과의 행 이름으로도 그대로 쓰입니다.

실제 반환값이 FUN.VALUE의 형식과 다르면 결과를 조용히 리스트로 바꾸는 대신 그 자리에서 즉시 오류를 냅니다. 대규모 코드나 함수 내부에서 예상치 못한 입력으로부터 스스로를 방어해야 할 때 sapply()보다 안전한 선택입니다.

# vapply로 fivenum(최솟값, Q1, 중위수, Q3, 최댓값) 계산 - 출력 이름까지 지정
vapply(iris[1:4], fivenum,
       FUN.VALUE = c(최소값 = 0, Q1 = 0, 중위수 = 0, Q3 = 0, 최대값 = 0))
#>        Sepal.Length Sepal.Width Petal.Length Petal.Width
#> 최소값          4.3         2.0         1.00         0.1
#> Q1              5.1         2.8         1.60         0.3
#> 중위수          5.8         3.0         4.35         1.3
#> Q3              6.4         3.3         5.10         1.8
#> 최대값          7.9         4.4         6.90         2.5

앞서 sapply()가 조용히 리스트로 단순화에 실패했던 예제를 vapply()로 다시 실행하면, 형식이 어긋난 원소를 바로 지목하며 오류가 발생합니다.

lst <- list(a = c(1, 2, 3), b = c(5, NA), c = c(7, 8, 9))
tryCatch(
  vapply(lst, \(v) v[!is.na(v)], FUN.VALUE = numeric(3)),
  error = function(e) cat("에러:", conditionMessage(e), "\n")
)
#> 에러: values must be length 3,
#>  but FUN(X[[2]]) result is length 1 

tapply()

tapply(X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)는 벡터 X를 팩터(요인) INDEX의 수준별로 나누어 FUN을 적용합니다.

  • X : 그룹별로 나눌 벡터. 반드시 벡터 하나만 넣을 수 있으며, 데이터프레임 전체를 그룹별로 처리하려면 by()(아래) 또는 aggregate()(8.3절)를 사용해야 합니다.
  • INDEX : 그룹을 나눌 팩터 하나 또는 팩터를 담은 리스트. 팩터가 아닌 값을 넣어도 내부적으로 자동 변환됩니다.
  • FUN : 그룹마다 적용할 함수.
  • default : 데이터가 하나도 없는 그룹 조합을 어떤 값으로 채울지 지정합니다(기본값 NA).
  • simplify : TRUE(기본값)이면 배열로 단순화하고, FALSE이면 리스트로 반환합니다.
f1 <- factor(c("A", "A", "B"), levels = c("A", "B", "C"))   # C 수준은 실제 데이터가 없음
x <- c(10, 20, 30)
tapply(x, f1, sum)                # 데이터가 없는 C는 기본값 NA로 채워짐
#>  A  B  C 
#> 30 30 NA 

tapply(x, f1, sum, default = 0)   # default를 0으로 지정하면 0으로 채워짐
#>  A  B  C 
#> 30 30  0 
tapply(iris$Sepal.Length, iris$Species, mean)
#>     setosa versicolor  virginica 
#>      5.006      5.936      6.588 

팩터를 2개 이상(리스트로) 넣으면 교차표 형태로 결과가 만들어집니다.

tapply(warpbreaks$breaks, warpbreaks[c("wool", "tension")], mean)
#>     tension
#> wool        L        M        H
#>    A 44.55556 24.00000 24.55556
#>    B 28.22222 28.77778 18.77778

mapply()

mapply(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)는 sapply()의 다변량(multivariate) 버전입니다. sapply()가 벡터 하나의 원소를 순서대로 FUN에 넘기는 반면, mapply()는 여러 개의 벡터·리스트를 나란히 놓고 같은 위치의 원소끼리 짝지어 FUN에 전달합니다.

  • FUN : 여러 인자를 받는 함수.
  • ... : 원소별로 짝지어 순회할 벡터·리스트들. 이름을 붙이면(x = ..., y = ...) 그 이름이 FUN의 인자 이름과 매칭됩니다.
  • MoreArgs : 원소마다 바뀌지 않고 매번 동일하게 넘길 인자를 리스트로 지정합니다.
  • SIMPLIFY : sapply()의 simplify와 같은 역할로, 결과를 벡터·행렬로 단순화할지 정합니다(기본값 TRUE).
  • USE.NAMES : sapply()와 동일합니다.
mapply(\(x, y) x + y, x = 1:3, y = 4:6)
#> [1] 5 7 9
# rep(times=1, x=4), rep(times=2, x=3), rep(times=3, x=2), rep(times=4, x=1)
mapply(rep, times = 1:4, x = 4:1)
#> [[1]]
#> [1] 4
#> 
#> [[2]]
#> [1] 3 3
#> 
#> [[3]]
#> [1] 2 2 2
#> 
#> [[4]]
#> [1] 1 1 1 1
# MoreArgs: x = 23은 매번 동일하게 넘기고, times만 1~4로 바꿈
mapply(rep, 1:4, MoreArgs = list(x = 23))
#> [[1]]
#> [1] 23
#> 
#> [[2]]
#> [1] 23 23
#> 
#> [[3]]
#> [1] 23 23 23
#> 
#> [[4]]
#> [1] 23 23 23 23
mapply(\(i, s) sprintf("%d번: %s", i, s),
       i = 1:3, s = c("사과", "바나나", "포도"))
#> [1] "1번: 사과"   "2번: 바나나" "3번: 포도"  

eapply()

eapply(env, FUN, ..., all.names = FALSE, USE.NAMES = TRUE)는 환경(environment)에 들어 있는 변수들을 하나씩 FUN에 적용합니다.

  • env : 대상이 되는 환경.
  • FUN : 각 변수에 적용할 함수.
  • all.names : FALSE(기본값)이면 점(.)으로 시작하는 이름의 변수는 건너뛰고, TRUE이면 그런 변수도 포함합니다.
  • USE.NAMES : sapply()와 동일합니다.

함수를 패키지 단위로 관리하거나 환경을 직접 다뤄야 하는 고급 프로그래밍에서 주로 쓰입니다.

env <- new.env()
env$a <- 10
env$.hidden <- 999   # 점으로 시작하는 변수는 기본적으로 숨겨짐

eapply(env, \(v) v * 2)                  # .hidden은 결과에서 빠짐
#> $a
#> [1] 20
#> 

eapply(env, \(v) v * 2, all.names = TRUE) # .hidden까지 포함
#> $a
#> [1] 20
#> 
#> $.hidden
#> [1] 1998

by()

by(data, INDICES, FUN, ..., simplify = TRUE)는 데이터프레임(또는 행렬) 전체를 팩터 INDICES의 수준별로 나누어 FUN을 적용합니다.

  • data : 그룹별로 나눌 데이터프레임(또는 행렬).
  • INDICES : 그룹을 나눌 팩터 하나 또는 팩터 리스트. data의 행 수와 길이가 같아야 합니다.
  • FUN : 그룹마다(부분 데이터프레임 단위로) 적용할 함수.
  • simplify : TRUE(기본값)이면 가능한 경우 배열로 단순화하고, FALSE이면 항상 by 객체(리스트 형태)로 반환합니다.

tapply()가 벡터 하나만 다룰 수 있는 것과 달리, by()는 여러 열로 이루어진 데이터프레임을 그룹별 부분 데이터프레임으로 나누어 넘긴다는 점이 핵심입니다.

by(iris[1:4], iris$Species, colMeans)
#> iris$Species: setosa
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>        5.006        3.428        1.462        0.246 
#> ------------------------------------------------------------ 
#> iris$Species: versicolor
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>        5.936        2.770        4.260        1.326 
#> ------------------------------------------------------------ 
#> iris$Species: virginica
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>        6.588        2.974        5.552        2.026 

더 알아보기: rapply() — 중첩 리스트를 위한 재귀적 apply

지금까지의 apply 계열은 리스트 안에 또 리스트가 들어 있는(중첩된) 구조까지는 파고들지 않습니다. rapply(object, f, classes = "ANY", deflt = NULL, how = "unlist", ...)는 리스트를 재귀적으로 탐색하면서, 리스트가 아닌 말단(leaf) 값에만 함수 f를 적용합니다.

  • object : 탐색할 (중첩) 리스트.
  • f : 말단 값에 적용할 함수.
  • classes : f를 적용할 대상의 클래스를 제한합니다(기본값 "ANY"는 모든 클래스).
  • deflt : classes에 해당하지 않는 말단 값을 무엇으로 대신할지 지정합니다(how = "unlist"/"replace"일 때).
  • how : "unlist"(기본값)이면 결과를 벡터로 펼치고, "replace"이면 원래의 중첩 구조를 그대로 유지한 채 값만 바꾸며, "list"이면 구조는 유지하되 매칭되지 않은 원소를 deflt가 아닌 NULL로 채웁니다.
lst <- list(a = 1:3, b = list(c = 4:6, d = 7:9))
rapply(lst, sum, how = "unlist")
#>   a b.c b.d 
#>   6  15  24 

rapply(lst, \(x) x * 10, how = "replace")
#> $a
#> [1] 10 20 30
#> 
#> $b
#> $b$c
#> [1] 40 50 60
#> 
#> $b$d
#> [1] 70 80 90

classes와 deflt를 지정하면 특정 자료형에만 함수를 적용하고 나머지는 지정한 값으로 채울 수 있습니다.

lst2 <- list(a = 1:3, b = "문자열", c = list(d = 4:6))
rapply(lst2, sum, classes = "integer", deflt = NA, how = "unlist")
#>   a   b c.d 
#>   6  NA  15 

실무에서 자주 쓰이는 함수는 아니지만, JSON을 읽어 들인 결과처럼 깊이가 일정하지 않은 중첩 리스트를 다룰 때 유용합니다.

8.2 생성·반복 함수

rep()

rep(x, times = 1, length.out = NA, each = 1)은 x를 반복해서 벡터를 만듭니다.

  • times : x 전체(또는 원소별로 다른 횟수를 지정하려면 x와 길이가 같은 벡터)를 몇 번 반복할지.
  • each : 원소 하나하나를 몇 번씩 연달아 반복할지.
  • length.out : 최종 결과의 길이. 반복하다가 이 길이를 채우면 잘라내고, 모자라면 처음부터 다시 반복해 채웁니다.
rep(1:3, times = 2)          # 전체 반복
#> [1] 1 2 3 1 2 3

rep(1:3, each = 2)           # 원소별 반복
#> [1] 1 1 2 2 3 3

rep(1:3, times = c(1, 2, 3)) # 원소마다 다른 횟수로 반복
#> [1] 1 2 2 3 3 3

rep_len(1:3, 8)              # 길이를 8로 맞춰서 반복(rep(x, length.out = 8)과 동일)
#> [1] 1 2 3 1 2 3 1 2

each·times·length.out은 함께 지정할 수도 있습니다. 이때는 each를 먼저 적용한 뒤 times로 통째로 반복하거나, length.out으로 길이를 자르는 순서로 계산됩니다.

rep(1:3, each = 2, times = 2)          # 원소별로 2번씩 반복한 결과를 다시 2번 반복
#>  [1] 1 1 2 2 3 3 1 1 2 2 3 3

rep(1:5, each = 2, length.out = 6)     # each 적용 후 length.out으로 길이를 잘라냄
#> [1] 1 1 2 2 3 3

rep.int()·rep_len()은 인자가 제한된 대신 rep()보다 실행 속도가 약간 더 빠릅니다.

seq()

seq(...)는 일련번호 숫자를 생성하여 반환합니다. ...에는 인자로 from(시작 숫자), to(끝 숫자), by(증가 간격), length.out(생성되는 일련번호의 길이), along.with(다른 객체와 같은 길이로 일련번호 생성)이 들어갈 수 있습니다.

seq.int()는 seq()와 결과가 거의 동일하면서 조금 더 빠르고, seq_len(length.out)·seq_along(along.with)는 무조건 1부터 시작하는 정수 시퀀스만 만드는 대신 더 빠르고 안전합니다(아래 "주의" 참고).

가장 많이 사용하는 형태는 다음과 같습니다.

  • seq(from, to)
  • seq(from, to, by = )
  • seq(from, to, length.out = )
  • seq(along.with = )
  • seq(from)
  • seq(length.out = )
seq(2, 7)     # seq(from = 2, to = 7)과 동일
#> [1] 2 3 4 5 6 7

seq(7, 2)     # 거꾸로 감소하는 시퀀스도 가능
#> [1] 7 6 5 4 3 2

seq(2, 10, by = 2)
#> [1]  2  4  6  8 10

seq(2, 10, by = pi)
#> [1] 2.000000 5.141593 8.283185

seq(2, 10, length.out = 3)
#> [1]  2  6 10

seq(along.with = c(2:7))         # c(2:7)의 길이(6)만큼 1부터 생성
#> [1] 1 2 3 4 5 6

seq(2, 10, along.with = c(2:7))
#> [1]  2.0  3.6  5.2  6.8  8.4 10.0

seq(7)                # 1부터 7까지
#> [1] 1 2 3 4 5 6 7

seq(length.out = 12)  # 1부터 12까지
#> [1]  1  2  3  4  5  6  7  8  9 10 11 12
seq.int(2, 10, by = pi)   # seq()와 결과는 같지만 조금 더 빠름
#> [1] 2.000000 5.141593 8.283185

seq_len(12)               # seq(length.out = 12)와 동일하나 더 빠르고 안전
#> [1]  1  2  3  4  5  6  7  8  9 10 11 12

seq_along(c(2:7))
#> [1] 1 2 3 4 5 6

더 알아보기: 예전에는 seq_len()·seq_along()이 seq()보다 눈에 띄게 빨랐지만, 최근 R은 아주 큰 정수 시퀀스를 실제 메모리에 펼치지 않고 "시작·끝·간격" 정보만 담아 두는 압축 표현(ALTREP)을 내부적으로 활용하기 때문에, n이 매우 커도 두 방식의 속도 차이가 사실상 사라졌습니다.

n <- 999999999999999   # 약 10^15
system.time(seq(length.out = n))
system.time(seq_len(n))
#>    user  system elapsed 
#>       0       0       0 
#>    user  system elapsed 
#>       0       0       0 

따라서 오늘날 seq_len()·seq_along()을 쓰는 진짜 이유는 속도보다 안전성입니다(아래 "주의" 참고).

날짜·시각 벡터도 만들 수 있어 시계열 데이터의 기준 축을 만들 때 자주 쓰입니다(10장 참고).

# 1년 간격
seq(as.Date("2016/1/1"), as.Date("2026/1/1"), "years")
#>  [1] "2016-01-01" "2017-01-01" "2018-01-01" "2019-01-01" "2020-01-01"
#>  [6] "2021-01-01" "2022-01-01" "2023-01-01" "2024-01-01" "2025-01-01"
#> [11] "2026-01-01"

# 월 간격
seq(as.Date("2026/1/1"), by = "month", length.out = 12)
#>  [1] "2026-01-01" "2026-02-01" "2026-03-01" "2026-04-01" "2026-05-01"
#>  [6] "2026-06-01" "2026-07-01" "2026-08-01" "2026-09-01" "2026-10-01"
#> [11] "2026-11-01" "2026-12-01"

# 분기 간격
seq(as.Date("2023/1/1"), as.Date("2026/1/1"), by = "quarter")
#>  [1] "2023-01-01" "2023-04-01" "2023-07-01" "2023-10-01" "2024-01-01"
#>  [6] "2024-04-01" "2024-07-01" "2024-10-01" "2025-01-01" "2025-04-01"
#> [11] "2025-07-01" "2025-10-01" "2026-01-01"

# 월 간격을 거꾸로(오늘 날짜 기준 1년 전까지)
seq(as.Date("2026-7-19"), as.Date("2025-7-19"), by = "-1 month")
#>  [1] "2026-07-19" "2026-06-19" "2026-05-19" "2026-04-19" "2026-03-19"
#>  [6] "2026-02-19" "2026-01-19" "2025-12-19" "2025-11-19" "2025-10-19"
#> [11] "2025-09-19" "2025-08-19" "2025-07-19"

시(hour) 단위처럼 날짜보다 세밀한 시각이 필요하면 ISOdate()·as.POSIXct()와 함께 씁니다. "DSTday"는 일광절약시간(DST)을 자동으로 보정한 하루 간격을 뜻합니다.

seq(ISOdate(2021, 1, 1), ISOdate(2026, 1, 1), "years")
#> [1] "2021-01-01 12:00:00 GMT" "2022-01-01 12:00:00 GMT"
#> [3] "2023-01-01 12:00:00 GMT" "2024-01-01 12:00:00 GMT"
#> [5] "2025-01-01 12:00:00 GMT" "2026-01-01 12:00:00 GMT"

seq(c(ISOdate(2026, 3, 20)), by = "DSTday", length.out = 10)
#>  [1] "2026-03-20 12:00:00 GMT" "2026-03-21 12:00:00 GMT"
#>  [3] "2026-03-22 12:00:00 GMT" "2026-03-23 12:00:00 GMT"
#>  [5] "2026-03-24 12:00:00 GMT" "2026-03-25 12:00:00 GMT"
#>  [7] "2026-03-26 12:00:00 GMT" "2026-03-27 12:00:00 GMT"
#>  [9] "2026-03-28 12:00:00 GMT" "2026-03-29 12:00:00 GMT"

seq(c(ISOdate(2026, 3, 20)), by = "7 DSTdays", length.out = 5)
#> [1] "2026-03-20 12:00:00 GMT" "2026-03-27 12:00:00 GMT"
#> [3] "2026-04-03 12:00:00 GMT" "2026-04-10 12:00:00 GMT"
#> [5] "2026-04-17 12:00:00 GMT"
seq(as.POSIXct("2026-3-14 17:22:15"), as.POSIXct("2026-3-15 9:12:25"),
    by = "hours")
#>  [1] "2026-03-14 17:22:15 UTC" "2026-03-14 18:22:15 UTC"
#>  [3] "2026-03-14 19:22:15 UTC" "2026-03-14 20:22:15 UTC"
#>  [5] "2026-03-14 21:22:15 UTC" "2026-03-14 22:22:15 UTC"
#>  [7] "2026-03-14 23:22:15 UTC" "2026-03-15 00:22:15 UTC"
#>  [9] "2026-03-15 01:22:15 UTC" "2026-03-15 02:22:15 UTC"
#> [11] "2026-03-15 03:22:15 UTC" "2026-03-15 04:22:15 UTC"
#> [13] "2026-03-15 05:22:15 UTC" "2026-03-15 06:22:15 UTC"
#> [15] "2026-03-15 07:22:15 UTC" "2026-03-15 08:22:15 UTC"

주의: for (i in 1:length(x))처럼 1:length(x)를 반복 조건으로 쓰는 습관은 x가 빈 벡터(길이 0)일 때 1:0이 c(1, 0)이 되어 버려 의도와 다르게 두 번 반복되는 함정이 있습니다. 이런 경우를 대비해 seq_len(length(x)) 또는 seq_along(x)를 쓰는 것이 안전합니다.

x <- numeric(0)   # 빈 벡터
1:length(x)        # 의도와 달리 c(1, 0)이 되어버림
seq_along(x)       # 빈 벡터에 대해 올바르게 빈 정수형 벡터를 반환
#> [1] 1 0
#> integer(0)

sequence()

sequence(nvec, from = 1, by = 1)는 nvec에 담긴 각 숫자만큼 seq()를 반복 수행하고, 그 결과를 이어 붙입니다. 즉 nvec = c(3, 2)이면 seq(3)과 seq(2)를 실행한 뒤 c(1, 2, 3, 1, 2)를 반환하는 식입니다.

  • nvec : 그룹별로 몇 개씩 만들지 정하는 개수 벡터.
  • from : 그룹마다 시작할 숫자. 길이 1이면 모든 그룹에 동일하게, nvec와 길이가 같은 벡터면 그룹마다 다르게 적용됩니다.
  • by : 그룹마다 증가폭. from과 마찬가지로 그룹별로 다르게 지정할 수 있습니다.
sequence(c(3, 2))                    # c(1:3, 1:2)와 동일
#> [1] 1 2 3 1 2

sequence(c(3, 2), from = c(1, 10))   # 그룹마다 시작 숫자를 다르게 지정
#> [1]  1  2  3 10 11
sequence(c(3, 2), from = 2, by = -2)   # 시작값 2에서 -2씩 감소
#> [1]  2  0 -2  2  0

sequence(c(3, 2), by = c(2, -2))       # 그룹마다 증가폭을 다르게 지정
#> [1]  1  3  5  1 -1

gl()

gl(n, k, length = n*k, labels = seq_len(n), ordered = FALSE)는 요인(factor) 수준을 규칙적으로 반복 생성합니다("generate levels"의 약자).

  • n : 수준(level)의 개수.
  • k : 각 수준을 연달아 몇 번 반복할지.
  • length : 최종 결과의 길이. 기본값은 n * k이지만, 더 짧게 자르거나 더 길게(처음부터 다시 반복해서) 지정할 수 있습니다.
  • labels : 수준에 붙일 이름. 기본값은 1, 2, ..., n이라는 숫자 이름입니다.
  • ordered : TRUE이면 순서형(ordered) 요인으로 만듭니다.
gl(2, 3, labels = c("대조군", "처치군"))
#> [1] 대조군 대조군 대조군 처치군 처치군 처치군
#> Levels: 대조군 처치군
gl(2, 1, length = 8, labels = c("M", "F"))   # 1개씩 번갈아 8개 생성
#> [1] M F M F M F M F
#> Levels: M F

replicate()

replicate(n, expr, simplify = "array")는 expr을 n번 반복 실행합니다.

  • n : 반복 횟수.
  • expr : 반복 실행할 코드(표현식). 매번 새로 평가되므로 rnorm()·sample()처럼 실행할 때마다 다른 결과가 나오는 코드에 적합합니다.
  • simplify : 결과를 배열·행렬로 정리할지("array", 기본값) 아니면 리스트로 반환할지(FALSE)를 정합니다. sapply()의 simplify와 같은 방식으로 동작합니다.
set.seed(123)
replicate(3, rnorm(2))   # rnorm(2)를 3번 실행해 2x3 행렬로 결합
#>            [,1]       [,2]      [,3]
#> [1,] -0.5604756 1.55870831 0.1292877
#> [2,] -0.2301775 0.07050839 1.7150650
replicate(3, sample(1:5, 2), simplify = FALSE)   # 결과 길이가 매번 같아도 리스트로 받고 싶을 때
#> [[1]]
#> [1] 1 2
#> 
#> [[2]]
#> [1] 3 4
#> 
#> [[3]]
#> [1] 5 3

Vectorize()

Vectorize(FUN, vectorize.args = arg.names, SIMPLIFY = TRUE, USE.NAMES = TRUE)는 인자를 하나만 받도록 설계된 함수를, 벡터를 넘기면 원소별로 나누어 처리하는 함수로 바꿔 줍니다.

  • FUN : 벡터화하고 싶은 원래 함수.
  • vectorize.args : FUN의 인자 중 벡터로 넘겼을 때 원소별로 순회하도록 만들 인자 이름(문자 벡터). 지정하지 않으면 FUN의 인자 전부가 대상이 됩니다.
  • SIMPLIFY, USE.NAMES : mapply()와 같은 역할을 합니다(Vectorize()는 내부적으로 mapply()를 감싼 함수입니다).

예를 들어 grepl()의 pattern 인자는 여러 패턴을 벡터로 넣어도 첫 번째만 사용하고 나머지는 무시합니다.

# grepl()은 pattern에 벡터를 넣어도 첫 번째 값만 사용됨(경고 발생)
x <- grepl(pattern = c("^vi", "^se"), x = iris$Species)
head(x)
#> [1] FALSE FALSE FALSE FALSE FALSE FALSE
# pattern을 벡터화 대상으로 지정해 함수를 새로 만듦
vgrepl <- Vectorize(grepl, vectorize.args = "pattern")
x2 <- vgrepl(pattern = c("^vi", "^se"), x = iris$Species)
head(x2)
#>        ^vi  ^se
#> [1,] FALSE TRUE
#> [2,] FALSE TRUE
#> [3,] FALSE TRUE
#> [4,] FALSE TRUE
#> [5,] FALSE TRUE
#> [6,] FALSE TRUE

같은 결과는 sapply()로도 얻을 수 있습니다. Vectorize()는 사실 내부적으로 mapply()를 감싼 함수이므로, 위 코드는 "이 함수의 이 인자는 항상 벡터화해서 쓰겠다"는 의도를 이름 붙은 함수로 고정해 두고 싶을 때 유용합니다.

sapply(c("^vi", "^se"), \(p) grepl(pattern = p, x = iris$Species)) |> head()
#>        ^vi  ^se
#> [1,] FALSE TRUE
#> [2,] FALSE TRUE
#> [3,] FALSE TRUE
#> [4,] FALSE TRUE
#> [5,] FALSE TRUE
#> [6,] FALSE TRUE

8.3 분할·집계 함수

split() / unsplit()

split(x, f, drop = FALSE)는 팩터(또는 팩터 리스트) f의 수준에 따라 x(벡터·데이터프레임 등)를 리스트로 나눕니다. unsplit(value, f)는 그렇게 나뉜 리스트를 f 기준으로 다시 원래 순서의 벡터·데이터프레임으로 되돌립니다.

  • x : 나눌 대상(벡터, 데이터프레임 등).
  • f : 그룹을 나눌 팩터(또는 팩터 리스트).
  • drop : FALSE(기본값)이면 실제 데이터가 하나도 없는 수준도 빈 그룹으로 남기고, TRUE이면 그런 수준은 리스트에서 아예 제거합니다.
f <- factor(c("A", "A", "B"), levels = c("A", "B", "C"))   # C 수준은 데이터가 없음
x <- c(10, 20, 30)
str(split(x, f))               # drop = FALSE(기본): 빈 그룹 C도 리스트에 남음
#> List of 3
#>  $ A: num [1:2] 10 20
#>  $ B: num 30
#>  $ C: num(0) 

str(split(x, f, drop = TRUE))  # drop = TRUE: 데이터가 없는 그룹은 제거
#> List of 2
#>  $ A: num [1:2] 10 20
#>  $ B: num 30

iris 데이터를 종(Species)별로 완전히 나눈 뒤, 나뉜 각 부분에 원하는 계산을 적용하고 다시 하나의 표로 모아 보겠습니다.

grouped <- split(iris[1:4], iris$Species)   # 종별로 데이터프레임을 통째로 분할
sapply(grouped, colMeans)                    # 나뉜 조각마다 열 평균을 구해 다시 결합
#>              setosa versicolor virginica
#> Sepal.Length  5.006      5.936     6.588
#> Sepal.Width   3.428      2.770     2.974
#> Petal.Length  1.462      4.260     5.552
#> Petal.Width   0.246      1.326     2.026
x <- split(iris, iris$Species)
str(x, max.level = 1)
#> List of 3
#>  $ setosa    :'data.frame':  50 obs. of  5 variables:
#>  $ versicolor:'data.frame':  50 obs. of  5 variables:
#>  $ virginica :'data.frame':  50 obs. of  5 variables:
y <- unsplit(x, iris$Species)
str(y)
#> 'data.frame':    150 obs. of  5 variables:
#>  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
#>  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
#>  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
#>  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
#>  $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...

split()로 나눈 뒤 sapply()·lapply()로 그룹마다 원하는 계산을 적용하는 "분할-적용-결합" 조합은 tapply()·by()로 표현하기 번거로운 복잡한 그룹별 처리(예: 그룹마다 다른 모형을 적합하는 경우)에 특히 유용합니다.

sweep()

sweep(x, MARGIN, STATS, FUN = "-", ...)는 행렬·데이터프레임 x의 행 또는 열 방향으로 통계량 STATS를 FUN으로 일괄 적용합니다.

  • x : 대상 행렬·배열(또는 데이터프레임).
  • MARGIN : STATS를 어느 방향 기준으로 맞춰 적용할지. 1이면 행마다(행 개수만큼의 STATS를 각 행에), 2이면 열마다 적용합니다.
  • STATS : 적용할 값(통계량). 보통 apply()로 미리 구한 행별·열별 합계·평균 벡터를 넣습니다.
  • FUN : 적용할 연산(기본값은 뺄셈 "-"). "/"(나누기)처럼 다른 연산자 문자열도 넣을 수 있습니다.
  • ... : FUN에 추가로 넘길 인자.

주로 apply()로 구한 행·열별 합계나 평균을 원본에 다시 적용해 비율이나 편차를 구할 때 사용합니다.

(x <- matrix(1:12, nrow = 4))
#>      [,1] [,2] [,3]
#> [1,]    1    5    9
#> [2,]    2    6   10
#> [3,]    3    7   11
#> [4,]    4    8   12
# 행 합계로 나누어 각 행을 비율(%)로 변환
round(sweep(x, 1, apply(x, 1, sum), FUN = "/") * 100, 1)
#>     [,1] [,2] [,3]
#> [1,]  6.7 33.3 60.0
#> [2,] 11.1 33.3 55.6
#> [3,] 14.3 33.3 52.4
#> [4,] 16.7 33.3 50.0
# 행 평균을 빼서 행별로 중심화(centering)
round(sweep(x, 1, apply(x, 1, mean), FUN = "-"), 2)
#>      [,1] [,2] [,3]
#> [1,]   -4    0    4
#> [2,]   -4    0    4
#> [3,]   -4    0    4
#> [4,]   -4    0    4

aggregate()

aggregate(x, by, FUN, ...)는 데이터 x를 by에 지정한 그룹별로 나누어 FUN을 적용하고, 결과를 데이터프레임으로 반환합니다.

  • x : 요약할 데이터(벡터·데이터프레임). aggregate(formula, data, FUN)처럼 수식(formula) 표기로 x와 by를 한꺼번에 지정할 수도 있습니다.
  • by : 그룹 기준이 되는 팩터들을 담은 리스트(팩터 하나만 있어도 리스트로 감싸야 합니다).
  • FUN : 그룹마다 적용할 함수.
  • drop : TRUE(기본값)이면 실제 데이터가 없는 그룹 조합은 결과에서 아예 빼고, FALSE이면 그런 조합도 남기되 값은 NA로 채웁니다(aggregate(formula, ...) 방식에는 없는, 데이터프레임 방식 전용 인자입니다).
g <- factor(c("A", "A", "B"), levels = c("A", "B", "C"))   # C 수준은 데이터가 없음
x <- c(10, 20, 30)
aggregate(x, by = list(g = g), sum)                # drop = TRUE(기본): 데이터 없는 C는 빠짐
#>   g  x
#> 1 A 30
#> 2 B 30

aggregate(x, by = list(g = g), sum, drop = FALSE)  # drop = FALSE: 없는 조합도 NA로 표시
#>   g  x
#> 1 A 30
#> 2 B 30
#> 3 C NA
aggregate(iris[1:4], by = list(종 = iris$Species), mean)
#>          종 Sepal.Length Sepal.Width Petal.Length Petal.Width
#> 1     setosa        5.006       3.428        1.462       0.246
#> 2 versicolor        5.936       2.770        4.260       1.326
#> 3  virginica        6.588       2.974        5.552       2.026
# 수식(formula) 표기: 반응변수 ~ 그룹변수
aggregate(Sepal.Length ~ Species, data = iris, mean)
#>      Species Sepal.Length
#> 1     setosa        5.006
#> 2 versicolor        5.936
#> 3  virginica        6.588

FUN이 길이 1보다 큰 값을 반환하면 여러 통계량을 한 번에 계산해 열로 펼쳐 줍니다.

aggregate(Sepal.Length ~ Species, data = iris,
          FUN = \(x) c(평균 = mean(x), 표준편차 = sd(x)))
#>      Species Sepal.Length.평균 Sepal.Length.표준편차
#> 1     setosa         5.0060000              0.3524897
#> 2 versicolor         5.9360000              0.5161711
#> 3  virginica         6.5880000              0.6358796

8.4 함수형 도구

R은 함수를 값처럼 변수에 담고 다른 함수의 인자로 넘길 수 있는, 함수를 일급 객체(first-class object)로 다루는 함수형 프로그래밍 언어이기도 합니다.

Reduce()

Reduce(f, x, init, right = FALSE, accumulate = FALSE)는 이항 함수 f(인자 2개를 받는 함수)를 이용해 벡터·리스트 x의 원소를 차례로 하나로 접습니다.

  • f : 인자 2개를 받아 하나의 값으로 합치는 함수.
  • x : 접어 나갈 벡터 또는 리스트.
  • init : 계산을 시작할 초깃값. 지정하면 x의 첫 원소 대신 이 값에서부터 f를 적용하기 시작합니다. 생략하면 x의 원소만으로 접는데, x가 빈 벡터일 때는 계산할 것이 없어 NULL을 반환합니다.
  • right : FALSE(기본값)이면 왼쪽부터, TRUE이면 오른쪽부터 접습니다.
  • accumulate : TRUE로 지정하면 최종 결과뿐 아니라 접어 나가는 중간 과정을 모두 벡터·리스트로 보여줍니다.
Reduce(`+`, 1:5, 100)                      # 초깃값 100에서 시작해 누적
#> [1] 115

Reduce(`+`, 1:5, 100, accumulate = TRUE)   # 초깃값부터 누적되는 과정을 모두 표시
#> [1] 100 101 103 106 110 115

init을 지정하면 x가 빈 벡터라도 안전하게 초깃값을 그대로 돌려받을 수 있습니다.

Reduce(`+`, integer(0))              # 접을 원소가 없고 init도 없어 NULL 반환
#> NULL

Reduce(`+`, integer(0), init = 0)    # init이 있으면 빈 벡터라도 안전하게 처리됨
#> [1] 0
Reduce(`+`, 1:5)                       # ((((1+2)+3)+4)+5)
#> [1] 15

Reduce(`+`, 1:5, accumulate = TRUE)    # 누적되는 과정을 그대로 보여줌
#> [1]  1  3  6 10 15
Reduce(\(acc, x) acc * x, 1:5, accumulate = TRUE)   # 팩토리얼의 누적 과정(1!, 2!, ..., 5!)
#> [1]   1   2   6  24 120

right 인자로 접는 방향을 바꾸면 결과(특히 문자열 결합처럼 순서에 민감한 연산)가 달라질 수 있습니다.

Reduce(\(a, b) paste0("(", a, "-", b, ")"), c("A", "B", "C", "D"))
#> [1] "(((A-B)-C)-D)"

Reduce(\(a, b) paste0("(", a, "-", b, ")"), c("A", "B", "C", "D"), right = TRUE)
#> [1] "(A-(B-(C-D)))"

Filter() / Find() / Position()

Filter(f, x)는 조건 함수(predicate) f가 TRUE를 반환하는 원소만 골라 x와 같은 자료구조(벡터 또는 리스트)로 반환합니다. Find(f, x, right = FALSE, nomatch = NULL)는 조건을 만족하는 첫 번째 값을, Position(f, x, right = FALSE, nomatch = NA_integer_)는 그 값의 위치를 반환합니다.

  • f : TRUE/FALSE를 반환하는 조건 함수.
  • x : 검사할 벡터 또는 리스트.
  • right : FALSE(기본값)이면 앞에서부터, TRUE이면 뒤에서부터 찾습니다.
  • nomatch : 조건을 만족하는 원소가 하나도 없을 때 대신 반환할 값(Find()는 기본 NULL, Position()은 기본 NA).
nums <- 1:20
Filter(\(x) x %% 3 == 0, nums)
#> [1]  3  6  9 12 15 18

리스트에 적용하면 특정 조건(자료형 등)을 만족하는 원소만 걸러낼 수 있습니다.

lst <- list(a = 1:3, b = data.frame(x = 1), c = "문자열", d = data.frame(y = 2))
Filter(is.data.frame, lst)
#> $b
#>   x
#> 1 1
#> 
#> $d
#>   y
#> 1 2
Find(\(x) x %% 7 == 0, nums)      # 7의 배수 중 처음 나오는 값
#> [1] 7

Position(\(x) x %% 7 == 0, nums)  # 그 값의 위치(인덱스)
#> [1] 7
Find(\(x) x %% 7 == 0, nums, right = TRUE)      # 뒤에서부터 찾은 첫 값
#> [1] 14

Position(\(x) x %% 7 == 0, nums, right = TRUE)  # 그 위치
#> [1] 14

조건을 만족하는 값이 하나도 없으면 nomatch로 지정한 값을 대신 돌려받습니다.

Position(\(x) x > 100, nums)                # 기본값 NA
#> [1] NA

Position(\(x) x > 100, nums, nomatch = 0)   # nomatch로 대체값 지정
#> [1] 0

Map()

Map(f, ...)은 여러 개의 벡터·리스트를 원소 위치별로 짝지어 함수 f에 전달합니다.

  • f : 여러 인자를 받는 함수.
  • ... : 원소별로 짝지어 순회할 벡터·리스트들.

내부적으로 mapply(f, ..., SIMPLIFY = FALSE)와 동일하게 동작하며, 결과를 단순화하지 않고 항상 리스트로 돌려준다는 점이 mapply()와의 차이입니다. 다만 mapply()의 MoreArgs에 해당하는 인자는 따로 없으므로, 원소마다 바뀌지 않는 값을 함께 넘기려면 익명 함수로 감싸서 그 값을 직접 채워 넣어야 합니다.

# times = 3은 매번 동일하게 넘기고 싶은 상수 -> 익명 함수 안에서 직접 지정
Map(\(x) rep(x, times = 3), 1:3)
#> [[1]]
#> [1] 1 1 1
#> 
#> [[2]]
#> [1] 2 2 2
#> 
#> [[3]]
#> [1] 3 3 3
Map(\(x, y) x + y, 1:3, 4:6)
#> [[1]]
#> [1] 5
#> 
#> [[2]]
#> [1] 7
#> 
#> [[3]]
#> [1] 9
identical(Map(\(x, y) x + y, 1:3, 4:6),
          mapply(\(x, y) x + y, 1:3, 4:6, SIMPLIFY = FALSE))
#> [1] TRUE

do.call()

do.call(what, args)는 리스트 args에 담긴 값들을 함수 what의 인자로 하나씩 풀어서 호출합니다.

  • what : 호출할 함수. 함수 객체를 직접 넘겨도 되고, "rbind"처럼 함수 이름을 문자열로 넘겨도 됩니다.
  • args : what에 넘길 인자들을 담은 리스트. 이름이 붙어 있으면(list(x = ..., na.rm = TRUE)) 그 이름 그대로 인자 이름에 매칭됩니다.

함수의 인자를 미리 리스트로 조립해 두었다가 나중에 한꺼번에 호출하고 싶을 때, 그리고 lapply()·Map()·split()으로 나온 리스트 형태의 결과를 다시 하나의 행렬·데이터프레임으로 결합할 때 특히 자주 쓰입니다.

do.call("rbind", list(1:3, 4:6))   # what 자리에 함수 이름을 문자열로 지정해도 됨
#>      [,1] [,2] [,3]
#> [1,]    1    2    3
#> [2,]    4    5    6
args_list <- list(x = 1:10, na.rm = TRUE)
do.call(mean, args_list)   # mean(x = 1:10, na.rm = TRUE)와 동일
#> [1] 5.5
# split()으로 나눈 뒤 lapply()로 그룹별 평균을 구하고, do.call(rbind, ...)로 한 데이터프레임으로 결합
grouped <- split(iris, iris$Species)
means_list <- lapply(grouped, \(df) colMeans(df[1:4]))
do.call(rbind, means_list)
#>            Sepal.Length Sepal.Width Petal.Length Petal.Width
#> setosa            5.006       3.428        1.462       0.246
#> versicolor        5.936       2.770        4.260       1.326
#> virginica         6.588       2.974        5.552       2.026