스파이더차트 그리기

개요

이번 글에서는 여러 변수의 특성을 비교할 때 유용한 스파이더차트(Spider chart) 또는 레이더차트(Radar chart)를 ggplot2를 사용해 직접 그리는 방법을 소개합니다. 이 차트는 데이터를 중심에서 뻗어나가는 축에 배치하여 각 그룹의 상대적 강점과 약점을 한눈에 파악할 수 있도록 도와줍니다.

2023년 기준 시도별 인구 특성을 나타내는 실제 데이터를 사용하며, 데이터를 처리하고 좌표를 계산하여 스파이더차트를 구현하는 전 과정을 다룹니다. 이번 글은 How to Make a Spider Chart in R Using ggplot2을 참고해 작성되었으며, 더 이해하기 쉽고 실용적으로 수정된 코드를 제공합니다.

다만, 이 글에서는 삼각함수를 활용해 각도를 계산하고, 데이터를 변환해 좌표를 직접 지정하는 과정을 상세히 다루기 때문에, 복잡하게 느껴질 수 있습니다. 하지만 과정을 하나씩 따라가며 이해하면, 스파이더차트를 활용한 데이터 시각화의 강력한 가능성을 느낄 수 있을 것입니다.

데이터 준비하기

시도별 청년인구 비율, 고령인구 비율, 전입/전출 비율, 출생아 비율, 사망자 비율을 계산한 데이터를 사용하려고 합니다. 아래 원자료를 가공해 각 변수를 구하고 스파이더차트를 그릴 준비를 합니다.

# 패키지 로드
library(tidyverse)
library(readxl)
library(ggplot2)
library(showtext)
library(scales)

# 글꼴 설정
font_add("kopub", "C:/Users/.../AppData/Local/Microsoft/Windows/Fonts/KoPub Dotum Medium.ttf")
showtext_auto()
showtext_opts(dpi=300)

theme.size = 12
text.size = theme.size / .pt

# 데이터 불러오기
pop <- read_xlsx("데이터/시도 인구 특성/5세별주민등록인구_시도_2023.xlsx")
in_pop <- read_xlsx("데이터/시도 인구 특성/전입인구_시도_2023.xlsx")
out_pop <- read_xlsx("데이터/시도 인구 특성/전출인구_시도_2023.xlsx")
births <- read_xlsx("데이터/시도 인구 특성/출생아수_시도_2023.xlsx", skip=1)
deaths <- read_xlsx("데이터/시도 인구 특성/사망자수_시도_2023.xlsx")

5세별 주민등록인구 데이터를 가공하여 시도별 전체 인구 수, 청년인구 수, 고령인구 수를 계산합니다. 청년인구는 만 20~34세, 고령인구는 만 65세 이상으로 정의했습니다. 출생아 수와 사망자 수를 나타낸 데이터의 일부 지역명을 수정하여 데이터 간 지역명을 일치시켜줍니다. 모든 데이터를 하나로 통합한 후 비율을 계산하고 데이터를 정리하여 완성합니다.

# 연령 정의
age <- unique(pop$`5세별`)
youth <- age[5:6]
senior <- age[-(1:13)]

# 시도 순서
sido_order <- unique(pop$`행정구역(동읍면)별`)[-1]

# 5세별 주민등록인구 데이터 정리하기
pop_sum <- pop %>% 
  rename(시도 = `행정구역(동읍면)별`) %>% 
  mutate(구분 = case_when(`5세별` %in% youth ~ "청년인구 수",
                        `5세별` %in% senior ~ "고령인구 수",
                        T ~ "그 외 인구 수")) %>% 
  group_by(시도, 구분) %>% 
  summarise(`인구 수` = sum(`2023`, na.rm = T)) %>% 
  pivot_wider(id_cols = 시도, names_from = 구분, values_from = `인구 수`) %>% 
  mutate(`인구 수` = `청년인구 수` + `고령인구 수` + `그 외 인구 수`) %>% 
  select(-`그 외 인구 수`) %>% 
  filter(시도 != "전국")

