iBetter Books
수정

Ch 08. 피벗과 재구성 (pivot_longer, pivot_wider)

같은 데이터라도 형태에 따라 분석과 시각화에 적합한 모습이 다릅니다. ggplot2는 세로로 긴 형태(tidy data)를 선호하지만, 엑셀 보고서는 가로로 넓은 형태가 읽기 편합니다. 두 형태를 자유롭게 전환하는 것이 pivot_longer()pivot_wider()의 역할입니다.

넓은 형태와 긴 형태

학생별 월간 점수 데이터를 예로 들겠습니다.

library(tidyverse)

# 넓은 형태 (wide format)
scores_wide <- tibble(
  name    = c("김철수", "이영희", "박민준"),
  jan     = c(85, 92, 78),
  feb     = c(88, 87, 82),
  mar     = c(91, 95, 75)
)

scores_wide
# A tibble: 3 × 4
  name     jan   feb   mar
  <chr>  <dbl> <dbl> <dbl>
1 김철수    85    88    91
2 이영희    92    87    95
3 박민준    78    82    75

이 형태는 한눈에 읽기 좋습니다. 그런데 ggplot2로 월별 추세를 그리거나 group_by(month)로 월별 평균을 내려면 아래와 같은 긴 형태가 필요합니다.

name    month  score
김철수  jan    85
김철수  feb    88
김철수  mar    91
이영희  jan    92
...

pivot_longer()로 넓은 → 긴 형태

pivot_longer()는 여러 열을 하나의 열로 모읍니다.

scores_long <- scores_wide %>%
  pivot_longer(
    cols     = c(jan, feb, mar),   # 모을 열
    names_to = "month",            # 열 이름이 들어갈 새 열
    values_to = "score"            # 값이 들어갈 새 열
  )

scores_long
# A tibble: 9 × 3
  name   month score
  <chr>  <chr> <dbl>
1 김철수 jan      85
2 김철수 feb      88
3 김철수 mar      91
4 이영희 jan      92
5 이영희 feb      87
6 이영희 mar      95
7 박민준 jan      78
8 박민준 feb      82
9 박민준 mar      75

3행 4열이 9행 3열로 바뀌었습니다. 이 형태에서 월별 평균을 바로 구할 수 있습니다.

scores_long %>%
  group_by(month) %>%
  summarise(avg_score = mean(score))
# A tibble: 3 × 2
  month avg_score
  <chr>     <dbl>
1 feb        85.7
2 jan        85  
3 mar        87  

cols 지정 방법

cols에는 여러 방식으로 열을 지정합니다.

# 열 이름으로
pivot_longer(scores_wide, cols = c(jan, feb, mar), ...)

# 제외할 열로 지정 (name 열 제외 = 나머지 모두)
pivot_longer(scores_wide, cols = -name, ...)

# starts_with 사용
pivot_longer(scores_wide, cols = starts_with("j"), ...)

# 범위로
pivot_longer(scores_wide, cols = jan:mar, ...)

실무에서는 cols = -name 패턴을 많이 씁니다. 열이 추가되어도 코드를 바꾸지 않아도 됩니다.

pivot_wider()로 긴 → 넓은 형태

pivot_wider()는 반대 방향입니다. 긴 형태를 넓게 펼칩니다.

scores_wide_again <- scores_long %>%
  pivot_wider(
    names_from  = "month",   # 열 이름이 될 열
    values_from = "score"    # 값이 될 열
  )

scores_wide_again
# A tibble: 3 × 4
  name     jan   feb   mar
  <chr>  <dbl> <dbl> <dbl>
1 김철수    85    88    91
2 이영희    92    87    95
3 박민준    78    82    75

원래 형태로 돌아왔습니다. 보고서 출력이나 엑셀 저장 전에 pivot_wider()로 변환하면 읽기 좋은 형태가 됩니다.

실전 예제: 지역별 연도별 데이터

좀 더 복잡한 경우입니다. 지역별, 연도별 판매 데이터입니다.

