본문 바로가기

Theory/DataScience

시각화하기 좋은 우리나라 지도 그리는 법을 소개해 드립니다.^^

데이터를 가지고 노는 취미(^^)를 가지게 되면 초반에 나타나는 현상이 무조건 그래프로 그려본다(^^)와 지역적인 문제만 들어가면 지도에 표현해보고 싶어진다입니다.^^. 지난번에 저는 서울시 범죄현황에 대한 분석[바로가기]에서 Folium[바로가기]을 이용해서 서울시의 구별 범죄현황을 지도에 시각화 했었는데요. 당연히 좀 더 많은 데이터를 전국규모로 그려보고 싶어집니다. 그런데 실제로 그려보면 약간 이상하다는 느낌이 듭니다. 바로 지리적으로 정확한 지도를 이용하면 약간 정보전달력에 문제가 있는게 아닌가하는 생각이 들거든요... 그래서 제가 방법을 찾다가 역시 구세주같은 분을 만났습니다.

바로 Hyeshik Chang(장혜식)님인데요. 이분의 블로그[바로가기]에서 해답을^^ 찾았거든요. 일제 이분의 코드는

위에 나타난 버거지수와 관련된 글[바로가기]입니다. 아주아주 멋진 진행 방법을 가지고 있으니 한 번씩 들어가서 흐름을 보시면 좋겠습니다. 그러나 이 글은 2015년에 작성된 글로 그 후 롯데리아 등의 홈페이지가 개편되면서 살짝 동작이 안맞긴 합니다. 뭐 그래도 멋진 글의 흐름을 보는데는 문제가 없죠^^ 아무튼 저는 이 분이 공개하신 코드를 아주 조금 바꾼거니 제 글의 제목처럼 소개라고 말씀드린 겁니다.^^

이분의 글을 따라가면 저런 방식의 한국 지도를 얻을 수 있거든요. 응? 뭐지? 뭐 일종의 Cartogram으로 보시면 되겠습니다.

[출처] 위키백과의 우리나라 svg 파일

사실.. 위에 보이는 지도는 지리적으로는 당연히 정확하겠지만... 시각화 입장에서는 지리적 데이터를 사용하는게 아니라면 약간 판독에 문제가 있습니다.

위 그림은 제가 전국데이터에 대한 json파일을 이용해서 인구수를 시각화시켜본건데요. 물론 느낌으로는 어디가 밀집지역이군 하도 알 수 있지만, 인구가 많이 모여있는 수도권은 알기가 어려워서 한번에 판독하기가 약간 어렵거든요.

[출처] 오마이뉴스의 20대 국회의원 개표결과

위 그림을 보시면 됩니다. 20대 국회의원의 정당별 결과인데 왼쪽을 사용하면 마지 당시 새누리당인 한 80% 득표된듯이 나오거든요. 그러나 오른쪽처럼하면 정당별 의석수를 정확히 시각화할 수 있는거죠. 그래서 저도 그런 표현을 찾아다닌거고.. 아까도 이야기했지만, Hyeshik님의 코드를 보게 된 것 . 입니다.^^

data_draw_korea.csv.zip

위 데이터를 사용할 겁니다.^^.

import pandas as pd
import numpy as np

import platform
import matplotlib.pyplot as plt

%matplotlib inline

from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry~~~~')    

plt.rcParams['axes.unicode_minus'] = False

일단 필요한 모듈들을 import하구요... 아까 올려드린 데이터를 

data_draw_korea = pd.read_csv('data/data_draw_korea.csv', index_col=0, encoding='UTF-8')
data_draw_korea.head()

읽어옵니다.~ 대충 그 내용은

이렇게 생겼습니다^^ 저 테이블이 만들어진 길도 아주 멀고 먼 험난한 길이었지만, 그건 Hyeshik님의 블로그에 담긴 내용으로 넘기구요~ 저는 저렇게 만들어지는 과정이 아니라 지도를 그리는 함수만 소개해 드리고 싶으니 그 부분은 패스하죠~

