3. 대화형 지도#
대화형 지도(Interactive map)이란, 웹 상에서 지도의 확대/축소, 위치 이동, 지형지물 표시 등 다양한 정보를 제공하는 지도를 가리킵니다. 대화형 지도는 일반적으로 여러 애플리케이션에 사용되는 스크립트 언어인 자바스크립트(JavaScript), 에크마스크립트(ECMAScript)를 사용하여 구현됩니다.
오픈소스로 공개된 대화형 지도 도구로는, 이 책에서 사용할 Leaflet과 OpenLayers를 포함하여 대화형 웹 지도 제작을 위해 개발된 자바스크립트 기반의 여러 라이브러리가 있습니다. 자바스크립트 기반이라고 해서 걱정하지 않으셔도 됩니다. 파이썬의 Folium 패키지를 사용하면 자바스크립트를 작성하지 않고도 geopandas.GeoDataFrame
에 저장된 데이터로 Leaflet 지도를 만들 수 있습니다.
3.1. 간단한 대화형 지도#
우선, 배경 지도(base map)만 포함된 간단한 대화형 지도를 만드는 것부터 시작하겠습니다. folium.Map
객체를 생성하고, 지도에 표시될 중심 위치(location
)와 초기 확대/축소 수준(zoom_start
; 0 ~ 20 범위)을 지정합니다. control_scale
을 True
로 설정하면, 지도에 스케일 막대를 표시하게 됩니다.
import pathlib
NOTEBOOK_PATH = pathlib.Path().resolve()
DATA_DIRECTORY = NOTEBOOK_PATH / "data"
# 대화형 지도 저장 경로
HTML_DIRECTORY = NOTEBOOK_PATH / "html"
HTML_DIRECTORY.mkdir(exist_ok=True)
import folium
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=10,
control_scale=True
)
interactive_map
3.1.1. 지도 저장#
생성한 지도를 웹 브라우저에서 열 수 있는 HTML 파일로 저장하려면, folium.Map.save()
를 사용합니다.
interactive_map.save(HTML_DIRECTORY / "base-map.html")
3.1.2. 배경 지도 변경#
Folium에서는 기본적인 배경 지도로 OpenStreetMap을 지원합니다. 이외에 다른 레이어를 사용하려는 경우, folium.Map
은 tiles
파라미터를 통해 내장된 지도 제공자 중 하나를 선택하거나 사용자정의 타일셋 URL을 설정할 수 있습니다. folium.Map
에 내장된 지도 제공자는 아래와 같습니다.
OpenStreetMap
Stamen Terrain
Stamen Toner
Stamen Watercolor
CartoDB positron
CartoDB dark_matter
CloudmadeMapbox BrightMapbox Control Room
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12,
tiles="Stamen Terrain"
)
interactive_map
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12,
tiles="Stamen Toner"
)
interactive_map
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12,
tiles="Stamen Watercolor"
)
interactive_map
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12,
tiles="CartoDB positron"
)
interactive_map
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12,
tiles="CartoDB dark_matter"
)
interactive_map
3.2. 포인트 마커 추가#
Folium 지도에 마커를 추가하려면 folium.Marker
를 사용합니다. icon
파라미터에 folium.Icon
을 입력하여 마커 스타일을 지정하고, 마우스를 마커 위에 놓았을 때 유용한 텍스트가 표시되도록 툴팁(tooltip
)을 설정합니다.
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12
)
cityhall = folium.Marker(
location=(37.566, 126.978),
tooltip="서울시청",
icon=folium.Icon(color="green", icon="ok-sign")
)
cityhall.add_to(interactive_map)
interactive_map
3.3. 포인트 레이어 추가#
Folium은 geopandas.GeoDataFrame
과 같은 레이어를 추가할 수 있도록 지원합니다. Folium은 folium.features.GeoJson
클래스에 Leaflet의 geoJSON 레이어를 구현합니다. 지리 데이터프레임을 사용하여 이러한 클래스 및 레이어를 초기화하고 이를 지도에 추가할 수 있습니다. 아래 예시에서는 3단원에서 생성한 address.gpkg
데이터셋을 사용해 보겠습니다.
import geopandas as gpd
CH3_DIRECTORY = pathlib.Path("../3. 지오코딩과 공간 쿼리/")
addresses = gpd.read_file(CH3_DIRECTORY / "data" / "addresses.gpkg")
addresses
address | 지점 | 지점주소 | geometry | |
---|---|---|---|---|
0 | 서울특별시아동학대예방센터, 광평로34길, 06352, 광평로34길, 서울, 대한민국 | 400 | 서울특별시 강남구 일원동 580 | POINT (127.08775 37.47971) |
1 | 서초구립 라클라스 어린이집, 서초중앙로, 06601, 서초중앙로, 서울특별시 서초구... | 401 | 서울특별시 서초구 서초동 1416번지 | POINT (127.01410 37.50038) |
2 | 서울특별시 동부기술교육원, 183, 고덕로, 05235, 고덕로, 고덕동, 대한민국 | 402 | 서울특별시 강동구 고덕로 183 | POINT (127.14557 37.55604) |
3 | 롯데월드, 240, 올림픽로, 05554, 올림픽로, 송파구, 대한민국 | 403 | 서울특별시 송파구 올림픽로 240 | POINT (127.09823 37.51110) |
4 | 월드메르디앙 201동, 양천로1길, 07602, 양천로1길, 서울, 대한민국 | 404 | 서울특별시 강서구 양천로 201 | POINT (126.81027 37.57468) |
5 | 405-298, 목동동로12길, 08005, 목동동로12길, 서울, 대한민국 | 405 | 서울특별시 양천구 목동동로 298 | POINT (126.87355 37.52290) |
6 | 서울특별시립도봉도서관, 시루봉로, 01375, 시루봉로, 쌍문4동, 대한민국 | 406 | 서울특별시 도봉구 시루봉로 173 | POINT (127.02772 37.65291) |
7 | NaN | 407 | 서울특별시 노원구 공릉동 사서함 230-3호 사서함 77호 | None |
8 | 서울시립대학교, 163, 서울시립대로, 02504, 서울시립대로, 서울특별시, 대한민국 | 408 | 서울특별시 동대문구 서울시립대로 163 | POINT (127.05918 37.58302) |
9 | 32, 면목로84길, 02162, 면목로84길, 서울시 중랑구, 대한민국 | 409 | 서울특별시 중랑구 면목로57길 32 | POINT (127.08852 37.59191) |
10 | 동작보라매자이더포레스트, 서울특별시 동작구 여의대방로22길 121, 대한민국 | 410 | 서울특별시 동작구 여의대방로16길 61 | POINT (126.92597 37.49854) |
11 | NaN | 411 | 서울특별시 마포구 창전동 산1-75 | None |
12 | 연세대학교 신촌캠퍼스, 50, 연세로, 03722, 연세로, 서울특별시, 대한민국 | 412 | 서울특별시 서대문구 연세로 50 | POINT (126.93940 37.56778) |
13 | NaN | 413 | 서울특별시 광진구 자양2동 680-67 | None |
14 | 국민대학교우편취급국, 77 종합복지관 2층, 정릉로, 02707, 정릉로, 정릉동,... | 414 | 서울특별시 성북구 정릉로 77 | POINT (126.99594 37.61029) |
15 | NaN | 415 | 서울특별시 용산구 이촌로 255 | None |
16 | 서울특별시 소방학교, 통일로, 03312, 통일로, 진관동, 대한민국 | 416 | 서울특별시 은평구 진관동 산26 | POINT (126.91181 37.63268) |
17 | 서울독산초등학교, 31, 시흥대로104길, 08618, 시흥대로104길, 금천구, ... | 417 | 서울특별시 금천구 시흥대로104길 31 | POINT (126.90003 37.46524) |
18 | 여의도 이랜드크루즈, 280, 여의동로, 07337, 여의동로, 서울, 대한민국 | 418 | 서울특별시 영등포구 여의동로 280 | POINT (126.93841 37.52578) |
19 | 83, 소파로, 04630, 소파로, 예장동, 대한민국 | 419 | 서울특별시 중구 소파로 83 \t | POINT (126.98384 37.55617) |
20 | 서울숲아이파크, 서울특별시 성동구 동일로 237, 대한민국 | 421 | 서울특별시 성동구 서울숲길 18 | POINT (127.06943 37.55290) |
21 | 종로경찰서, 종로17길, 03140, 종로17길, 서울특별시 종로구, 대한민국 | 422 | 서울특별시 종로구 북악산로267 | POINT (126.98893 37.57054) |
22 | NaN | 423 | 서울특별시 구로구 부일로 893 | None |
23 | 강북세일학원, 13, 정릉로48길, 02801, 정릉로48길, 서울특별시 성북구, ... | 424 | 서울특별시 강북구 도봉로89길 13 | POINT (127.02682 37.60338) |
24 | NaN | 425 | 서울특별시 관악구 남현동 산100-1420003호 | None |
25 | 서울봉천동우체국, 249-1, 관악로, 08727, 관악로, 서울특별시 관악구, 대한민국 | 509 | 서울특별시 관악구 관악로 1 | POINT (126.95671 37.48677) |
26 | 맨하탄21리빙텔, 20, 국회대로74길, 07238, 국회대로74길, 서울특별시 영... | 510 | 서울특별시 영등포구 국회대로53길 20 | POINT (126.92153 37.52963) |
27 | 김대중대통령묘소, 210, 현충로, 06984, 현충로, 서울, 대한민국 | 889 | 서울특별시 동작구 현충로 210 | POINT (126.97018 37.49665) |
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12
)
addresses_layer = folium.features.GeoJson(
addresses,
name="서울시 내 기상관측소"
)
addresses_layer.add_to(interactive_map)
interactive_map
3.4. 다각형 레이어 추가#
이전 3단원에 작업했던 서울 인구밀도 격자 데이터셋을 다시 다뤄보겠습니다.
population_grid = gpd.read_file(CH3_DIRECTORY / "data" / "국토통계_인구정보_1KM_서울특별시_202304", encoding="utf-8")
population_grid
gid | lbl | val | geometry | |
---|---|---|---|---|
0 | 다사6453 | 9172.00 | 9172.0 | POLYGON ((964000.000 1953000.000, 964000.000 1... |
1 | 다사5651 | 12198.00 | 12198.0 | POLYGON ((956000.000 1951000.000, 956000.000 1... |
2 | 다사6157 | 28897.00 | 28897.0 | POLYGON ((961000.000 1957000.000, 961000.000 1... |
3 | 다사5858 | 23351.00 | 23351.0 | POLYGON ((958000.000 1958000.000, 958000.000 1... |
4 | 다사5347 | 11380.00 | 11380.0 | POLYGON ((953000.000 1947000.000, 953000.000 1... |
... | ... | ... | ... | ... |
705 | 다사4053 | NaN | NaN | POLYGON ((940000.000 1953000.000, 940000.000 1... |
706 | 다사7249 | NaN | NaN | POLYGON ((972000.000 1949000.000, 972000.000 1... |
707 | 다사4552 | NaN | NaN | POLYGON ((945000.000 1952000.000, 945000.000 1... |
708 | 다사6265 | NaN | NaN | POLYGON ((962000.000 1965000.000, 962000.000 1... |
709 | 다사5259 | NaN | NaN | POLYGON ((952000.000 1959000.000, 952000.000 1... |
710 rows × 4 columns
인구밀도를 표현하기 위해 folium.Choropleth
를 사용하겠습니다. 단계구분(Choropleth) 지도도 마찬가지로 folium.features.GeoJson
레이어로 표시됩니다. 이 때 주의해야 할 점은 folium.Choropleth
클래스가 공간정보를 갖는 데이터셋과 표현하고자 하는 값을 갖는 데이터셋을 입력받아 결합하므로, 문자열(str
) 타입의 인덱스 열이 있는 입력 데이터셋이 사용되어야 합니다. 아래 예시에서 geo_data
가 공간정보를 갖는 데이터셋, data
가 값을 갖는 데이터셋을 가리킵니다.
이러한 열을 생성하는 좋은 방법은, 아래와 같이 데이터프레임의 인덱스를 새로운 열(id
)에 문자열 타입으로 복사하는 것입니다.
population_grid["id"] = population_grid.index.astype(str)
population_grid
gid | lbl | val | geometry | id | |
---|---|---|---|---|---|
0 | 다사6453 | 9172.00 | 9172.0 | POLYGON ((964000.000 1953000.000, 964000.000 1... | 0 |
1 | 다사5651 | 12198.00 | 12198.0 | POLYGON ((956000.000 1951000.000, 956000.000 1... | 1 |
2 | 다사6157 | 28897.00 | 28897.0 | POLYGON ((961000.000 1957000.000, 961000.000 1... | 2 |
3 | 다사5858 | 23351.00 | 23351.0 | POLYGON ((958000.000 1958000.000, 958000.000 1... | 3 |
4 | 다사5347 | 11380.00 | 11380.0 | POLYGON ((953000.000 1947000.000, 953000.000 1... | 4 |
... | ... | ... | ... | ... | ... |
705 | 다사4053 | NaN | NaN | POLYGON ((940000.000 1953000.000, 940000.000 1... | 705 |
706 | 다사7249 | NaN | NaN | POLYGON ((972000.000 1949000.000, 972000.000 1... | 706 |
707 | 다사4552 | NaN | NaN | POLYGON ((945000.000 1952000.000, 945000.000 1... | 707 |
708 | 다사6265 | NaN | NaN | POLYGON ((962000.000 1965000.000, 962000.000 1... | 708 |
709 | 다사5259 | NaN | NaN | POLYGON ((952000.000 1959000.000, 952000.000 1... | 709 |
710 rows × 5 columns
이제 단계구분 다각형 레이어를 생성하고, 이를 지도 객체에 추가하면 됩니다. Folium이 조금은 복잡한 구조를 갖기 때문에, 다음과 같은 여러 파라미터를 입력해야 합니다.
geo_data
,data
: 공간정보를 갖는 데이터셋(geo_data
)과 표현할 값을 갖는 데이터셋(data
)이며, 동일한geopandas.GeoDataFrame
일 수 있음columns
:data
에 입력한 데이터셋에서 사용할 열 이름으로, 인덱스 열과 표현할 값이 포함된 열key_on
:data
와 조인에 사용할geo_data
의 열
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12
)
population_grid_layer = folium.Choropleth(
geo_data=population_grid,
data=population_grid,
columns=("id", "val"),
key_on="id"
)
population_grid_layer.add_to(interactive_map)
interactive_map
지도를 더 멋지게 만들기 위해, 더 많은 범주(bins
)로 나누고 색상 맵을 변경하고(fill_color
), 선 두께(line_weight
)를 0으로 설정하고, 범례에 레이어 이름(legend_name
)을 추가해 보겠습니다.
interactive_map = folium.Map(
location=(37.55, 127.0),
zoom_start=12
)
population_grid_layer = folium.Choropleth(
geo_data=population_grid,
data=population_grid,
columns=("id", "val"),
key_on="id",
bins=9,
fill_color="YlOrRd",
line_weight=0,
legend_name="2023년 4월 인구밀도",
highlight=True
)
population_grid_layer.add_to(interactive_map)
interactive_map
3.4.1. 단계구분도에 툴팁 추가#
이러한 대화형 지도에서는, 마우스를 위에 올리면 각 격자 셀의 값을 표시하는 것이 정보전달 측면에서 좋습니다. Folium에서는 간단한 방법으로 이러한 기능을 추가할 수 있습니다. folium.features.GeoJson
을 사용하여 투명한 다각형 레이어를 추가하고, 툴팁을 표시하도록 구성합니다.
위에서 만든 지도 interactive_map
을 유지하고, 여기에 새로운 레이어를 추가하기만 하면 됩니다.
def style_function(feature):
return {
"color": "transparent",
"fillColor": "transparent"
}
tooltip = folium.features.GeoJsonTooltip(
fields=("val",),
aliases=("Population:",)
)
tooltip_layer = folium.features.GeoJson(
population_grid,
style_function=style_function,
tooltip=tooltip
)
tooltip_layer.add_to(interactive_map)
interactive_map