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) {...} 축약 문법을 쓸 수 있습니다.
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()는 행렬을 넣어도 원소를 하나씩(벡터처럼) 풀어서 전달한다는 점에 주의해야 합니다.
리스트 형태의 결과가 불편하면 unlist()로 벡터로 바꿀 수 있습니다(4.6.5절).
데이터프레임을 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
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
팩터를 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()와 동일합니다.
# 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)를 쓰는 것이 안전합니다.
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) 요인으로 만듭니다.
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).
리스트에 적용하면 특정 조건(자료형 등)을 만족하는 원소만 걸러낼 수 있습니다.
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
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