BORDER_LINES = [
    [(3, 2), (5, 2), (5, 3), (9, 3), (9, 1)], # 인천
    [(2, 5), (3, 5), (3, 4), (8, 4), (8, 7), (7, 7), (7, 9), (4, 9), (4, 7), (1, 7)], # 서울
    [(1, 6), (1, 9), (3, 9), (3, 10), (8, 10), (8, 9),
     (9, 9), (9, 8), (10, 8), (10, 5), (9, 5), (9, 3)], # 경기도
    [(9, 12), (9, 10), (8, 10)], # 강원도
    [(10, 5), (11, 5), (11, 4), (12, 4), (12, 5), (13, 5),
     (13, 4), (14, 4), (14, 2)], # 충청남도
    [(11, 5), (12, 5), (12, 6), (15, 6), (15, 7), (13, 7),
     (13, 8), (11, 8), (11, 9), (10, 9), (10, 8)], # 충청북도
    [(14, 4), (15, 4), (15, 6)], # 대전시
    [(14, 7), (14, 9), (13, 9), (13, 11), (13, 13)], # 경상북도
    [(14, 8), (16, 8), (16, 10), (15, 10),
     (15, 11), (14, 11), (14, 12), (13, 12)], # 대구시
    [(15, 11), (16, 11), (16, 13)], # 울산시
    [(17, 1), (17, 3), (18, 3), (18, 6), (15, 6)], # 전라북도
    [(19, 2), (19, 4), (21, 4), (21, 3), (22, 3), (22, 2), (19, 2)], # 광주시
    [(18, 5), (20, 5), (20, 6)], # 전라남도
    [(16, 9), (18, 9), (18, 8), (19, 8), (19, 9), (20, 9), (20, 10)], # 부산시
]

gamma = 0.75

blockedMap = data_draw_korea
targetData = '인구수'

whitelabelmin = (max(blockedMap[targetData]) - min(blockedMap[targetData])) * 0.25 + min(blockedMap[targetData])

datalabel = targetData

vmin = min(blockedMap[targetData])
vmax = max(blockedMap[targetData])

mapdata = blockedMap.pivot(index='y', columns='x', values=targetData)
masked_mapdata = np.ma.masked_where(np.isnan(mapdata), mapdata)

한국 지도를 그리는 방법은 적당히 위치(x,y)를 잡고, 도 혹은 광역시별 경계선을 그리고, 데이터를 배치하고 colormap을 적용시키는 과정을 거칩니다.

즉... 각 시도별 행정구별의 위치에 표현하고자하는 데이터를 놓는다는 거죠.. 위 데이터가 한국 지도랑 비슷하게 생겼죠^^

아~ 그리고.. colormap은 위 그림중 괜찮은걸 사용하면 되는데.. 그냥 Blues랑 Reds가 제일 낫더라구요^^ 

cmapname = 'Blues' #'Reds'

plt.figure(figsize=(8, 13))
plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmapname, edgecolor='#aaaaaa', linewidth=0.5)

# 지역 이름 표시
for idx, row in blockedMap.iterrows():
    annocolor = 'white' if row[targetData] > whitelabelmin else 'black'
    
    # 광역시는 구 이름이 겹치는 경우가 많아서 시단위 이름도 같이 표시한다. (중구, 서구)
    if row['광역시도'].endswith('시') and not row['광역시도'].startswith('세종'):
        dispname = '{}\n{}'.format(row['광역시도'][:2], row['행정구역'][:-1])
        if len(row['행정구역']) <= 2:
            dispname += row['행정구역'][-1]
    else:
        dispname = row['행정구역'][:-1]

    # 서대문구, 서귀포시 같이 이름이 3자 이상인 경우에 작은 글자로 표시한다.
    if len(dispname.splitlines()[-1]) >= 3:
        fontsize, linespacing = 9.5, 1.5
    else:
        fontsize, linespacing = 11, 1.2

    plt.annotate(dispname, (row['x']+0.5, row['y']+0.5), weight='bold',
                 fontsize=fontsize, ha='center', va='center', color=annocolor,
                 linespacing=linespacing)
    
# 시도 경계 그린다.
for path in BORDER_LINES:
    ys, xs = zip(*path)
    plt.plot(xs, ys, c='black', lw=4)

plt.gca().invert_yaxis()
#plt.gca().set_aspect(1)

plt.axis('off')
    
cb = plt.colorbar(shrink=.1, aspect=10)
cb.set_label(datalabel)

plt.tight_layout()
plt.show()

그리고 위 코드를 이용해서 그림을 그렸습니다. 주석처리를 Hyeshik님이 잘 해주셨네요...

저 코드를 실행하면.. 이제 이와같이 그림이 나옵니다... 오호.. 수원과 창원의 인구가 엄청 많군요... 뭐 아무튼.. 이제 이걸.. def 함수화하죠~^^ 

