본문 바로가기

코딩

Plotly를 활용하여 지도 시각화 하기 (Choropleth Map 그리기)

python의 plotly 패키지를 활용하여 아래와 같은 choropleth map을 그리는 방법을 소개하겠습니다.

 

[시군구별 인구 현황_'23년 8월]

 

 

1. 준비물

① 시군구별 인구 데이터

아래 링크에서 제공하는 csv 파일을 활용합니다.

https://jumin.mois.go.kr/index.jsp#

 

주민등록 인구통계 행정안전부

 

jumin.mois.go.kr

전체시군구현황 체크 → csv파일 다운로드

② GeoJSON 파일

아래 zip 파일에서 법정구역_시군구_simplified.geojson 파일을 사용합니다.

https://drive.google.com/file/d/1VkVQJcyEUq2KW9E9mOUeSDIoiZibVcaG/view?usp=sharing

 

법정구역 GeoJSON 데이터_23년8월.zip

 

drive.google.com

대한민국 법정구역 정보를 담고있는 GeoJSON 파일을 직접 만드는 방법이 궁금한 분들은 아래 포스트를 참고해주세요.

https://jgws.tistory.com/9

 

대한민국 법정구역 SHP 파일을 GeoJSON으로 변환하기

튜토리얼 없이 GeoJSON파일만 필요한 분들은 아래 링크로 다운받으시면 됩니다. https://drive.google.com/file/d/1VkVQJcyEUq2KW9E9mOUeSDIoiZibVcaG/view?usp=sharing 법정구역 GeoJSON 데이터_23년8월.zip drive.google.com 튜토

jgws.tistory.com

 

 

2. 데이터 준비

pandas를 활용하여 다운받은 데이터를 전처리해줍니다.

import pandas as pd

df = pd.read_csv('202308_202308_주민등록인구및세대현황_월간.csv',encoding='cp949')
# csv파일 읽어오기 (공공데이터는 웬만하면 encoding이 cp949인 것 같습니다.)

df = df[df['2023년08월_총인구수']!= '0'].copy()
# 인구정보가 없는 행을 삭제합니다.

df['2023년08월_총인구수']= df['2023년08월_총인구수'].str.replace(",","").astype('int')
# 총인구수가 str으로 저장되어있기 때문에 int 형식으로 바꾸어줍니다.
# '140,032' → 140032
def split_code_name(x):
    splited = x.split('(')
    return pd.Series([splited[0].strip(" "), splited[1][:5]])

df[['구역명','구역코드']] = df['행정구역'].apply(lambda x: split_code_name(x))

행정구역 열에 행정구역명과 코드가 함께 str로 작성되어있으므로 이를 구역명과 구역코드로 나눠주는 함수를 작성하여 적용합니다.

df['시도명'] = df['구역명'].str.split(' ').apply(lambda x: x[0])
# 구역명에서 시도명을 구분해줍니다. (ex. 서울특별시 종로구 → 서울특별시)

def abbreviate_province(name):
    if name.endswith('북도') or name.endswith('남도'):
        return name[0] + name[2]
    else:
        return name[:2]
 # 시도명을 축약해주는 함수입니다. (ex. 서울특별시 → 서울, 경상남도 → 경남)
 
df['시도명(축약)'] = df['시도명'].apply(lambda x: abbreviate_province(x))
# 위 함수를 적용해줍니다.
import json

file = open('법정구역_시군구_simplified.geojson', encoding='utf-8')
gjson = json.load(file)
# geojson 파일을 읽어옵니다.

geojson 파일은 여러개의 Feature 데이터로 이루어져있으며, 각각의 Feature는 행정구역코드, 행정구역명 등의 properties와 좌표정보를 담고 있는 geometry로 구성되어있습니다.

GeoJSON 파일의 구조

시도_in_gjson = {feat['properties']['SIG_CD'] : feat['properties']['SIG_KOR_NM'] for feat in gjson['features']}
# GeoJSON에 포함된 feature들의 SIG_CD(행정구역코드) 와 SIG_KOR_NM(행정구역명)을 dictionary로 가져옵니다.

df['구역명확인'] = df['구역코드'].map(시도_in_gjson)
# df.구역코드에 들어있는 코드들이 geojson에도 동일하게 포함되어있는지 확인합니다.
# geojson에서 코드가 확인되지 않는 행들은 nan값을 가지게 됩니다.

not_found = df[df['구역명확인'].isna()].copy()

GeoJSON에는 없는 구역

GeoJSON은 시군구 데이터로 이루어져있기 때문에 서울특별시, 경기도 등 시도 데이터는 없는 것을 확인할 수 있습니다.

또한 수원시, 성남시 등은 수원시 장안구, 수원시 영통구, 성남시 분당구, 성남시 수정구 등 구단위로 나뉘어져있기 때문에 시로 합쳐진 구역데이터는 GeoJSON에 없습니다.

df = df[df['구역명확인'].notna()].copy().reset_index(drop=True)
# GeoJSON에 없는 구역들은 삭제해줍니다.

 

 

3. 지도로 시각화 하기 (Choropleth Map 그리기)

import plotly.graph_objects as go

fig = go.Figure(data=go.Choropleth(geojson=gjson,
                                    featureidkey='properties.SIG_CD',#GeoJSON에서 활용할 id키
                                    locationmode= 'geojson-id',
                                    locations=df['구역코드'],
                                    z = df['2023년08월_총인구수'],
                                    colorscale = [[0, '#F1EFEF'], [0.2, '#CCC8AA'],   [0.4, '#B0AA7C'],  [0.6, '#7D7C7C'], [0.8,'#606060'], [1, '#191717']],
                                    # 컬러스케일은 원하는 갯수의 색상으로 구성할 수 있습니다.
                                    hovertext = df['구역명(최종)']+": " +df['2023년08월_총인구수'].apply(lambda x: f'{x:,} 명'),
                                    hoverinfo = 'text',
                                    marker_line_color='#555',
                                    # marker_line : 행정구역의 경계선
                                    marker_line_width=1,                            
                                    )
                )
                
 fig.update_layout(geo = dict(fitbounds="locations", # 설정하지 않으면 세계지도가 표시됩니다
                             visible = False, # 설정하지 않으면 세계지도가 표시됩니다
                             showframe=False,
                             ),
                  margin={"r":0,"t":0,"l":0,"b":0},
                  )
                  
html = fig.to_html()
with open('시군구별 인구 현황_202308.html', "w", encoding="utf-8") as f:
    f.write(html)
# html 파일로 export 해줍니다.

생성된 html파일은 아래와 같습니다. 마우스를 지도에 가져다대면 해당 구역의 인구수가 표시되며 휠로 지도 크기를 조정할 수 있습니다. 원래 크기로 돌아오려면 마우스로 더블클릭 해줍니다.

[시군구별 인구 현황_202308.html]

 

우측의 칼라바가 거슬리는 경우 아래와 같이 정리해줄 수 있습니다.

fig.update_traces(colorbar = dict(len=0.4,
                                  thickness=10,
                                  dtick = 300000,
                                  orientation = 'h',
                                  exponentformat= "none", 
                                  separatethousands= True,
                                  )
                    )

[시군구별 인구 현황_v2_202308.html]

 

디자인 요소를 추가하고 싶은 경우 차트를 html div로 출력하여 html 문서 내에 포함시킬 수 있습니다.

import plotly.offline as pyo

chart_div = pyo.plot(fig, include_plotlyjs=True, output_type='div')
title = '시군구별 인구 현황_202308'

dashboard = open('시군구별 인구 현황_v3_202308.html', 'w', encoding='utf-8')
dashboard.write(f'''
<html>
    <meta charset="UTF-8">
    <style>
        body {{
            background-color: white;
            margin : auto;
            width : 80%;
            max-width : 800px;
        }}
        .title {{
            text-align: left;
            margin: 10px 0;
            padding: 0 20px;
            font-family: '맑은 고딕';
            font-weight: bold;
            border-left: 3px solid #C00000;
                
        }}
        .chart {{
            padding: 10px 0; 
            font-family: '맑은 고딕';
        }}
    </style>
    <body>
        <div class = 'title'>{title}
        </div>
        <hr> 
        <div class = 'chart'>{chart_div}
        </div>
    </body>
</html>
''')

[시군구별 인구 현황_v3_202308.html]