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

최대한 친절하게 쓴 R로 지도에 점 찍고, 선 긋고, 색 칠하기


"낡은 지도로는 새로운 세상을 탐험할 수 없다. - 알베트르 아인슈타인


두말할 것 없이 사회과부도였습니다. 학창시절 해마다 새 교과서를 받으면 제일 먼저 펼쳐보던 책 말입니다. 평범한 지도 위에 점을 찍고, 선을 긋고, 색을 칠해 만든 특별한 지도는 언제나 저를 설레게 했습니다. 


어른이 되어서도 지도는 곧잘 제 마음을 뒤흔들어 놓았습니다. 그래서 이렇게 컴퓨터를 가지고 지도 위에 점을 찍고, 색을 칠하기도 했습니다. 


요즘에는 이런 작업을 지리공간정보시스템(GIS)이라는 어려운 말로 부릅니다. GIS 전용 소프트웨어도 많이 있지만 R를 가지고도 제법 훌륭한 결과를 만들 수 있습니다. 


R가 뭔지 낯선 분도 계실 겁니다. R 공식 홈페이지를 통해 "R는 통계 계산과 그래픽에 활용하는 무료 소프트웨어 환경(R is a free software environment for statistical computing and graphics)"이라고 스스로를 소개합니다.


그러니까 일단 공짜인 겁니다. 아직 만약 컴퓨터에 R를 깔지 않으셨다면 이 다운로드 페이지에서 내려받아 설치하는 것부터 시작하시면 됩니다. 컴퓨터에 다른 프로그램을 설치하시는 것하고 똑같은 과정입니다. 설치가 끝나고 R를 실행하시면 다음 같은 화면과 만나보실 수 있습니다.



화면 아래 쪽에 붉은 색으로 print("Hello, World")라고 나온 게 제가 직접 입력한 부분입니다. print라는 명령어(함수)는 영어 낱말 뜻 그대로 괄호 안에 있는 내용을 출력하라는 뜻입니다. 실제도 다음 줄에 그렇게 나왔습니다.


이런 'Hello, World 프로그램'은 컴퓨터 프로그래밍(코딩)을 처음 시작할 때 쓰는 가장 유명한 코드입니다.


이제부터 R에 직접 함수를 입력하는 방식으로 차근차근 GIS 코딩을 해볼 겁니다. 그냥 Ctrl+C/V로 제가 쓴 코드를 컴퓨터 R창에 붙여 넣으시면 100% 독같은 결과나 나옵니다.


그러니 '프로그래밍을 하나도 모르는데 어떡하지?'하고 걱정하실 필요 없이 천천히 원리만 이해하면서 가시면 됩니다.



print("hello, ggmap")

GIS에 R를 활용할 수 있게 해주는 가장 손쉬운 '패키지'는 ggmap입니다. 패키지는 R에 좀더 다양한 기능을 더해주는 확장 프로그램이라고 생각하시면 됩니다. R에 새 패키지를 설치하면 새로운 함수를 쓸 수 있게 되고 이를 활용해 새로운 작업을 할 수 있게 됩니다.


ggmap 패키지는 R에서 시각화 대표 주자로 꼽히는 ggplot2 패키지를 활용합니다. ggplot2는 이 패키지로 그린 이미지만 구글에서 한번 찾아 보셔도 차고 넘칠 만큼 정말 인기가 많고, 기능도 뛰어난 시각화 도구입니다.


단, 이 패키지를 설명한 책이 따로 있을 정도니까 처음 배울 때 시간과 노력이 필요한 '러닝 커브'를 무시할 수는 없습니다.


이번에도 크롤링을 할 때처럼 한번 하나 하나 해보겠습니다. 어떤 패키지를 쓰려면 일단 설치하고 불러오는 것부터 시작해야 합니다. 패키지를 설치하는 함수는 'install.packages()'이고 불러오는 건 'library()'입니다.

install.packages("ggplot2")
install.packages("ggmap")
library(ggmap)


ggplot2를 따로 부르지 않은 건 ggmap을 부르면 자동으로 따라오기 때문입니다. 만약 그렇지 않다면 library(ggplot2)로 불러들이시면 됩니다. 패키지를 불러왔으니 한국 지도를 한번 그려볼까요?


2018년 10월 현재 구글 지도 정책 변경으로 아래 코드를 작동하시려면 구글 API 키가 필요합니다. ggmap '404 forbidden' 에러 해결하기 포스트를 따라 하시면 이 키를 발급받는 데 도움이 될 겁니다.

ggmap(get_map(location='south korea', zoom=7))


잘 나왔습니다. 아주 쉽죠? 이 코드 한 줄은 두 부분으로 나눌 수 있습니다. 자세히 보시면 ggmap() 함수가 get_map(location='south korea', zoom=7)을 둘러 싸고 있습니다. ggmap은 이름 그대로 ggplot 기능을 활용해 지도를 그리라는 뜻입니다.


get_map은 지도 정보를 가져오는 함수입니다. 'south korea' 위치(location) 지도를 7이라는 배율(zoom)로 가져오라는 의미죠. zoom은 3부터 21까지 지정하실 수 있습니다. 숫자가 작을수록 넓은 지역이 나옵니다. 한번 배율 3과 21로 각각 지도를 그려보죠, 뭐.



지도를 가장 크게 확대한 오른쪽 지도를 보니까 덕유산에 있는 용궁사라는 절이 한 가운데 나오네요. 원래 한국 지도를 그릴 때 쓰는 중심점북위 35도57분, 동경 128도15분으로 경북 성주군 벽진면에 있습니다. 배율 차이 등으로 중심점이 약간 달라졌습니다. 


앞 단락에 등장한 북위는 위도(latitude), 동경은 경도(longtitude)를 가리킵니다. 적도보다 북쪽에 있어서 북위, 영국 그리니치 천문대를 지나는 본초 자오선보다 동쪽에서 있어서 동경입니다. ggmap 역시 이 경도 위도를 가지고 지도를 그릴 수 있습니다.