# 시군구 명칭 통일시키기
births <- births %>% 
  mutate(시군구별 = case_when(시군구별 == "전라북도" ~ "전북특별자치도",
                          T ~ 시군구별))

deaths <- deaths %>% 
  mutate(시군구별 = case_when(시군구별 == "강원도" ~ "강원특별자치도",
                          시군구별 == "전라북도" ~ "전북특별자치도",
                          시군구별 == "제주도" ~ "제주특별자치도",
                          T ~ 시군구별))

# 비율 계산하기
data <- pop_sum %>% 
  left_join(in_pop, by=c("시도"="행정구역(시군구)별(1)")) %>% 
  select(-`성별(1)`) %>% 
  rename(`전입인구 수` = `2023`) %>% 
  left_join(out_pop, by=c("시도"="행정구역(시군구)별(1)")) %>% 
  select(-`성별(1)`) %>% 
  rename(`전출인구 수` = `2023`) %>% 
  left_join(births, by=c("시도"="시군구별")) %>% 
  rename(`출생아 수` = `계 (명)`) %>%
  left_join(deaths, by=c("시도"="시군구별")) %>% 
  select(-성별) %>% 
  rename(`사망자 수` = `2023`) %>% 
  mutate(`고령인구 비율` = `고령인구 수`/`인구 수`,
         `청년인구 비율` = `청년인구 수`/`인구 수`,
         `전입인구 비율` = `전입인구 수`/`인구 수`,
         `전출인구 비율` = `전출인구 수`/`인구 수`,
         `출생아 비율` = `출생아 수`/`인구 수`,
         `사망자 비율` = `사망자 수`/`인구 수`,
         시도 = factor(시도, levels = sido_order)) %>% 
  select(시도, contains("비율"))

# 데이터 확인하기
head(data)
## # A tibble: 6 × 7
## # Groups:   시도 [6]
##   시도           `고령인구 비율` `청년인구 비율` `전입인구 비율` `전출인구 비율`
##   <fct>                    <dbl>           <dbl>           <dbl>           <dbl>
## 1 강원특별자치도           0.240          0.107           0.113           0.114 
## 2 경기도                   0.156          0.123           0.123           0.120 
## 3 경상남도                 0.206          0.0993          0.0924          0.0974
## 4 경상북도                 0.247          0.0972          0.0996          0.103 
## 5 광주광역시               0.165          0.135           0.115           0.121 
## 6 대구광역시               0.196          0.119           0.125           0.127 
## # ℹ 2 more variables: `출생아 비율` <dbl>, `사망자 비율` <dbl>

스파이더차트의 기본 원리

ggradar 함수를 이용하면 스파이더차트를 간편하게 그릴 수 있습니다. 하지만, 차트의 세부요소를 조정하기 쉽지 않다는 단점이 있습니다. 이보다 복잡하지만 ggplot2를 이용해 직접 차트를 그리면 세부요소를 마음대로 조정할 수 있어서 이번 시간에는 ggplot2를 이용하는 방법을 알려드리겠습니다.

스파이더차트에서 각 변수는 중심에서 뻗어나가는 축을 따라 배치됩니다. 점의 위치는 반지름(r)과 각도(θ)를 기반으로 계산할 수 있습니다. 점의 좌표를 구하는 공식은 x = r·cos(θ), y = r·sin(θ)이며, 원리는 아래 도표와 같습니다.

삼각함수를 이용한 좌표 구하는 법

스파이더차트를 그리기 위해서는 각 변수의 값이 동일한 범위(예: 0~100%)로 변환되어야 합니다. 사실 이번 예제에서 사용할 데이터는 모두 비율 데이터로 0~100% 사이의 값을 지녀, 값을 변환할 필요가 없습니다. 하지만, 고령인구 비율이 11.0~26.1% 사이의 값인 반면 출생아 비율은 0.4~0.7% 사이의 값을 지니기 때문에 축의 범위를 0~100%로 동일하게 설정했을 때 지역 간 출생아 비율 차이를 파악하기 쉽지 않습니다. 따라서, 최소값은 차트의 r = 0.2에 대응하고 최대값은 r = 1에 대응하도록 아래 수식을 이용하여 변수별로 범위를 조정할 필요가 있습니다.

