聰明不如鈍筆
총명불여둔필
assignment KindeR

R에서 묶은 막대 그래프에 레이블 쓰기(feat. position_dodge)


이건 사실 말보다는 그림으로 설명 드리는 게 백배 낫습니다.


그러니까 그래프를 묶어서 그릴 때 (적어도) 아래 그림처럼 레이블이 나타나게 하고 싶은데


 

그냥 손가락이 가는 대로 코드를 쓰면 레이블이 아래 그림처럼 레이블이 나오는 현상 말입니다.



이럴 때는 어떻게 해야 각 레이블이 자기 자리를 찾아가도록 만들 수 있을까요?


일단 정답은 제목에 쓴 것처럼 position 속성에 position_dodge 값을 주는 겁니다.


그러니까 geom_text(position=position_dodge(width=1))처럼 쓰시면 됩니다.


정답은 이미 알려드렸으니 이제부터 차근차근 레이블을 벌리는(?) 연습을 해보도록 하겠습니다.


첫 단계는 물론 tidyverse 패키지 불러오기입니다.

#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages --------------------------------------------------------------------------------- tidyverse 1.3.0 --
## √ ggplot2 3.2.1     √ purrr   0.3.2
## √ tibble  2.1.3     √ dplyr   0.8.3
## √ tidyr   0.0.3     √ stringr 1.4.0
## √ readr   1.3.1     √ forcats 0.3.0
## -- Conflicts ------------------------------------------------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()


이어서 데이터를 마련해야 합니다.


이 블로그 전통에 따라 지난해 프로야구 10개 구단 타율, 출루율, 장타력 자료를 가지고 작업을 진행하겠습니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) 
## # A tibble: 10 x 4
##    팀     타율 출루율 장타력
##    <chr> <dbl>  <dbl>  <dbl>
##  1 키움  0.282  0.354  0.414
##  2 NC    0.278  0.345  0.416
##  3 두산  0.278  0.355  0.389
##  4 KT    0.277  0.344  0.385
##  5 삼성  0.256  0.329  0.389
##  6 SK    0.262  0.334  0.384
##  7 LG    0.267  0.333  0.384
##  8 KIA   0.264  0.336  0.369
##  9 한화  0.256  0.324  0.362
## 10 롯데  0.25   0.316  0.358


그런데 막상 10개 구단 기록을 가지고 그래프를 그려 보니 문제가 하나 있습니다.


10개 구단을 모두 쓰면 그래프가 너무 좁아서 글자가 잘 보이지 않는다는 것.


그런 이유로 sample() 함수를 써서 5개 구단만 임의로 골라내도록 하겠습니다.


숫자 1~10 가운데 다섯 개를 골라내라는 걸 코드로 쓰면 'sample(1:10, 5)가 됩니다.


그다음 행 번호(row_number())가 이 숫자 안에 들어 있을 때(%in%)만 필터링(filter())하겠습니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
)  %>%
  filter(row_number() %in% sample(1:10, 5))
## # A tibble: 5 x 4
##   팀     타율 출루율 장타력
##   <chr> <dbl>  <dbl>  <dbl>
## 1 키움  0.282  0.354  0.414
## 2 두산  0.278  0.355  0.389
## 3 SK    0.262  0.334  0.384
## 4 한화  0.256  0.324  0.362
## 5 롯데  0.25   0.316  0.358


아, 참고로 코드를 실행할 때마다 추출한 숫자가 달라지기 때문에 팀 선택도 달라집니다.


지금은 팀이 바뀌는 게 중요한 문제가 아니니 그냥 이대로 진행하도록 하겠습니다.


현재 우리가 입력한 데이터는 와이드 폼입니다. 


pivot_longer() 함수를 써서 롱 폼으로 바꾸겠습니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값')
## # A tibble: 15 x 3
##    팀    기록      값
##    <chr> <chr>  <dbl>
##  1 두산  타율   0.278
##  2 두산  출루율 0.355
##  3 두산  장타력 0.389
##  4 SK    타율   0.262
##  5 SK    출루율 0.334
##  6 SK    장타력 0.384
##  7 LG    타율   0.267
##  8 LG    출루율 0.333
##  9 LG    장타력 0.384
## 10 KIA   타율   0.264
## 11 KIA   출루율 0.336
## 12 KIA   장타력 0.369
## 13 롯데  타율   0.25 
## 14 롯데  출루율 0.316
## 15 롯데  장타력 0.358


혹시 이렇게 데이터 구조를 바꾸는 작업이 잘 이해가 가지 않으시다면 '최대한 친절하게 쓴 R로 데이터 깔끔하게 만들기(feat. tidyr)' 포스트가 도움이 될 수 있습니다.


이 포스트를 쓸 때는 gather() 함수가 '와이드 폼 → 롱 폼' 변환을 맡았는데 그 뒤로 pivot_longer() 동사가 나왔습니다.


데이터 구조를 바꾸는 작업까지 끝났으니 일단 그래프를 하나 그리겠습니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값)) +
  geom_bar(stat='identity')


우리는 타율 출루율 장타력을 따로 따로 나타내고 싶으니까, 그러려고 와이드 폼을 롱 폼으로 바꾼 거니까 색깔로 구분을 짓겠습니다.


특정 변수에 들어 있는 값에 따라 색깔을 바꾸고 싶을 때는 aes(fill='변수') 형태로 쓰시면 됩니다.  

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity')


