본문 바로가기
  • plotly로 바로쓰는 동적시각화 in R & 파이썬
지도 시각화

전국 사설 학원수 범주형 단계 구분도 in R - 연속형 변수를 범주형 변수로 변환

by 아참형인간 2022. 7. 16.
categorize.knit

<사용데이터 : 전국 사설 학원 수 단계 구분도 그리기 in R >

범주형 단계 구분도(Choropleth map)

전국 사설 학원 수 단계 구분도 그리기 in R에서 그린 전국 사설학원수에 대한 단계 구분도는 단계를 구분하는 색이 연속형 변수로 색조의 변화와 변수가 매핑되어 있다. 하지만 그 지도에서도 보이듯이 서울, 경기 지역의 학원수가 타 시도에 비해 매우 크기 때문에 그 변화가 잘 눈에 띄지지 않는다. 따라서 이런 경우에는 연속형 변수를 범주형으로 바꾸어서 그 변수들의 단계가 명확히 보이도록 그리는게 좋을 것이다.

그렇다면 먼저 연속형 변수인 사설학원수를 범주형으로 바꾸어야 한다. 연속형 변수를 범주형 변수로 바꾸는 방법에는 여러가지가 있지만 여기서는 cut(), discretize(), cut_*(), frq()를 사용하는 네 가지 방법을 사용하겠다.

cut()

cut()은 R base에서 제공하는 함수로 수치형 연속형 변수를 분할하여 범주형으로 만들어 주는 함수중에 하나로 가장 기본적인 함수이다. 따라서 R base에서 제공하는 cut()을 사용할 수 있고 이 함수에서 파생된 함수를 사용하는 방법이 있다. 여기서는 ggplot2에서 제공하는 cut() 파생 함수에 대해 알아본다.

R base의 cut()

cut()은 범주화하는데 필요한 분할점을 명기함으로써 그 분할점으로 범주를 나누는 함수이다. cut()을 사용하여 범주형 단계 구분도를 그리면 다음과 같다.

df_joined <- df_joined |> 
  mutate(level_cut_break = cut(학원수, breaks = c(-Inf, 1000, 3000, 5000, 7000, 9000, Inf), 
                     labels = c('1,000개 미만', '1,000~2,999', '3,000~4,999', '5,000~6,999', '7,000~8,999', '9000개이상')
                      )
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_cut_break) |> head()
##   시도 학원수 level_cut_break
## 1 강원   1987     1,000~2,999
## 2 경기  20381      9000개이상
## 3 경남   5546     5,000~6,999
## 4 경북   3257     3,000~4,999
## 5 광주   3009     3,000~4,999
## 6 대구   3508     3,000~4,999
as.data.frame(df_joined) |> count(level_cut_break)
##   level_cut_break n
## 1    1,000개 미만 1
## 2     1,000~2,999 7
## 3     3,000~4,999 6
## 4     5,000~6,999 1
## 5      9000개이상 2
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_cut_break), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

cut()은 분할점을 지정하여 범주화 할 수도 있고 범주수를 지정하여 범주화할 수도 있다. 다음은 전체를 5개의 범주로 나누어 단계화하는 방법이다.

df_joined <- df_joined |> 
  mutate(level_cut_num = cut(학원수, breaks = 7, dig.lab = 5)
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_cut_num) |> head()
##   시도 학원수 level_cut_num
## 1 강원   1987 (747.39,3569]
## 2 경기  20381 (17579,20401]
## 3 경남   5546   (3569,6371]
## 4 경북   3257 (747.39,3569]
## 5 광주   3009 (747.39,3569]
## 6 대구   3508 (747.39,3569]
as.data.frame(df_joined) |> count(level_cut_num)
##   level_cut_num  n
## 1 (747.39,3569] 12
## 2   (3569,6371]  3
## 3  (9173,11975]  1
## 4 (17579,20401]  1
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_cut_num), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

ggplot2의 cut_*()

보통 ggplot2패키지는 시각화에 전용으로 사용되는 패키지로 알고 있다. 하지만 ggplot2에도 데이터 전처리를 위한 일부 함수가 제공되고 있다는 것이다. 물론 이 함수들도 시각화에 사용되기 위해 설계된 함수이지만 일반적인 데이터 전처리에도 사용될 수 있다. 여기서 다루고자 하는 cut_*()ggplot2 패키지에서 제공되는 함수이다.

