iBetter Books
수정

Ch 06. 그룹화와 요약 (group_by, summarise)

데이터 분석에서 "그룹별 평균은 얼마인가", "부서별 최댓값은 어느 팀인가" 같은 질문을 가장 많이 하게 됩니다. group_by()summarise()를 조합하면 이런 집계를 간결하게 처리합니다.

이 챕터는 반복 실습을 통해 패턴을 완전히 익히는 것이 목표입니다.

기본 패턴: group_by() %>% summarise()

mpg 데이터에서 제조사별 평균 고속도로 연비를 구합니다.

library(tidyverse)

mpg %>%
  group_by(manufacturer) %>%
  summarise(avg_hwy = mean(hwy))
# A tibble: 15 × 2
   manufacturer avg_hwy
   <chr>          <dbl>
 1 audi            26.4
 2 chevrolet       21.9
 3 dodge           17.9
 4 ford            19.4
 5 honda           32.6
 6 hyundai         26.9
...

group_by()는 데이터를 내부적으로 그룹으로 나눕니다. 겉보기에는 변화가 없지만, 이후 summarise()가 각 그룹에 대해 계산을 수행합니다.

다양한 집계 함수

summarise() 안에서 쓸 수 있는 집계 함수입니다.

mpg %>%
  group_by(class) %>%
  summarise(
    n         = n(),               # 행 수
    avg_hwy   = mean(hwy),         # 평균
    med_hwy   = median(hwy),       # 중앙값
    max_hwy   = max(hwy),          # 최댓값
    min_hwy   = min(hwy),          # 최솟값
    sd_hwy    = sd(hwy),           # 표준편차
    total_cty = sum(cty)           # 합계
  )
# A tibble: 7 × 8
  class          n avg_hwy med_hwy max_hwy min_hwy sd_hwy total_cty
  <chr>      <int>   <dbl>   <dbl>   <int>   <int>  <dbl>     <int>
1 2seater        5    24.8      25      26      23   1.30        73
2 compact       47    28.3      27      44      23   4.51       868
3 midsize       41    27.3      27      32      23   2.14       756
4 minivan       11    22.4      23      24      17   2.07       183
5 pickup        33    16.9      17      22      12   2.56       466
6 subcompact    35    28.1      26      44      20   6.11       621
7 suv           62    18.1      17      27      12   3.06       921

n()은 각 그룹의 행 수를 셉니다. 인수 없이 쓰면 현재 그룹의 크기를 반환합니다.

n()의 형제들

# 고유값의 수
mpg %>%
  group_by(class) %>%
  summarise(n_mfr = n_distinct(manufacturer))
# A tibble: 7 × 2
  class      n_mfr
  <chr>      <int>
1 2seater        1
2 compact        4
3 midsize        6
...

n_distinct()는 중복을 제외한 고유값의 수를 셉니다. 차종별로 몇 개의 제조사가 있는지 알 수 있습니다.

다중 그룹화

두 개 이상의 열로 그룹화할 수 있습니다.

mpg %>%
  group_by(manufacturer, class) %>%
  summarise(
    n       = n(),
    avg_hwy = mean(hwy),
    .groups = "drop"
  )
# A tibble: 38 × 4
   manufacturer class          n avg_hwy
   <chr>        <chr>      <int>   <dbl>
 1 audi         compact       15    28.3
 2 audi         midsize        3    26.3
 3 chevrolet    2seater        5    24.8
 4 chevrolet    midsize        5    27  
 5 chevrolet    suv           19    18.0
...

.groups = "drop"summarise() 후 그룹을 해제합니다. 다중 그룹화 후에는 명시적으로 설정하는 습관을 들이면 예상치 못한 동작을 예방합니다.

arrange()로 결과 정렬

summarise() 결과를 정렬하면 순위를 한눈에 볼 수 있습니다.

mpg %>%
  group_by(manufacturer) %>%
  summarise(avg_hwy = mean(hwy)) %>%
  arrange(desc(avg_hwy))