우리는 이렇게 누적으로 그래프를 그리고 싶은 게 아니라 묶음으로 그리고 싶습니다.


이럴 때는 geom_bar() 안에 position='dodge' 속성을 지정하면 됩니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge')


잘 나왔습니다. 이제 각 그래프에 레이블을 붙여 보겠습니다. geom_text()를 쓰면 되겠죠?

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge') +
  geom_text(aes(label=값))


이 글을 쓰게 만든 그래프가 드디어 등장했습니다.


글 초반에 말씀드린 것처럼 이때는 geom_text() 안에 position=position_dodge(width=1)를 추가하면 됩니다. 일단 넣어보겠습니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge') +
  geom_text(aes(label=값), position=position_dodge(width=1))


일단 레이블을 옆으로 벌리는 데는 성공했습니다.


참고로 위에서는 geom_bar(position='dodge')라고 썼는데 이 부분 역시 position=position_dodge()라고 쓰셔도 같은 결과가 나옵니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position=position_dodge()) +
  geom_text(aes(label=값), position=position_dodge(width=1))


레이블을 막대 끝에 그대로 두는 것보다는 위로 살짝 띄우면 더 좋겠죠?


이럴 때는 geom_text()에서 y값을 수정하면 됩니다.


현재는 ggplot() 코드 맨 처음에 있는 aes(x=팀, y=값, fill=기록)에 따라 geom_text() 역시 '값'을 y값으로 쓰고 있는 상태입니다. 이 '값'에 .0125를 더해보겠습니다. 

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge') +
  geom_text(aes(y=값+.0125, label=값), position=position_dodge(width=1))


이걸로 일단 기본적인 틀은 모두 잡았습니다.


그래도 이대로 끝나면 아쉬우니까 몇 가지 장난(?)을 더 쳐보도록 하겠습니다.


일단 오른쪽에 있는 범례(legend)를 아래로 내려볼까요?


이 때는 theme() 함수를 쓰고 그 안에 legend.position 속성을 활용해 위치를 조정하시면 됩니다.


우리는 범례를 아래로 보내고 싶으니까 legend.poisition='bottom'이라고 쓰면 됩니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge') +
  geom_text(aes(y=값+.0125, label=값), position=position_dodge(width=1)) +
  theme(legend.position='bottom')


현재 그래프는 한 팀을 놓고 볼때 장타력 출루율 타율 그래프가 전부 붙어 있는 형태입니다.


이 그래프 사이에 간격을 주고 싶을 때는 어떻게 할까요?


가장 간단한 방법은 position='dodge' 뒤에 2를 붙이는 겁니다.


그냥 geom_bar(position='dodge2')처럼 쓰면  그만입니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=기록)) +
  geom_bar(stat='identity', position='dodge2') +
  geom_text(aes(y=값+.0125, label=값), position=position_dodge(width=1)) +
  theme(legend.position='bottom')


야구를 좋아하시는 분이라면 타격 기록을 쓸 때는 타율/출루율/장타력 순서가 기본이라는 걸 알고 계실 겁니다.


지금은 순서가 반대로 나와 있습니다. 이를 바로 잡으려면 어떻게 할까요?


먼저 factor() 함수로 값을 요인(factor) 형태로 만든 뒤 forcats 패키지에 들어 있는 fct_rev() 함수를 써서 순서를 바꿔주면 됩니다.


위에서 tidyverse 패지키를 불러오셨다면 forcats 패키지는 따로 부르지 않으셔도 됩니다.


forcats 패지키는 요인을 깔끔하게 처리할 수 있도록 도와주는 패키지입니다.


이 그래프에서는 'fill=factor(기록) %>% fct_rev()'로 쓰시면 됩니다. 

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=factor(기록) %>% fct_rev())) +
  geom_bar(stat='identity', position='dodge2') +
  geom_text(aes(y=값+.0125, label=값), position=position_dodge(width=1)) +
  theme(legend.position='bottom')


이렇게 쓰다 보니 범례 이름이 좀 이상하게 나왔습니다.


이때는 scale_fill_discrete() 함수 안에 name 속성을 써서 이름을 바꿀 수 있습니다.


원래대로 '기록'이라고 나타내려면 scale_fill_discrete(name='기록')이라고 쓰면 됩니다.

tribble(
  ~팀, ~타율, ~출루율, ~장타력,
  '키움', .282, .354, .414,
  'NC', .278, .345, .416,
  '두산', .278, .355, .389,
  'KT', .277, .344, .385,
  '삼성', .256, .329, .389,
  'SK', .262, .334, .384,
  'LG', .267, .333, .384,
  'KIA', .264, .336, .369,
  '한화', .256, .324, .362,
  '롯데', .250, .316, .358
) %>%
  filter(row_number() %in% sample(1:10, 5)) %>%
  pivot_longer(-팀, names_to='기록', values_to='값') %>%
  ggplot(aes(x=팀, y=값, fill=factor(기록) %>% fct_rev())) +
  geom_bar(stat='identity', position='dodge2') +
  geom_text(aes(y=값+.0125, label=값), position=position_dodge(width=1)) +
  scale_fill_discrete(name='기록') +
  theme(legend.position='bottom')


이런 식으로 자료를 정리해 그래프를 그리고 싶은데 레이블이 엉뚱한 데 붙어서 어려움을 겪는 분들이 계시다면 이 포스트가 도움이 되었기를 바랍니다.


그럼 Happy Charting!

댓글,

KindeR | 카테고리 다른 글 더 보기