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

남이 쓴 ggplot 코드 훔쳐서 움직이는 막대 그래프 그리기(feat. gganimate)


인터넷에서 만난 낯선 그래프를 보고 '아, 이거 ggplot로 그렸네'하는 생각이 들 때가 있습니다. 때로는 그 그래프를 따라 그리고 싶다는 생각도 듭니다.


R를 비롯한 프로그래밍 언어가 좋은 건 소스 코드와 데이터만 있다면 어떤 시각화 결과물을 똑같이 그리는 게 가능하다는 점. 따라서 어떤 그래프를 따라 그리고 싶을 때는 원래 데이터 구조에서 숫자만 바꾸고 원래 ggplot 소스를 가져다 붙이면 그만입니다. 참 쉽죠?


그럼 이제부터 인터넷에서 우연히 만난 ggplot 그래프를 실제로 어떻게 훔치는지 연습해 보도록 하겠습니다.


Target Locked

우리가 훔칠 대상은 제가 스택 오버플로에서 발견한 이 녀석입니다. 막대 그래프가 자라면서 순위에 따라 자리를 바꾸는 이런 애니메이션 그래프 요즘 많이 보셨죠?



아래 코드를 R 콜손에 입력하시면 여러분도 이 그래프와 똑같은 그래프를 얻으실 수 있습니다. 아, 물론 tidyverse, gganimate, gapminer 패키지를 설치하지 않으셨다면 install.packages()로 설치하는 과정이 필요합니다. 셋 다 없으시다면 install.packages(c('tidyverse', 'gganimate', 'gapminer'))를 입력하시면 됩니다.

library(tidyverse)
library(gganimate)
library(gapminder)
theme_set(theme_classic())

gdp <- read.csv("https://raw.github.com/datasets/gdp/master/data/gdp.csv")
words <- scan(
  text="world income only total dividend asia euro america africa oecd",
  what= character())
pattern <- paste0("(",words,")",collapse="|")
gdp  <- subset(gdp, !grepl(pattern, Country.Name , ignore.case = TRUE))
colnames(gdp) <- gsub("Country.Name", "country", colnames(gdp))
colnames(gdp) <- gsub("Country.Code", "code", colnames(gdp))
colnames(gdp) <- gsub("Value", "value", colnames(gdp))
colnames(gdp) <- gsub("Year", "year", colnames(gdp))

gdp$value <- round(gdp$value/1e9)

gap <- gdp %>%
  group_by(year) %>%
  # The * 1 makes it possible to have non-integer ranks while sliding
  mutate(rank = min_rank(-value) * 1,
         Value_rel = value/value[rank==1],
         Value_lbl = paste0(" ",value)) %>%
  filter(rank <=10) %>%
  ungroup()