scaled = (x - xmin)/(xmax - xmin)·(1 - 0.2) + 0.2

이렇게 변환된 값을 기준으로 각 변수의 좌표를 계산합니다. 원의 한 바퀴는 360도이며 이를 라디안으로 표현하면 입니다. 만약 n개의 변수가 있다면, 변수 하나가 차지하는 각도는 2π/n이 됩니다. 이를 코드로 구현하면 seq(0, 2 * pi, length.out = n + 1)이 됩니다. n + 1을 하는 이유는 원의 시작점(theta)에서 다시 원의 시작점(2 * pi)까지 총 n + 1개 요소를 뽑기 때문입니다. 원의 시작점이 요소에 포함되기 때문에 n + 1개 요소를 뽑도록 지정해 주어야 합니다. 아래 그림은 n=6일 때, 스파이더차트가 어떻게 구성되는지를 보여줍니다. rn의 조합으로 각 점의 특정할 수 있습니다.

스파이더차트에서 r과 n의 의미

스파이더차트의 배경 그리기

  1. 차트에서 표현할 변수의 축 개수를 구합니다.
  2. 변수명을 추출하고 변수의 순서를 지정합니다.
  3. 차트의 중심이 0이고 축의 길이를 1로 가정하며, 그리드 간격을 0.2로 설정합니다.
  4. 각 변수의 최소값과 최대값이 0.2와 1에 대응하도록 변환하는 함수(scaling)를 정의합니다.
  5. 반지름(r)과 변수 개수(n_axis)를 입력하면, 특정 반지름에 해당하는 점 좌표를 변수 개수만큼 생성하는 함수(gen_coords)를 정의합니다.
  6. 변수 벡터(x), 그리드 간격(offset)를 입력하면, 해당 변수를 축에 표현할 때 그리드가 그려지는 지점의 실제값과 해당 위치의 반지름(r)을 반환하는 함수(axis_text)를 정의합니다.
# 변수 개수
n_axis <- ncol(data) - 1

# 변수 순서
vars_nth <- tibble(구분 = colnames(data)[-1],
                   n = 1:n_axis)

# 그리드 간격
offset <- 0.2

# 데이터 스케일링 함수(최소값이 0.2, 최대값이 1에 대응하도록 스케일링하는 함수)
scaling <- function(x, min_val = offset, max_val = 1){
  x_min <- min(x, na.rm = T)
  x_max <- max(x, na.rm = T)
  
  scaled <- (x - x_min) / (x_max - x_min) * (max_val - min_val) + min_val
  
  return(scaled)
}

# 데이터 좌표 계산하는 함수 정의
gen_coords <- function(r, n_axis, closed = FALSE){
  angles <- seq(0, 2 * pi, length.out = n_axis + 1) + pi / 2
  
  if(closed==FALSE){
    angles <- angles[-length(angles)]
  }
  
  x <- r * cos(angles)
  y <- r * sin(angles)
  
  n <- 1:length(angles)
  
  tibble(x, y, r, n)
}

# 변수별 축 텍스트 함수
axis_text <- function(x, offset){
  min <- min(x, na.rm = T)
  max <- max(x, na.rm = T)
  
  label <- seq(min, max, length.out = 1 / offset)
  r <- seq(offset, 1, by = offset) # 최소값이 차트 반지름 0.2에 대응, 최대값이 차트 반지름 1에 대응
  
  tibble(label, r)
}

스파이더차트의 배경은 외곽배경, 그리드선, 축선, 축선에 표시될 축 타이틀과 텍스트로 구성됩니다. 아래 코드는 스파이더차트의 외곽배경, 그리드선, 축선을 만드는 과정을 보여줍니다. panel_data는 스파이더차트를 구성하는 배경 점 좌표를 계산하여 저장한 데이터프레임입니다. seq(0, 1, by = offset)[-1]은 차트의 중심에서 외곽까지의 반지름 값을 나타내고, 각 반지름 값에 대해 gen_coords 함수를 호출하여 점 좌표를 계산합니다. 함수의 결과는 모든 반지름 값에 대한 좌표를 하나의 데이터프레임으로 결합한 형태가 됩니다.

