Day 4
고급 시각화와
대시보드의 설계

이상일(서울대학교 지리교육과 교수)

2024-08-08

고차원적 탐색과 커뮤니케이션을 위한 시각화

https://r4ds.hadley.nz/intro

내용

  • 동적ㆍ반응형 시각화

    • 역동성과 상호작용성이 부가된 시각화
    • 방식
      • 임베딩(embedding)
      • 동적ㆍ반응형 테이블과 그래프 제작
  • 지리공간적 시각화

    • “지도는 텍스트, 테이블, 챠트 보다 훨씬 더 효과적으로 정보를 전달할 수 있다.”
    • 공간사이언스(spatial science)의 지식 필요
    • 정적, 동적ㆍ반응형

자바스크립트 라이브러리: 정의

  • JavaScript Library

  • 자바스크립트 프로그래밍 언어를 사용하여 웹 개발을 더 쉽고 효율적으로 할 수 있도록 도와주는 재사용 가능한 코드 모음

    • 동적ㆍ반응형 웹 페이지 제작을 위한 다양한 기능 제공

    • HTML의 script 태그를 통해 웹사이트에 임베딩 가능

  • R의 래퍼 패키지를 통해 손쉽게 사용 가능

자바스크립트 라이브러리: 종류

https://data-flair.training/blogs/javascript-libraries/

자바스크립트 라이브러리: 데이터 시각화

https://www.geeksforgeeks.org/javascript-libraries-for-data-visualization/

동적ㆍ반응형 시각화

임베딩 Embedding

  • 동적ㆍ반응형 시각화가 구현되어 있는 웹사이트를 내재화

  • HTML의 iframe 태그 활용

임베딩 Embedding: 사례 1

<iframe src="https://kosis.kr/edu/share.do?shareID=S0500_16" 
loading="lazy" style="width: 100%; height: 600px; border: 
0px none;" allow="web-share; clipboard-write"></iframe>

임베딩 Embedding: 사례 2

<iframe src="https://ourworldindata.org/grapher/child-mortality?time=earliest..latest&tab=chart" 
loading="lazy" style="width: 100%; height: 600px; border: 0px none;" allow="web-share; clipboard-write"></iframe>

테이블 Tables

  • 테이블 역시 시각화의 일부

    • 데이터 변형 및 요약을 거친 테이블

    • 시각성이 가미된 테이블

    • 인트렉티브 테이블

테이블 Tables

https://r-graph-gallery.com/table.html

테이블 Tables: gt 패키지

https://gt.rstudio.com/

테이블 Tables: gt 패키지

https://gt.rstudio.com/

테이블 Tables: gt 패키지

https://towardsdatascience.com/exploring-the-gt-grammar-of-tables-package-in-r-7fff9d0b40cd

테이블 Tables: DT 패키지

  • DataTables: 자바스크립트 라이브러리, 인트렉티브 테이블 객체 생성

    • DT: R 래퍼 패키지

https://datatables.net/

테이블 Tables: DT 패키지

  • Pagination: 페이지 이동 기능

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

  • Ordering/sorging: 컬럼 정렬 기능

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

  • Filtering: 값 추림 기능

  • Editable: 셀 값 수정 기능

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

테이블 Tables

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

그래프 Graphs

자바스크립트 라이브러리 R 래퍼 패키지
Plotly plotly
D3 r2d3
Highcharts highcharter
ECharts echarts4r
dygraphs dygraphs
Google Charts googleVis
Chart.js chartjs

그래프 Graphs: plotly 패키지

  • Plotly: 자바스크립트 라이브러리, 동적ㆍ반응형 그래프 생성

  • R 래퍼 패키지: plotly package

    • ggplotly() 함수

https://plotly.com/

그래프 Graphs: plotly 패키지

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

그래프 Graphs: plotly 패키지

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

그래프 Graphs: ggplotly() 함수

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

그래프 Graphs: gganimate 패키지

