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

대학 입학생별 학과수 - 축 변환 in R

by 아참형인간 2021. 12. 4.
trans.knit

축 변환 사용법

데이터를 시각화할 때 만나는 몇가지 문제점 중에 많이 만나는 문제는 한쪽으로 치우친(Skewed)된 데이터일 것이다. 특히 데이터를 전반적으로 표현하는 형태의 시각화, 그 중에서도 histogram에서 이런 경우를 접해 본 경험이 있을 것이다. 이럴 경우에는 데이터를 효과적으로 표현하기 위해 축을 수학적 변환 공식에 따라 변형해주는 방법을 소개해보고자 한다.

치우친 데이터(Skewed Data)란?

치우친 데이터는 아래의 그림과 같이 데이터의 분포가 한쪽으로 몰려있는 경우를 의미한다. 아래의 그림처럼 데이터가 왼쪽이나 오른쪽으로 치우쳐 있고 반대쪽으로 꼬리가 길게 늘어뜨려진 데이터의 형태이다. 이러한 치우친 데이터는 위에서 언급한 바와 같이 주로 데이터의 사례수를 표현하는 histogram 시각화에서 많이 나타나게 된다. 아래의 그림은 치우침이 비교적 심하지 않아 적절히 데이터의 분포를 확인할 수 있겠지만 데이터의 치우침이 큰 경우에는 histgram의 시각화가 큰 의미가 없을 떄도 있다. 이와 같이 데이터의 치우침을 계산할 때 ’왜도’라는 지수를 사용한다. R에서는 왜도 함수를 통해 데이터의 왜도를 비교할 수 있다.

Data Import

이번 포스트에서 왜도를 계산하고 histogram에서 왜도가 심한 데이터를 효과적으로 표현하기 위해 축 변환 방법을 설명하고자 한다. 이를 위해 사용하는 데이터는 한국교육개발원 교육통계서비스 홈페이지학교/학과별 데이터셋 - 대학 - 학과별(상반기) - 2021를 활용하겠다.

library(readxl)
library(tidyverse)

df <- read_excel('./21년 고등 학과별 입학정원 입학 지원 재적 재학 휴학 외국인유학생 졸업 교원_211119.xlsx', skip = 12, na = '-', sheet = '학과별 주요 현황', col_names = T, col_types = c(rep('text', 8), rep('numeric', 56)))

## 전체 데이터 중에 대학교 데이터만 사용하겠다. .  
df.전처리 <- df |>
  filter(학제 %in% c('대학교'))

df.전처리$대계열 <- factor(df.전처리$대계열)

histogram 그리기

데이터의 전체 분포를 확인하기 위해서 사용하는 시각화 방법 중 많이 쓰는 방법이 histgram을 통해 분포를 확인하는 것이다. histgram은 각각의 변량에 대한 사례수를 표현하는 막대 그래프를 의미한다. histogram의 Y축은 사례수(n)으로 고정되기 때문에 대표적인 단변량 시각화이다. 따라서 다른 ggplot2 그래프와는 달리 X, Y축의 매핑값을 지정하지 않고 X축의 매핑값만으로 시각화가 가능하다. 앞서 전처리된 데이터에 대한 histogram을 다음과 같이 생성한다.

df.전처리 |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram() +
  labs(x = '입학학생수', y = '학과수')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

샘플 데이터는 하나의 학과당 하나의 레코드이기 때문에 사례수(n)은 학과수와 동일하다. 따라서 Y축의 이름을 count가 아닌 학과수로 표현하였다. 이 histogram은 매우 왜도가 심한 것으로 보인다. 왜도의 정도를 moments 패키지의 skewness()를 사용하여 측정하면 다음과 같다.

library(moments)
skewness(df.전처리$입학자_전체_계)
## [1] 24.34325

일단 왜도 값이 플러스 값이 나왔다. 이는 왼쪽의 봉우리가 높고 오른쪽으로 꼬리가 길게 치우쳐진 histogram을 의미한다. 그리고 값은 20이 넘게 나온다. 이 값은 매우 큰 값이다. 값의 정도를 비교해 보기 위해 다음의 예를 살펴보자.

set.seed(123)

##  베타 확률분포 함수를 이용하여 왼쪽으로 치우친(left skewed) 데이터 10000개를 생성
samples <- rbeta(10000,5,1)

## 시뮬레이션을 위한 데이터 프레임을 생성
sim.data <- data.frame(samples)

ggplot(data = sim.data, aes(x = samples)) + geom_histogram()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

