Plotly | 월드맵 시각화(1)

plotly
Author

강신성

Published

2023-12-11

이번엔 plotly로 그래픽스를 만들거다… ~(용량이 더럽게 커서 깃허브가 하지 말랜다)~

1. 라이브러리 imports

import pandas as pd
import numpy as np
import plotly.express as px  ## 코로플레스를 만들 수 있게 해준다.
import json
import requests

2. 에너지사용량 시각화

전국 에너지 사용량 정보와 코로플레스를 이용하여 시각화를 해보자.

A. 데이터 불러오기


global_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-provinces-2018-geo.json').text)
local_dict = json.loads(requests.get('https://raw.githubusercontent.com/southkorea/southkorea-maps/master/kostat/2018/json/skorea-municipalities-2018-geo.json').text)
#--#
url = 'https://raw.githubusercontent.com/guebin/DV2022/main/posts/Energy/{}.csv'
prov = ['Seoul', 'Busan', 'Daegu', 'Incheon',
        'Gwangju', 'Daejeon', 'Ulsan', 'Sejongsi',
        'Gyeonggi-do', 'Gangwon-do', 'Chungcheongbuk-do',
        'Chungcheongnam-do', 'Jeollabuk-do', 'Jeollanam-do',
        'Gyeongsangbuk-do', 'Gyeongsangnam-do', 'Jeju-do']
df = pd.concat([pd.read_csv(url.format(p+y)).assign(년도=y, 시도=p) for p in prov for y in ['2018', '2019', '2020', '2021']]).reset_index(drop=True)\
.assign(년도 = lambda df: df.년도.astype(int))\
.set_index(['년도','시도','지역']).applymap(lambda x: int(str(x).replace(',','')))\
.reset_index()
df.head()
/tmp/ipykernel_235844/2433387610.py:12: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map instead.
  .set_index(['년도','시도','지역']).applymap(lambda x: int(str(x).replace(',','')))\
년도 시도 지역 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
0 2018 Seoul 종로구 17929 9141777 64818 82015 111
1 2018 Seoul 중구 10598 10056233 81672 75260 563
2 2018 Seoul 용산구 17201 10639652 52659 85220 12043
3 2018 Seoul 성동구 14180 11631770 60559 107416 0
4 2018 Seoul 광진구 21520 12054796 70609 130308 0

지역 좌표 정보를 가진 데이터와, 지역의 에너지 사용량 관련 정보를 가진 데이터를 불러왔다.

### B. 데이터 정리(한글로 변환)

df의 영어로 된 시도global_dict를 이용하여 한글로 바꿔주자.

(1) global_dict 내의 영어이름과 df의 영어이름이 일치하는지 확인

set(df.시도) == {l['properties']['name_eng'] for l in global_dict['features']}  ## global_dict는 커다란 딕셔너리에 features가 있고, 그곳에 지역들의 리스트가 있다.
True
{l['properties']['name_eng'] for l in global_dict['features']}
{'Busan',
 'Chungcheongbuk-do',
 'Chungcheongnam-do',
 'Daegu',
 'Daejeon',
 'Gangwon-do',
 'Gwangju',
 'Gyeonggi-do',
 'Gyeongsangbuk-do',
 'Gyeongsangnam-do',
 'Incheon',
 'Jeju-do',
 'Jeollabuk-do',
 'Jeollanam-do',
 'Sejongsi',
 'Seoul',
 'Ulsan'}

딕셔너리의 key만으로 딕셔너리 컴프리헨션 하면 set()을 한 것처럼 알파벳 순서대로 key값만 나온다.(아니면 그냥 리스트 컴프리헨션 하고 set()걸어주던지…)

(2) global_dict 내의 영어이름과 한글이름을 이용해 변환을 위한 dictionary 생성

_dct = {l['properties']['name_eng'] : l['properties']['name'] for l in global_dict['features']}
_dct
{'Seoul': '서울특별시',
 'Busan': '부산광역시',
 'Daegu': '대구광역시',
 'Incheon': '인천광역시',
 'Gwangju': '광주광역시',
 'Daejeon': '대전광역시',
 'Ulsan': '울산광역시',
 'Sejongsi': '세종특별자치시',
 'Gyeonggi-do': '경기도',
 'Gangwon-do': '강원도',
 'Chungcheongbuk-do': '충청북도',
 'Chungcheongnam-do': '충청남도',
 'Jeollabuk-do': '전라북도',
 'Jeollanam-do': '전라남도',
 'Gyeongsangbuk-do': '경상북도',
 'Gyeongsangnam-do': '경상남도',
 'Jeju-do': '제주특별자치도'}

(3) df에 변환을 수행하여 영어지명을 한글지명으로 변환