p <- ggplot(gap, aes(rank, group = country, 
                     fill = as.factor(country), color = as.factor(country))) +
  geom_tile(aes(y = value/2,
                height = value,
                width = 0.9), alpha = 0.8, color = NA) +
  geom_text(aes(y = 0, label = paste(country, " ")), vjust = 0.2, hjust = 1) +
  geom_text(aes(y=value,label = Value_lbl, hjust=0)) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +

  labs(title='{closest_state}', x = "", y = "GDP in billion USD",
       caption = "Sources: World Bank | Plot generated by Nitish K. Mishra @nitishimtech") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(),  # These relate to the axes post-flip
        axis.text.y  = element_blank(),  # These relate to the axes post-flip
        plot.margin = margin(1,1,1,4, "cm")) +

  transition_states(year, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

animate(p, 200, fps = 10, duration = 40, width = 800, height = 600, renderer = gifski_renderer("gganim.gif"))


저는 코드를 실행해 아래 GIF를 얻었습니다. 위에서 보여드린 것과 모양이 살짝 다른데 제가 코드를 Ctrl+C/V하는 과정에서 'theme_set(theme_classic())'을 빼먹어서 그렇습니다.



코드가 탈 없이 작동한다는 걸 확인했으니 이제 본격적으로 그래프를 훔쳐보도록 하겠습니다.



What Matters Most

'최대한 친절하게 쓴 R로 데이터 깔끔하게 만들기(feat. tidyr)' 포스트에 쓴 것처럼 전체 데이터 분석 시간 중 80% 정도는 데이터를 정제하고 준비하는 시간입니다.


우리가 훔치려는 코드도 마찬가지. 'p <- ggplot()…' 이 등장하기 전까지는 전부 데이터를 정제하고 준비하는 내용입니다. 이 작업이 중요하지 않다는 뜻은 절대 아니지만 그래프를 훔칠 때는 이런 과정이 필요가 없습니다. 데이터 구조가 다르면 전(前)처리 코드도 전부 달라질 수밖에 없기 때문입니다.


그러니 실제로 그래프를 그릴 때 활용하는 데이터를 들여다 보면 됩니다. 이 코드에서는 'gap'입니다. (혹시 왜 그런지 이해가 가지 않으신다면 '최대한 친절하게 쓴 R로 그래프 그리기(feat. ggplot2)' 포스트를 읽어보시는 게 도움이 될 수 있습니다.) gap은 이렇게 생겼습니다.

gap
# A tibble: 573 x 7
   country                       code   year value  rank Value_rel Value_lbl
   <fct>                         <fct> <int> <dbl> <dbl>     <dbl> <chr>    
 1 Heavily indebted poor countr~ HPC    1960    17    10    0.0313 " 17"    
 2 Heavily indebted poor countr~ HPC    1961    18    10    0.0320 " 18"    
 3 Heavily indebted poor countr~ HPC    1962    20    10    0.0331 " 20"    
 4 Heavily indebted poor countr~ HPC    1963    24     9    0.0376 " 24"    
 5 Argentina                     ARG    1962    24     9    0.0397 " 24"    
 6 Argentina                     ARG    1964    26     9    0.0379 " 26"    
 7 Argentina                     ARG    1965    28     9    0.0376 " 28"    
 8 Argentina                     ARG    1966    29     9    0.0356 " 29"    
 9 Australia                     AUS    1960    19     9    0.0350 " 19"    
10 Australia                     AUS    1961    20     9    0.0355 " 20"    
# ... with 563 more rows


이미 각 열이 어떤 구실을 하는지 눈치채신 분도 계실 거고, 전혀 모르겠다는 분도 계실 겁니다. 소스를 뜯어 보면서 한 번 의미를 알아볼까요?

 ggplot(gap, aes(rank, group = country, 
                     fill = as.factor(country), color = as.factor(country))) +


제일 처음 등장하는 열은 'rank'입니다. 원래 코드를 쓴 사람은 아무 흔적도 남기지 않았지만 저 자리에 건 x축에 어떤 값을 놓는다는 의미입니다. 'x=rank'와 같은 의미. 그리고 당연히 국가별 데이터를 다루고 있으니 'country'로 그룹을 구분해 외곽선과 바탕색을 칠한다는 사실도 알 수 있습니다.

geom_tile(aes(y = value/2,
                height = value,
                width = 0.9), alpha = 0.8, color = NA) +
  geom_text(aes(y = 0, label = paste(country, " ")), vjust = 0.2, hjust = 1) +
  geom_text(aes(y=value,label = Value_lbl, hjust=0))


그다음 코드를 통해 geom_bar() 등으로 막대 그래프를 직접 그린 게 아니라 geom_tile()을 써서 차트 영역에 색을 칠하는 방식을 선택했다는 걸 확인할 수 있습니다. 그 아래 geom_text()를 쓴 두 줄은 나라 이름과 국내총생산(GDP)을 표시하는 구실을 합니다.


여기까지 코드에 들어간 열은 △country △value △rank △Value_lbl 등 네 가지. 이 열에 우리가 필요한 데이터를 넣으면 똑같은 그래프를 그릴 수 있는 겁니다.




Height Still Matters

그러면 세계 보건 과학자 네트워크(NCD·RiSc) 홈페이지에서 국가별 평균 키 데이터를 내려받아서 같은 그래프를 한번 그려보겠습니다.


 파일을 read_csv()로 불러와서 열어보면 이렇게 생겼습니다.

height <- read_csv('http://ncdrisc.org/downloads/height/NCD_RisC_eLife_2016_height_age18_countries.csv')
d with column specification:
cols(
  Country = col_character(),
  ISO = col_character(),
  Sex = col_character(),
  `Year of birth` = col_integer(),
  `Mean height (cm)` = col_double(),
  `Mean height lower 95% uncertainty interval (cm)` = col_double(),
  `Mean height upper 95% uncertainty interval (cm)` = col_double()
height
# A tibble: 40,400 x 7
   Country ISO   Sex   `Year of birth` `Mean height (c~ `Mean height lo~
   <chr>   <chr> <chr>           <int>            <dbl>            <dbl>
 1 Afghan~ AFG   Men              1896             161.             154.
 2 Afghan~ AFG   Men              1897             161.             155.
 3 Afghan~ AFG   Men              1898             161.             155.
 4 Afghan~ AFG   Men              1899             161.             155.
 5 Afghan~ AFG   Men              1900             161.             155.
 6 Afghan~ AFG   Men              1901             161.             155.
 7 Afghan~ AFG   Men              1902             161.             155.
 8 Afghan~ AFG   Men              1903             161.             155.
 9 Afghan~ AFG   Men              1904             161.             155.
10 Afghan~ AFG   Men              1905             161.             155.
# ... with 40,390 more rows, and 1 more variable: `Mean height upper 95%
#   uncertainty interval (cm)` 


왜 이 데이터를 골랐는지 아시겠죠? 기본적으로 국가별, 연도별 데이터라는 구조가 같습니다. 우리는 최대한 원래 코드를 Ctrl+C/V해서 그래프를 완성할 예정이니까 붙여넣기 편하게 열 이름을 원래 코드하고 똑같이 바꾸겠습니다.

names(height)[1] <- 'country'
names(height)[4] <- 'year'
names(height)[5] <- 'value'


이 자료에는 나라별 남성과 여성 평균 키가 각각 들어 있습니다. 남성 키 데이터만 뽑아서 쓰기로 하겠습니다.

height <- height %>% filter(Sex=='Men')


이렇게 쓴 코드가 이해가 가지 않으시면 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트가 도움이 될 수 있습니다.


데이터 정리 첫 단계가 끝났으니 원래 코드를 가져다가 데이터 정리 마무리 작업을 진행하겠습니다. 위에 쓴 코드에서 gdp를 height으로 바꿨을 뿐 나머지는 그대로입니다.

gap <- height %>%
  group_by(year) %>%
  # The * 1 makes it possible to have non-integer ranks while sliding
  mutate(rank = min_rank(-value) * 1,
         Value_rel = value/value[rank==1],
         Value_lbl = paste0(" ",value)) %>%
  filter(rank <=10) %>%
  ungroup()


의심하지 말고 직접 그래프를 그리는 코드까지 그대로 이어서!

p <- ggplot(gap, aes(rank, group = country, 
                     fill = as.factor(country), color = as.factor(country))) +
  geom_tile(aes(y = value/2,
                height = value,
                width = 0.9), alpha = 0.8, color = NA) +
  geom_text(aes(y = 0, label = paste(country, " ")), vjust = 0.2, hjust = 1) +
  geom_text(aes(y=value,label = Value_lbl, hjust=0)) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +

  labs(title='{closest_state}', x = "", y = "GDP in billion USD",
       caption = "Sources: World Bank | Plot generated by Nitish K. Mishra @nitishimtech") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(),  # These relate to the axes post-flip
        axis.text.y  = element_blank(),  # These relate to the axes post-flip
        plot.margin = margin(1,1,1,4, "cm")) +

  transition_states(year, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

animate(p, 200, fps = 10, duration = 40, width = 700, height = 500, renderer = gifski_renderer("gganim.gif"))


눈치가 빠른 분이라면 맨 마지막 코드에서 원래 800×600이던 이미지 크기를 700×500으로 바꿨다는 사실을 확인하셨을지 모르겠습니다. 그걸 빼면 나머지 코드는 전부 똑같습니다. 이 코드를 실행하면 이 그림이 나옵니다.



고치고 싶은 데가 없는 건 아니지만 확실히 기본 동작에는 무리가 없습니다. 일단 코드를 훔쳐오는 데는 성공했다고 할 수 있습니다.


저는 일단 여기서 그래프 오른쪽에 조금 더 여백을 주고, 키를 소수점 첫 번째 자리까지만 보여주도록 만들겠습니다. 현재 내용과 무관하게 남아 있는 옛날 그래프 설명도 수정. 그러면 코드가 이렇게 살짝 바뀝니다.

p <- ggplot(gap, aes(rank, group = country, 
                     fill = as.factor(country), color = as.factor(country))) +
  geom_tile(aes(y = value/2,
                height = value,
                width = 0.9), alpha = 0.8, color = NA) +
  geom_text(aes(y = 0, label = paste(country, " ")), vjust = 0.2, hjust = 1) +
  geom_text(aes(y=value, label = format(round(value, 1), nsmall=1)), hjust=-.25) +
  coord_flip(clip = "off", expand = FALSE) +
  scale_y_continuous(labels = scales::comma) +
  scale_x_reverse() +
  guides(color = FALSE, fill = FALSE) +

  labs(title='{closest_state}', x = "", y = "출생연도별 남성 평균 키", caption = "자료: NCD·RiSc") +
  theme(plot.title = element_text(hjust = 0, size = 22),
        axis.ticks.y = element_blank(),  # These relate to the axes post-flip
        axis.text.y  = element_blank(),  # These relate to the axes post-flip
        plot.margin = margin(1,1,1,4, "cm")) +

  transition_states(year, transition_length = 4, state_length = 1) +
  ease_aes('cubic-in-out')

animate(p, 200, fps = 10, duration = 40, width = 700, height = 500, renderer = gifski_renderer("gganim.gif"))

제가 원하던 모양대로 나왔습니다. 이런 식으로 원래 코드를 토대로 소스를 조금씩 조금씩 고치면 점점 더 원하시는 그래프에 가까워질 겁니다. 원래 코드 어디 가지 않으니까 과감하게 이것저것 만져보셔도 좋을 겁니다.


저는 추가로 이런 걸 그려봤습니다. 열 이름을 마음대로 정할 수 있지만 코드 붙여넣기 쉬우라고 선수 이름을 담은 열에 country라는 이름을 쓴 게 함정입니다.


여러분도 도둑질에 성공하시길 바라며 멋진 그림이 나왔다면 저한테도 구경시켜 주세요. 그럼 Happy Charting!



댓글,

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