앞서 cut()discretize()에서는 등간격, 등분포 등의 범주화 방법을 매개변수를 사용하여 결정하였는데 ggplot2에서는 등분포 범주화에 cut_number(), 등간격 범주화에 cut_interval(), cut_width()를 제공한다. cut_interval()cut_width()의 차이는 cut_interval()은 벡터의 최소값에서부터 시작해서 등간격을 나누지만 cut_width()는 최소값을 간격의 시작점으로 설정할수도 있지만 중간값으로 설정할 수 있는 옵션이 있다는 것이다.

이 함수들은 cut()을 기본으로 설계되었기 때문에 cut()에서 사용하는 매개변수를 사용할 수 있다.

df_joined <- df_joined |> 
  mutate(level_cut_cut_interval = cut_interval(학원수, 7, dig.lab = 5)
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_cut_cut_interval) |> head()
##   시도 학원수 level_cut_cut_interval
## 1 강원   1987             [767,3569]
## 2 경기  20381          (17579,20381]
## 3 경남   5546            (3569,6371]
## 4 경북   3257             [767,3569]
## 5 광주   3009             [767,3569]
## 6 대구   3508             [767,3569]
as.data.frame(df_joined) |> count(level_cut_cut_interval)
##   level_cut_cut_interval  n
## 1             [767,3569] 12
## 2            (3569,6371]  3
## 3           (9173,11975]  1
## 4          (17579,20381]  1
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_cut_cut_interval), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

df_joined <- df_joined |> 
  mutate(level_cut_cut_width = cut_width(학원수, 2000, center = TRUE, dig.lab = 5)
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_cut_cut_width) |> head()
##   시도 학원수 level_cut_cut_width
## 1 강원   1987         (1001,3001]
## 2 경기  20381       (19001,21001]
## 3 경남   5546         (5001,7001]
## 4 경북   3257         (3001,5001]
## 5 광주   3009         (3001,5001]
## 6 대구   3508         (3001,5001]
as.data.frame(df_joined) |> count(level_cut_cut_width)
##   level_cut_cut_width n
## 1         [-999,1001] 1
## 2         (1001,3001] 7
## 3         (3001,5001] 6
## 4         (5001,7001] 1
## 5       (11001,13001] 1
## 6       (19001,21001] 1
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_cut_cut_width), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

discretize()

discretize()arules 패키지에서 제공하는 함수이다. 이 함수는 cut()와 같이 전체 범주의 수를 지정함으로써 해당 개수만큼 전체를 고르게 범주화하는 함수이다. 다음은 전국의 사설학원수를 5개의 범주로 나누어 단계 구분도를 그리는 코드이다.

if(!require(arules)) {
  install.packages(arules)
  library(arules)
}

df_joined <- df_joined |> 
  mutate(level_discretize_freq = discretize(학원수, breaks = 7)
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_discretize_freq) |> head()
##   시도 학원수 level_discretize_freq
## 1 강원   1987   [1.92e+03,2.18e+03)
## 2 경기  20381   [5.21e+03,2.04e+04]
## 3 경남   5546   [5.21e+03,2.04e+04]
## 4 경북   3257   [2.66e+03,3.29e+03)
## 5 광주   3009   [2.66e+03,3.29e+03)
## 6 대구   3508   [3.29e+03,3.54e+03)
as.data.frame(df_joined) |> count(level_discretize_freq)
##   level_discretize_freq n
## 1        [767,1.92e+03) 3
## 2   [1.92e+03,2.18e+03) 2
## 3   [2.18e+03,2.66e+03) 2
## 4   [2.66e+03,3.29e+03) 3
## 5   [3.29e+03,3.54e+03) 2
## 6   [3.54e+03,5.21e+03) 2
## 7   [5.21e+03,2.04e+04] 3
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_discretize_freq), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

위의 결과를 보면 cut()break에 범주의 수로 그룹화하는것과 discretize()로 그룹화하는것의 결과가 차이가 있다. 이는 discretize()의 범주화에 사용하는 방식의 차이이다. cut()은 값의 최대값과 최소값을 기준으로 등간격의 범주로 나누는 반면 discretize()에서는 범주를 나누는 방식을 ‘interval’(등간격), ‘frequency’(등분포), ‘cluster’(k-means 클러스터링), ‘fixed’(breaks에 명기된 분할점)의 네 가지 방법을 사용하는데 기본적으로 ’frequency’를 사용하기 때문에 각각의 범주에 배치되는 사례수가 최대한 같게 범주를 나눈다. 만약 K-means 클러스터링을 사용하여 범주화한다면 다음과 같이 결과가 나온다.