df.assign(시도 = lambda _df : _df.시도.apply(lambda x : [v for k, v in _dct.items() if x == k].pop()))
년도 시도 지역 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
0 2018 서울특별시 종로구 17929 9141777 64818 82015 111
1 2018 서울특별시 중구 10598 10056233 81672 75260 563
2 2018 서울특별시 용산구 17201 10639652 52659 85220 12043
3 2018 서울특별시 성동구 14180 11631770 60559 107416 0
4 2018 서울특별시 광진구 21520 12054796 70609 130308 0
... ... ... ... ... ... ... ... ...
995 2019 제주특별자치도 서귀포시 34729 7233931 34641 1306 0
996 2020 제주특별자치도 제주시 66504 19819923 99212 22179 0
997 2020 제주특별자치도 서귀포시 34880 7330040 35510 1639 0
998 2021 제주특별자치도 제주시 67053 20275738 103217 25689 0
999 2021 제주특별자치도 서귀포시 35230 7512206 37884 2641 0

1000 rows × 8 columns

df.시도.map(_dct)  ## 람다 이것저것 안쓰고 이렇게 하면 한번에 할 수도 있다.
0        서울특별시
1        서울특별시
2        서울특별시
3        서울특별시
4        서울특별시
        ...   
995    제주특별자치도
996    제주특별자치도
997    제주특별자치도
998    제주특별자치도
999    제주특별자치도
Name: 시도, Length: 1000, dtype: object

(4) local_dictglobal_dict의 지명정보를 정리하여 데이터프레임으로 만듦

# 예비학습

pd.DataFrame(
    [{'X':100,'y':0},
     {'X':101,'y':1}]
)    ## 딕셔너리를 리스트로 넣어버리면 행이 분리된 채로 넣어줄수도 있다.
X y
0 100 0
1 101 1

위와 동일한 원리로 코드와 이름에 해당하는 애들을 추출함('properties'에는 지역에 대한 코드 정보가 딕셔너리로 포함되어 있음)

pd.DataFrame([l['properties'] for l in local_dict['features']])
name base_year name_eng code
0 종로구 2018 Jongno-gu 11010
1 중구 2018 Jung-gu 11020
2 용산구 2018 Yongsan-gu 11030
3 성동구 2018 Seongdong-gu 11040
4 광진구 2018 Gwangjin-gu 11050
... ... ... ... ...
245 함양군 2018 Hamyang-gun 38380
246 거창군 2018 Geochang-gun 38390
247 합천군 2018 Hapcheon-gun 38400
248 제주시 2018 Jeju-si 39010
249 서귀포시 2018 Seogwipo-si 39020

250 rows × 4 columns

df_local = pd.DataFrame([l['properties'] for l in local_dict['features']])\
.drop(['name_eng','base_year'],axis=1)
df_local
name code
0 종로구 11010
1 중구 11020
2 용산구 11030
3 성동구 11040
4 광진구 11050
... ... ...
245 함양군 38380
246 거창군 38390
247 합천군 38400
248 제주시 39010
249 서귀포시 39020

250 rows × 2 columns

df_global = pd.DataFrame([l['properties'] for l in global_dict['features']])\
.drop(['name_eng','base_year'],axis=1)
df_global
name code
0 서울특별시 11
1 부산광역시 21
2 대구광역시 22
3 인천광역시 23
4 광주광역시 24
5 대전광역시 25
6 울산광역시 26
7 세종특별자치시 29
8 경기도 31
9 강원도 32
10 충청북도 33
11 충청남도 34
12 전라북도 35
13 전라남도 36
14 경상북도 37
15 경상남도 38
16 제주특별자치도 39

(5) df_local에서 “전주시완산구”와 같이 정리된 지명들을 “완산구”로 변환

df_local.name
0       종로구
1        중구
2       용산구
3       성동구
4       광진구
       ... 
245     함양군
246     거창군
247     합천군
248     제주시
249    서귀포시
Name: name, Length: 250, dtype: object
_dct = {n : n.split('시')[-1] for n in df_local.name if '시' in n and len(n) > 3 and n[-1] == '구'}
_dct
{'수원시장안구': '장안구',
 '수원시권선구': '권선구',
 '수원시팔달구': '팔달구',
 '수원시영통구': '영통구',
 '성남시수정구': '수정구',
 '성남시중원구': '중원구',
 '성남시분당구': '분당구',
 '안양시만안구': '만안구',
 '안양시동안구': '동안구',
 '안산시상록구': '상록구',
 '안산시단원구': '단원구',
 '고양시덕양구': '덕양구',
 '고양시일산동구': '일산동구',
 '고양시일산서구': '일산서구',
 '용인시처인구': '처인구',
 '용인시기흥구': '기흥구',
 '용인시수지구': '수지구',
 '청주시상당구': '상당구',
 '청주시서원구': '서원구',
 '청주시흥덕구': '흥덕구',
 '청주시청원구': '청원구',
 '천안시동남구': '동남구',
 '천안시서북구': '서북구',
 '전주시완산구': '완산구',
 '전주시덕진구': '덕진구',
 '포항시남구': '남구',
 '포항시북구': '북구',
 '창원시의창구': '의창구',
 '창원시성산구': '성산구',
 '창원시마산합포구': '마산합포구',
 '창원시마산회원구': '마산회원구',
 '창원시진해구': '진해구'}

