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

대학 입학생별 학과수 - ggplot2로 그리는 histogram in R

by 아참형인간 2021. 12. 2.
bins.knit

histogram으로 데이터 설명하기

데이터의 도수분포를 시각화할 때 많이 사용되는 시각화가 histogram을 사용하는 것이다. histogram은 변수의 변화에 따라 데이터의 사례수가 몇 개인지를 표현하는데 사용된다. 유사한 방법으로 확률 분포를 표현할 수도 있고 최대값을 1로 두고 상대적 비율을 표현하는 방식으로도 사용이 가능하다. 그런데 사용하다보면 histogram을 설명하기가 어려운 경우가 있을 것이다. 이 경우가 어떤 경우인지 알아보고 이 경우 어떻게 해결할 지에 대해 살펴보자.

Data Import

이번 포스트에서는 한국교육개발원 교육통계서비스 홈페이지에서 제공하는 대학의 전체 학과 데이터 셋학교/학과별 데이터셋 - 대학 - 학과별(상반기) - 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('대학교'))

histogram 그리기

ggplot2로 histogram을 그리는 가장 간단한 방법은 geom_histogram()을 사용하는 것이다. geom_histogram()은 대표적인 단변량 그래프이다. 단변량이란 하나의 변수만으로 시각화가 구현된다는 것이다. 보통 X축과 Y축의 두 축에 두개의 변수를 매핑해서 그리는 그래프는 다변량 그래프이다. 하지만 geom_histogram()은 하나의 축에만 변수를 매핑함으로써 그래프가 그려진다. 아래의 그래프는 입학생 수에 따른 학과수의 분포를 geom_histogram()을 사용해 그린 것이다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram()

위의 코드에서도 보이듯이 geom_histogram()에 사용하는 변수 하나(입학자_전체_계)만을 X축에 매핑해서 간단히 그릴수 있다. 하지만 원칙적으로는 Y축에 변수의 수를 세는 통계치의 매핑이 생략되어 있는 것 뿐이다. 위의 코드는 사실상 아래의 코드로 변환되어 실행되는 것이다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(aes(y = ..count..))
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

여기서 Y축에 매핑한 ..count..의 의미는 Y축에 사례수(count) 통계치를 사용하겠다는 의미이다. Y축에 매핑 가능한 통계치는 다음과 같다.

표현 통계치
..count.. 사례수
..density.. 확률밀도함수
..ncount.. 최대값을 1로 두는 사례수
..ndensity.. 최대값을 1로 두는 확률밀도함수
..width.. bin의 넓이

따라서 Y축에 어떤 통계치를 매핑시키느냐에 따라 histogram의 표현이 달라진다. 아래의 코드는 사례수 대신 최대값을 1로 두는 사례수(..ncount..)를 사용한 그래프이다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(aes(y = ..ncount..))
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

위의 그래프를 보면 앞선 그래프와 같아보이지만 Y축의 범위가 최대 1로 설정되어 있다.

histogram에 도수 표현하기

앞선 그래프를 사용하여 누군가(상사??)에게 데이터를 설명한다고 생각해 보자. 데이터에 대한 설명을 듣는 사람이 사례가 가장 많은 것은 몇개라는 질문을 받으면 당신은 어떻게 설명할 것인가? 결국 사례수를 표현할 필요가 있다. 이를 위해서는 stat_bin()를 사용할 수 있다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  stat_bin(aes(y=..count.., label=..count..), geom="text", vjust=-.5) +
  geom_histogram(aes(y = ..count..))
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

stat_bin()geom_histogram()geom_fraqpoly()에서 사용하는 bin을 설정할 수 있는 함수이다.

사실 geom_histogram()도 bin을 사용하는 그래프이기 때문에 stat_bin()을 이용해서 다음과 같이 그릴 수 있다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  stat_bin(aes(y=..count.., label=..count..), geom="text", vjust=-.5) +
  stat_bin(aes(y = ..count..), geom = 'bar')
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

위의 코드들의 특징은 기하학적 모형(geom) 함수를 사용할 것인가, 통계적 변환(stat) 함수를 사용할 것인가이다. 하지만 기하학적 모형을 사용하는 함수에는 결국 통계적 변환을 매개변수로 전달하게 되고 통계적 변환 함수를 사용하는 경우에는 기하학적 모형을 매개변수로 전달하게 되기 때문에 결국 같은 그래프를 그릴수 밖에 없다.

