이번 문서의 목표: dplyr 패키지의 6대 동사(filter·select·mutate·arrange·group_by·summarise)로 데이터프레임을 원하는 형태로 가공하고, 파이프 연산자로 여러 단계를 이어 쓰며, apply 계열 함수(apply·sapply·lapply·tapply)가 각각 어떤 타입을 반환하는지 실행 결과 수준까지 설명할 수 있다.
dplyr가 필요한 이유
왜 필요한가
11편에서 배운 데이터프레임 인덱싱(df[df$age >= 25, ] 같은 코드)은 정확하지만, 여러 조건을 동시에 걸거나 여러 단계를 거쳐 데이터를 가공하려면 코드가 금세 복잡해지고 읽기 어려워진다. dplyr는 데이터프레임을 다루는 작업을 “동사(verb) 하나 = 작업 하나”로 표현하도록 설계된 R 패키지로, 코드가 사람이 읽는 문장처럼 자연스럽게 이어진다는 것이 가장 큰 장점이다. R로 하는 실제 데이터분석 작업의 대부분이 이 dplyr 문법을 기반으로 이루어질 만큼, R 생태계에서 사실상 표준으로 자리 잡은 도구다.
쉽게 말하면: dplyr는 “걸러라(filter), 골라라(select), 만들어라(mutate), 정렬하라(arrange), 요약하라(summarise)“처럼 동사 하나로 표에 대한 작업 하나를 표현하는 도구 상자다.
dplyr의 6대 동사 한눈에 보기
| 동사 | 역할 | 비유 |
|---|---|---|
filter() | 조건을 만족하는 행을 골라낸다 | 조건에 맞는 사람만 남기고 나머지는 제외 |
select() | 원하는 열만 골라낸다 | 표에서 필요한 항목(열)만 오려내기 |
mutate() | 기존 열을 바탕으로 새로운 열을 추가한다 | 계산해서 새 항목을 표에 덧붙이기 |
arrange() | 지정한 열을 기준으로 행의 순서를 정렬한다 | 성적순으로 줄 세우기 |
group_by() | 지정한 열의 값을 기준으로 데이터를 그룹으로 나눈다 | 반별로 묶기 |
summarise() | 그룹(또는 전체)에 대해 평균·합계 같은 요약값을 계산한다 | 반별 평균 점수 뽑기 |
이후 예제 전체에서 사용할 학생 데이터프레임을 먼저 만들어 두자.
students <- data.frame(
name = c("지훈", "수민", "예린", "도윤", "하은"),
class = c("A", "B", "A", "B", "A"),
score = c(85, 90, 78, 92, 88)
)
students
## name class score
## 1 지훈 A 85
## 2 수민 B 90
## 3 예린 A 78
## 4 도윤 B 92
## 5 하은 A 88filter(): 조건에 맞는 행 걸러내기
왜 필요한가
전체 학생 중 “성적이 85점 이상인 학생만” 보고 싶을 때처럼, 조건을 만족하는 행만 남기고 나머지는 제외해야 할 때가 많다. filter()는 이 작업을 데이터프레임 인덱싱보다 훨씬 읽기 쉬운 형태로 표현한다.
쉽게 말하면: filter는 조건이라는 체(sieve)로 걸러 통과한 행만 남기는 것이다.
정의와 예시
library(dplyr)
filter(students, score >= 85)
## name class score
## 1 지훈 A 85
## 2 수민 B 90
## 3 도윤 B 92
## 4 하은 A 88filter(students, score >= 85)는 students에서 score 열이 85 이상인 행만 남긴다. 11편의 students[students$score >= 85, ]와 결과는 동일하지만, $나 대괄호 없이 열 이름을 바로 쓸 수 있어 코드가 더 짧고 읽기 쉽다.
여러 조건을 동시에 걸 때는 쉼표(,)나 &(그리고, AND)를 쓴다.
filter(students, class == "A", score >= 85)
## name class score
## 1 지훈 A 85
## 2 하은 A 88쉼표로 나열한 조건은 모두 동시에 만족해야 하는 AND 조건으로 처리된다. “A반이면서 동시에 85점 이상”인 학생만 남는다.
자주 틀리는 점
filter(students, class = "A")처럼 비교 연산자==대신 할당 연산자=을 쓴다. dplyr의 조건식에서는 반드시==을 써야 하며,=을 쓰면 에러가 발생한다.- 쉼표로 나열한 여러 조건이 OR(또는)로 처리된다고 착각한다. 실제로는 AND(그리고)로 처리되며, OR 조건이 필요하면
|기호를 명시해야 한다.
select(): 원하는 열만 골라내기
왜 필요한가
수십 개의 열을 가진 실제 데이터에서 분석에 필요한 열 몇 개만 추려내고 싶을 때 select()를 쓴다. 열이 몇 개 없는 예시 데이터에서는 효과가 잘 안 느껴지지만, 실무처럼 열이 수십~수백 개인 데이터에서는 필수적인 작업이다.
쉽게 말하면: select는 표에서 보고 싶은 열의 이름만 콕 집어 오려내는 것이다.
정의와 예시
select(students, name, score)
## name score
## 1 지훈 85
## 2 수민 90
## 3 예린 78
## 4 도윤 92
## 5 하은 88특정 열을 빼고 싶을 때는 열 이름 앞에 마이너스 기호를 붙인다. 이것은 11편에서 본 벡터의 음수 인덱싱(제외를 뜻함)과 같은 논리다.
select(students, -class)
## name score
## 1 지훈 85
## 2 수민 90
## 3 예린 78
## 4 도윤 92
## 5 하은 88자주 틀리는 점
select()에 열 이름을 따옴표로 감싸야 한다고 착각한다. dplyr 계열 함수 대부분은 열 이름을 따옴표 없이 그대로 쓴다(비표준평가라는 dplyr의 설계 방식 때문이다).
mutate(): 새로운 열 추가하기
왜 필요한가
기존 열의 값을 계산해 새로운 열로 붙여야 할 때가 많다. 예를 들어 점수에 가산점을 더한 최종 점수 열을 만들거나, 두 열의 비율을 계산한 새 열을 추가하는 경우다. mutate()는 이런 파생 변수(derived variable, 기존 변수를 가공해 만든 새 변수)를 만드는 동사다.
쉽게 말하면: mutate는 기존 열을 재료로 계산해서 표에 새 열을 하나 더 붙이는 것이다.
정의와 예시
mutate(students, bonus_score = score + 5)
## name class score bonus_score
## 1 지훈 A 85 90
## 2 수민 B 90 95
## 3 예린 A 78 83
## 4 도윤 B 92 97
## 5 하은 A 88 93mutate(students, bonus_score = score + 5)는 기존 score 열에 5를 더한 값을 bonus_score라는 새 열로 추가한다. 원본 열(score)은 그대로 남고 새 열만 오른쪽에 덧붙는다.
자주 틀리는 점
mutate()가 기존 열을 수정해 없앤다고 착각한다. 실제로는 기존 열은 그대로 두고 새 열을 추가만 한다(다만 새 열의 이름을 기존 열과 똑같이 지으면 그 열을 덮어쓴다).
arrange(): 행 정렬하기
왜 필요한가
성적순으로 학생을 줄 세우거나, 날짜순으로 기록을 나열하는 것처럼 특정 기준으로 행의 순서를 바꿔야 할 때 arrange()를 쓴다.
쉽게 말하면: arrange는 지정한 기준으로 표의 행을 줄 세우는 것이다.
정의와 예시
arrange(students, score)
## name class score
## 1 예린 A 78
## 2 지훈 A 85
## 3 하은 A 88
## 4 수민 B 90
## 5 도윤 B 92기본은 오름차순(작은 값부터)이다. 내림차순(큰 값부터)으로 정렬하려면 desc()(descending, 내림차순)로 감싼다.
arrange(students, desc(score))
## name class score
## 1 도윤 B 92
## 2 수민 B 90
## 3 하은 A 88
## 4 지훈 A 85
## 5 예린 A 78자주 틀리는 점
arrange()의 기본 정렬 방향을 내림차순으로 착각한다. 별도 지정이 없으면 항상 오름차순이며, 내림차순은desc()를 명시해야 한다.
group_by()와 summarise(): 그룹별 요약값 계산하기
왜 필요한가
“전체 평균”이 아니라 “반별 평균”처럼 그룹을 나눠서 요약 통계를 내야 할 때가 실무에서 훨씬 흔하다. group_by()로 데이터를 그룹으로 쪼갠 뒤 summarise()로 그룹마다 요약값을 계산하는 조합이 dplyr에서 가장 강력한 패턴이다.
쉽게 말하면: group_by는 반을 나누는 것이고, summarise는 나뉜 반마다 성적표 한 줄(평균·합계 등)을 뽑는 것이다.
정의와 예시
grouped <- group_by(students, class)
summarise(grouped, avg_score = mean(score), count = n())
## # A tibble: 2 x 3
## class avg_score count
## <chr> <dbl> <int>
## 1 A 83.7 3
## 2 B 91 2group_by(students, class)는 class 열의 값(“A”, “B”)을 기준으로 데이터를 두 그룹으로 나눈다. 이어서 summarise(grouped, avg_score = mean(score), count = n())는 각 그룹마다 score의 평균(mean(score))과 그룹에 속한 행의 개수(n(), 그룹 안 관측치 개수를 세는 dplyr 전용 함수)를 계산한다. A반은 3명(85, 78, 88점)의 평균이 약 83.7점, B반은 2명(90, 92점)의 평균이 91점으로 계산된다.
자주 틀리는 점
group_by()만으로 요약값이 계산된다고 착각한다.group_by()는 그룹으로 나누는 표시만 해 둘 뿐이고, 실제 요약 계산은 뒤따르는summarise()가 수행한다.summarise()안에서mean()에na.rm = TRUE를 빠뜨려, 그룹 안에 결측치가 하나라도 있으면 그룹 평균 전체가NA가 되는 것을 놓친다.
파이프 연산자로 여러 단계 잇기
왜 필요한가
지금까지는 filter(students, ...), select(students, ...)처럼 매번 데이터프레임 이름을 함수의 첫 인자로 반복해 썼다. 하지만 “필터링하고 → 정렬하고 → 필요한 열만 고르는” 것처럼 여러 동사를 연달아 적용해야 할 때, 매번 이름을 반복하고 중간 결과를 변수에 저장하면 코드가 지저분해진다. 파이프 연산자(pipe operator)는 “앞 결과를 다음 함수의 첫 인자로 그대로 넘겨준다”는 뜻으로, 여러 동사를 물 흐르듯 이어 쓸 수 있게 해 준다.
쉽게 말하면: 파이프는 “이것을 가지고 다음 작업을 해라”를 코드로 그대로 옮긴 화살표다.
정의: %>%와 |>
dplyr가 원래 도입한 파이프는 %>%(매그리트 파이프, magrittr 패키지에서 옴)이고, R 4.1 버전부터는 R 자체에 내장된 |>(네이티브 파이프)도 쓸 수 있다. 시험에서는 두 표기 모두 등장할 수 있으므로 둘 다 알아 두어야 한다.
students %>%
filter(score >= 85) %>%
arrange(desc(score)) %>%
select(name, score)
## name score
## 1 도윤 92
## 2 수민 90
## 3 하은 88
## 4 지훈 85이 코드는 “students를 가지고 → score가 85 이상인 행만 남기고 → score 내림차순으로 정렬하고 → name과 score 열만 골라라”는 문장을 그대로 코드로 옮긴 것이다. 파이프로 이으면 각 단계마다 데이터프레임 이름을 반복해 쓰지 않아도 되고, 위에서 아래로 읽으면 처리 순서가 그대로 읽힌다.
같은 코드를 네이티브 파이프로 쓰면 다음과 같다.
students |>
filter(score >= 85) |>
arrange(desc(score)) |>
select(name, score)두 파이프는 “앞의 결과를 뒤 함수의 첫 번째 인자로 넘긴다”는 동작이 같아서 결과도 동일하다.
자주 틀리는 점
- 파이프로 이어진 코드에서 중간 단계의 순서를 바꿔도 결과가 같다고 착각한다.
filter()다음arrange()를 하는 것과arrange()다음filter()를 하는 것은 중간 결과가 다를 수 있어, 최종 결과에 영향을 줄 수 있다(다만 이 경우처럼 필터링과 정렬 대상 열이 다르면 최종 행 구성 자체는 같을 수 있으니, 항상 단계별로 무엇이 바뀌는지 확인해야 한다). %>%와|>가 서로 완전히 다른 문법이라 섞어 쓸 수 없다고 착각한다. 동작 방식은 같지만 하나의 코드 안에서는 관례상 한 가지 표기로 통일해 쓴다.
apply 계열 함수: 반복 계산을 짧게
왜 필요한가
dplyr 이전부터 R에는 벡터·행렬·리스트의 각 원소(또는 각 행·열, 각 그룹)에 같은 함수를 반복 적용하는 apply 계열 함수가 있었다. dplyr가 데이터프레임 전용이라면, apply 계열은 벡터·행렬·리스트를 포함해 더 넓게 쓰이고 지금도 R 코드 곳곳에 등장하므로 반드시 함께 알아 두어야 한다. 특히 함수마다 반환하는 데이터 타입이 다르다는 점이 ADsP 실행 결과 예측 문제의 단골 함정이다.
쉽게 말하면: apply 계열은 “이 함수를 여러 개에 반복해서 적용하고 결과를 한 번에 모아 와라”는 명령이다.
정의: apply·sapply·lapply·tapply 비교
| 함수 | 적용 대상 | 반환 타입 | 사용 예 |
|---|---|---|---|
apply() | 행렬(또는 데이터프레임)의 행 또는 열 | 벡터(결과 길이가 다르면 리스트) | 행렬의 행별 합계, 열별 평균 |
sapply() | 벡터·리스트의 각 원소 | 벡터 또는 행렬(단순화 가능하면 단순화) | 각 열의 평균을 벡터로 |
lapply() | 벡터·리스트의 각 원소 | 항상 리스트 | 각 열에 대한 계산 결과를 리스트로 |
tapply() | 벡터를 그룹(factor 기준)으로 나눈 뒤 | 배열(그룹이 1차원이면 벡터처럼 보임) | 그룹별 평균 계산 |
apply()는 두 번째 인자(margin)로 방향을 지정한다. 1은 행(row) 방향, 2는 열(column) 방향이다.
m <- matrix(1:6, nrow = 2, ncol = 3)
apply(m, 1, sum)
## [1] 9 12
apply(m, 2, sum)
## [1] 3 7 11apply(m, 1, sum)은 각 행의 합을, apply(m, 2, sum)은 각 열의 합을 계산한다. margin을 1과 2 중 무엇으로 주느냐에 따라 계산 방향이 완전히 달라지므로, “1은 행, 2는 열”이라는 것을 정확히 기억해야 한다.
sapply()와 lapply()는 같은 계산을 하더라도 반환 타입이 다르다.
nums <- list(a = c(1, 2, 3), b = c(4, 5, 6))
sapply(nums, mean)
## a b
## 2 5
lapply(nums, mean)
## $a
## [1] 2
##
## $b
## [1] 5sapply(nums, mean)은 결과를 이름이 붙은 벡터로 단순화(simplify)해서 돌려주지만, lapply(nums, mean)은 항상 리스트 형태를 유지한다. 두 함수는 계산 내용은 완전히 같지만 결과를 감싸는 그릇의 모양이 다르다는 것이 핵심 차이다.
tapply()는 factor 기준으로 그룹을 나눠 계산할 때 쓴다.
tapply(students$score, students$class, mean)
## A B
## 83.66667 91.00000students$score를 students$class(A, B) 기준으로 나눠 각 그룹의 평균을 계산한다. 이 결과는 앞서 group_by() + summarise()로 계산한 것과 같은 값(A는 약 83.7, B는 91)이다. dplyr의 group_by() + summarise() 조합이 나오기 전부터 R에서 그룹별 통계를 낼 때 표준적으로 쓰이던 방식이 바로 tapply()다.
자주 틀리는 점
sapply()와lapply()가 항상 같은 형태의 결과를 돌려준다고 착각한다. 계산 결과는 같아도sapply()는 벡터(가능하면),lapply()는 항상 리스트를 돌려준다.apply()의 margin 인자에서 1과 2의 의미를 반대로 기억한다. 1은 행, 2는 열이다.tapply()가 데이터프레임 전체에 바로 적용된다고 착각한다. 실제로는 “값 벡터, 그룹 기준 벡터, 적용할 함수” 세 가지를 각각 인자로 넘겨야 한다.
결측치(NA) 처리와 dplyr
왜 필요한가
10편에서 배운 것처럼 NA가 섞인 데이터로 평균이나 합계를 계산하면 결과도 NA가 된다. 실제 데이터 전처리 작업에서는 이 결측치를 어떻게 처리할지 결정하는 것이 핵심 단계 중 하나다.
쉽게 말하면: 결측치 처리는 “빈 자리를 무시하고 계산할지, 빈 자리를 다른 값으로 채울지”를 정하는 작업이다.
정의와 예시
students_na <- data.frame(
name = c("지훈", "수민", "예린"),
score = c(85, NA, 78)
)
summarise(students_na, avg = mean(score))
## avg
## 1 NA
summarise(students_na, avg = mean(score, na.rm = TRUE))
## avg
## 1 81.5mean(score)에 결측치가 섞여 있으면 결과가 NA이지만, na.rm = TRUE를 추가하면 결측치를 제외한 나머지(85, 78)의 평균인 81.5가 계산된다. filter()로 결측치가 있는 행 자체를 걸러낼 수도 있다.
filter(students_na, !is.na(score))
## name score
## 1 지훈 85
## 2 예린 78!is.na(score)는 “score가 결측치가 아니다”라는 조건이다(!는 논리값을 뒤집는 부정 연산자). 이 조건으로 필터링하면 결측치가 있는 행이 제외된다.
자주 틀리는 점
filter(students_na, is.na(score))와filter(students_na, !is.na(score))를 헷갈린다. 앞은 “결측치가 있는 행만” 남기고, 뒤는 “결측치가 없는 행만” 남긴다는 정반대의 조건이다.
핵심 정리
- dplyr 6대 동사:
filter()(행 거르기),select()(열 고르기),mutate()(열 추가),arrange()(정렬),group_by()(그룹 나누기),summarise()(요약값 계산). - 파이프 연산자(
%>%또는|>)는 앞 결과를 다음 함수의 첫 인자로 넘겨, 여러 동사를 순서대로 이어 쓸 수 있게 한다. - apply 계열은 함수마다 반환 타입이 다르다:
apply()는 벡터,sapply()는 (가능하면) 벡터,lapply()는 항상 리스트,tapply()는 그룹별 결과를 담은 배열(또는 벡터 형태). apply()의 margin 인자는 1이 행, 2가 열이다.- 결측치가 있는 데이터를 요약할 때는
na.rm = TRUE를 빠뜨리지 않아야 한다.