myLocation <- c(lon=128.25, lat=35.95)
ggmap(get_map(location=myLocation, zoom=7))


역시 location='south korea'라고 했을 때보다 약간 더 왼쪽으로 치우친 지도가 나옵니다. 저기는 굳이 lat, lon을 입력했는데 그냥 myLocation <- c(128.25, 35.95)라고 써도 똑같은 결과가 나옵니다. (c는 concatenate를 줄인 말로 저 둘을 합친다는 뜻입니다.) 


get_map은 이렇게 구글 (지형·terrain) 지도를 가져오는 게 기본. source 속성을 사용하면 스타멘(stamen) 디자인에서 만든 지도하고 (오픈 소스 지도 서비스) 오픈스트리트맵(osm·Open Street Map)도 가져올 수 있습니다. 또 한 업체에서도 서로 다른 디자인(maptype)을 마련하고 있습니다. 기본형을 모두 풀어 쓰면 아래처럼 나옵니다.

get_map(location='south korea', zoom=7, source='google', maptype='terrain')


여기서 source와 maptype을 조합하면 지도를 총 8가지 디자인으로 그릴 수 있습니다. 여기서 끝이 아닙니다. 이 디자인 8개 모두 color='bw'를 추가해 흑백으로 바꿔 그릴 수 있습니다. 컬러하고 흑백하고 별 차이가 나지 않는 것도 있지만 이론적으로는 디자인이 총 16개 들어 있는 셈입니다.


아, 가끔 osm이 오류일 때가 있습니다. 하필 이 글을 쓰는 지금이 그렇습니다. 문자 그대로 '오픈 소스'라서 생기는 일이니 그럴 때는 그냥 기다리는 법 말고는 방법이 없습니다.    


지도를 그리는 법을 알았으니 이제 점을 찍을 차례. 바탕으로 쓸 지도를 그려보겠습니다.

map <- get_map(location='south korea', zoom=7, maptype='roadmap', color='bw')

이건 구글에서 흑백 도로(roadmap) 지도를 받아와서 map이라는 빈 방에 넣으라는 명령어입니다. (따라 오셨죠?) 이런 빈 방을 컴퓨터 프로그래밍(코딩)에서는 변수라고 부릅니다. map에 지도 데이터가 들어 있으니까 ggmap(map)이라고 치면 R가 지도를 그립니다. 이렇게 말입니다.




R로 지도에 점 찍기

바탕으로 쓸 지도를 그렸으니까 이제 본격적으로 '지도 놀이'를 해보겠습니다. 첫 단계는 지도 위에 점을 찍는 것. 어떤 점을 찍을까 고민하다가 공공 와이파이(wifi) 위치를 표시해 보기로 했습니다.


와이파이 위치를 고른 건 문재인 대통령은 공약 때문. 문 대통령은 후보 시절 가계통신비 절감 차원에서 공공 와이파이 확대를 공약했고, kt를 마지막으로 통신 3사 모두 이 공약에 따르기로 결정했습니다.


구글링을 해보니 '한국통신사업자연합회(KTOA)'라는 곳에서 공공 와이파이 위치 정보를 담은 마이크로소프트(MS) 엑셀 파일(.xls)을 공개하고 있네요. 다행스럽게 경도 위도 정보도 들어 있습니다.


인터넷에 올라와 있는 엑셀 파일도 R에서 직접 불러 와서 원하는 형태로 가공할 수 있지만 우리는 지금 지도 놀이를 하고 있는 거니까 그냥 편하게 컴퓨터에서 파일을 불러오겠습니다. 일단 아래 파일을 내려받으시면 됩니다.


wifi.csv


만약 작업 폴더를 바꾼 적이 있다면 해당 폴더에 아니라면 (MS 윈도 기준으로) '내 문서'에 저장하셔야 R가 제대로 불러 옵니다.


이 파일 확장자는 .csv입니다. .csv(Comma Separated Values)는 엑셀 문서를 텍스트로 바꾼 형태입니다. 크롤링한 자료를 저장할 때 쓰기도 했죠. 자료를 쓸 때는 write.csv 함수를 썼으니 읽을 때는 read.csv를 쓰면 됩니다. 아래는 자료를 긁어와 wifi라는 변수에 넣으라는 뜻입니다. (아, 쉽다, 쉬워.)

wifi <- read.csv("wifi.csv", header=T, as.is=T)


header=T(rue)는 맨 첫 줄에 변수 이름이 있다는 뜻이고, as.is=T는 현재 형태 그대로 불러 오라는 뜻입니다. KTOA 홈페이지에서 내려 받은 엑셀 파일에는 변수가 더 많았지만 우리가 필요한 건 점 찍을 때 사용할 것뿐이니까 그것만 남겨 놓고 지웠습니다.


한번 파일이 어떻게 생겼는지 볼까요? 이럴 때는 head 함수를 쓰면 기본적으로 처음 여섯 줄만 확인하실 수 있습니다.

head(wifi)
  company      lat      lon
1      KT 37.74417 128.9056
2      KT 37.72806 128.9543
3      KT 37.75710 128.8900
4      KT 37.74769 128.8840
5      KT 37.74866 128.9073
6      KT 37.74281 128.8827


잘 들어왔네요. 이제 ggplot2 사용법을 알아볼 차례가 됐습니다. 일단 다음 줄을 한번 입력해 보세요. 그러면 지도 위에 점을 찍습니다.

ggmap(map) + geom_point(data=wifi, aes(x=lon, y=lat, color=company))


ggplot2는 ggplot() 함수를 바탕으로 여러 레이어(layer)를 겹치는 방식으로 작동합니다. 이번에는 원래 ggplot()이 들어가야 할 자리에 ggmap()이 대신 들어간 것뿐입니다. 


