🔥알림🔥
① 테디노트 유튜브 - 구경하러 가기!
② LangChain 한국어 튜토리얼 바로가기 👀
③ 랭체인 노트 무료 전자책(wikidocs) 바로가기 🙌

5 분 소요

지도위에 데이터를 interactive하게 표현해 주는 대표적인 파이썬 지도 시각화 라이브러리 folium에 대해서 알아보고 그 사용법을 파헤쳐보도록 하겠습니다.

folium 개요

folium은 leaflet.js 기반으로 만들어진 Python 지도 시각화 라이브러리 입니다.

도큐먼트

folium을 사용하여 인터랙티브한 지도를 생성하고 마커를 추가하여 시각화하거나 원으로 범위를 표기하고 html 파일로 내보내기 등을 수행할 수 있습니다.

folium 설치

pip install folium으로 라이브러리를 설치할 수 있습니다.

!pip install folium

모듈 import

import folium

기본 좌표 설정

location에 위도, 경도 정보를 입력하여 입력한 위,경도 좌표를 기준으로 지도를 그릴 수 있습니다.

이때 zoom_start 정보를 지정하여 확대의 정도를 지정할 수 있습니다.

  • 참고: zoom_start의 범위는 최대 18 입니다.
# 위도
latitude = 37.394946
# 경도
longitude = 127.111104
m = folium.Map(location=[latitude, longitude],
               zoom_start=17, 
               width=750, 
               height=500
              )
m

image-20211108010122376

마커 추가

  • location: 마커를 추가할 위도/경도 좌표를 입력 후

  • popup: 표기할 팝업 문구 지정 (마우스 클릭시 표기되는 문구)

  • tooltip: 표기할 툴팁 지정 (마우스 오버시 표기되는 문구)

마커를 생성 후 기존에 생성된 지도 m에 추가합니다.

folium.Marker([latitude, longitude],
              popup="판교역",
              tooltip="판교역 입구").add_to(m)
m

image-20211108010023935

마커에 대한 스타일 변경도 가능합니다. 스타일 변경시 icon 파라미터에 folium.Icon(color=?, icon=?)을 지정합니다.

folium.Marker([latitude, longitude],
              popup="판교역",
              tooltip="판교역 입구", 
              icon=folium.Icon('red', icon='star'),
             ).add_to(m)
m

image-20211108010204489

popup이나 tooltip에 다음과 같이 html 코드를 삽입하여 이미지를 표기하거나 심지어 YouTube 영상도 삽입할 수 있습니다.

folium.Marker([latitude, longitude],
              popup='<iframe width="560" height="315" src="https://www.youtube.com/embed/dpwTOQri42s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
              tooltip="판교역 입구").add_to(m)
m

image-20211108010231136

folium.CircleMarker([latitude, longitude],
                    color='tomato',
                    radius = 50, 
                    tooltip='판교역 상권').add_to(m)
m

image-20211108010255073

import pandas as pd
df = pd.read_csv('소상공인시장진흥공단_상가(상권)정보_경기_202109.csv')
df.head(3)
상가업소번호 상호명 지점명 상권업종대분류코드 상권업종대분류명 상권업종중분류코드 상권업종중분류명 상권업종소분류코드 상권업종소분류명 표준산업분류코드 ... 구우편번호 신우편번호 동정보 층정보 호정보 전화번호 경도 위도 상권번호 데이터기준일자
0 20713599 눈높이러닝센타 NaN R 학문/교육 R13 학문교육기타 R13A01 학습지보급 P85503 ... 423010.0 14221.0 NaN NaN 2 02-2066-9109 126.858147 37.478530 NaN 2016-01-26
1 20642964 유니베라 구성대리점 D 소매 D16 화장품소매 D16A01 화장품판매점 G47813 ... 446525.0 16919.0 NaN 2 NaN 031-8005-7071 127.116575 37.294690 NaN 2021-10-22
2 24510829 엠아이케이21 NaN D 소매 D14 운동/경기용품소매 D14A01 운동/경기용품 G47631 ... 463937.0 13622.0 901 NaN 90 070-8699-8912 127.123291 37.340903 NaN 2016-01-26

3 rows × 42 columns