# A tibble: 15 × 2
   manufacturer avg_hwy
   <chr>          <dbl>
 1 honda           32.6
 2 volkswagen      29.2
 3 hyundai         26.9
 4 audi            26.4
 5 toyota          24.9
...

혼다가 고속도로 연비 1위입니다.

filter()와 결합: 그룹 내 필터링

group_by() 이후 filter()를 쓰면 그룹 내에서 조건을 적용합니다.

# 각 제조사에서 hwy가 가장 높은 모델
mpg %>%
  group_by(manufacturer) %>%
  filter(hwy == max(hwy)) %>%
  select(manufacturer, model, hwy) %>%
  arrange(manufacturer)
# A tibble: 18 × 3
   manufacturer model         hwy
   <chr>        <chr>       <int>
 1 audi         a4             31
 2 chevrolet    corvette       26
 3 dodge        caravan 2wd    24
...

각 제조사의 최고 연비 모델이 한 줄씩 표시됩니다. max(hwy)가 그룹 안에서 계산되기 때문에 제조사별 최댓값과 비교합니다.

mutate()와 group_by() 결합

group_by()summarise() 대신 mutate()를 쓰면 그룹 통계를 원래 데이터에 추가합니다.

# 제조사 평균 대비 각 차량의 연비 비율
mpg %>%
  group_by(manufacturer) %>%
  mutate(
    mfr_avg_hwy = mean(hwy),
    ratio       = hwy / mfr_avg_hwy
  ) %>%
  ungroup() %>%
  select(manufacturer, model, hwy, mfr_avg_hwy, ratio)
# A tibble: 234 × 5
   manufacturer model        hwy mfr_avg_hwy ratio
   <chr>        <chr>      <int>       <dbl> <dbl>
 1 audi         a4            29        26.4  1.10
 2 audi         a4            29        26.4  1.10
 3 audi         a4            31        26.4  1.17
...

제조사 내 평균 대비 각 모델의 상대적 위치를 볼 수 있습니다. 행 수는 줄어들지 않습니다.

ungroup()의 중요성

group_by() 후 데이터는 그룹이 지정된 상태로 남습니다. 이후 작업에 영향을 줄 수 있습니다.

# 그룹 확인
mpg_grouped <- mpg %>% group_by(manufacturer)
groups(mpg_grouped)
[[1]]
manufacturer

ungroup()으로 그룹을 해제합니다.

mpg_ungrouped <- mpg_grouped %>% ungroup()
groups(mpg_ungrouped)
list()

summarise() 후에는 보통 자동으로 마지막 그룹이 제거되지만, 다중 그룹화의 경우 중간 그룹이 남을 수 있습니다. 작업이 끝난 후 ungroup()을 명시적으로 호출하는 것이 안전합니다.

실전 종합 예제

starwars 데이터로 종족별 신체 통계를 구합니다.

starwars %>%
  filter(!is.na(species), !is.na(height), !is.na(mass)) %>%
  group_by(species) %>%
  summarise(
    n          = n(),
    avg_height = mean(height),
    avg_mass   = mean(mass),
    avg_bmi    = mean(mass / (height / 100)^2)
  ) %>%
  filter(n >= 2) %>%                    # 2명 이상인 종족만
  arrange(desc(avg_bmi)) %>%
  ungroup()
# A tibble: 9 × 5
  species      n avg_height avg_mass avg_bmi
  <chr>    <int>      <dbl>    <dbl>   <dbl>
1 Hutt         1       175    1358    443.  
2 Dug          1        94      40     45.3 
3 Yoda's …     1        66      17     39.0 
...

n이 2 이상인 종족 중 평균 BMI가 높은 순으로 정렬됩니다. group_by() %>% summarise() %>% filter() %>% arrange()가 하나의 흐름처럼 이어집니다.

이 패턴은 앞으로 가장 자주 쓰게 될 분석 패턴입니다. 자연스럽게 손에 익을 때까지 연습하겠습니다.

다음은 여러 데이터를 합치는 방법입니다.