요컨대 이 명령어는 일단 맨 밑에 지도를 한 장 깔고, 그 위에 점 그래프를 그릴 때 쓰는 geom_point()로 레이어를 한 장 더 그리라는 뜻입니다.


geom_point() 같은 ggplot2 패키지 함수 안에는 속성을 넣을 수도 있습니다. data=wifi는 문자 그대로 wifi라는 변수를 데이터로 삼겠다는 뜻입니다.


뒤이어 나오는 aes는 aesthetic(미적인)에서 따온 말입니다. aes는 변수 안에 여러 자료(열)가 있을 때 어떤 자료를 어떻게 쓰면 된다고 알려주는 기능을 합니다.


우리가 쓰고 있는 wifi에는 통신사(company), 위도(lat), 경도(lon) 열이 있던 거 기억하시죠? aes 안에 있는 건 이 wifi 변수에서 자료를 가져다 점을 찍는데 '위치는 (x, y) = (경도, 위도)로 해라', '점 색깔(color)은 통신사별로 다르게 찍어라' 이렇게 컴퓨터에게 알려주는 겁니다. 이해하시겠죠?


점이 겹쳐서 어느 곳에 공공 와이파이가 많은지 알아보기 힘드니까 이번에는 stat_density_2d() 함수를 활용해 보겠습니다. 이 함수는 이름 그대로 2차원(2D) 밀도(density)를 보여주는 녀석입니다.

ggmap(map) + stat_density_2d(data=wifi, aes(x=lon, y=lat))


geom_point 때는 점이 나오더니 이번에는 날씨 예보에서 기압선을 그리는 모양처럼 나왔네요. 높이가 같은 점을 이은 등고선처럼 보이기도 하고요. 이 정도만으로도 그냥 점을 찍었을 때보다 서울과 광역시 중심으로 공공 와이파이가 많다는 게 더 잘 눈에 들어 옵니다. 그래도 좀더 가보겠습니다.


ggmap(map) + stat_density_2d(data=wifi, aes(x=lon, y=lat, fill=..level.., alpha=..level..), geom='polygon', size=2, bins=30)

뭔가 코드가 복잡합니다. 일단 aes 안에 들어 있는 fill은 문자 그대로 색깔로 채우라는 뜻입니다. ..level..은 레벨(level)이 높을수록, 그러니까 앞서 예를 든 것처럼 기압이 높거나 고도가 높을수록 더 진한 색깔을 칠하라는 뜻입니다. alpha는 투명도를 나타냅니다. 역시 같은 원리로 레벨이 높으면 불투명하게(색이 더 잘 드러나게) 칠하고 낮을 때는 투명하게(희미하게) 칠하라는 의미입니다.


geom='polygon'에서 polygon은 다각형이라는 뜻입니다. 기본은 위에서 본 것처럼 선(線)으로 돼 있는데 그것 말고 도형으로 그리라고 명령을 준 겁니다. 선을 기준으로 size는 선 굵기, bins는 선 간격이라는 뜻입니다. 이름이 이렇게 붙은 건 원래 점을 이은 선을 기준으로 삼고 있는 까닭입니다. 결과는 이렇습니다.




커닝 페이퍼를 활용하자.

이렇게 함수 안에 들어가는 속성을 다 외울 수도 없고 외울 필요도 없습니다. 어떤 함수를 써야겠다는 생각이 들면 구글링을 해보세요. 각 함수를 어떻게 쓰는지 나온 설명서를 어렵지 않게 찾을 수 있습니다. 예를 들어 stat_density_2d()를 설명한 문서는 여기 있습니다. 문서 아래 예제도 있으니까 '아, 이렇게 바꾸면 저렇게 되는구나'하고 적용해 보면서 가장 좋은 놈을 찾으면 그만입니다.


어떤 함수가 있는지는 어떻게 알 수 있을까요? 이럴 때 제일 도움이 되는 건 아래 나오는 ggplot2 치트 시트(cheat sheet)입니다.



이걸 외우실 필요도 없습니다. 치트 시트는 우리 말로 '커닝 페이퍼' 정도 되겠죠. 누가 커닝하다 살펴 보는 선생님도 없으니까 필요할 때마다 꺼내 보시면 만사OK입니다.


ggplot()은 레이어를 겹치는 방식이기 때문에 그래프를 변수에 넣어두면 편하게 쓸 수 있습니다. '여기까지는 됐다' 싶으면 일단 변수에 넣어두고 나머지만 바꿔보는 겁니다. 이렇게 말입니다.

p <- ggmap(map) + stat_density_2d(data=wifi, aes(x=lon, y=lat, fill=..level.., alpha=..level..), geom='polygon', size=7, bins=28)


이때 R 콘솔에 p를 치면 어떻게 될까요? 네, 예상하시는 것처럼 p에 넣어놓은 그래프가 나옵니다. print()를 써셔 print(p)라고 쳐도 마찬가지입니다.


이제 밀도를 나타내는 색깔을 바꿔 보겠습니다. 범위에 따라 단계적으로 색깔이 변하면 좋겠죠? 그럴 때 쓰는 명령어가 scale_fill_gradient()입니다. 방금전 말한 걸 영어로 바꾼 게 함수 이름인 셈입니다. 실제 코드는 이렇게 씁니다.

p + scale_fill_gradient(low='yellow', high='red')


p는 좀 전까지 있던 그래프를 가져오라는 의미라는 거 다들 아시죠? 뒷 부분은 값이 낮을 때는 노란색(yellow), 높을 때는 빨간색(red)를 가져와서 단계적으로 칠하라는 뜻입니다. (지금은 미술 시간이 아니니까 색깔이 적절한지는 다음에 생각하도록 하겠습니다.)