# 배경 점 좌표
panel_data <- map_df(
  seq(0, 1, by = offset)[-1],
  ~gen_coords(.x, n_axis, closed=T)
  )

panel_bg는 생성된 배경 좌표 데이터를 이용해 차트를 그린 결과물입니다. geom_polygon을 사용해 반지름 값이 1인 외곽선으로 구성된 폴리곤을 회색으로 채우고, 투명도를 조절해 배경의 기본 윤곽을 만듭니다. geom_path를 통해 각 반지름 값을 따라 점선을 그려 그리드를 완성합니다. 마지막으로 geom_line을 이용해 중심에서 각 축 끝까지 선을 그려 변수를 표현할 축을 추가합니다. 불필요한 시각적 요소는 theme_void를 통해 제거하여 차트를 깔끔하게 정리합니다.

# 배경 그리기
panel_bg <- ggplot() +
  # 외곽 배경(r = 1)
  geom_polygon(data = panel_data %>% filter(r == 1),
               aes(x, y),
               fill = "gray",
               alpha = 0.2) +
  # 점선 그리드
  geom_path(data = panel_data, 
            aes(x, y, group = r),
            linetype = "dotted",
            color = "gray") +
  # 축 추가
  geom_line(data = panel_data,
            aes(x = x, y = y, group = n),
            color = "gray") +
  coord_fixed(ratio = 1,
              clip = "off") +
  theme_void()

panel_bg

스파이더차트 배경

다음으로, 축에 변수명과 텍스트를 추가하겠습니다. axis_text_data는 각 축에 표시될 값인 축 텍스트를 계산한 결과를 담고 있습니다. pivot_longer 함수를 이용해 데이터를 길게 변환하고, axis_text 함수를 호출해 각 변수별로 축 텍스트에 해당하는 값과 반지름을 계산합니다. 그리고, vars_nthpanel_data 데이터를 결합하여 변수의 순서와 텍스트를 배치할 좌표를 구합니다.

# 축 텍스트 데이터
axis_text_data <- data %>% 
  pivot_longer(cols = -시도, names_to = "구분", values_to = "실제값") %>% 
  group_by(구분) %>% 
  # 변수별 축 데이터 실제값 구하기
  reframe(axis_text(실제값, offset)) %>% 
  # 변수 순서 지정하기
  left_join(vars_nth, by = "구분") %>% 
  # 좌표 추가하기
  left_join(panel_data, by = c("r", "n"))

# 데이터 확인하기
head(axis_text_data)
## # A tibble: 6 × 6
##   구분            label     r     n        x     y
##   <chr>           <dbl> <dbl> <int>    <dbl> <dbl>
## 1 고령인구 비율 0.110     0.2     1 1.22e-17 0.2  
## 2 고령인구 비율 0.148     0.4     1 2.45e-17 0.4  
## 3 고령인구 비율 0.186     0.6     1 3.67e-17 0.6  
## 4 고령인구 비율 0.223     0.8     1 4.90e-17 0.8  
## 5 고령인구 비율 0.261     1       1 6.12e-17 1    
## 6 사망자 비율   0.00413   0.2     6 1.73e- 1 0.100

변수명을 축의 끝부분에 배치하기 위해 좌표를 새로 계산해야 합니다. gen_coords 함수를 이용하여, 반지름 1.15, 변수 6개에 대한 좌표를 계산하고 axis_title_coords에 저장합니다. 이후 변수명에 변수의 순서(vars_nth)와 좌표(axis_title_coords)를 결합하여 axis_title_data를 만듭니다.

# 축 타이틀 좌표
axis_title_coords <- gen_coords(1.15, 6)