def drawKorea(targetData, blockedMap, d1, d2, cmapname):
    gamma = 0.75

    whitelabelmin = (max(blockedMap[targetData]) - min(blockedMap[targetData])) * 0.25 + min(blockedMap[targetData])

    datalabel = targetData

    vmin = min(blockedMap[targetData])
    vmax = max(blockedMap[targetData])

    BORDER_LINES = [
        [(3, 2), (5, 2), (5, 3), (9, 3), (9, 1)], # 인천
        [(2, 5), (3, 5), (3, 4), (8, 4), (8, 7), (7, 7), (7, 9), (4, 9), (4, 7), (1, 7)], # 서울
        [(1, 6), (1, 9), (3, 9), (3, 10), (8, 10), (8, 9),
         (9, 9), (9, 8), (10, 8), (10, 5), (9, 5), (9, 3)], # 경기도
        [(9, 12), (9, 10), (8, 10)], # 강원도
        [(10, 5), (11, 5), (11, 4), (12, 4), (12, 5), (13, 5),
         (13, 4), (14, 4), (14, 2)], # 충청남도
        [(11, 5), (12, 5), (12, 6), (15, 6), (15, 7), (13, 7),
         (13, 8), (11, 8), (11, 9), (10, 9), (10, 8)], # 충청북도
        [(14, 4), (15, 4), (15, 6)], # 대전시
        [(14, 7), (14, 9), (13, 9), (13, 11), (13, 13)], # 경상북도
        [(14, 8), (16, 8), (16, 10), (15, 10),
         (15, 11), (14, 11), (14, 12), (13, 12)], # 대구시
        [(15, 11), (16, 11), (16, 13)], # 울산시
        [(17, 1), (17, 3), (18, 3), (18, 6), (15, 6)], # 전라북도
        [(19, 2), (19, 4), (21, 4), (21, 3), (22, 3), (22, 2), (19, 2)], # 광주시
        [(18, 5), (20, 5), (20, 6)], # 전라남도
        [(16, 9), (18, 9), (18, 8), (19, 8), (19, 9), (20, 9), (20, 10)], # 부산시
    ]

    mapdata = blockedMap.pivot(index='y', columns='x', values=targetData)
    masked_mapdata = np.ma.masked_where(np.isnan(mapdata), mapdata)
    
    plt.figure(figsize=(8, 13))
    plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmapname, edgecolor='#aaaaaa', linewidth=0.5)

    # 지역 이름 표시
    for idx, row in blockedMap.iterrows():
        annocolor = 'white' if row[targetData] > whitelabelmin else 'black'

        # 광역시는 구 이름이 겹치는 경우가 많아서 시단위 이름도 같이 표시한다. (중구, 서구)
        if row[d1].endswith('시') and not row[d1].startswith('세종'):
            dispname = '{}\n{}'.format(row[d1][:2], row[d2][:-1])
            if len(row[d2]) <= 2:
                dispname += row[d2][-1]
        else:
            dispname = row[d2][:-1]

        # 서대문구, 서귀포시 같이 이름이 3자 이상인 경우에 작은 글자로 표시한다.
        if len(dispname.splitlines()[-1]) >= 3:
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2

        plt.annotate(dispname, (row['x']+0.5, row['y']+0.5), weight='bold',
                     fontsize=fontsize, ha='center', va='center', color=annocolor,
                     linespacing=linespacing)
        
    # 시도 경계 그린다.
    for path in BORDER_LINES:
        ys, xs = zip(*path)
        plt.plot(xs, ys, c='black', lw=4)

    plt.gca().invert_yaxis()
    #plt.gca().set_aspect(1)

    plt.axis('off')

    cb = plt.colorbar(shrink=.1, aspect=10)
    cb.set_label(datalabel)

    plt.tight_layout()
    plt.show()

위 코드가 오늘의 핵심이네요...^^. 뭐.. 단순 소개이지만, 그래도 살짝 뿌듯해지네요^^...

drawKorea('면적', data_draw_korea, '광역시도', '행정구역', 'Blues')

이제 위 한줄의 코드를 실행하면

넵... 면적에 대해 나옵니다. 인제와 홍천이 엄청 넓은 땅을 가지고 있네요^^

data_draw_korea['밀도'] = data_draw_korea['인구수']/data_draw_korea['면적']
drawKorea('밀도', data_draw_korea, '광역시도', '행정구역', 'Reds')

이번에는 면적으로 인구를 나눠서 밀도를 보죠~

역시.. 수도권이네요~^^ 아무튼.. 이렇게 해서 한국지도를 시각화에 이용할 수 있는 것을 살짝 보여드렸습니다. 다시 말씀드리지만.. 원본코드는 따로 있구요.. 저는 정~말 일부만 살짝 만진 다음(최근 Python 버젼에서 동작할 수 있도록) 공개해 드리는 겁니다. 이 모든 것은 다시 말씀드리지만, Hyeshik님의 글을 통해서입니다. 언제 뵙게 되면 커피라도 대접해 드리고 싶네요^^...

반응형