sales <- tibble(
  region = rep(c("서울", "부산", "대구"), each = 3),
  year   = rep(c(2022, 2023, 2024), times = 3),
  sales  = c(1200, 1350, 1500, 800, 870, 920, 650, 700, 780)
)

sales
# A tibble: 9 × 3
  region  year sales
  <chr>  <dbl> <dbl>
1 서울    2022  1200
2 서울    2023  1350
3 서울    2024  1500
4 부산    2022   800
5 부산    2023   870
6 부산    2024   920
7 대구    2022   650
8 대구    2023   700
9 대구    2024   780

이 긴 형태를 지역을 행으로, 연도를 열로 하는 넓은 형태로 변환합니다.

sales %>%
  pivot_wider(
    names_from  = year,
    values_from = sales
  )
# A tibble: 3 × 4
  region `2022` `2023` `2024`
  <chr>   <dbl>  <dbl>  <dbl>
1 서울     1200   1350   1500
2 부산      800    870    920
3 대구      650    700    780

지역별 연도별 매출 비교 표가 완성됩니다. 열 이름에 숫자가 오면 백틱(`)으로 감싸야 합니다.

separate()로 열 분리

하나의 열에 두 가지 정보가 합쳐져 있을 때 분리합니다.

combined <- tibble(
  info  = c("서울_2022", "부산_2023", "대구_2024"),
  value = c(1200, 870, 780)
)

combined %>%
  separate(info, into = c("region", "year"), sep = "_")
# A tibble: 3 × 3
  region year  value
  <chr>  <chr> <dbl>
1 서울   2022   1200
2 부산   2023    870
3 대구   2024    780

sep에는 구분자 문자열이나 정규표현식을 씁니다. 위치 기반 분리도 됩니다.

# 앞 2자리를 연도로, 나머지를 코드로
tibble(code = c("22SEOUL", "23BUSAN")) %>%
  separate(code, into = c("year", "city"), sep = 2)
# A tibble: 2 × 2
  year  city  
  <chr> <chr> 
1 22    SEOUL 
2 23    BUSAN 

unite()로 열 합치기

separate()의 반대입니다. 두 열을 하나로 합칩니다.

split_data <- tibble(
  region = c("서울", "부산", "대구"),
  year   = c(2022, 2023, 2024)
)

split_data %>%
  unite("region_year", region, year, sep = "_")
# A tibble: 3 × 1
  region_year
  <chr>      
1 서울_2022  
2 부산_2023  
3 대구_2024  

pivot_longer와 pivot_wider를 활용한 데이터 변환 흐름

실무에서 자주 등장하는 패턴입니다. 넓은 형태로 받아서 분석하고, 결과를 다시 넓은 형태로 출력합니다.

# 1. 넓은 형태로 시작
scores_wide

# 2. 긴 형태로 변환 → 분석
result <- scores_wide %>%
  pivot_longer(-name, names_to = "month", values_to = "score") %>%
  group_by(month) %>%
  summarise(avg = mean(score), max = max(score), min = min(score))

result
# A tibble: 3 × 4
  month   avg   max   min
  <chr> <dbl> <dbl> <dbl>
1 feb    85.7    92    82
2 jan    85      92    78
3 mar    87      95    75
# 3. 보고서용 넓은 형태로 출력
result %>%
  pivot_wider(names_from = month, values_from = c(avg, max, min))
# A tibble: 1 × 9
  avg_feb avg_jan avg_mar max_feb max_jan max_mar min_feb min_jan min_mar
    <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
1    85.7      85      87      92      92      95      82      78      75

PART 03을 모두 마쳤습니다. tibble부터 시작해서 데이터를 읽고, 필터링하고, 결측치를 처리하고, 변환하고, 집계하고, 합치고, 형태를 바꾸는 전 과정을 다루었습니다.

이제 정제된 데이터로 시각화에 도전할 준비가 되었습니다. 다음 파트에서는 ggplot2로 데이터를 그림으로 표현합니다.