처음 그래프를 그릴 때는 투명도도 조절을 했었네요. 이번에도 같은 방식으로 적용하면 코드를 이렇게 쓸 수 있습니다.

p + scale_fill_gradient(low='yellow', high='red', guide=F) + scale_alpha(range=c(0.02, 0.8), guide=F)

투명도에는 범위(range)를 정해줬습니다. 레벨에 따라 2%에서 80%까지 투명도를 알아서 조절하라는 뜻입니다. (c가 뭔지는 기억하시죠?) 'guide=F'에서 F는 FALSE, 그러니까 범례를 표시하지 말라는 명령어입니다. 아래 그림처럼 말입니다.



잘 따라오고 계시죠? 눈으로만 보시면 어려울지 몰라도 R에 직접 쳐보시면 어려울 게 없습니다. 입력한 대로 그대로 나오니까요.



점을 짝지어 지도에 선 긋기

그래도 어려울 수 있으니 쉬운 걸 다시 한번 해보겠습니다. 이번에도 먼저 데이터 파일을 내려받겠습니다. 이번에는 파일이 두 개입니다.

airport.csv

route.csv


airport.csv에는 한국에 있는 공항 위치(경도, 위도)가 route.csv에는 이들을 연결하는 국내선 비행노선이 들어 있습니다. 위와 마찬가지로 작업 폴더를 바꾸신 적이 없다면 내 문서에 내려받셔야 제대로 작동합니다.



파일을 잘 내려받으셨으면 R에 파일을 불러와야 써먹을 수 있습니다. csv 파일을 불러오는 함수는? 많은 분들이 read.csv를 잊지 않으셨으리라고 믿습니다.

airport <- read.csv("airport.csv", header=T, as.is=T)
route <- read.csv("route.csv", header=T, as.is=T)
head(airport)
  airport iata     lon     lat
1    강릉  KAG 128.944 37.7536
2    광주  KWJ 126.809 35.1264
3    군산  KUV 126.616 35.9038
4    김포  GMP 126.791 37.5583
5    대구  TAE 128.659 35.8941
6    목포  MPK 126.380 34.7589


head(route)
  id airport     lon     lat
1  1     CJJ 127.499 36.7166
2  7     CJJ 127.499 36.7166
3 45     CJJ 127.499 36.7166
4 77     CJJ 127.499 36.7166
5  2     CJJ 127.499 36.7166
6  8     CJJ 127.499 36.7166


IATA는 국제항공운송협회(The International Air Transport Association) 약자입니다. 이 협회는 전 세계 공항에 코드를 부여하고 있는데 그 정보가 airport$iata에 들어 있는 겁니다. 원래 데이터를 처리할 때는 저 코드가 꼭 필요했는데 지금은 사실 별 필요는 없습니다. 앞서 말씀드린 것처럼 우리는 시각화를 연습하고 있는 거지 데이터 처리를 공부하는 건 아니니까요.


그럼 먼저 공항을 지도에 그려 볼까요? 여전히 변수 map에 공항이 들어 있는 상태입니다. 그러니 한국 지도를 그리려면 그냥 ggmap(map)을 치면 됩니다. 공항 위치는 어떻게 표시할까요? 네, 바로 아래처럼 입력하면 그만입니다.

ggmap(map) + geom_point(data=airport, aes(x=lon, y=lat))


참고로 ggmap 패키지에는 geocode라는 함수가 들어 있습니다. 이 함수는 특정 장소 위도, 경도값을 찾아주는 기능을 합니다. 물론 아주 '듣보잡' 장소는 안 되고 구글 지도에는 나올 정도가 되어야 합니다. 공항 정도면 유명 장소겠죠?  인천공항하고 김포공항 경·위도는 이렇게 알아낼 수 있습니다.

geocode(c('incheon airport', 'gimpo airport'))
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=incheon%20airport&sensor=false
Information from URL : http://maps.googleapis.com/maps/api/geocode/json?address=gimpo%20airport&sensor=false
       lon      lat
1 126.4407 37.46019
2 126.7945 37.55865


airport 변수에 들어 있는 경·위도 정보는 다른 곳에서 가져 온 거라 100% 맞아 떨어지지는 않습니다. 오차범위 안에 있는 정도지요. 공항이 워낙 넓으니까요.


이제 노선을 그릴 차례. 점을 찍을 때 geom_point를 썼으니까 선을 그릴 때는 뭘 쓸까요? 예, 맞습니다. 정답은 geom_line입니다. 먼저 위에 그린 그래프를 변수 p에 놓고 나서 이렇게 그리면 그만입니다.

p <- ggmap(map) + geom_point(data=airport, aes(x=lon, y=lat))
p + geom_line(data=route, aes(x=lon, y=lat, group=id))


여기서 주의해야 할 건 'group=id'라는 부분입니다. 한 점과 다른 점 사이를 이은 게 바로 선입니다. 그래서 어떤 점하고 어떤 점을 연결하면 되는지 id로 미리 짝을 지어준 겁니다. id 순서대로 자료를 뽑아 보면 1번 id는 제주공항하고 청주공항을 이어주는 노선이라는 걸 알 수 있습니다.

head(route[order(route$id),])
   id airport     lon     lat
1   1     CJJ 127.499 36.7166
83  1     CJU 126.493 33.5113
2   2     CJU 126.493 33.5113
84  2     CJJ 127.499 36.7166
3   3     CJU 126.493 33.5113
85  3     GMP 126.791 37.5583


이 코드도 복잡합니다. 또 한번 천천히 뜯어 보죠. 일단 맨 앞에 나오는 head는? 맞습니다. 변수 내용을 처음 여섯 줄만 보여주는 함수입니다. 이걸 벗겨내면 route[order(route$id),]가 남습니다.


이게 무슨 뜻인지 route[1, 1]을 입력해 볼까요? 1이 나옵니다. route 변수 맨 첫 줄 맨 첫 칸에 들어 있는 게 1이거든요. 다시 route를 불러 놓고 볼까요?