skewness(sim.data$sample)
## [1] -1.169109
set.seed(123)
##  베타 확률분포 함수를 이용하여 오른쪽으로 치우친(right skewed) 데이터 10000개를 생성
samples <- rbeta(10000,1,5)

## 시뮬레이션을 위한 데이터 프레임을 생성
sim.data <- data.frame(samples)

ggplot(data = sim.data, aes(x = samples)) + geom_histogram()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

skewness(sim.data$sample)
## [1] 1.169109

위의 예에서 보이듯이 다소 치우쳐진 데이터들이 1을 겨우 넘는 작은 값을 보여주는 것을 보면 20이 넘는 값은 매우 치우침이 크다는 것을 알 수 있다.

그럼 원래 데이터로 돌아가서 얼마나 큰 값을 가진 데이터가 있길래 저렇게 오른쪽으로 치우쳤는지 확인해볼 필요가 있다. 다음과 같이 입학자의 규모가 큰 순서대로 20개, 작은 순서대로 20개를 확인해보자

df.전처리 |>
  arrange(desc(입학자_전체_계)) |>
  select(학과명, 입학자_전체_계)
## # A tibble: 8,075 x 2
##    학과명       입학자_전체_계
##    <chr>                 <dbl>
##  1 간호학과              10415
##  2 경영학부               6041
##  3 경영학과               5902
##  4 사회복지학과           3703
##  5 컴퓨터공학과           3003
##  6 전자공학과             2868
##  7 의예과                 2855
##  8 영어영문학과           2445
##  9 행정학과               2441
## 10 기계공학과             2329
## # ... with 8,065 more rows
df.전처리 |>
  arrange(입학자_전체_계) |>
  select(학과명, 입학자_전체_계)
## # A tibble: 8,075 x 2
##    학과명                                 입학자_전체_계
##    <chr>                                           <dbl>
##  1 언어과학과                                          0
##  2 국제한국어교육학과                                  0
##  3 언어인지과학과                                      0
##  4 스토리텔링융복합전공                                0
##  5 아시아언어문명학부(일본언어문명)                    0
##  6 아시아언어문명학부(동남아시아언어문명)              0
##  7 아시아언어문명학부(인도언어문명)                    0
##  8 아시아언어문명학부(서아시아언어문명)                0
##  9 국제언어학부                                        0
## 10 국제언어문화학과                                    0
## # ... with 8,065 more rows

위의 데이터에서 보듯이 입학자가 가장 많은 학과는 간호학과로 1만명이 넘어간다. 또 입학자가 가장 작은 학과는 여러 학과가 있는데 입학자가 0명이다. 입학자가 10명 이하의 학과수는 다음과 같이 구할 수 있다.

df.전처리 |>
  group_by(입학자_전체_계) |>
  count() |>
  head(10)
## # A tibble: 10 x 2
## # Groups:   입학자_전체_계 [10]
##    입학자_전체_계     n
##             <dbl> <int>
##  1              0  5077
##  2              1     9
##  3              2    13
##  4              3    11
##  5              4     8
##  6              5     8
##  7              6    20
##  8              7    15
##  9              8    15
## 10              9    15

위의 데이터에서 보면 입학생이 없는 학과가 5077개로 나타났다. 입학생이 없는 학과는 의미가 없으므로 제외하고 다시 histogram을 생성해본다.

df.전처리 |>
  filter(입학자_전체_계 != 0) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram() +
  labs(x = '입학학생수', y = '학과수')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

skewness(subset(df.전처리, 입학자_전체_계 != 0)$입학자_전체_계)
## [1] 15.74267

입학자가 0인 학과를 제거했지만 아직도 왜도가 15가 넘어가는 매우 치우친 그래프가 그려졌다. 이를 완화시키는 방법 중에 많이 사용되는 방법이 축 스케일을 적절히 변환하는 방법이다.

축 스케일이란?

축 스케일은 축이 나뉘어지는 단위(Unit)을 의미한다. R에서 스케일은 크게 이산형(descrete) 스케일과 연속형(countinuous) 스케일의 두가지로 제공된다. 이산형 스케일은 보통 팩터 변수를 축에 매핑함으로써 표현된다. 반면 연속형 스케일은 일반적으로 수치로 표현되는데 일반적으로는 선형적(linear) 스케일이 사용된다. 선형적(linear) 스케일이라는 것은 축의 어느 위치에서나 동일한 축의 거리는 동일한 값의 단위를 나타낸다는 것이다. 아래의 예를 살펴보자.

