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

R로 인터랙티브 지도 그리기(feat.leaflet)


'최대한 친절하게 쓴 R로 지도에 점 찍고, 선 긋고, 색칠하기' 포스트를 쓰면서 인터랙티브 지도를 그릴 때는 R보다 태블로(tablebau) 같은 소프트웨어를 쓰는 게 더 나을 수 있다고 말씀드렸습니다.


개인적으로는 지금도 그 생각에는 큰 변함이 없습니다.


그러다가 어른들 사정으로 R를 가지고 인터랙티브 지도를 어떻게 그리는지 다른 분들께 말씀드려야 할 일이 있었습니다. 그래서 어차피 자료 및 코드를 정리해 놓은 김에 블로그에도 내용을 남겨 놓습니다.


'최대한 친절하게 쓴 R로 지도에 점 찍고, 선 긋고, 색칠하기' 포스트에서 말씀드린 것처럼 R에서 인터랙티브 지도를 그릴 수 있도록 도와주는 제일 유명한 패키지는 leaflet입니다.


일단 패키지를 (설치하고) 불러오는 것부터 시작해야겠죠? 아, 물론 R를 시작할 때 제일 먼저 해야 하는 건 tidyverse를 불러오기입니다.

#install.packages('tidyverse')
library('tidyverse')
## Registered S3 methods overwritten by 'ggplot2':
##   method         from 
##   [.quosures     rlang
##   c.quosures     rlang
##   print.quosures rlang
## -- Attaching packages ---------------------------------------------------------------- tidyverse 1.2.1 --
## √ ggplot2 3.1.1     √ purrr   0.3.2
## √ tibble  2.1.3     √ dplyr   0.8.1
## √ tidyr   0.8.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()
#install.packages('leaflet')
library('leaflet')


그다음 아래처럼 코드를 입력하면 서울 지역 지도가 뜹니다. 아래 코드는 서울이 자리잡은 동경 126.9784, 북위 37.566를 중심으로 줌(zoom)을 11로 해서 지도를 그려달라는 뜻입니다.  

leaflet() %>%
  setView(lng=126.9784, lat=37.566, zoom=11) %>%
  addTiles()


보시는 것처럼 잘 나왔습니다. 이제 점을 찍을 차례입니다.


이번 포스트에서는 '별 다방' 각 지점 위치를 찍어보려고 합니다. 아래 있는 파일을 내려 받으신 다음에 불러오시면 됩니다.


starbucks.csv


원래 tidyverse에는 CSV 파일을 읽을 때 쓰라고 read_csv() 함수가 들어 있습니다. 그런데 이 함수를 쓰면 한글 인코딩 문제가 생기는 일이 많습니다. 그래서 저는 보통 아래 같은 방식으로 CSV 파일을 읽습니다.

sb <- read.csv('starbucks.csv') %>% as_tibble
sb
## # A tibble: 1,362 x 7
##    address                          sido  sigungu code     lat  long SIG_CD
##    <fct>                            <fct> <fct>   <fct>  <dbl> <dbl>  <int>
##  1 "강원도 강릉시 경강로 2096 (임당동)033-645-~ 강원  강릉    강릉    37.8  129.  42150
##  2 "강원도 강릉시 경강로 2400 (송정동)033-652-~ 강원  강릉    강릉    37.8  129.  42150
##  3 "강원도 강릉시 교동광장로 114 (교동)033-642-~ 강원  강릉    강릉    37.8  129.  42150
##  4 "강원도 강릉시 창해로14번길 40 (견소동)033-65~ 강원  강릉    강릉    37.8  129.  42150
##  5 "강원도 고성군 토성면 미시령옛길 1153033-636-~ 강원  고성    강원고성~  38.2  128.  42820
##  6 "강원도 동해시 중앙로 219 (천곡동)033-532-3~ 강원  동해    동해    37.5  129.  42170
##  7 "강원도 속초시 동해대로 4114 (조양동)033-633~ 강원  속초    속초    38.2  129.  42210
##  8 "강원도 속초시 미시령로2983번길 111 (장사동)03~ 강원  속초    속초    38.2  129.  42210
##  9 "강원도 속초시 중앙로 123 (금호동)033-637-8~ 강원  속초    속초    38.2  129.  42210
## 10 "강원도 원주시 남원로 588 (명륜동)033-764-2~ 강원  원주    원주    37.3  128.  42130
## # ... with 1,352 more rows


leaflet에서 점을 찍을 때는 addCircles()라는 함수를 쓰시면 됩니다. 보시는 것처럼 이 자료에는 각 지점 경도(long) 위도(lat)가 들어 있기 때문에 이 자료를 가져가다 점을 찍으라고 명령을 내리면 됩니다.


