Skip to Content
자격증ADsP12. R 데이터 전처리: dplyr 기초

이번 문서의 목표: 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 88

filter(): 조건에 맞는 행 걸러내기

왜 필요한가

전체 학생 중 “성적이 85점 이상인 학생만” 보고 싶을 때처럼, 조건을 만족하는 행만 남기고 나머지는 제외해야 할 때가 많다. filter()는 이 작업을 데이터프레임 인덱싱보다 훨씬 읽기 쉬운 형태로 표현한다.

쉽게 말하면: filter는 조건이라는 체(sieve)로 걸러 통과한 행만 남기는 것이다.

정의와 예시

library(dplyr) filter(students, score >= 85) ## name class score ## 1 지훈 A 85 ## 2 수민 B 90 ## 3 도윤 B 92 ## 4 하은 A 88

filter(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 93

mutate(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 2

group_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 11

apply(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] 5

sapply(nums, mean)은 결과를 이름이 붙은 벡터로 단순화(simplify)해서 돌려주지만, lapply(nums, mean)은 항상 리스트 형태를 유지한다. 두 함수는 계산 내용은 완전히 같지만 결과를 감싸는 그릇의 모양이 다르다는 것이 핵심 차이다.

tapply()는 factor 기준으로 그룹을 나눠 계산할 때 쓴다.

tapply(students$score, students$class, mean) ## A B ## 83.66667 91.00000

students$scorestudents$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.5

mean(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를 빠뜨리지 않아야 한다.

마무리 복습

문제 14지선다
다음 코드의 실행 결과로 옳은 것은? df = data.frame(name = c('A','B','C'), score = c(70, 85, 60)); filter(df, score 조건이 70 이상)의 행 개수는?
문제 24지선다
dplyr의 mutate()에 대한 설명으로 옳은 것은?
문제 34지선다
다음 코드를 실행했을 때 결과로 옳은 것은? arrange(df, score), df의 score 열은 70, 85, 60
문제 44지선다
group_by(df, class) 다음에 summarise(avg = mean(score))를 실행하는 이유에 대한 설명으로 옳은 것은?
문제 54지선다
다음 코드의 실행 결과 반환 타입으로 옳은 것은? nums = list(a = c(1,2,3), b = c(4,5,6)); lapply(nums, sum)
문제 64지선다
다음 코드의 실행 결과로 옳은 것은? m = matrix(1:6, nrow=2, ncol=3); apply(m, 2, sum)
문제 74지선다
다음 코드의 실행 결과로 옳은 것은? scores = c(80, NA, 90); summarise(data.frame(scores), avg = mean(scores))
문제 84지선다
파이프 연산자에 대한 설명으로 옳지 않은 것은?

참고 자료

Last updated on