본문 바로가기

데이터와 함께 탱고를/데이터 시각화

[지도 데이터 시각화] Part 4. Folium 살펴보기

첫 번째로 살펴볼 시각화 패키지는 Folium 입니다.
leaflet.js 기반으로 지도를 그려주고, 모바일에서도 쓸 수 있을만큼 가볍습니다.
나온지도 다른 라이브러리들에 비해 상대적으로 오래된 패키지입닌다만, 그만큼 안정적입니다.
한편, 분석 프레임워크로 가장 많이쓰는 pandas 와 쉽게 연동되어 간편하게 시각화가 가능합니다.
파이썬에서 지도 시각화 하면 가장 유명한 패키지일지도 모르겠습니다.

자 그럼 시작해보겠습니다.

1. 설치

먼저 pip install 로 folium 을 설치하고 임포트해줍니다.

pip install folium
import folium
folium.__version__
'0.9.0'

2. 샘플 하나 해보기

https://python-visualization.github.io/folium/quickstart.html#Getting-Started

위 링크로 가시면 어떻게 사용하는지 문서화가 아주 잘되있습니다.
이 글에서는 제가 미리 만둘어둔 데이터를 통해 어떤 식으로 데이터를 시각화하는지 살펴보겠습니다.

import pandas as pd

# 미리 만들어둔 데이터를 불러옵니다.
df = pd.read_csv('data/older_population.csv')
df.head()

인구 남자 여자
0 종로구 사직동 9700 4375 5325
1 종로구 삼청동 3013 1443 1570
2 종로구 부암동 10525 5002 5523
3 종로구 평창동 18830 8817 10013
4 종로구 무악동 8745 4078 4667

샘플로 쓸 데이터는 서울시 행정동 단위의 고령 인구 데이터입니다.
구, 동 별로 데이터가 있는 것을 확인하실 수 있습니다.

이번엔 행정동 단위의 지도 데이터 파일을 지정해보겠습니다.

geo_data = 'data/seoul-dong.geojson'

이 데이터를 열어보면 다음과 같이 생겼습니다.