'시'가 들어간 이름들을 변환

df_local.set_index('name').rename(_dct).reset_index()  ## 인덱스 지정과 rename을 이용하여 딕셔너리를 활용함!!
name code
0 종로구 11010
1 중구 11020
2 용산구 11030
3 성동구 11040
4 광진구 11050
... ... ...
245 함양군 38380
246 거창군 38390
247 합천군 38400
248 제주시 39010
249 서귀포시 39020

250 rows × 2 columns

이제 시도라는 중복정보를 뺀 데이터프레임이 마련되었다.

(6) df_localdf_global의 정보를 정리하여 merge, 합쳐진 정보를 df_json에 저장

df_local.set_index('name').rename(_dct).reset_index()\
.rename({'code':'code_local','name':'name_local'},axis=1)\
.assign(code = lambda df: df.code_local.str[:2])

## 코드의 앞 두자리를 공통분모로 삼을 것이다. df_global에는 code 앞 두자리를 넣어놨음
name_local code_local code
0 종로구 11010 11
1 중구 11020 11
2 용산구 11030 11
3 성동구 11040 11
4 광진구 11050 11
... ... ... ...
245 함양군 38380 38
246 거창군 38390 38
247 합천군 38400 38
248 제주시 39010 39
249 서귀포시 39020 39

250 rows × 3 columns

df_json = df_local.set_index('name').rename(_dct).reset_index()\
.rename({'code':'code_local','name':'name_local'},axis=1)\
.assign(code = lambda df: df.code_local.str[:2])\
.merge(df_global)  ## 공통열이 `code`밖에 없으므로, 알아서 엮어준다.
df_json
name_local code_local code name
0 종로구 11010 11 서울특별시
1 중구 11020 11 서울특별시
2 용산구 11030 11 서울특별시
3 성동구 11040 11 서울특별시
4 광진구 11050 11 서울특별시
... ... ... ... ...
245 함양군 38380 38 경상남도
246 거창군 38390 38 경상남도
247 합천군 38400 38 경상남도
248 제주시 39010 39 제주특별자치도
249 서귀포시 39020 39 제주특별자치도

250 rows × 4 columns

(7) df_jsondf의 정보를 merge하기 위하여 ’서울특별시-종로구’와 같은 형식으로 공통열을 각각 생성, 생성된 공통열의 원소가 일치하는지 비교

s1 = df_json.assign(on = lambda df: df.name + '-' + df.name_local)['on']
s1
0         서울특별시-종로구
1          서울특별시-중구
2         서울특별시-용산구
3         서울특별시-성동구
4         서울특별시-광진구
           ...     
245        경상남도-함양군
246        경상남도-거창군
247        경상남도-합천군
248     제주특별자치도-제주시
249    제주특별자치도-서귀포시
Name: on, Length: 250, dtype: object
s2 = df.assign(
    시도 = df.시도.map({l['properties']['name_eng']:l['properties']['name'] for l in global_dict['features']})
).assign(on = lambda df: df.시도 + '-' + df.지역)['on']
s2
0         서울특별시-종로구
1          서울특별시-중구
2         서울특별시-용산구
3         서울특별시-성동구
4         서울특별시-광진구
           ...     
995    제주특별자치도-서귀포시
996     제주특별자치도-제주시
997    제주특별자치도-서귀포시
998     제주특별자치도-제주시
999    제주특별자치도-서귀포시
Name: on, Length: 1000, dtype: object

(싱글벙글)당연히 똑같겠지?

set(s1)-set(s2), set(s2)-set(s1)
({'인천광역시-남구'}, {'인천광역시-미추홀구'})

똑같은 방식이었을 텐데 다른 값이 있다.(df_json에는 남구라고 되어있고, df에는 미추홀구라고 되어있다.)

지역 명이 미추홀구로 바뀌었음!!!

(8) 지역명을 적절히 변환(df_json을 바꿈)