histogram에 bin 개수 설정

위의 그래프에서 막대의 수는 몇개인가? geom_histogram()의 막대수 기본값은 30개이다. 전체 X값의 범위를 30개의 구간으로 분리하고 이들 구간에 포함된 개체의 수를 막대로 표현한다. 막대의 개수는 bins 매개변수로 설정할 수 있다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 20) +
  stat_bin(aes(y=..count.., label=..count..), bins = 20, geom="text", vjust=-.5) +
  labs(title = 'bins = 20')

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 30) +
  stat_bin(aes(y=..count.., label=..count..), bins = 30, geom="text", vjust=-.5) +
  labs(title = 'bins = 30(default)')

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 60) +
  stat_bin(aes(y=..count.., label=..count..), bins = 60, geom="text", vjust=-.5) +
  labs(title = 'bins = 60')

하지만 각 막대의 범위를 알기 위해서는 전체 X축의 범위를 30개로 나누어야 알 수 있다. 이 방법은 전체 X 범위를 동일한 간격으로 나눌수 있는 장점이 있지만 각각의 범위를 인지하기 어렵다는 단점이 있다. 이를 위해 사용하는 매개변수가 binwidth이다. binwidth는 bin의 구간크기를 직접 지정한다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(binwidth = 20) +
  labs(title = 'binwidth = 20') + 
  stat_bin(aes(y=..count.., label=..count..), binwidth = 20, geom="text", vjust=-.5) +
  scale_x_continuous(breaks = seq(from = 0, to = 60, by = 10))

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(binwidth = 30) +
  labs(title = 'binwidth = 30') + 
  stat_bin(aes(y=..count.., label=..count..), binwidth = 30, geom="text", vjust=-.5) +
  scale_x_continuous(breaks = seq(from = 0, to = 60, by = 10))

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(binwidth = 60) +
  labs(title = 'binwidth = 60') + 
  stat_bin(aes(y=..count.., label=..count..), binwidth = 60, geom="text", vjust=-.5) +
  scale_x_continuous(breaks = seq(from = 0, to = 60, by = 10))

막대의 구간은?

위와 같이 histogram에서 막대의 수를 원하는 대로 설정할 수 있다. 그럼 이번에는 그래프를 보는 사람이 무엇을 궁금해 할까? ’가장 사례수가 많은 구간은 어디인가?’를 물어본다면 당신은 어떻게 대답할 것인가?

geom_histogram()이 bin의 구간을 어떻게 나누는지 알아보자. 막대의 수를 3개로 놓고 확인하면 이를 명확하게 알아볼 수 있다.

df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61) |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 3) +
  stat_bin(aes(y=..count.., label=..count..),bins = 3, geom="text", vjust=-.5) +
  labs(title = 'bins = 3')

위의 그래프를 보면 일단 0보다 왼쪽으로 막대가 빠져나가 있다. 그리고 데이터는 입학자수가 60보다 작게 설정했는데 막대의 오른쪽 끝은 73정도까지 빠져나간것 같다.

geom_histogram()은 bin을 결정할 때 전체 X축의 중간값을 중간 막대의 가운데로 두고 시작한다. 아래의 그래프를 보면 이를 쉽게 알 수 있다.

df.전처리.vline <- df.전처리 |>
  ## 그래프의 표현을 위해 입학자의 수를 제한하였다. 
  filter(입학자_전체_계 >= 1, 입학자_전체_계 <= 61)

range.x <- max(df.전처리.vline$입학자_전체_계) - min(df.전처리.vline$입학자_전체_계)

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 3) +
  labs(title = 'bins = 3') + 
  geom_vline(xintercept = range.x/2, color = 'red') + 
  stat_bin(aes(y=..count.., label=..count..),bins = 3, geom="text", vjust=-.5) +
  scale_x_continuous(breaks = c(min(df.전처리.vline$입학자_전체_계),
                                range.x/2, 
                                max(df.전처리.vline$입학자_전체_계)
                                )
  )