library(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 + transition_time(year) +
  labs(title = "Year: {frame_time}")

지리공간적 시각화

지리공간적 데이터의 종류

  • 벡터(vector) 데이터

    • 포인트, 라인, 폴리곤

    • 형상 데이터 + 속성 데이터

  • 래스터(raster) 데이터

    • 그리드 셀(grid cell)

    • 일체형

지리공간적 데이터의 종류

지리공간적 데이터의 종류

벡터 데이터

  • 벡터 데이터: 형상 데이터 + 속성 데이터

    • 형상 데이터 (기하, 도형, 공간 데이터)

      • 행정구역과 같은 지리공간적 객체 자체에 대한 데이터

      • 포인트(점), 라인(선), 폴리곤(역)로 구분

      • 버텍스(vertex)의 좌표값

    • 속성 데이터

      • 지리공간적 객체가 보유한 속성

      • 기존 일반 데이터와 동일

  • 조인: left_join() 함수

    • 왼편: 형상 데이터

    • 오른편: 속성 데이터

벡터 데이터

  • 셰이프 파일(shape file): 가장 널리 사용되는 형상 데이터

    • sigungu.shp: 버텍스의 좌표값이 포함된 핵심 파일

    • sigungu.shx: 공간적 인덱싱 파일

    • sigungu.dbf: 기본 속성 파일

    • sigungu.prj: 투영 정보 파일

  • 특수한 패키지 필요: sf 패키지

    • st_read() 함수

sf 패키지

https://allisonhorst.com/r-packages-functions

sf 패키지: 주요 함수

  • 읽고 쓰기: st_read(), st_write()

  • 투영 관련: st_crs(), st_transform()

  • 기하 측정: st_area(), st_length(), st_perimeter(), st_distance()

  • 기하 변형: st_centroid(), st_buffer(), st_boundary(), st_simplify()

  • 기하 생성: st_point(), st_voronoi() , st_convex_hull(), st_make_grid()

  • 기하 검토: st_is_valid(), st_make_valid()

  • 기하 중첩: st_intersection(), st_union(), st_crop()

  • 기타: st_coordinates(), st_cast(), st_as_sf(), st_graticule(), st_join()

sf 패키지: st_read()

library(sf)
library(tmap)
sigungu_shp <- st_read("sigungu.shp", options = "ENCODING=CP949")
qtm(sigungu_shp)

CRS

https://datacarpentry.org/organization-geospatial/03-crs.html

CRS: 정의

  • 좌표참조계 Coordinate Reference System

  • 모든 지리공간데이터는 특정한 좌표참조계에 의거해 제작되며 이러한 좌표참조계는 매우 다양함

    • 준거타원체

    • 투영법(map projection)

    • 투영 파라미터: 투영축, 투영격, 중앙경선, 가상원점 등

  • 지리공간데이터의 SRID(Spatial Reference System Identifiers, 공간참조계식별자)

  • sf 패키지: st_crs() 함수

CRS: 방식

  • PROJ 정형문자열

    • https://proj.org/en/9.4/
    • 준거타원체, 투영법, 투영 파라미터를 + 기호로 연결해 작성한 문자열
    • UTM-K
      • +proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m
  • EPSG 숫자코드

    • https://epsg.io/
    • 모든 CRS에 1024~32767 사이의 고유 숫자를 부여
    • UTM-K
      • EPSG: 5179

CRS: PROJ 정형문자열

  • 세계지도를 위한 주요 투영법의 PROJ 별명(alias)
투영법 PROJ 파라미터
정적원통 도법 Equal Area Cylindrical +proj=cea
컴펙트 밀러 도법 Compact Miller +proj=comill
에케르트 IV 도법 Eckert IV +proj=eck4
정거원통 도법 Equidistant Cylindrical +proj=eqc
구드 도법 Goode Homolosine +proj=goode
단열형 구드 도법 Interrupted Goode Homolosine +proj=igh
메르카토르 도법 Mercator +proj=merc
몰바이데 도법 Mollweide +proj=moll
로빈슨 도법 Robinson +proj=robin
시뉴소이드 도법 Sinusoidal +proj=sinu
빈켈트리펠 도법 Winkel Tripel +proj=wintri

CRS: EPSG 숫자코드

  • 널리 사용되는 CRS의 EPSG
적용 스케일 EPSG 숫자코드 설명
전세계 EPSG:4326 WGS84, 측지좌표계, GPS에 사용
EPSG:3857 웹 메르카토르 도법, 구글 맵스, 오픈스트리트맵에서 사용
EPSG:7789 ITRF2014
미국 EPSG:2163 알베르스 정적원추 도법
유럽 EPSG:3035 람베르트 정적방위 도법
우리나라 EPSG:5179 UTM-K
EPSG:5185 서부원점
EPSG:5186 중부원점
EPSG:5187 동부원점
EPSG:5188 동해원점

CRS: 세계지도에 적용

ggplot() +
  geom_sf(data = world) +
  geom_sf(data = ne_bbox, fill = NA) +
  coord_sf(crs = "+proj=eqc") +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.9, seq(-60, 60, 30), 89.9)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )

ggplot() +
  geom_sf(data = world) +
  geom_sf(data = ne_bbox, fill = NA) +
  coord_sf(crs = "+proj=comill") +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.9, seq(-60, 60, 30), 89.9)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )

ggplot() +
  geom_sf(data = world) +
  geom_sf(data = ne_bbox, fill = NA) +
  coord_sf(crs = "+proj=robin") +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.9, seq(-60, 60, 30), 89.9)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )

ggplot() +
  geom_sf(data = world) +
  geom_sf(data = ne_bbox, fill = NA) +
  coord_sf(crs = "+proj=eck4") +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.9, seq(-60, 60, 30), 89.9)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )

지리공간적 시각화: 코로플레스맵

지리공간적 시각화: 두 가지 관점

  • “지도도 그래프다” 관점: 일반성

    • ggplot2 패키지
      • geom_map(), coord_map()
      • geom_sf(), coord_sf()
  • “지도는 지도이다” 관점: 특수성

    • tmap 패키지
      • 3.99.9001

ggplot2 vs tmap: 세계지도

library(tidyverse)
library(spData)
library(sf)
data(world)
world <- st_as_sf(world)
wpp_2022 <- read_rds("wpp_2022.rds")
my_wpp <- wpp_2022 |> 
  filter(year == 2024)
world_data <- world |>
  left_join(my_wpp, join_by(iso_a2 == ISO2))
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

tm_world_map <- tm_shape(world_data, projection = "+proj=robin") +
  tm_graticules(labels.show = FALSE) + 
  tm_polygons(
    col = "TFR", style = "cont", 
    palette = "viridis", id = "name_long"
  ) +
  tm_layout(frame = FALSE)
tm_world_map

ggplot2 vs tmap: 우리나라 지도

library(tidyverse)
library(sf)
sido_shp <- st_read("sido.shp", options = "ENCODING=CP949")
sigungu_shp <- st_read("sigungu.shp", options = "ENCODING=CP949")
data_sigungu <- read_rds("data_sigungu.rds")
sigungu_data <- sigungu_shp |> 
  left_join(
    data_sigungu, join_by(SGG1_CD == C1)
  )
library(ggspatial)
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_map <- ggplot() +
  geom_sf(
    data = sigungu_data, 
    aes(fill = index_class, text = 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
  ) +
  annotation_scale(
    location = "br", 
    bar_cols = c("gray40", "white"), 
    width_hint = 0.4
  )
ggplot_map

class_color <- c("#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641")
tmap_map <- tm_shape(sigungu_data) + 
  tm_polygons(
    col = "index", style = "fixed", 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", id = "SGG1_FNM"
  ) +
  tm_shape(sido_shp) + tm_borders(lwd = 1.5) +
  tm_legend(
    legend.position = c(0.7, 0.1)
  ) +
  tm_scale_bar(breaks = seq(0, 200, 50), position = c(0.6, 0.01)) 
tmap_map

인터랙티브 지도: ggplotly() 함수

ggplotly(world_map)
ggplotly(world_map)

인터렉티브 지도: 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: 자바스크립트 라이브러리

https://leafletjs.com/

leaflet: 단순 일반도

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

leaflet: 매시업(mashup) 주제도

library(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"
  )
library(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"
  )
library(tmap)
class_color <- c("#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#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
library(tmap)
class_color <- c("#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#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