head(route)
  id airport     lon     lat
1  1     CJJ 127.499 36.7166
2  7     CJJ 127.499 36.7166
3 45     CJJ 127.499 36.7166
4 77     CJJ 127.499 36.7166
5  2     CJJ 127.499 36.7166
6  8     CJJ 127.499 36.7166


그럼 route[1, 2]는? 네, 이번에는 'CJJ'가 나옵니다. route[1, ]라고 치면? 첫 줄이 다 나옵니다.

route[1, ]
  id airport     lon     lat
1  1     CJJ 127.499 36.71666


변수 이름 뒤에 []를 쓰는 건 어떤 위치에 있는 자료를 가져오라는 뜻입니다.


마지막으로 order는 순서라는 뜻대로 자료를 정렬하라는 의미입니다. 좀더 정확하게는 순서를 알려줍니다. 이렇게 말입니다.

order(c(2, 10, 6, 8, 4))
[1] 1 5 3 4 2


종합하면 route[order(route$id), ]는 변수 route 안에 있는 내용을 route$id 순서로 오름차림 정렬해서 보여 달라는 뜻입니다. 결과는 이렇습니다.

head(route[order(route$id),])
   id airport     lon     lat
1   1     CJJ 127.499 36.7166
38  1     CJU 126.493 33.5113
5   2     CJJ 127.499 36.7166
9   2     CJU 126.493 33.5113
10  3     CJU 126.493 33.5113
84  3     GMP 126.791 37.5583


종합해 말씀드리자면 선을 그릴 때는 geom_line을 씁니다. 이 선은 점과 점을 이은 것. 어떤 점과 어떤 점을 이어야 할지는 미리 짝으로 엮어서 R가 알아볼 수 있게 합니다. 이 원리를 이해하셨다면 선 긋기도 끝입니다. 이제 색칠만 남았습니다.



지도에 색칠할 땐 미농지가 필요하다

점과 선은 지도 위에 곧바로 찍고 그으면 그만이었지만 면에 색을 칠하려면 '미농지'를 한장 얹어야 합니다. 왜냐하면 보통 색을 칠할 때는 인위적인 구분이 필요하거든요. 예를 들어 지방자치단체를 기준으로 삼아 색을 칠하려면 미농지로 위에 지자체 경계선을 그리는 것처럼 R에도 어떤 좌표는 어느 지자체에 속한다고 알려줘야 합니다.


당연히 이번에도 우리가 하나 하나 선을 딸 필요는 없습니다. raster라는 패키기자 도와주거든요. 먼저 설치하고 읽어 드리는 것부터 시작해야겠죠?

install.packages("raster")
library(raster)


raster 패키지에는 전 세계 행정구역 데이터베이스인 GADM(Database of Global Administrative Areas) 자료를 불러올 수 있는 getData()라는 함수가 들어 있습니다. 한국 시군구 단위 지도를 불러오겠습니다.

korea <- getData('GADM', country='kor', level=2)


GADM은 위에서 말씀드린 것처럼 행정구역 지도를 가져오라는 뜻, country='kor'은 국제표준화기구(ISO) 국가 코드에 따라 한국을 지정한 겁니다. country='kr'을 입력해도 마찬가지로 한국 자료를 가지고 옵니다. level은 얼마나 상세하게 그릴지 설명하는 겁니다. 0은 구분 없이 한국 전체, 1은 광역자치단체(시도)까지 구분, 2는 시군구까지 나눠줍니다.


잘 들어왔는지 확인을 해봐야겠죠? 이번에는 ggmap을 곧바로 쓰면 에러가 나옵니다. 대신 ggolot 스타일로 풀어줘야 합니다

ggplot() + geom_polygon(data=korea, aes(x=long, y=lat, group=group), fill='white', color='black')


잘 보시면 예전에 못 보던 게 하나 보입니다. 네, group 속성이 등장했습니다. 점을 모아 선을 그을 때 짝을 지어줘야 했던 것처럼 선을 모아 면을 만들 때도 짝을 찾아줘야 하는 겁니다.



이 지도는 '옛날 지도'라는 것만 빼면 완벽합니다. 충북 청원군이 여전히 청주시와 따로 있고, 현재 통합 창원시도 아직 '마창진' 형태를 유지하고 있습니다. 이대로 쓰실 분은 쓰셔도 좋지만 기왕이면 새 지도를 찾는 게 좋겠죠?


콘솔에 그냥 korea라고 치면 알기 어려운 표현이 마구 나옵니다.

korea
class       : SpatialPolygonsDataFrame 
features    : 229 
extent      : 125.0818, 130.9404, 33.11208, 38.61215  (xmin, xmax, ymin, ymax)
coord. ref. : +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0 
variables   : 15
names       : OBJECTID, ID_0, ISO,      NAME_0, ID_1, NAME_1, ID_2,  NAME_2, HASC_2, CCN_2, CCA_2, TYPE_2, ENGTYPE_2,      NL_NAME_2,   VARNAME_2 
min values  :        1,  213, KOR, South Korea,    1,  Busan,    1,  Andong,       ,    NA,      ,     Gu,      City, 가평군| 加平郡,             
max values  :      229,  213, KOR, South Korea,   17,  Ulsan,  229, Yuseong,       ,    NA,      ,     Si,  District, 횡성군| 橫城郡, Ulleung-gu


이렇게 생긴 녀석을 GIS에서는 셰이프파일(shape file)이라고 부릅니다. 아래 파일은 제가 이 사이트에서 지난해(2016년) 2월 기준 시군구 셰이프파일을 받아 이번 강의에 맞게 일부 수정한 자료입니다. (한번에 올릴 수 있는 파일이 10메가바이트라 둘로 나눴습니다.) 압축을 푸시면 파일이 총 4개 나오는데 모두 같은 폴더 안에 있어야 작동합니다. 한번 더 말씀드리자면 작업 폴더를 바꾼 일이 없으신 경우 내 문서에 푸시면 됩니다.


