聰明不如鈍筆
총명불여둔필
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!



댓글, 26

  • 댓글 수정/삭제 초이스
    2019.06.12 10:25

    KBO 리그 그래프 관련해 궁금한 게 있습니다. 처음에 데이터 처리할 때, 은퇴한 선수의 데이터는 어떻게 하셨는지 궁금합니다. 예를 들어 양준혁 같은 경우는 2011년부터 2019년까지 성적이 없는데, 9년간 351홈런이라는 데이터가 계속 남아있는지 의문입니다. 커리어 중간에 군대 또는 해외 진출로 KBO 리그에서 빠진 선수들도 마찬가지고요.

    올려주신 그래프 보고 만들어보려 했는데 처음부터 막히네요...

  • 댓글 수정/삭제 초이스
    2019.06.13 09:45

    제가 개념을 잘못 잡은 듯합니다. 해당 연도까지의 누적합을 불러오도록 하고, 그 연도에 성적이 없으면 안 불러오는 식으로 구문을 짰나 봐요. (말이 어렵네요)

    아무튼 올려주신 구문 열심히 베껴서 해보겠습니다!

  • 댓글 수정/삭제 궁금해요
    2019.11.21 00:39

    In HR/HR[rank == 1] :
    longer object length is not a multiple of shorter object length

    이 오류는 왜 뜰까요 ㅠㅠ

  • 댓글 수정/삭제 궁금해요
    2019.11.21 00:53

    mlbhr <- mlbi %>%
    group_by(Season) %>%
    mutate(rank = min_rank(-HR) * 1,
    Value_rel = HR/HR[rank==1],
    Value_lbl = paste0(" ",HR)) %>%
    filter(rank <=10) %>%
    ungroup()

    이렇게 했습니다

  • 댓글 수정/삭제 궁금해요
    2019.11.21 17:43

    흑 제가 너무 단서를 안드려서 죄송합니다.

    gdp 움짤 코드를 이용해서 저는 1982년부터 2019년까지 메이저리그 홈런 탑 10을 만들어 보고 싶었습니다. 맨 아래 kbo 통산 홈런 10걸처럼요 그래서 value를 홈런(HR)로 치환해서 해봤는데 In HR/HR[rank == 1] :
    longer object length is not a multiple of shorter object length
    이런식으로 오류가 뜨는데 이건 해결 방법을 모르겠네요..

    그리고 mlbhr이라고 시작하는 코드는 gap대신 저 명령어로 저장해본거고 mlbi는 csv파일을 저렇게 저장해서 그렇습니다. 그룹by는 연도가 아닌 시즌으로 저장했는데 차라리 HR을 Value라는 이름으로 바꿔서 해볼려합니다 일단 지금 당장 해봐야 하니.

    •  수정/삭제 kini
      2019.11.22 10:36 신고

      아, 내용은 짐작하고 있었는데
      데이터가 어떻게 생겼는지 알지 못해
      답변을 드리기가 어렵다는 말씀이었습니다.
      메시지 내용을 보면
      데이터 숫자가 충분하지 않아서 생기는 에러일 테니까요.

  • 댓글 수정/삭제 궁금해요
    2019.11.21 18:50

    자료 자체의 이름을 바꿔보았는데도
    In value/value[rank == 1] :
    longer object length is not a multiple of shorter object length
    이런 오류가 뜨네요 ㅠㅠ

    사진으로 보여드리고 싶은데 가능한가요? 페메로 해도 될까요?

  • 댓글 수정/삭제 열심남
    2020.03.02 23:01 신고

    저도 한번 도둑질(?) 한번 해봐야겠네요. 좋은 포스팅 감사합니다.

  • 댓글 수정/삭제 열심남
    2020.03.06 15:57 신고

    네 성공했습니다. 네이버 블로그라. ㅋㅋ


    http://m.blog.naver.com/uuincity/221837325342

  • 댓글 수정/삭제 안녕하세요
    2020.03.14 13:14

    게시글이 1년이 넘어서 그런지
    plot.margin = margin(1,1,1,4, "cm"))
    여기가 이제 오류가 뜨네요. (Error in margin(1, 1, 1, 4, "cm") : unused arguments (1, 4, "cm"))
    이게 margin을 쓰는 방법이 달라진 걸까요?

    •  수정/삭제 kini
      2020.03.19 14:36 신고

      지금 해봐도 잘 되는데
      한번 코드를 다른 데 옮겼다가
      다시 붙여 넣어보시겠어요?

  • 댓글 수정/삭제 왕초보
    2020.03.26 18:00

    코드 따라해보다가 궁금해서 문의 남깁니다! 남자 키 값만 출력하지 않고 남자여자 키를 합친 평균을 나타내고 싶을때는 어떻게 해야하는지 아시나요..?

    필터적용하는 행
    height <- height %>% filter(Sex=='Men')
    을 지우면 따로 놀더라구요,,

    ex) 그래프 만들었을때 맨 위에 스웨덴이 뜨고 밑에 또 스웨덴이 뜨는 식으로요!
    이 두개를 합친 평균값으로 나타내고 싶습니다...

    •  수정/삭제 kini
      2020.03.27 14:17 신고

      이렇게 해보시겠어요?

      height <- height %>%
      select(country, year, Sex, value) %>%
      pivot_wider(names_from='Sex', values_from='value') %>%
      mutate(value=(Men+Women)/2)


  • 댓글 수정/삭제 green
    2020.04.14 21:52

    Error in order(ind) : 인자 1는 벡터가 아닙니다

    이런 오류가 뜨는데 뭐가 문제일까요?

  • 댓글 수정/삭제 ggnn
    2020.06.02 02:08

    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()

    이걸 실행 시켰는데
    Error in gdp %>% group_by(year) %>% mutate(rank = min_rank(-value) * 1, :
    함수 "%>%"를 찾을 수 없습니다
    라고 뜨네요ㅠㅠ

  • 댓글 수정/삭제 ggnn님
    2020.06.21 10:03

    ggnn님 %>%가 안되는 건 dplyr 패키지가 없어서 그렇습니다.
    한번 설치하고 돌려보세요!

account_circle
vpn_key
web

security

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