pd.Series(df.columns)
0        상가업소번호
1           상호명
2           지점명
3     상권업종대분류코드
4      상권업종대분류명
5     상권업종중분류코드
6      상권업종중분류명
7     상권업종소분류코드
8      상권업종소분류명
9      표준산업분류코드
10      표준산업분류명
11         시도코드
12          시도명
13        시군구코드
14         시군구명
15        행정동코드
16         행정동명
17        법정동코드
18         법정동명
19        PNU코드
20       대지구분코드
21        대지구분명
22        지번본번지
23        지번부번지
24         지번주소
25        도로명코드
26          도로명
27        건물본번지
28        건물부번지
29       건물관리번호
30          건물명
31        도로명주소
32        구우편번호
33        신우편번호
34          동정보
35          층정보
36          호정보
37         전화번호
38           경도
39           위도
40         상권번호
41      데이터기준일자
dtype: object
sub_df = df.loc[df['행정동명'].isin(['백현동', '정자동', '삼평동'])]
sub_df.head(3)
상가업소번호 상호명 지점명 상권업종대분류코드 상권업종대분류명 상권업종중분류코드 상권업종중분류명 상권업종소분류코드 상권업종소분류명 표준산업분류코드 ... 구우편번호 신우편번호 동정보 층정보 호정보 전화번호 경도 위도 상권번호 데이터기준일자
35 24527550 코리아세븐분당 정자3호점 D 소매 D03 종합소매점 D03A01 편의점 G47122 ... 463834.0 13612.0 NaN 1 NaN 031-718-9733 127.113508 37.362807 NaN 2016-01-27
102 28523933 처가방 NaN Q 음식 Q10 별식/퓨전요리 Q10A02 샤브샤브전문 I56111 ... 463420.0 13529.0 NaN NaN NaN 031-5170-1908 127.112071 37.392785 NaN 2016-11-17
244 20786763 백현 NaN L 부동산 L01 부동산중개 L01A01 부동산중개 L68221 ... 463887.0 13532.0 NaN 1 1 031-8016-8100 127.110756 37.389333 NaN 2017-10-02

3 rows × 42 columns

sub_df[['위도', '경도', '상호명']]
위도 경도 상호명
35 37.362807 127.113508 코리아세븐분당
102 37.392785 127.112071 처가방
244 37.389333 127.110756 백현
284 37.401265 127.108639 야쿤커피앤토스트판교점
678 37.368770 127.112015 해동검도
... ... ... ...
514805 37.397736 127.111414 에세이스튜디오
515501 37.362357 127.113513 아몽옷고치는전문집
515824 37.384859 127.111408 티랩
516332 37.392951 127.118687 우디크리빙
516457 37.395832 127.113503 써브웨이

3322 rows × 3 columns

from folium.plugins import MarkerCluster


m = folium.Map(
    location=[latitude, longitude],
    zoom_start=15
)

coords = sub_df[['위도', '경도']]


marker_cluster = MarkerCluster().add_to(m)

for lat, long in zip(coords['위도'], coords['경도']):
    folium.Marker([lat, long], icon = folium.Icon(color="green")).add_to(marker_cluster)
m

image-20211108010346971

서울 지도에서 행정 구역별 표시

import requests
import json

# 서울 행정구역 json raw파일(githubcontent)
r = requests.get('https://raw.githubusercontent.com/southkorea/seoul-maps/master/kostat/2013/json/seoul_municipalities_geo_simple.json')
c = r.content
seoul_geo = json.loads(c)

서울 지역의 구별 boundary 시각화

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=11, 
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

m

image-20211108010409003

tiles 옵션 변경을 통해 지도의 테마 변경

지도의 기본 테마(tiles)는 OpenStreetMap으로 설정되어 있는데, 이를 변경하여 다른 지도 테마를 적용할 수 있다.

Stamen Toner 적용시

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=11, 
    tiles='Stamen Toner'
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

m

image-20211108010437892

cartodbpositron 적용시

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=11, 
    tiles='cartodbpositron'
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

m

image-20211108010459001

서울시 자치구별 상권정보 시각화

seoul 데이터프레임에 소상공인시장진흥공단에서 제공하는 서울시 상권정보 csv 파일을 로드합니다.

seoul = pd.read_csv('소상공인시장진흥공단_상가(상권)정보_서울_202109.csv')

# 필요한 컬럼 정보만 가져옵니다
seoul = seoul[['시군구명', '상권업종대분류명', '상권업종중분류명', '위도', '경도']]
seoul
시군구명 상권업종대분류명 상권업종중분류명 위도 경도
0 송파구 소매 의복의류 37.493054 127.147321
1 영등포구 소매 건강/미용식품 37.520613 126.907168
2 성동구 소매 취미/오락관련소매 37.566857 127.049018
3 동작구 음식 한식 37.487105 126.980952
4 종로구 음식 한식 37.572387 126.981794
... ... ... ... ... ...
325875 마포구 부동산 부동산중개 37.557971 126.907290
325876 은평구 소매 시계/귀금속소매 37.604195 126.936049
325877 은평구 소매 애견/애완/동물 37.596790 126.905613
325878 광진구 음식 커피점/카페 37.556004 127.085023
325879 강북구 생활서비스 이/미용/건강 37.629348 127.017622

325880 rows × 5 columns