SIG_201703.zip

SIG_201703.z01


shapefile() 함수를 쓰면 R에 셰이프파일을 불러올 수 있습니다. 이 함수는 raster 패키지에 들어 있습니다. 우리는 raster를 불러들인 상태니까 바로 입력하면 됩니다. 그리고 잘 들어왔는지 확인.

korea <- shapefile('TL_SCCO_SIG.shp')
ggplot() + geom_polygon(data=korea, aes(x=long, y=lat, group=group), fill='white', color='black')


지난 지도보다 한반도가 좀 날씬해졌네요. 그냥 그림 비율을 정하는 과정에서 달리 보이는 것뿐이니까 크게 신경 쓰실 필요는 없습니다. 여기까지 따라 오셨으면 미농지는 깨끗하게 얹은 상태입니다.



색칠할 땐 데이터 짝도 찾야줘야 한다.

이 부분은 퍽 길고 지루합니다. 앞에서는 이렇게 데이터를 처리할 일이 있으면 우리는 그저 지도 놀이를 하는 것뿐이라면서 중간 과정은 다 건너뛴 게 사실입니다. 이번에는 데이터 처리를 강조할 수밖에 없습니다. 그 과정이 안 되면 아예 지도에 색을 칠할 수 없거든요.


시군구마다 다른 색깔을 칠하려면 어떤 기준이 있어야 할 터. 흔히 선거 결과를 정리할 때 색깔을 많이 칠하니까 우리도 해보죠. 아래 파일은 제19대 대통령 선거 시군구별 득표율을 정리한 CSV 파일입니다.


result.csv



이번에도 데이터를 불러와야겠죠? 어떻게 하는지 다들 아실 거라고 믿고 확인하는 과정까지 한번에 가보겠습니다.

result <- read.csv("result.csv", header=T, as.is=T)
head(result)
  Sido    Sigun    id  moon  hong   ahn   yoo  shim   etc
1 서울   종로구 11110 0.416 0.218 0.218 0.073 0.070 0.005
2 서울     중구 11140 0.412 0.217 0.235 0.071 0.060 0.005
3 서울   용산구 11170 0.393 0.239 0.217 0.080 0.066 0.004
4 서울   성동구 11200 0.428 0.200 0.226 0.078 0.064 0.004
5 서울   광진구 11215 0.441 0.194 0.221 0.072 0.069 0.004
6 서울 동대문구 11230 0.421 0.219 0.227 0.064 0.064 0.005


보시는 것처럼 Sido는 시도, Sigun은 시군구 자료입니다. moon은 문 대통령, hong은 홍준표, ahn은 안철수, yoo는 유승민, shim은 심상정 후보 득표율을 각각 나타냅니다. etc는 나머지 후보 득표율이고요.


위에서 설명하지 않은 id는 행정자치부에서 쓰고 있는 법정동 코드를 기초자치단체(시군구) 수준에서 자른 겁니다. 중앙선거관리위원회에서 19대 대선 자료를 내려받을 때는 이런 코드가 들어 있지 않았습니다. 그렇다면 왜 이런 코드를 넣었냐고요? 그래야 짝을 찾아줄 수 있으니까요.


ggplot2는 데이터프레임(Data Frame)이라는 자료형을 기반으로 합니다. 아주 쉽게 말씀드리면 엑셀 같은 데서 볼 수 있는 표 형태가 바로 데이터프레임입니다.


이런 자료가 있다고 가정해 보겠습니다. (앞으로 세 차례 올림픽을 개최하는 도시와 올림픽이 열리는 연도입니다.)

개최지 연도
평창  2018
도쿄  2020
베이징 2022


이걸 데이터프레임으로 만들어 보겠습니다. 이때 쓰는 함수는 (예상하시는 것처럼) data.frame()입니다.

a <- c('평창', '도쿄', '베이징')
b <- c(2018, 2020, 2022)
c <- data.frame(a, b)
colnames(c) <- c('개최지', '연도')
c
  개최지 연도
  개최지 연도
1   평창 2018
2   도쿄 2020
3 베이징 2022


colnames() 함수는 열(column)에 이름을 붙이는 기능을 합니다. colnamse(c)는 원래 c라는 데이터프레임에 있는 열 이름을 보여달라는 뜻입니다. 여기서는 변수에 값을 집어 넣는 <-를 썼으니까 그 이름 자리에 차례대로 개최지, 연도를 넣게 됩니다.


이어서 변수 d에는 나라를 넣은 다음 도시하고 연결해 보겠습니다.

d <- c('한국', '일본', '중국')
e <- data.frame(d, a)
colnames(e) <- c('국가', '개최지')
e
  국가 개최지
1 한국   평창
2 일본   도쿄
3 중국 베이징


도시, 국가, 연도가 한꺼번에 들어 있는 데이터프레임은 어떻게 만들까요? 어떤 걸 병합하는 건 영어로? merge죠. 그럼 이런 걸 합쳐주는 함수 이름은? 맞습니다. merge()입니다.

merge(c, e, by='개최지')
1   도쿄 2020 일본
2 베이징 2022 중국
3   평창 2018 한국


코곧내(코드가 곧 내용)입니다. c와 e를 합치는데 개최지를 기준으로 삼으라는 겁니다. 실제로 그렇게 합쳤습니다.


그러면 korea 미농지도 대선 결과(result)하고 합쳐줘야 색깔을 칠하겠죠? 해봅시다.


제일 먼저 해야 할 건 셰이프파일 형태인 korea를 데이터프레임으로 바꾸는 겁니다. ggplot2에는 이럴 때 쓰라고 fortify()라는 함수를 마련해두고 있습니다. 이 명령을 실행하기 전에 korea 변수가 어떻게 생겼나 확인부터 해볼까요?