# 축 타이틀 데이터
axis_title_data <- data %>% 
  pivot_longer(cols = -시도, names_to = "구분", values_to = "실제값") %>% 
  group_by(구분) %>% 
  summarise() %>% 
  # 변수 순서 지정하기
  left_join(vars_nth, by = "구분") %>% 
  # 좌표 추가하기
  left_join(axis_title_coords, by = c("n")) %>% 
  mutate(hjust = case_when(x > 0.01 ~ 0,
                           x < -0.01 ~ 1,
                           T ~ 0.5))

위 데이터를 활용하여 스파이더차트에 축 텍스트와 변수명을 추가해줍니다.

panel_bg_with_axis <- panel_bg +
  # 축 텍스트 표기
  geom_text(data = axis_text_data, 
            aes(x, y, label = percent(label, accuracy = .01, suffix = "")),
            color = "gray30",
            family = "kopub",
            size = text.size * 0.8) +
  # 축 변수명 표기
  geom_text(data = axis_title_data,
            aes(x, y, label = paste0(구분, "(%)")),
            family = "kopub",
            size = text.size,
            hjust = axis_title_data*hjust)

panel_bg_with_axis

축을 추가한 스파이더차트 배경

배경에 데이터 추가하기

각 데이터 포인트를 좌표로 변환하고 경로가 닫히도록 시도별 첫번째 행을 복사해서 행에 추가해 줍니다.

# 데이터 가공
tmp <- data %>% 
  pivot_longer(cols = -시도, names_to = "구분", values_to = "실제값") %>% 
  # 실제값을 스케일링하기
  group_by(구분) %>% 
  mutate(변환값 = scaling(실제값)) %>% 
  # 변환값의 좌표 구하기
  group_by(시도) %>% 
  mutate(coords = gen_coords(변환값, n_axis)) %>% 
  unnest(cols = c(coords)) %>% 
  # 부산, 울산, 경남만 추출하기
  filter(시도 %in% c("부산광역시", "울산광역시", "경상남도"))

# geom_path 사용 시 닫힌 경로 만들기 위해 시도별 첫번째 행 복사하기
tmp_first_row <- tmp %>% 
  group_by(시도) %>% 
  filter(row_number() == 1)

final_data <- tmp %>% 
  bind_rows(tmp_first_row)

이제 마지막입니다! 스파이더차트 배경 위에 데이터를 표현하여 차트를 완성합니다. geom_pathgeom_point 함수를 이용하여 데이터 라인과 포인트를 추가합니다. geom_path는 각 시도의 데이터를 연결하여 라인을 그리는 역할을 하고, geom_point는 라인 위에 데이터 포인트를 표시하는 역할을 합니다.

데이터 라인과 포인트 색상은 scale_color_brewer(palette = "Set2")로 설정했으며, RColorBrewer 팔레트의 “Set2” 색상 세트를 적용했습니다. theme을 사용하여 범례 제목을 없애고 범례 위치와 범례 항목의 사이즈를 조정해 주었습니다.

panel_bg_with_axis +
  # 데이터 라인 그리기
  geom_path(data = final_data,
            aes(x, y, group = 시도 , color = 시도),
            linewidth = 1) +
  # 데이터 포인트 그리기
  geom_point(data = final_data,
             aes(x, y, group = 시도, color = 시도),
             size = 3,
             shape = 21,
             fill = "white") +
  scale_color_brewer(palette = "Set2") +
  theme(
    legend.position = "bottom",
    legend.title = element_blank(),
    legend.key.height = unit(theme.size, "pt"),
    legend.margin = margin(5, 0, 0, 0),
    legend.background = element_rect(fill = NA, color = NA)
  )

데이터를 추가하여 완성한 스파이더차트

스파이더차트는 다차원 데이터를 한눈에 비교하고 시각화할 수 있는 강력한 도구입니다. 이번 글에서는 데이터를 변환하고 좌표를 계산하여 ggplot2를 사용해 스파이더차트를 그리는 방법을 알아보았습니다. 스파이더차트를 제작하는 과정은 처음에는 복잡하게 느껴질 수 있지만, 데이터의 상대적인 패턴과 차이를 시각적으로 명확히 표현할 수 있으니 한 번 활용해 보세요!