위에서 보이듯이 세개의 막대중 가운데 막대의 중간을 정확히 X축의 중간값이 지나가고 있다. 그렇다면 다음과 같이 확장해 볼 수 있겠다.

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(bins = 3) +
  labs(title = 'bins = 3') + 
  stat_bin(aes(y=..count.., label=..count..),bins = 3, geom="text", vjust=-.5) +
  geom_vline(xintercept = range.x/2, color = 'red') + ## X축의 1/2 위치
  geom_vline(xintercept = range.x/2/2, color = 'red') + ## X축의 1/4 위치 
  geom_vline(xintercept = range.x/2/2*3, color = 'red') + ## X축의 3/4 위치
  scale_x_continuous(breaks = c(min(df.전처리.vline$입학자_전체_계), 
                                range.x/2/2, 
                                range.x/2,
                                range.x/2/2*3,
                                max(df.전처리.vline$입학자_전체_계)
                                )
  )

위의 그래프를 보면 중간 막대의 범위는 15에서 45사이로 나타난다. 그럼 나머지 막대는 1부터 14까지, 46부터 61까지로 결정된다. 그럼 binwidth로 설정하면 어떻게 될 것인가?

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_histogram(binwidth = 10) +
  labs(title = 'binwidth = 10') + 
  stat_bin(aes(y=..count.., label=..count..), binwidth = 10, geom="text", vjust=-.5) +
  geom_vline(xintercept = 5, color = 'blue') + ## X축의 1/2 위치
  geom_vline(xintercept = 10, color = 'red') + ## X축의 1/2 위치
  geom_vline(xintercept = 15, color = 'blue') + ## X축의 1/2 위치
  geom_vline(xintercept = 20, color = 'red') + ## X축의 1/2 위치
  geom_vline(xintercept = 25, color = 'blue') + ## X축의 1/2 위치
  geom_vline(xintercept = 30, color = 'red') + ## X축의 1/4 위치 
  geom_vline(xintercept = 35, color = 'blue') + ## X축의 1/2 위치
  geom_vline(xintercept = 40, color = 'red') + ## X축의 1/2 위치
  geom_vline(xintercept = 45, color = 'blue') + ## X축의 1/2 위치
  geom_vline(xintercept = 50, color = 'red') + ## X축의 3/4 위치
  scale_x_continuous(breaks = c(min(df.전처리.vline$입학자_전체_계), 5,
                                10, 15, 
                                20, 25, 
                                30, 35,
                                40, 45, 
                                50,
                                max(df.전처리.vline$입학자_전체_계)
                                )
  )

위의 그래프는 binwidth를 10으로 설정한 그래프이다. 보통 binwidth를 10으로 설정하면 1부터 10까지, 11부터 20까지로 생각하기 쉽지만 위에서 보듯이 10을 중심으로 막대를 생성하게 된다. 여기서 하나 살펴볼것이 맨 왼쪽의 막대는 1부터 5까지로 다른 막대의 범위인 10보다 작다. 이 부분을 잘 이해해야 한다.

하지만 우리는 이렇게 생각할까? 보통 간격이 10이라면 1부터 10까지, 11부터 20까지와 같이 생각하지 않는가? 이렇게 그리려면 어떻게 해야할까?

이렇게 시작부터 동일한 간격으로 그리기 위해서는 geom_bar()scale_x_binned()를 다음과 같이 사용한다.

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_bar() +
  scale_x_binned(n.breaks = 3, right = T) +
  labs(title = 'n.breaks = 3')

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_bar() +
  scale_x_binned(n.breaks = 5, right = T) +
  labs(title = 'n.breaks = 5')

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_bar() +
  scale_x_binned(n.breaks = 10, right = T) +
  labs(title = 'n.breaks = 10')

위와 같이 geom_bar()scale_x_binned()를 사용하면 처음부터 동일한 간격으로 설정된 막대가 생성된다. 여기에 몇가지 더 장점이 있어보이지 않는가? 첫번째는 막대간의 살짝 간격이 생긴다. 그래프가 훨씬 보기 좋다. 게다가 X축에 간격을 표현해주는 의미있는 구간이 표현된다. 그래프를 보는 사람이 구간을 인식하기가 훨씬 좋다.

이제 histogram이 좀 보기가 좋아졌다. 각각의 막대의 사례수는 stat_bin()으로 잘 표현되지 않는다.

이를 표현하기 위해 geom_text()를 다음과 같이 사용해 보자.

df.전처리.vline |>
  ggplot(aes(x = 입학자_전체_계)) +
  geom_bar() +
  scale_x_binned(n.breaks = 10, right = T) +
  geom_text(stat = 'count', aes(label = ..count..), vjust = -0.2) +
  labs(title = 'n.breaks = 10')

댓글