head(korea)
 SIG_CD  SIG_ENG_NM SIG_KOR_NM
0 11110   Jongno-gu   종로구
1 11140    Jung-gu    중구
2 11170  Yongsan-gu   용산구
3 11200 Seongdong-gu   성동구
4 11215  Gwangjin-gu   광진구
5 11230 Dongdaemun-gu  동대문구


엄청 복잡해 보였던 korea는 속살에 이런 코드를 감추고 있었습니다. 저 SIG_CD가 바로 시군구 코드입니다. 저걸 기준으로 데이터를 정리하면 되겠죠?

korea <- fortify(korea, region='SIG_CD')
head(korea)
      long      lat order  hole piece    id   group
1 127.0115 37.58100     1 FALSE     1 11110 11110.1
2 127.0115 37.58098     2 FALSE     1 11110 11110.1
3 127.0116 37.58098     3 FALSE     1 11110 11110.1
4 127.0117 37.58096     4 FALSE     1 11110 11110.1
5 127.0120 37.58093     5 FALSE     1 11110 11110.1
6 127.0120 37.58092     6 FALSE     1 11110 11110.1


SIG_CD였던 녀석이 자료형을 바꾸는 과정에서 id로 바뀌었습니다. result에는 뭐가 있었죠? 네, id가 있었습니다. 그러면 id를 기준으로 두 자료를 합칠 수 있습니다.

korea <- merge(korea, result, by='id')
head(korea)
     id     long      lat order  hole piece   group Sido  Sigun  moon  hong
1 11110 127.0115 37.58100     1 FALSE     1 11110.1 서울 종로구 0.416 0.218
2 11110 127.0115 37.58098     2 FALSE     1 11110.1 서울 종로구 0.416 0.218
3 11110 127.0116 37.58098     3 FALSE     1 11110.1 서울 종로구 0.416 0.218
4 11110 127.0117 37.58096     4 FALSE     1 11110.1 서울 종로구 0.416 0.218
5 11110 127.0120 37.58093     5 FALSE     1 11110.1 서울 종로구 0.416 0.218
6 11110 127.0120 37.58092     6 FALSE     1 11110.1 서울 종로구 0.416 0.218


짝짝짝, 정말 정말 고생하셨습니다. 이제 지도 위에 색을 칠할 준비를 모두 마쳤습니다.



드디어 지도에 색칠하기

이제 실제로 색을 칠할 차례. 문 대통령이 어디서 얼마나 지지율을 얻었는지 한번 색을 칠해 보겠습니다.

ggplot() + geom_polygon(data=korea, aes(x=long, y=lat, group=group, fill=moon))


이번에는 색을 R에서 자동으로 골랐지만 이 색깔도 바꿀 수 있습니다. 이 그래프를 p에 넣은 다음 wifi 때 썼던 scale_fill_gradient() 함수를 활용하겠습니다.

p <- ggplot() + geom_polygon(data=korea, aes(x=long, y=lat, group=group, fill=moon))
p + scale_fill_gradient(low='white', high='#004ea2')

가만히 보니까 숫자가 높을 때 쓰라고 한 색깔이 이상합니다. # 다음에 나온 숫자+문자 조합은 16진법 체계입니다. 더불어민주당 당색웹에서 색깔을 나타낼 때 쓰는 16진법 체계로 바꾸면 저렇게 나오는 겁니다. 



wifi 때도 그랬지만 전문 디자이너가 아닌 이상 색깔을 고른다는 건 쉬운 일이 아닙니다. 이번에도 걱정할 게 없습니다. R에는 전문가가 미리 만들어둔 색상 패키지도 있거든요. 색맹이나 색약인 분들도 쉽게 단계를 구분할 수 있게 만든 viridis 패키지 도움을 받아볼까요? 이번에도 일단 설치 & 로드.