아, leaflet으로 지도를 그릴 때는 addProviderTiles() 함수로 지도 스타일을 바꿀 수 있습니다. 어떤 스타일을 쓸 수 있는지는 이 링크에서 확인하시면 됩니다.


이번에 우리 'CartoDB.Positron'을 쓰겠습니다. 점 색깔도 별 다방 컬러로 바꾸겠습니다.

leaflet(sb) %>%
  setView(lng=126.9784, lat=37.566, zoom=11) %>%
  addProviderTiles('CartoDB.Positron') %>%
  addCircles(lng=~long, lat=~lat, color='#006633')


그러면 점 색깔을 특정 기준에 따라 달리하고 싶을 때는 어떻게 하면 될까요? ggplot 문법에서는 ase() 안에 변수를 지정하면 곧바로 색을 바꿀 수 있습니다. 애석하게도 leaflet은 그렇지 않습니다. 아래처럼 color*() 함수로 따로 팔레트를 만들어야 합니다.


우리는 시군구마다 다른 색을 찍을 거니까 colorFactor() 함수를 씁니다. 만약 숫자 크기에 따라 색을 달리하고 싶을 때는 colorNumeric()을 쓰시면 됩니다.

pal <- colorFactor("viridis", sb$code)


이렇게 팔레트를 만들도 나면 pal() 안에 원하는 변수 이름을 쓰면 각 값에 맞는 색으로 점을 찍게 됩니다. 아래처럼 말입니다.

leaflet(sb) %>%
  setView(lng=126.9784, lat=37.566, zoom=11) %>%
  addProviderTiles('CartoDB.Positron') %>%
  addCircles(lng=~long, lat=~lat, color=~pal(code))


지도에는 보통 점보다 마커라고 부르는 표시를 남기는 일이 더 흔합니다. leaflet에서는 addMarkers() 함수를 쓰면 마커를 표시할 수 있습니다.


아래 코드는 동경 127.7669, 북위 35.90776를 중심으로 줌 6을 준 지도를 그리고 그 위에 마커를 표시하면서 address 열에 있는 데이터를 레이블로 삼으라는 뜻입니다. 

leaflet(sb) %>%
  setView(lng=127.7669, lat=35.90776, zoom=6) %>%
  addProviderTiles('CartoDB.Positron') %>%
  addMarkers(lng=~long, lat=~lat, label=~address)


마커 위에 마우스를 올려보시면 각 지점 주소+전화번호가 뜨는 걸 확인할 수 있습니다.


이어서 선을 그어 보겠습니다.


'최대한 친절하게 쓴 R로 지도에 점 찍고, 선 긋고, 색칠하기'에서 설명드린 것과 마찬가지로 선을 그으려면 점과 점을 그룹으로 묶어주는 과정이 필요합니다.


여기서는 가장 서쪽에 있는(=lat가 제일 작은) 별 다방 지점과 가장 동쪽에 있는 지점을 선으로 연결해 보겠습니다.


dplyr 패키지에 있는 filter() 함수를 써서 먼저 두 지점만 선택합니다. 이어서 두 지점을 1이라는 group으로 묶습니다.


이렇게 정리를 하고 나면 addPolylines() 함수로 선을 그리라고 명령하면 그만입니다.

sb %>%
  filter(lat==min(lat) | lat==max(lat)) %>%
  mutate(group=1) %>%
  leaflet() %>%
  setView(lng=127.7669,lat=35.90776, zoom=6) %>%
  addProviderTiles('CartoDB.Positron') %>%
  addCircles(lng=~long, lat=~lat) %>%
  addPolylines(lng=~long, lat=~lat, group=~group, weight=.5)


이제 마지막으로 색을 칠할 차례.


역시 '최대한 친절하게 쓴 R로 지도에 점 찍고, 선 긋고, 색칠하기'에서 말씀드렸던 것처럼 색을 칠하려면 셰이프파일이라는 '미농지'가 필요합니다. 우리가 쓸 미농지는 아래 첨부 파일에 들어 있습니다.


SIG_201703.zip

SIG_201703.z01


※티스토리에 한 번에 올릴 수 있는 파일 크기가 10메가바이트라 둘로 나눴을 뿐 다른 이유는 없습니다.


압축을 풀고 나면 파일 네 개가 들어 있습니다. 그 가운데 우리에게 필요한 건 'TL_SCCO_SIG.shp'입니다.


이 파일을 R 안으로 불러들이려면 raster 패키지가 필요합니다.

#install.packages('raster')
library('raster')
## Loading required package: sp
## 
## Attaching package: 'raster'
## The following object is masked from 'package:dplyr':
## 
##     select
## The following object is masked from 'package:tidyr':
## 
##     extract


셰이프파일은 shapefile()이라는 함수로 불러옵니다. 그냥 아래처럼 쓰시면 됩니다. 

korea <- shapefile('TL_SCCO_SIG.shp')


