Day 4: 동적, 반응형 시각화

Author

이상일(서울대학교 지리교육과)
김세창(서울대학교 지리교육과 석사)
김우형(서울대학교 지리교육과 석사과정)

Modified

October 27, 2024

오늘 사용할 패키지

  1. tidyverse

  2. gapminder

  3. DT

  4. plotly

  5. gganimate

  6. spData

  7. sf

  8. readr

  9. tmap

  10. ggiraph

  11. leaflet

1 테이블

우리는 지금까지 테이블의 중요성에 대해 거의 다루지 않았다. 그러나 상호작용형 테이블 혹은 대화형 테이블이 되었을 때, 많은 경우, 테이블은 가장 효과적인 정보 전달 도구가 된다. 특히, 데이터 변형하기를 통해 새로운 요약 테이블을 생성하고, 그것을 대화형으로 제시하는 것은 매우 중요한 데이터사이언스의 과정이다. 여기서는 DT(Data Tables) 패키지를 활용하여 간단한 대화형 테이블을 만들어 본다.

그리고 gapminder 데이터를 datatable() 함수를 통해 불러온다.

datatable(gapminder)

DT 패키지는 DataTables라고 하는 JavaScript 라이브러리(https://datatables.net/)에 기반하고 있다. 즉, DT 패키지는 DataTables의 래퍼(wrapper) 패키지이다. DT 패키지는 테이블의 상호작용성과 관련하여 몇 가지 기능을 제공한다.

  • Pagination: 페이지를 이동할 수 있는 기능

  • Instant search: 즉각적인 찾기 기능(Search에 타이핑하기 시작하면 즉각적으로 검색 결과 보여줌)

  • Multi-column ordering: 다중 컬럼 정렬 기능(컬럼 하나를 선택한 후 ctrl을 누른 상태에서 다른 컬럼을 선택)

  • Filtering: 값을 정렬할 수 있는 기능

  • Editable: 셀 값을 수정할 수 있는 기능

  • Buttons: 셀 숨기기, CSV, PDF, XLSX 등의 확장자로 내보내기 등을 수행하는 버튼 생성 기능

그 중 몇 가지 기능을 여기에서 살펴본다. 다음의 웹사이트를 참고할 수 있다.

https://rstudio.github.io/DT/

1.1 테이블 CSS 클래스

datatable 함수의 class 아규먼트를 통해 테이블의 외관을 바꿀 수 있다. 다음과 같은 옵션이 가능하다.

Class name Description
display stripe, hover, row-border, order-column을 동시 적용한 디폴트
cell-border 모든 셀의 상하좌우에 경계선 표시
compact 여백 축소
hover 마우스의 위치에 따라 점멸 효과
nowrap 줄바꿈 없이 텍스트 표시
order-column 정렬의 키가 되는 컬럼에 하이라이트 표시
row-border 행별 경계선 표시
stripe 행을 스트라이프로 표시

아래와 같이 cell-border과 compact를 함께 실행해 보고 테이블의 변화를 확인한다.

datatable(head(gapminder), class = "cell-border compact")

또한 특정 컬럼의 정렬 방식(왼편, 오른편, 중앙)을 변경할 수 있다. 사용가능한 옵션은 다음과 같다.

Class name Description
dt[-head|-body]-left 왼편 정렬
dt[-head|-body]-center 가운데 정렬
dt[-head|-body]-right 오른편 정렬
dt[-head|-body]-justify 양쪽 맞춤
dt[-head|-body]-nowrap 줄바꿈 없는 맞춤

아래는 첫 번째와 두 번째 컬럼(country, continent)의 내용(body)을 가운데 정렬로 나타낸다.

datatable(head(gapminder),
          options = list(
            columnDefs = list(list(className = "dt-body-center", targets = 1:2))
          ))

1.2 테이블 에디팅

editable 아규먼트를 통해 테이블의 값을 수정할 수 있게 만들 수 있다. 테이블의 특정 셀에 더블클릭하면 수정할 수 있다.

datatable(head(gapminder), editable = "cell")

1.3 컬럼 필터

다음과 같은 방식으로 필터를 설정할 수 있다.

datatable(gapminder, filter = "top", 
          options = list(
            pageLength = 5, 
            autoWidth = TRUE
          ))

1.4 버튼 기능

extenstion에 Buttons, dom에 Bftip, buttonsc("copy", "excel", "pdf", "print")를 입력하면 버튼 기능을 활성화할 수 있다. 각 아규먼트가 무엇을 의미하는지는 다음의 사이트(https://datatables.net/extensions/buttons/)를 참고할 수 있다.

datatable(gapminder, filter = "top",
          extensions = "Buttons",
          options = list(
            pageLength = 5,
            autoWidth = TRUE,
            dom = "Bfrtip",
            buttons = c("copy", "excel", "pdf", "print")
          ))

2 그래프

2.1 반응형 그래프

반응형 시각화 도구라 최근 널리 각광을 받고 있는 것이 Plotly이다. Plotly는 사실 캐나다 퀘백에 본사를 두고 있는 데이터 시각화 전문 회사 이름이다. 그러나 보통 데이터 시각화용 JavaScript 라이버러리를 일컽는다. 이 라이브러리는 다양한 오픈소스 프로그래밍 언어에서 사용가능하며, R의 래퍼 프로그램이 plotly 패키지이다(https://plotly.com/r/).

gapminder 데이터를 이용하여 간단한 그래프를 그려보자. 문법이 ggplot2와 크게 다르지 않음을 알 수 있다.

gapminder |> 
  filter(year == 2007) |> 
  plot_ly(x = ~gdpPercap, y = ~lifeExp, color = ~continent,
          text = ~paste("Country: ", country, 
                        "<br>GDP per capita: ", gdpPercap, 
                        "$<br>Life Expectancy at Birth:", lifeExp))

줌(zoom), 팬(pan), 박스 선택(box select), 라소 선택(Lasso select), 줌인(zome in), 줌 아웃(zoom out) 등과 같은 상호작용 기능을 확인할 수 있다. 또 그래프 상의 데이터 포인트 위에 마우스를 올리면 text 아규먼트를 통해 설정한 내용을 볼 수 있다. 그리고 범례를 클릭하면 특정 continent의 국가를 나타나지 않게 할 수 있다.

이와 같이 plotly 패키지를 직접 사용하면 다양한 기능을 활용할 수 있겠지만, plotly 패키지가 제공하는 ggplotly() 함수를 활용하면 ggplot2로 만들어진 그래프를 단숨에 plotly 그래프로 바꿀 수 있다. 물론 정확히 같지는 않다.

P <- gapminder |> 
  filter(year == 2007) |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, color = continent)) +
  geom_point() + 
  scale_color_brewer(palette = "Set2") +
  theme_minimal()
ggplotly(P)

다음 예제 역시 반응형이지만 바로 다음에서 다룰 동적인 특성도 동시에 가지고 있는 그래프를 만드는 것이다. 역시 plotly 패키지를 이용한다.

gapminder |> 
  plot_ly(x = ~log10(gdpPercap), y = ~lifeExp,
          text = ~paste("Country: ", country)) |> 
  add_markers(color = ~continent, size = ~pop, frame = ~year, 
              marker = list(sizeref = 0.2, sizemode = "area"))

하단에 있는 Play 버튼을 누르면 연도에 따라 그래프가 바뀌면서 동적인 효과가 나타나게 된다.

2.2 동적 그래프

이 실습에서는 gganimate 패키지(https://gganimate.com/)를 활용하여, 동적 그래프(animated graphs)를 만드는 방법을 익히도록 한다. gganimate 패키지를 설치한 후 불러온다.

우선 정적인 그래프를 그린다.

P <- gapminder |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
  geom_point(show.legend = FALSE, alpha = 0.7) +
  scale_x_log10() +
  scale_size(range = c(2, 12))
P

이 그래프는 두 변수 간에 양적인 관련성이 있다는 사실은 명백히 보여주지만, 데이터 변형의 측면에서는 잘못된 것이다. 모든 연도(1952~2007년간 5년 단위)가 나타나 있어서 한 국가가 그래프에 12번 등장한다.

이를 해결하기 위해 ggplot2 패키지의 facet_wrap() 함수를 활용한다.

P + facet_wrap(~year)

이 그래프는 두 변수간의 양적인 상관관계가 12개 모두의 연도에서 나타난다는 사실을 명확히 보여준다. 그러나 그래프를 세세히 살펴보면 알 수 있듯이, 두 변수의 관련성이라는 측면에서 개별 국가가 시간의 흐름에 따라 어떻게 변화해 나가는지에 대한 사항을 파악하기는 매우 어렵다.

gganimate 패키지의 transition_time() 함수를 활용하여 동적인 그래프를 작성해 본다.

P + transition_time(year) +
  labs(title = "Year: {frame_time}")

대륙별로 분할하여 표현할 수도 있다.

P + facet_wrap(~continent) +
  transition_time(year) +
  labs(title = "Year: {frame_time}")

움직임을 조금 더 역동적이게 만들어 볼 수 있다.

P + transition_time(year) +
  labs(title = "Year: {frame_time}") +
  shadow_wake(wake_length = 0.1, alpha = FALSE)

그래프를 저장하고 싶으면 anim_save() 함수를 활용할 수 있다. ggsave() 함수와 동일한 문법을 갖는다.

3 지도

3.1 정적 지도

3.1.1 세계 지도

ggplot2 패키지를 이용하여 정적 지도를 그려본다. 데이터는 지난 실습에서 사용한 WPP 2024(World Population Prospects 2024)이다. 2024년 전세계 국가별 TFR(Total Fertility Rate, 합계출산율) 지도를 그려본다.

일반적으로 지도는 형상 데이터와 속성 데이터를 결합해야만 제작할 수 있다. 여기서 형상 데이터는 전세계 국가 경계 데이터이고, 속성 데이터는 TFR이 포함된 WPP 2022 데이터이다. 형상 데이터는 spData 패키지에 들어 있는 world를 사용한다. 이러한 형상 데이터를 다루는데 있어 거의 표준처럼 사용되고 있는 것이 sf 패키지(https://r-spatial.github.io/sf/)이다. 벡터 형식의 데이터는 sf 패키지의 st_as_sf() 함수를 통해 sf 객체로 변환하는 것이 좋다.

WPP 2022 데이터를 불러와 2024년만 골라낸다.

wpp_2024 <- read_rds("wpp_2024.rds")
my_wpp <- wpp_2024 |> 
  filter(year == 2024)

두 데이터를 left_join 함수를 이용하여 결합한다.

world_data <- world |>
  left_join(my_wpp, join_by(iso_a2 == ISO2))

로빈슨 도법(Robinson projection)의 지도를 제작한다. ggplot2 패키지로 지도를 그리는 가장 좋은 방법은 geom_sf()coord_sf()를 결합하는 것이다. scale_x_continuous()scale_y_continuous()의 내용은 그래티큘(graticule, 경위선망)을 원하는 방식대로 지도에 포함시키기 위한 것이다. 그래프를 world_map으로 저장하는 것은 뒤에서 이 그래프를 사용하기 때문이다.

world_map <- ggplot() +
  geom_sf(data = world_data, aes(fill = TFR, text = name_long)) +
  coord_sf(crs = "+proj=robin") +
  scale_fill_viridis_c() +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.5, seq(-60, 60, 30), 89.5)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )
world_map

3.1.2 우리나라 지도

우리나라 지도도 그려본다. ’Lab07: 데이터 수집하기’에서 KOSIS의 API를 통해 수집, 정리한 시군구 단위 지역소멸위험지수를 지도화한다. 우선 우리나라 시군구 행정 경계에 대한 도형(형상, 기하) 데이터가 필요하다.

sido_shp <- st_read("sido.shp", options = "ENCODING=CP949")
sigungu_shp <- st_read("sigungu.shp", options = "ENCODING=CP949")

시군구 경계 파일을 그려본다. 지도 제작 전문 패키지인 tmapqtm() 함수를 이용하여 시군구 경계에 대한 지도를 빠르게 그려본다. 좀 더 복잡한 tmap의 문법을 사용한 지도 제작은 맨 뒤에서 다루기로 한다.

library(tmap)
qtm(sigungu_shp)

지역소멸위험지수 데이터를 불러온다. 아래 코드는 지난번 실습 때 rds 파일 포맷으로 저장해 둔 것을 가정한 것이다. 실습의 편의를 위해 파일을 제공하니 프로젝트 폴더에 저장한 후, 아래의 코드를 통해 불러온다.

data_sigungu <- read_rds("data_sigungu.rds")

도형 데이터(korea_sgg)와 속성 데이터(data_sigungu)를 공통 키(key)를 활용하여 결합한다.

sigungu_data <- sigungu_shp |> 
  left_join(
    data_sigungu, join_by(SGG1_CD == C1)
  )

이제 ggplot2 패키지를 이용하여 지도를 제작한다. ’Day2: 데이터의 수집과 변형’에서 인구소멸위험지수의 시도별 그래프를 제작한 것과 비교해 보라. 그 유사함에 깜짝 놀랄 수도 있다. ggplot2에서는 그래프와 지도의 구분이 없다. 이것은 ggplot2의 장점이자 단점이다.

sigungu_data <- sigungu_data |> 
  mutate(
    index_class = case_when(
      index < 0.2 ~ "1",
      index >= 0.2 & index < 0.5 ~ "2",
      index >= 0.5 & index < 1.0 ~ "3",
      index >= 1.0 & index < 1.5 ~ "4",
      index >= 1.5 ~ "5"
    ),
    index_class = fct(index_class, levels = as.character(1:5))
  )

class_color <- c("1" = "#d7191c", "2" = "#fdae61",
                 "3" = "#ffffbf", "4" = "#a6d96a", 
                 "5" = "#1a9641")
ggplot() +
  geom_sf(data = sigungu_data, aes(fill = index_class), show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(name = "Classes", 
                    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", 
                               "1.0 ~ 1.5", ">= 1.5"), 
                    values = class_color, drop = FALSE) 

3.2 반응형 지도

위에서 사용한 plotly 패키지의 ggplotly() 함수를 활용하면 반응형 지도를 생성할 수 있다. 앞의 코드 둘째 줄에 aes()text = name_long이 설정되어 있는데, 마우스로 국가를 가리킬 때 이름이 나타날 수 있게 조치한 것이다.

ggplotly(world_map)

지도 위에서 plotly 가 제공하는 다양한 기능을 적용해 볼 필요가 있다. 반응형 그래프에 비해 반응형 지도의 유용성이 더 높아 보인다.

우리나라 지도는 다른 방식으로 반응형으로 만들어 본다. 여기서는 ggiraph 패키지를 사용한다. 코드의 전반부는 커서를 특정 시군구 위에 올렸을 때 나타나는 정보를 좀 더 다양하게 하려는 조치이다. 중간의 코드가 핵심인데, 찬찬히 살펴보면 그렇게 복잡하지 않다. 마지막은 완전히 지엽적인 것인데, 커서를 특정 시군구 위에 올렸을 때 색이 회색으로 변하게 하기 위한 것이다.

library(ggiraph)
sigungu_data <- sigungu_data |> 
  mutate(
    index = format(index, digits = 4, nsmall = 4),
    my_tooltip = str_c("Name: ", SGG1_FNM, "\n Index: ", index)
  )
gg <- ggplot() +
  geom_sf_interactive(
    data = sigungu_data, 
    aes(
      fill = index_class, 
      tooltip = my_tooltip, 
      data_id = SGG1_FNM
      ), 
    show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(
    name = "Classes", 
    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", "1.0 ~ 1.5", ">= 1.5"), 
    values = class_color, drop = FALSE) 
girafe(ggobj = gg) |> 
  girafe_options(
    opts_hover(css = "fill: gray")
  )

그러나 반응형 지도 제작에 가장 널리 쓰이는 것은 leaflet이다. leaflet은 웹 상의 반응형 지도 제작에 특화된 JavaScript 라이브러리이다(https://leafletjs.com/). 이 라이브러리를 R에서 쓸 수 있게 도와주는 래퍼 패키지가 leaflet 패키지이다(https://rstudio.github.io/leaflet/).

매우 단순한 반응형 지도를 만들어 본다.

leaflet() |> 
  addTiles() |> 
  addPopups(126.955184, 37.460422, "Sang-Il's Office",
            options = popupOptions(closeButton = FALSE))

위에서 작성했던 TFR 세계지도를 leaflet 패키지의 다양한 함수와 아규먼트를 활용하여 반응형 지도를 제작해 본다.

world_data <- world_data |> 
  filter(
    !is.na(TFR)
  )

bins <- c(0, 1.5, 2.1, 3, 4, 5, Inf)
pal <- colorBin("YlOrRd", domain = world_data$TFR, bins = bins)
labels <- sprintf("<strong>%s</strong><br/>%g",
  world_data$name_long, world_data$TFR) |> lapply(htmltools::HTML)

leaflet(world_data) |> 
  addProviderTiles(providers$Esri.WorldTopoMap) |> 
  addPolygons(
    fillColor = ~pal(TFR),
    weight =  2, 
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.6,
    highlightOptions = highlightOptions(
      weight = 5,
      color = "#666",
      dashArray = "",
      fillOpacity = 0.6,
      bringToFront = TRUE),
    label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto")
  ) |> 
  addLegend(
    pal = pal, values = ~TFR, opacity = 0.6, title = NULL,
    position = "bottomright"
  )

우리나라 시군구 단위의 인구소멸위험지수에 대한 지도를 반응형으로 만들어 본다. 여기서는 tmap을 활용한다. 해당 시군구 위에 클릭하면 지역소멸위험지수가 나타난다.

class_color <- c("1" = "#d7191c", "2" = "#fdae61",
                 "3" = "#ffffbf", "4" = "#a6d96a", 
                 "5" = "#1a9641")
sigungu_data <- sigungu_data |> 
  mutate(
    index = as.numeric(index)
  )
tmap_mode(mode = "view")
my_tmap <- tm_shape(sigungu_data) + 
  tm_polygons(
    col = "index",
    palette = class_color, 
    breaks = c(0, 0.2, 0.5, 1.0, 1.5, Inf), 
    labels = c("< 0.2", "0.2~0.5", "0.5~1.0", "1.0~1.5", ">= 1.5"),
    title = "Classes", 
    popup.vars=c("지역소멸위험지수: " = "index"), 
    popup.format = list(index = list(digits = 3)), 
    id = "SGG1_FNM", 
    alpha = 0.6, 
    border.alpha = 0.5
  ) +
  tm_shape(sido_shp) + tm_borders(lwd = 2)
my_tmap
tmap_save(my_tmap, "지방소멸위험지수.html")