시각화 모듈을 import 하고 서울시의 업종별 개수를 시각화합니다.

import matplotlib.pyplot as plt
import seaborn as sns

# 한글 폰트 설정
plt.rcParams['font.family'] = 'NanumGothic'

plt.figure(figsize=(12, 30))
sns.countplot(y=seoul['상권업종중분류명'], order=seoul['상권업종중분류명'].value_counts().index)
plt.yticks(fontsize=12)
plt.title('서울시 업종별 개수')
plt.show()

한식 업종이 가장 많은 개수를 차지합니다. 커피점/카페 업종이 가장 많은 업종일 줄 알았는데요. 한식, 이미용/건강, 종합소매업 다음 4위를 차지하였습니다.

그럼 커피점/카페 업종은 자치구별로 어느 곳에 가장 많이 분포해 있는지 시각화해 보겠습니다.

plt.figure(figsize=(12, 10))
seoul.loc[seoul['상권업종중분류명'] == '커피점/카페'].groupby('시군구명')['상권업종대분류명'].count()\
                                                     .sort_values().plot(kind='barh', color='royalblue')
plt.yticks(fontsize=12)
plt.title('서울시 자치구별 커피점/카페 업종수')
plt.show()

강남구가 가장 많은 수를 차지하고 강북구는 가장 적은 수를 차지하였습니다.

이제 이를 folium 위에 시각화를 해보겠습니다.

seoul_coffee = seoul.loc[seoul['상권업종중분류명'] == '커피점/카페']
seoul_coffee
시군구명 상권업종대분류명 상권업종중분류명 위도 경도
29 종로구 음식 커피점/카페 37.583149 127.000171
109 도봉구 음식 커피점/카페 37.658728 127.034746
190 마포구 음식 커피점/카페 37.554599 126.929692
200 강서구 음식 커피점/카페 37.580941 126.813358
206 양천구 음식 커피점/카페 37.522055 126.842935
... ... ... ... ... ...
325715 서대문구 음식 커피점/카페 37.557293 126.950705
325754 금천구 음식 커피점/카페 37.468602 126.902047
325788 마포구 음식 커피점/카페 37.540241 126.948240
325791 강서구 음식 커피점/카페 37.532109 126.839080
325878 광진구 음식 커피점/카페 37.556004 127.085023

19703 rows × 5 columns

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=12, 
    tiles='cartodbpositron'
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

marker_cluster = MarkerCluster().add_to(m)

for lat, long in zip(seoul_coffee['위도'], seoul_coffee['경도']):
    folium.Marker([lat, long], icon = folium.Icon(color="green")).add_to(marker_cluster)

m

image-20211108010725839

seoul_group_data = seoul.loc[seoul['상권업종중분류명'] == '커피점/카페'].groupby('시군구명')['상권업종중분류명'].count()
seoul_group_data
시군구명
강남구     2253
강동구      763
강북구      330
강서구     1048
관악구      593
광진구      771
구로구      596
금천구      444
노원구      534
도봉구      373
동대문구     499
동작구      526
마포구     1427
서대문구     704
서초구     1277
성동구      640
성북구      668
송파구     1125
양천구      543
영등포구     859
용산구      730
은평구      575
종로구     1082
중구       895
중랑구      448
Name: 상권업종중분류명, dtype: int64

choropleth 를 사용하여 시각화를 하면 업종 별 개수에 따른 색상의 차이를 두어 시각화를 해줍니다.

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=11, 
    tiles='cartodbpositron'
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

m.choropleth(geo_data=seoul_geo,
             data=seoul_group_data, 
             fill_color='YlOrRd', # 색상 변경도 가능하다
             fill_opacity=0.5,
             line_opacity=0.2,
             key_on='properties.name',
             legend_name="지역구별 커피 업종 수"
            )
m

image-20211108010747723

bins를 만들어 1/4, 2/4, 3/4분위수별로 구간을 나누어 시각화할 수 있습니다.

bins = list(seoul_group_data.quantile([0, 0.25, 0.5, 0.75, 1]))

m = folium.Map(
    location=[37.559819, 126.963895],
    zoom_start=11, 
    tiles='cartodbpositron'
)

folium.GeoJson(
    seoul_geo,
    name='지역구'
).add_to(m)

m.choropleth(geo_data=seoul_geo,
             data=seoul_group_data, 
             fill_color='YlOrRd', # 색상 변경도 가능하다
             fill_opacity=0.5,
             line_opacity=0.2,
             key_on='properties.name',
             legend_name="지역구별 커피 업종 수", 
             bins=bins
            )
m

image-20211108010809526

Html 파일로 저장

저장은 save() 함수로 쉽게 html 파일로 저장할 수 있습니다.

m.save('map.html')

참고 (References)

댓글남기기