{
  "type": "FeatureCollection",
  "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
  "features": [{ 
    "type": "Feature", 
    "properties": { 
      "시": "서울특별시", 
      "구": "종로구", 
      "동": "사직동", 
      "행정동코드": 11110530 
    }, 
    "geometry": { 
      "type": "MultiPolygon", 
      "coordinates": [ [ [ [ 126.976888842748167, 37.575650779448786 ], [ 126.977034498877501, 37.569194530054546 ], [ 126.975974728212492, 37.569336299425764 ], ...] ] ] } 
      }, { 
    "type": "Feature", 
    "properties": { 
      "시": "서울특별시", 
      "구": "종로구", 
      "동": "삼청동", 
      "행정동코드": 11110540
    }, 
    "geometry": { 
      "type": "MultiPolygon",
      "coordinates": [ [ [ [ 126.98268938649305, 37.595065519422398 ], [ 126.983372584569992, 37.594351925516882 ], [ 126.983868097928024, 37.593850468126433 ], ... ] ] ] 
    },
    ...
  ]
}

features 에 각 동별 MultiPolygon 정보를 담고있는 일반적인 GeoJson 파일입니다.

이제 이 두 데이터로 인구수에 따른 Choropleth Map 을 만들어보겠습니다.

# 서울시 중심부의 위도, 경도 입니다.
center = [37.541, 126.986]

# 맵이 center 에 위치하고, zoom 레벨은 11로 시작하는 맵 m을 만듭니다.
m = folium.Map(location=center, zoom_start=10)

# Choropleth 레이어를 만들고, 맵 m에 추가합니다.
folium.Choropleth(
    geo_data=geo_data,
    data=df,
    columns=('동', '인구'),
    key_on='feature.properties.동',
    fill_color='BuPu',
    legend_name='노령 인구수',
).add_to(m)

# 맵 m을 출력합니다.
m

3. 동작 살펴보기

이제 어떻게 동작하는건지, 간단히 살펴보겠습니다.
먼저 folium 에서 데이터를 시각화하고 싶으면 항상 다음 2가지 파일을 준비해야합니다.

  • 지도 데이터 파일 (.geojson)
  • 시각화 하고자 하는 데이터 파일 (.csv 등)

여기서는 아래 두 파일이 이에 해당합니다.

geo_data = 'data/seoul-dong.geojson'
df = pd.read_csv('data/older_population.csv')

이후, Choropleth와 같은 레이어를 만들 때, 우리는 이 두 데이터를 파라미터로 넘겨줘야 합니다.
두 데이터는 각자 다른 파일에 있으므로, 시각화할 데이터를 지도에 얹으려면 두 데이터를 매핑해야 합니다.
위에서 사용한 folium.Choropleth 를 예로 들어보겠습니다.

folium.Choropleth(
    geo_data = "지도 데이터 파일 경로 (.geojson, geopandas.DataFrame)"
    data = "시각화 하고자 하는 데이터파일. (pandas.DataFrame)"
    columns = (지도 데이터와 매핑할 값, 시각화 하고자하는 변수),
    key_on = "feature.데이터 파일과 매핑할 값",
    fill_color = "시각화에 쓰일 색상",
    legend_name = "칼라 범주 이름",
).add_to(m)

추가적인 설명을 더하자면 다음과 같습니다.

  • columns 에는 tuple 형태의 2개의 값만 들어가야 합니다.
    • 하나는 geo_data 의 데이터 변수(열) 중, data 의 데이터와 매핑할 변수입니다.
    • 즉 아래 key_on 값과 매핑할 변수입니다.
    • 위 예의 경우, 으로 매핑하고 있습니다.
    • 2개 값의 순서는 상관없습니다.
  • key_on 에는 하나의 String 값이 들어가야 합니다.
    • 위에 .geojson 파일(geo_data)을 보면, features 라는 리스트 안에 데이터 하나하나가 들어있습니다.
    • 각 데이터의 속성(변수)는 properties 라는 dict 형태 안에 저장되어 있습니다.
    • 따라서, 위 예에서 feature.properties.동 으로 접근하여 매칭한 것입니다.
  • fill_color 는 종류가 정해져있는데, 이는 추후 설명하겠습니다.
  • geo_data 는 url 이 올 수도 있고, geopandas 로 읽은 데이터프레임이 올 수도 있습니다.

위 내용을 이해하면 Folium 의 다른 레이어들의 파라미터도 이해하기 쉽습니다.

이해를 응용해보겠습니다.
동 단위가 아니라, 구 단위로 데이터를 매핑하고 싶으면 다음과 같이 수정하면 됩니다.

# 먼저 인구를 구 단위로 묶습니다.
df_adm = df.groupby(['구'])['인구'].sum().to_frame().reset_index()

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.Choropleth(
    geo_data=geo_data,
    data=df_adm, # 여기가 바뀌었습니다.
    columns=('구', '인구'), # 여기가 바뀌었습니다.
    key_on='feature.properties.구', # 여기가 바뀌었습니다.
    fill_color='BuPu',
    legend_name='노령 인구수',
).add_to(m)

m

4. 다른 레이어들 살펴보기

기본적인 레이어들을 간단히 살펴보겠습니다.

4.1. Point 단위

Circle

지도에 각 포인트 단위로 원을 그려냅니다.
다음과 같이 구성되어있습니다.

folium.Circle(location, radius, popup=None, tooltip=None, **kwargs)
    location: tuple[float, float]
        Latitude and Longitude pair (Northing, Easting)
    popup: string or folium.Popup, default None
        Input text or visualization for object displayed when clicking.
    tooltip: str or folium.Tooltip, default None
        Display a text when hovering over the object.
    radius: float
        Radius of the circle, in meters.
    **kwargs
        Other valid (possibly inherited) options.

이 레이어는 Choropleth 와 다르게 geojson 을 째로 넘겨주지 않습니다.
대신, location 에 Point 를 리스트 형태로 전달합니다.

예로 서울시 공중 화장실 데이터를 시각화해보겠습니다.

df = pd.read_csv('data/toilet_seoul.csv')
df = df[['고유번호', '위도', '경도']]

df.head(3)

고유번호 위도 경도
0 92 37.501401 127.158647
1 93 37.644937 127.073728
2 94 37.640335 127.077637
center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.Circle(
    location = df.loc[0, ['위도', '경도']],
    tooltip = df.loc[0, '고유번호'],
    radius = 300
).add_to(m)

m

만약 여러 개의 원을 동시에 찍고 싶다면, 아래와 같이 반복문을 사용해야 합니다.
이 때 주의해야할 것이 있습니다.
시각화 하는 데이터가 너무 많으면, 브라우저에 따라 쥬피터에 렌더링이 되지 않습니다.
이게 Folium 의 엄청난 단점입니다.

방법이 있긴 합니다.
아래 코드처럼, 데이터 일부만 샘플링해서 시각화 하거나,
.to_html() 을 사용해서 쥬피터 환경 밖에서 html 파일을 열면 렌더링된 화면이 나옵니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

# 1000 개의 데이터만 그려냅니다.
for i in df.index[:1000]:
    folium.Circle(
        location = df.loc[i, ['위도', '경도']],
        tooltip = df.loc[i, '고유번호'],
        radius = 200
    ).add_to(m)
m

Circle Marker

Circle 과 파라미터가 동일하되, location Point 를 중심으로 radius 만큼의 원을 그립니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.CircleMarker(
    location = df.loc[0, ['위도', '경도']],
    tooltip = df.loc[0, '고유번호'],
    radius = 100
).add_to(m)

m

4.2. Path 단위

Path 단위로 데이터를 다루는 레이어들은 한 데이터에 여러 개의 Point 들이 리스트 형태로 들어갑니다.
아래에서 하나씩 살펴보겠습니다.

PolyLine

# Point 들로 이루어진 리스트를 하나 만들겠습니다.
lines = df[['위도', '경도']].values[:5].tolist()
lines
[[37.50140089999999, 127.1586471],
 [37.644936799999996, 127.0737283],
 [37.6403353, 127.0776372],
 [37.645723499999995, 127.02044939999999],
 [37.6597831, 127.0311865]]
center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.PolyLine(
    locations = lines,
    tooltip = 'PolyLine'
).add_to(m)

m

Rectangle

bounds 파라미터에 Points 리스트를 주면, 해당 데이터들의 위치 bound 박스를 그려냅니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.Rectangle(
    bounds = lines,
    tooltip = 'Rectangle'
).add_to(m)

m

Polygon

locations 에 Points 리스트를 주면, 해당 Point 들을 꼭지점 삼아 Polygon 을 만듭니다.
이 때, fill = True 로 파라미터를 주어야, 해당 폴리곤에 색이 칠해져서 나옵니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.Polygon(
    locations = lines,
    fill = True,
    tooltip = 'Polygon'
).add_to(m)

m

4.3. 추가적인 파라미터

위에서 설명한 레이어들은 다음과 같이 추가적인 파라미터를 줌으로써, 좀 더 섬세하게 시각화를 스타일링할 수 있습니다.

  • stroke (Bool, True)
    Whether to draw stroke along the path. Set it to false to disable borders on polygons or circles.
  • color (str, '#3388ff')
    Stroke color.
  • weight (int, 3)
    Stroke width in pixels.
  • opacity (float, 1.0)
    Stroke opacity.
  • line_cap (str, 'round' (lineCap))
    A string that defines shape to be used at the end of the stroke.
  • line_join (str, 'round' (lineJoin))
    A string that defines shape to be used at the corners of the stroke.
  • dash_array (str, None (dashArray))
    A string that defines the stroke dash pattern. Doesn’t work on Canvas-powered layers in some old browsers.
  • dash_offset (str, None (dashOffset))
    A string that defines the distance into the dash pattern to start the dash. Doesn’t work on Canvas-powered layers in some old browsers.
  • fill (Bool, False)
    Whether to fill the path with color. Set it to false to disable filling on polygons or circles.
  • fill_color (str, default to color (fillColor))
    Fill color. Defaults to the value of the color option.
  • fill_opacity (float, 0.2 (fillOpacity))
    Fill opacity.
  • fill_rule (str, 'evenodd' (fillRule))
    A string that defines how the inside of a shape is determined.
  • bubbling_mouse_events (Bool, True (bubblingMouseEvents))
    When true a mouse event on this path will trigger the same event on the map (unless L.DomEvent.stopPropagation is used).

예를 들어, 같이 파라미터를 추가하면 됩니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.CircleMarker(
    location = df.loc[0, ['위도', '경도']],
    tooltip = df.loc[0, '고유번호'],
    weight = 10, # 추가한 부분
    radius = 100
).add_to(m)

m

4.4. 이 외 Plugins Layer

위에서 소개한 레이어들 외에도 plugins 라는 패키지 내에 더 다양한 레이어들이 있습니다.
이에 대한 소개는 다음 두 포스트를 참고하세요.

folium 의 plugins 패키지 샘플 살펴보기
https://dailyheumsi.tistory.com/85

folium 의 plugins 패키지 샘플 살펴보기 2
https://dailyheumsi.tistory.com/92

5 맵 스타일링

맵 전반 스타일링에 대해 하나씩 살펴보겠습니다.

5.1. Map

View 관련 파라미터

우리가 맨 처음 지도를 그리게 되면, 어떤 모습으로 지도는 나타나게 됩니다.
구체적으로는, 현재 지도의 중심점, 줌 레벨, 좌우 각도, 상하 각도 등을 정해야 합니다.
이러한 설정들을 Map 객체를 초기화할 때 파라미터로 넘겨주어야 합니다.
잠깐 Map 을 살펴보면 다음과 같습니다.

folium.Map(
    location=None,
    width='100%',
    height='100%',
    left='0%',
    top='0%',
    position='relative',
    tiles='OpenStreetMap',
    attr=None,
    min_zoom=0,
    max_zoom=18,
    zoom_start=10,
    min_lat=-90,
    max_lat=90,
    min_lon=-180,
    max_lon=180,
    max_bounds=False,
    crs='EPSG3857',
    control_scale=False,
    prefer_canvas=False,
    no_touch=False,
    disable_3d=False,
    png_enabled=False,
    zoom_control=True,
    **kwargs,
)

왠만한 파라미터들은 다 직관적인 이름이라 설명안해도 될거라 생각합니다.
거의 필수적으로 설정해줘야 하는 파라미터는 location, zoom_start 정도입니다.

Basemap

데이터 밑에 깔리는 기본 basemap 을 tiles 파라미터를 통해 다른 스타일로 바꿀 수 있습니다.
예를 들어 다음과 같이 바꿀 수 있습니다.

center = [37.541, 126.986]
m = folium.Map(location=center, 
               tiles='cartodbpositron',
               zoom_start=10)
m

tiles 에 줄 수 있는 스타일은 다음과 같이 정의되어 있습니다.

- Open street map (기본 값입니다.)
- Map Quest Open
- MapQuest Open Aerial
- Mapbox Bright
- Mapbox Control Room
- Stamenterrain
- Stamentoner
- Stamenwatercolor
- cartodbpositron
- cartodbdark_matter

각 스타일이 어떤 느낌인지 보시고 싶으면 아래 링크를 참고하세요.

folium-map-tiles
https://deparkes.co.uk/2016/06/10/folium-map-tiles/

5.2. Color scheme

아까 choropleth 레이어를 시각화할 때, 그냥 넘어간 부분이 있습니다.
바로 fill_color 파라미터인데요. 이전에는 그냥 BuPu 라는 값을 주고 넘어갔었습니다.
그럼 이제 본격적으로 이 Color 들이 어떤 것들이 있는지 살펴보겠습니다.

여기에 들어가는 색상 값들은 folium 내 utilities 라는 파일에 이미 정의되어 있습니다.
다음과 같이 정의되어 있습니다.

'BuGn': 'Sequential',
'BuPu': 'Sequential',
'GnBu': 'Sequential',
'OrRd': 'Sequential',
'PuBu': 'Sequential',
'PuBuGn': 'Sequential',
'PuRd': 'Sequential',
'RdPu': 'Sequential',
'YlGn': 'Sequential',
'YlGnBu': 'Sequential',
'YlOrBr': 'Sequential',
'YlOrRd': 'Sequential',
'BrBg': 'Diverging',
'PiYG': 'Diverging',
'PRGn': 'Diverging',
'PuOr': 'Diverging',
'RdBu': 'Diverging',
'RdGy': 'Diverging',
'RdYlBu': 'Diverging',
'RdYlGn': 'Diverging',
'Spectral': 'Diverging',
'Accent': 'Qualitative',
'Dark2': 'Qualitative',
'Paired': 'Qualitative',
'Pastel1': 'Qualitative',
'Pastel2': 'Qualitative',
'Set1': 'Qualitative',
'Set2': 'Qualitative',
'Set3': 'Qualitative'

예를 들어 YlGn 으로 바꾸면 다음과 같이 나옵니다.

center = [37.541, 126.986]
m = folium.Map(location=center, zoom_start=10)

folium.Choropleth(
    geo_data=geo_data,
    data=df_adm, 
    columns=('구', '인구'), 
    key_on='feature.properties.구',
    fill_color='YlGn', # 여기가 바뀌었습니다.
    legend_name='노령 인구수',
).add_to(m)

m

각 color scheme 에 해당하는 색상은 아래 링크에 가보시면 직접 확인하실 수 있습니다.

colorbrewer2
http://colorbrewer2.org/

정의된 색상 외에 커스텀으로 색상을 만드는 법은 조금 까다롭습니다. (그래도 굳이 원한다면 할만은 합니다.)
여기서 다루지는 않고, 링크로 대체하겠습니다.

Folium custom colorscheme
https://nbviewer.jupyter.org/github/python-visualization/folium/blob/v0.2.0/examples/Colormaps.ipynb

6. 마무리

이렇게 Folium 에 대해 살펴봤습니다.
이 이상 더 무엇이 있는가하면, 사실 그렇게 많지는 않습니다.
Folium 으로 커버할 수 있는 내용은 이게 대부분이라고 생각합니다.
그만큼 단순하고, 빠른 시각화를 위한 용도에 알맞습니다.

만약 이보다 더 복잡하고, Fancy 한 지도 데이터 시각화를 하고 싶다면,
다른 시각화 패키지를 사용하시는 걸 추천 드립니다.

다음 포스팅에서는 더 멋지게 스타일링할 수 있는 패키지인 Mapboxgl-jupyter 를 살펴보겠습니다.