install.packages("viridis)
library(viridis)


이 패키지는 scale_fill_gradient()가 들어갈 자리에 scale_fill_viridis()를 쓰면 그만입니다. 이미 p에 그래프가 들어 있으니까 이렇게 쓰면 그만입니다.

p + scale_fill_viridis()


어쩐지 저는 저 색깔이 반대로 되면 더 좋을 것 같습니다. 기왕하는 김에 회색 격자도 없애고, 범례도 지워보죠.

p + scale_fill_viridis(direction=-1) + theme_void() + guides(fill=F)


방향을 바꿀 때는 'directions=-1'이라고 쓰기만 하면 됩니다. theme_void()는 void라는 테마를 불러 오라는 뜻입니다. 이렇게 미리 만든 ggplot 테마를 쓰시고 싶을 때는 이 링크를 참조하시면 됩니다. guides(fill=F)는 또 한번 설명 드리지 않아도 되겠죠?



result에는 다른 후보 결과 등도 들어 있으니 원하시는 대로 그릴 수 있습니다. 한번 문 대통령하고 홍 후보하더 어떤 지역에서 얼마나 차이를 냈는지 알아볼까요?

ggplot() + geom_polygon(data=korea, aes(x=long, y=lat, group=group, fill=final)) + scale_fill_viridis(direction=-1) + theme_void() + guides(fill=F)


final이라는 열은 그냥 '문 대통령 득표율 - 홍 후보 득표율'입니다. 이미 결과를 알고 있는 것처럼 광주·전북·전남 지역에서는 문 대통령, 대구·경북·경남 지역에서는 홍 후보가 표를 많이 얻었습니다. 이미 한국 지리에 익숙한 분들은 알지만 모르는 분들은 모르시겠죠? 이럴 때는 미농지를 지도 위에 대면 됩니다. 이렇게요.

ggmap(map) + geom_polygon(data=korea, aes(x=long, y=lat, group=group, fill=final), alpha=.75) + scale_fill_viridis(direction=-1) + theme_void() + guides(fill=F)

그냥 맨 밑에 ggmap(map)으로 지도를 먼저 그리고 그 위에 다시 색깔 지도를 올린 겁니다. 그냥 올리면 지도를 완전히 덮을 테니까 geom_polygon에 'alpha=.75'를 입력해 투명도를 75%로 조정했습니다. 



제가 직접 셰이프파일을 변환하다 보니 지도하고 '깔맞춤'은 아닙니다. 그래도 이 정도면 외곽선을 좀 굵게 그리면 커버할 수 있는 수준이라고 생각합니다. GADM으로 해보시면 100% 딱 맞는 지도를 확인하실 수 있을 겁니다.



수도권만 칠해보자

특정 지역, 예를 들어 서울만 그리고 싶을 때는 어떻게 할까요? 일단 우리는 지금 korea라는 변수에 시군구 코드(id)가 들어있다는 사실을 알고 있습니다. 이 시군구 코드가 1만1740 이하면 서울입니다.

seoul <- korea[korea$id <= 11740, ]
ggplot() + geom_polygon(data=seoul, aes(x=long, y=lat, group=group), fill='white', color='black')


GADM에서도 같은 원리로 서울만 그릴 수 있습니다.

korea_gadm <- getData('GADM', country='kor', level=2)
seoul_gadm <- korea_gadm[korea_gadm$NAME_1 %in% 'Seoul', ]


이번에는 NAME_1이라는 열에 Seoul이 들어 있는 행만 가져오라고 명령을 준 걸 빼면 앞하고 같은 원리입니다. 잘 들어왔나 확인해 봐야죠?

ggplot() + geom_polygon(data=seoul_gadm, aes(x=long, y=lat, group=group), fill='white', color='black')



모양이 우리가 쓴 셰이프파일하고 다르긴 하지만 서울은 분명 서울입니다. 경기하고 인천까지 포함해서 소위 수도권을 그리고 싶다면 어떻게 하면 될까요? 처음에 데이터를 걸러낼 때 인천(Incheon)하고 경기도(Gyeonggi-do)도 포함시켜 주면 됩니다.

sudogwon_gadm <-korea_gadm[korea_gadm$NAME_1  %in% c('Seoul', 'Incheon', 'Gyeonggi-do'), ]
ggplot() + geom_polygon(data=sudogwon_gadm, aes(x=long, y=lat, group=group), fill='white', color='black')


잘 나오기는 했는데 GADM에서 분류를 잘못한 지역이 있어서 수도권 위치가 왼쪽 위에 치우쳐 있습니다. 이럴 때는 축 범위를 정해주는 xlim(), ylim()으로 정리할 수 있습니다. 이름 그대로 x, y축 한계(limits)를 정해주는 함수입니다.

ggplot() + geom_polygon(data=sudogwon_gadm, aes(x=long, y=lat, group=group), fill='white', color='black') + xlim(c(125.5, 128)) + ylim(c(36.75, 38.25))


원하신다면 좀더 정밀하게 조정을 할 수도 있겠지만 저는 이 정도로 만족하겠습니다. 현재 korea에 들어 있는 셰이프파일 역시 같은 원리를 적용하면 수도권만 빠로 빼서 그릴 수 있습니다. 아래는 이번 대선 때 문 대통령 수도권 득표율만 따로 빼서 그린 겁니다.




도구 선택은 자유

당연한 얘기지만 R로만 이렇게 지도 놀이를 할 수 있는 건 아닙니다. 지도 위에 점 찍는 건 엑셀만 있어도 사실 충분합니다. 한때 저하고 같은 회사에서 일하시기도 했던 권혜진 건국대 언론홍보대학원 겸임교수는 태블로(tableau)를 이용해 이렇게 지진 발생지 지도를 그리기도 하셨습니다.



저 작업을 어떻게 하셨을지 한번 유추해 볼까요? 지도를 그리고 지진 발생지 경도와 위도를 확보해서 점을 찍었을 겁니다. 지진 규모에 따라서 원 크기를 다르게 한 걸 빼면 우리가 와이파이 지도를 그린 것하고 같은 프로세스였을 겁니다. 와이파이 지도 역시 태블로로 이렇게 똑같이 그릴 수 있습니다.


태블로로 지도 놀이를 하면 소위 인터랙티브(interactive)한 지도를 곧바로 웹에 공유할 수 있다는 장점도 있습니다. R로 작업했을 때는 leafletshiny라는 패키지를 더 거쳐야 합니다. (혹시 이 과정이 궁금하시면 'R로 인터랙티브 지도 그리기(feat.leaflet)' 포스트가 도움이 될 수 있습니다.)


또 아주 정교한 GIS 작업을 하려면 코드가 너무 길어서 차라리 GIS 전용 소프트웨어서 직접 화면을 보면서 작업하는 게 편할 수도 있습니다. R에서는 당장 지도에 제목을 붙이려고 해도 코드 한 줄이 필요하지만 그냥 MS 파워포인트를 사용하듯이 제목을 달 수 있는 소프트웨어도 있으니까요. 


하지만 R 문법을 알고 있으면 크게 어렵지 않게(정말 그랬어야 할 텐데요) 지도에 점을 그리고 선을 긋고 색을 칠할 수 있습니다. 요즘에는 자기 위치 정보를 추척하는 스마트폰 어플리케이션(앱)도 많으니 지도 데이터를 장난감 삼아 할 수 있는 일도 그만큼 많을 테고 말입니다.


그러니 어린 시절 사회과부도를 보고 설렌 적이 있었다면 그 추억을 생각해 보면서 지도를 그리고 놀아 보면 어떨까요? 어떤 지도를 그리셨는지 공유해 주셔도 환영합니다.


댓글,

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