df.전처리 |>
  ggplot(aes(x = 입학자_전체_계, y = 대계열))

위의 그래프를 보면 X축은 0부터 10000까지 연속형 스케일로 매핑되어 있다. 여기서 한 칸은 12500을 가리킨다. 이 간격은 축의 시작부터 끝까지 동일하게 유지된다. 축의 시작위치에서의 한칸이나 축의 마지막 부분에서의 한칸이나 모두 12500을 의미한다. 또 1칸의 거리는 2칸의 거리에 2배의 값을 가진다. 이것이 선형적 스케일이다. 반면 Y축은 7개의 대계열 팩터로 구성되어 있는 이산형 스케일이다. 사실 대계열 팩터가 순서형 팩터가 아니기 때문에 순서와 거리는 의미가 없다. 순서는 서로 바뀌어도 관계가 없고 1칸, 2칸의 거리는 단위의 2배를 의미하는 것이 아니다. 이럴 경우 보통 A, B, C 순서(한글은 가나다 순서)로 표기된다.

위의 코드에서 축 스케일을 따로 설정하지 않았지만 축에 매핑된 변수의 타입에 따라서 이산형 스케일이 설정될 지, 연속형 스케일이 설정될 지가 자동적으로 결정된다. 사실 위의 코드는 아래 코드 중 일부가 생략된 코드이다.

df.전처리 |>
  ggplot(aes(x = 입학자_전체_계, y = 대계열)) +
  scale_x_continuous() +
  scale_y_discrete()

축 스케일 변환

앞서 보았던 입학생수별 학과수를 다시 한번 살펴보자. 히스토그램을 잘 이해하기 위해 아래와 같이 그래프를 수정해보자.

df.전처리 |>
  filter(입학자_전체_계 != 0) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram() +
  stat_bin(aes(y=..count.., label=..count..), geom="text", vjust=-.5) +
  labs(x = '입학학생수', y = '학과수')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

오른쪽으로 치우쳐진 그래프이기 때문에 전체적인 분포를 알아볼 수 없다. 거의 모든 사례가 첫번째 막대에 분포되어 있기 때문에 histogram의 의미가 거의 없다. 이럴 경우 연속형 선형적 스케일을 변환하여 0에 가까운 구간은 한 유닛(칸)의 간격을 좁게 주고 0과 먼 구간은 한 유닛(칸)의 간격의 범위를 넓게 줄 수 있다. 이때 사용하는 변환이 log10 변환이다. log10 변환은 다음과 같이 코딩한다.

df.전처리 |>
  filter(입학자_전체_계 != 0) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram() +
  stat_bin(aes(y=..count.., label=..count..), geom="text", vjust=-.5) +
  scale_x_log10() +
  labs(x = '입학학생수', y = '학과수')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

X축의 스케일을 log10 변환하기 위해서는 scale_x_log10()을 사용한다. 반면 Y축의 스케일을 log10 변환을 위해서는 scale_y_log10()을 사용하면 된다. 위의 그래프와 같이 X축 스케일을 log10 변환하면 histogram의 분포가 훨씬 의미있게 나타났다. 이 그래프의 X축을 잘 살펴보면 몇가지 특징이 보일것이다.

첫번째는 X축에 표현된 라벨이 모두 10의 제곱수이다. 100, 101, 102, 103와 같다. 이 수치를 log10() 변환 결과는 0, 1, 2, 3이다. 결국 10진수의 표현은 1, 10, 100이지만 log10 변환 결과는 1, 2, 3이다.

두번쨰는 각각의 칸의 단위가 다르다는 것이다. 1 다음 X축의 2칸은 10이 표현된다. 그런데 그 다음 2칸 이후는 100이다. 앞의 2칸의 범위는 10이었지만 다음 2칸의 범위는 90이다. 그 다음 2칸의 범위는 900이다. 결국 log10의 값이 커지면 커질수록 한칸에 표현되는 값이 범위가 넓어진다는 것이다.

반면 거꾸로 0에 가까운 유닛의 범위를 넓게하고 0과 면 유닛의 범위를 좁게하려면 scale_x_sqrt()를 사용한다.

df.전처리 |>
  filter(입학자_전체_계 != 0) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram() +
  stat_bin(aes(y=..count.., label=..count..), geom="text", vjust=-.5) +
  scale_x_sqrt() +
  labs(x = '입학학생수', y = '학과수')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

댓글