df_json
name_local code_local code name
0 종로구 11010 11 서울특별시
1 중구 11020 11 서울특별시
2 용산구 11030 11 서울특별시
3 성동구 11040 11 서울특별시
4 광진구 11050 11 서울특별시
... ... ... ... ...
245 함양군 38380 38 경상남도
246 거창군 38390 38 경상남도
247 합천군 38400 38 경상남도
248 제주시 39010 39 제주특별자치도
249 서귀포시 39020 39 제주특별자치도

250 rows × 4 columns

df_json.assign(on = lambda _df : _df.name + '-' +  _df.name_local).drop(['name', 'name_local'], axis = 1)\
.set_index('on').rename({'인천광역시-남구':'인천광역시-미추홀구'}).reset_index()  ## 합치고 없애지 않으면, 모든 남구를 다 없애버린다.
on code_local code
0 서울특별시-종로구 11010 11
1 서울특별시-중구 11020 11
2 서울특별시-용산구 11030 11
3 서울특별시-성동구 11040 11
4 서울특별시-광진구 11050 11
... ... ... ...
245 경상남도-함양군 38380 38
246 경상남도-거창군 38390 38
247 경상남도-합천군 38400 38
248 제주특별자치도-제주시 39010 39
249 제주특별자치도-서귀포시 39020 39

250 rows × 3 columns

(9) 데이터프레임을 결합

df2 = df_json.assign(on = lambda _df : _df.name + '-' +  _df.name_local).drop(['name', 'name_local'], axis = 1)\
.set_index('on').rename({'인천광역시-남구':'인천광역시-미추홀구'}).reset_index()\
.merge(df.assign(시도 = df.시도.map({l['properties']['name_eng']:l['properties']['name'] for l in global_dict['features']})).assign(on = lambda df: df.시도 + '-' + df.지역))

df2.drop('on', axis = 1) ## 'on' 열은 단순히 `merge`를 위해 만들어둔 더미 열이므로 없애줌
code_local code 년도 시도 지역 건물동수 연면적 에너지사용량(TOE)/전기 에너지사용량(TOE)/도시가스 에너지사용량(TOE)/지역난방
0 11010 11 2018 서울특별시 종로구 17929 9141777 64818 82015 111
1 11010 11 2019 서울특별시 종로구 17851 9204140 63492 76653 799
2 11010 11 2020 서울특별시 종로구 17638 9148895 60123 71263 912
3 11010 11 2021 서울특별시 종로구 22845 18551145 125179 117061 0
4 11020 11 2018 서울특별시 중구 10598 10056233 81672 75260 563
... ... ... ... ... ... ... ... ... ... ...
995 39010 39 2021 제주특별자치도 제주시 67053 20275738 103217 25689 0
996 39020 39 2018 제주특별자치도 서귀포시 34154 6914685 34470 1597 0
997 39020 39 2019 제주특별자치도 서귀포시 34729 7233931 34641 1306 0
998 39020 39 2020 제주특별자치도 서귀포시 34880 7330040 35510 1639 0
999 39020 39 2021 제주특별자치도 서귀포시 35230 7512206 37884 2641 0

1000 rows × 10 columns

이럼 좌표정보가 들어갔다.(끝났어요!)

C. 시각화


그냥 folium 쓰는 것 마냥 사용하면 된다.

# px.choropleth_mapbox(
#     data_frame = df2.loc[df2.년도 == 2018],
#     geojson = local_dict,
#     featureidkey = 'properties.code',
#     locations = 'code_local',
#     color = '에너지사용량(TOE)/전기',
#     hover_data = ['시도','지역'],
#     #---#
#     mapbox_style = "carto-positron",
#     center={"lat": 36, "lon": 127.5},  ## 다른 맵박스랑 다르게 center를 지정하지 않아도 데이터쪽으로 시선을 돌려주진 않네...
#     zoom = 6,
#     height = 800,
#     width = 800
# )

용량이 어머가 없어서 생략했다.

### D. 시각화 2 : 서울의 전기 에너지 사용량

애니메이션도 간편하게 만들 수 있다!!

seoul_dict = local_dict.copy()
seoul_dict['features'] = [l for l in local_dict['features'] if l['properties']['code'][:2] == '11']  ## 서울에 해당하는 자료만 뽑음
fig = px.choropleth_mapbox(
    geojson = seoul_dict,
    featureidkey = 'properties.code',
    data_frame = df2,
    locations = 'code_local',
    color='에너지사용량(TOE)/전기',
    hover_data=['시도','지역'],
    animation_frame='년도',   ## 해당 옵션으로 프레임을 구분함
    #---#
    mapbox_style="carto-positron",
    range_color=[0,400000],   ## 이건 부수적임
    center={"lat": 37.5665, "lon": 126.9780},
    zoom=9,
    height=500,
    width=700,
)

fig.show(config = {'scrollZoom' : False})