df_joined <- df_joined |> 
  mutate(level_discretize_cluster = discretize(학원수, method ='cluster', breaks = 7)
         )

as.data.frame(df_joined) |> select(시도, 학원수, level_discretize_cluster) |> head()
##   시도 학원수 level_discretize_cluster
## 1 강원   1987      [1.47e+03,2.39e+03)
## 2 경기  20381       [1.6e+04,2.04e+04]
## 3 경남   5546       [4.59e+03,8.6e+03)
## 4 경북   3257      [3.18e+03,4.59e+03)
## 5 광주   3009      [2.39e+03,3.18e+03)
## 6 대구   3508      [3.18e+03,4.59e+03)
as.data.frame(df_joined) |> count(level_discretize_cluster)
##   level_discretize_cluster n
## 1           [767,1.47e+03) 2
## 2      [1.47e+03,2.39e+03) 4
## 3      [2.39e+03,3.18e+03) 3
## 4      [3.18e+03,4.59e+03) 5
## 5       [4.59e+03,8.6e+03) 1
## 6        [8.6e+03,1.6e+04) 1
## 7       [1.6e+04,2.04e+04] 1
df_joined |> 
  ggplot() + 
  geom_sf(aes(fill = level_discretize_cluster), color = 'gray80') +
  geom_text(data = df_central, aes(x = X, y = Y, label = paste0(시도, '\n', scales::comma(학원수))), color = 'black', lineheight = 0.5, size = 6) + 
  labs(x = '위도', y = '경도') +
  annotation_scale(location = "br") +
  annotation_north_arrow(location = "br", pad_y = unit(0.05, 'npc'),
                         style = north_arrow_nautical) +
  scale_fill_discrete(name = '학원수')

다만 discretize()에도 cut()에서 사용했던 dig.lag을 사용하여 표현되는 수치의 포맷을 설정할 수 있다고 매뉴얼에 기재되어 있지만 소스코드를 확인해도 이는 작동하지 않는 듯하다. 아마도 오류이지 않을까 싶다.

지금까지 각각의 함수를 사용하여 나눈 범주들을 비교하면 다음과 같다.

시도 학원수 level_cut_break level_cut_num level_cut_cut_interval level_cut_cut_width level_discretize_freq level_discretize_cluster
강원 1987 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [1.92e+03,2.18e+03) [1.47e+03,2.39e+03)
경기 20381 9000개이상 (17579,20401] (17579,20381] (19001,21001] [5.21e+03,2.04e+04] [1.6e+04,2.04e+04]
경남 5546 5,000~6,999 (3569,6371] (3569,6371] (5001,7001] [5.21e+03,2.04e+04] [4.59e+03,8.6e+03)
경북 3257 3,000~4,999 (747.39,3569] [767,3569] (3001,5001] [2.66e+03,3.29e+03) [3.18e+03,4.59e+03)
광주 3009 3,000~4,999 (747.39,3569] [767,3569] (3001,5001] [2.66e+03,3.29e+03) [2.39e+03,3.18e+03)
대구 3508 3,000~4,999 (747.39,3569] [767,3569] (3001,5001] [3.29e+03,3.54e+03) [3.18e+03,4.59e+03)
대전 1890 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [767,1.92e+03) [1.47e+03,2.39e+03)
부산 4364 3,000~4,999 (3569,6371] (3569,6371] (3001,5001] [3.54e+03,5.21e+03) [3.18e+03,4.59e+03)
서울 11647 9000개이상 (9173,11975] (9173,11975] (11001,13001] [5.21e+03,2.04e+04] [8.6e+03,1.6e+04)
세종 767 1,000개 미만 (747.39,3569] [767,3569] [-999,1001] [767,1.92e+03) [767,1.47e+03)
울산 2266 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [2.18e+03,2.66e+03) [1.47e+03,2.39e+03)
인천 3581 3,000~4,999 (3569,6371] (3569,6371] (3001,5001] [3.54e+03,5.21e+03) [3.18e+03,4.59e+03)
전남 2472 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [2.18e+03,2.66e+03) [2.39e+03,3.18e+03)
전북 3480 3,000~4,999 (747.39,3569] [767,3569] (3001,5001] [3.29e+03,3.54e+03) [3.18e+03,4.59e+03)
제주 1026 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [767,1.92e+03) [767,1.47e+03)
충남 2691 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [2.66e+03,3.29e+03) [2.39e+03,3.18e+03)
충북 2056 1,000~2,999 (747.39,3569] [767,3569] (1001,3001] [1.92e+03,2.18e+03) [1.47e+03,2.39e+03)

댓글