셰이프파일은 기본적으로 '덩치'가 있습니다. 인터랙티브 지도를 그릴 때 이 덩치 그대로 데이터를 불러오면 속도에 영향을 주는 게 당연한 일.


그래서 셰이프파일을 날 것 그대로 쓰지 않고 무게를 줄여주는 작업을 진행하는 일이 흔합니다.


R에서는 rmapshaper라는 패키지를 통해 이 작업을 진행할 수 있습니다.

#install.packages('rmapshaper')
library('rmapshaper')
## Registered S3 method overwritten by 'geojsonlint':
##   method         from 
##   print.location dplyr


현재는 korea라는 변수에 셰이프파일이 들어 있는데 ms_simplify() 함수로 이 파일 덩치를 줄여서 korea2 변수에 넣겠습니다.  

korea2 <- ms_simplify(korea)


셰이프파일 안에는 행정구역 이름과 코드 등을 담은 데이터 프레임이 들어 있습니다. 변수 이름 다음에 '@data'라고 입력하면 그 내용을 확인할 수 있습니다.

korea2@data %>% head
##   SIG_CD    SIG_ENG_NM                                       SIG_KOR_NM
## 1  11110     Jongno-gu               <U+FFFD><U+FFFD><U+FFFD>α<U+FFFD>
## 2  11140       Jung-gu                         <U+FFFD><U+07F1><U+FFFD>
## 3  11170    Yongsan-gu                               <U+FFFD><U+FFFD>걸
## 4  11200  Seongdong-gu <U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD>
## 5  11215   Gwangjin-gu <U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD>
## 6  11230 Dongdaemun-gu       <U+FFFD><U+FFFD><U+FFFD>빮<U+FFFD><U+FFFD>


계속 별 다방 자료를 가지고 놀고 있으니 각 시군구별로 이 다방이 몇 개 있는지 합친 다음 그 숫자로 지도에 색을 칠하도록 하겠습니다.


이미 별 다방 자료에도 이 셰이프파일과 마찬가지로 시군구 코드가 들어 있습니다. 이 코드별로 묶고 개수를 세어 보겠습니다. 그리고 그 결과를 sb2라는 변수에 넣겠습니다.   

sb %>% group_by(SIG_CD) %>%
  summarise(count=n()) -> sb2
sb2
## # A tibble: 166 x 2
##    SIG_CD count
##     <int> <int>
##  1  11110    36
##  2  11140    52
##  3  11170    18
##  4  11200    10
##  5  11215    15
##  6  11230     8
##  7  11260     6
##  8  11290    13
##  9  11305     5
## 10  11320     2
## # ... with 156 more rows


잘 들어왔습니다. 이제 이 결과를 셰이프파일과 합쳐야 합니다. 이때는 sp 패키지에 있는 merge() 함수를 쓰는 게 제일 간단합니다. 이렇게 병합을 거쳐서 korea3 변수를 만들겠습니다.

sp::merge(korea2, sb2) -> korea3


이번에도 잘 들어왔는지 확인해 봐야겠죠?

korea3@data %>% head
##   SIG_CD    SIG_ENG_NM                                       SIG_KOR_NM
## 1  11110     Jongno-gu               <U+FFFD><U+FFFD><U+FFFD>α<U+FFFD>
## 2  11140       Jung-gu                         <U+FFFD><U+07F1><U+FFFD>
## 3  11170    Yongsan-gu                               <U+FFFD><U+FFFD>걸
## 4  11200  Seongdong-gu <U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD>
## 5  11215   Gwangjin-gu <U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD><U+FFFD>
## 6  11230 Dongdaemun-gu       <U+FFFD><U+FFFD><U+FFFD>빮<U+FFFD><U+FFFD>
##   count
## 1    36
## 2    52
## 3    18
## 4    10
## 5    15
## 6     8


네, 잘 들어왔습니다. 이제 또 팔레트를 만들어야 합니다. 이번에는 숫자가 크고 적은 걸 구분해야 하니까 colorNumeric() 함수를 씁니다. 'reverse=TRUE'는 scale_viridis()에서 'direction=-1'을 준 것과 같은 효과입니다.

pal2 <- colorNumeric("viridis", korea3@data$count, reverse=TRUE)


이제 지도를 그리는 일만 남았습니다. 셰이프파일을 얹을 때는 addPolygons() 함수를 쓰시면 됩니다. 그리고 fillColor 속성에 위에서 만든 팔레트를 지정하면 변수값에 따라 단계를 구분해 색깔을 칠합니다.

leaflet(korea3) %>%
  setView(lng=127.7669,lat=35.90776, zoom=6) %>%
  addProviderTiles('CartoDB.Positron') %>%
  addPolygons(color='#444', weight=1, fillColor=~pal2(count))