본문으로 바로가기

한 주간 참 대단한 일들이 지나갔습니다. 대선이 있었고... 바로 대통령 당선인인 대통령이되고, 그리고 뭔가 대단한 (사실은 지극히 정상적으로 일처리를 하는 것인데도...) 큰 뭔가의 변화들이 있을 것 같아 기대도 됩니다.^^. 그래도 주말마다 취미로 데이터를 만지는게 낙인 저같은 소시민이 이런 대선이라는 큰 이슈에 글하나 남기지 않는다면 그것도 직무유기라 생각되어서 살짝꿍 뭐라도 끄적거리기로 했습니다. 그래서 제가 그간 제 블로그의 Data Science 카테고리에 올린 기본적인 기능을 가지고 이번 대선 득표율을 슬쩍 비교해볼려구요^^. 언제나 그렇듯.. 이 글에는 아무런 기술적 고난도 스킬은 없구요... 그저... 각 모듈을 튜토리얼 수준으로 편집해서 원하는 흐름을 만들었을 뿐입니다.^^

웹에서 득표율 모집하기...

일단 전국 시도단위로 누가 득표율을 표로 이쁘게 정리해 줬다면 참 좋았을텐데.ㅠㅠ. 없더라구요ㅠㅠ. 뭐 어쩌겠습니까... 중앙선거관리위원회로 달려가야죠~!

저기 왼쪽 구석에 데이터를 받을 수 있는 곳이 있습니다.

ㅎㅎ 있네요^^ 저 주소를 가지고... Selenium[바로가기]으로 접근할 겁니다. 위 코드를 실행하면 새로운 창이 뜨면서 해당 페이지로 접근하는데요.. 뭐 코드로 구현해도 되고, 직접 대통령선거라는 글자를 한번만(^^) 클릭해두세요^^

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

아.. 위 코드를 먼저 실행해 두어야 합니다.^^ 아무튼... 그러고 나서 저 윕페이지에서 각 지역별 득표율을 얻는 코드는

sido_name_list = []
sigun_name_list = []
pop = []
moon = []
hong = []
ahn = []

for sido_value in sido_names_values:
    element = driver.find_element_by_id("cityCode")
    element.send_keys(sido_value)
    
    time.sleep(1)
    
    sigun_list_raw = driver.find_element_by_xpath("""//*[@id="townCode"]""")
    sigun_list = sigun_list_raw.find_elements_by_tag_name("option")

    sigun_names_values = [option.text for option in sigun_list]
    sigun_names_values = sigun_names_values[1:]
    
    for sigun_value in sigun_names_values:
        element = driver.find_element_by_id("townCode")
        element.send_keys(sigun_value)
        
        time.sleep(1)
        
        driver.find_element_by_xpath("""//*[@id="spanSubmit"]/input""").click()
        
        time.sleep(1)
        
        html = driver.page_source
        soup = BeautifulSoup(html, 'lxml')
        
        tmp = soup.find_all('td', 'alignR')
        tmp_values = [float(tmp_val.get_text().replace(',', '')) for tmp_val in tmp[1:5]]
        
        sido_name_list.append(sido_value)
        sigun_name_list.append(sigun_value)
        pop.append(tmp_values[0])
        moon.append(tmp_values[1])
        hong.append(tmp_values[2])
        ahn.append(tmp_values[3])
        
        time.sleep(1)

위와 같습니다. 위 코드는 광역시도를 체크하고 목록을 얻어오고, 그 안에서 다시 시군 정보를 얻어서 목록을 얻어서 반복(for)하는 것입니다. 그리고 페이지가 다 로딩되면 표가하나 나타나는데.. 그 표에서 문재인 (당시) 후보, 홍준표, 안철수 후보의 득표수와 통 투표수를 얻어옵니다. 위 코드를 실행하면...

위 동영상처럼 스스로 반복하면서 정보를 다 얻게 될겁니다.^^ 동영상에서 보이는건 제가 클릭한 것이 아니라.. selenium을 통해 접근하고, 데이터는 소개한 적이 있는 beautiful soup[바로가기]으로 얻고 있습니다.

웹에서 얻은 데이터 다듬기...

이제 얻은 데이터를 좀 더 다듬어야 합니다. 항상 그렇듯이~~~^^...

election_result = pd.DataFrame({'광역시도':sido_name_list, '시군':sigun_name_list, 'pop':pop, 
                                'moon':moon, 'hong':hong, 'ahn':ahn})

ID = []

for n in election_result.index:
    if (election_result['광역시도'][n][-1] == '시') & (election_result['광역시도'][n] != '세종특별자치시'):
        if len(election_result['시군'][n]) == 2:
            ID.append(election_result['광역시도'][n][:2] + ' ' + election_result['시군'][n])
        else:
            ID.append(election_result['광역시도'][n][:2] + ' ' + election_result['시군'][n][:-1])
            
    elif (election_result['광역시도'][n][-1] == '도'):
        tmp = election_result['시군'][n]
        
        if tmp[0] not in ['시','군']:
            tmp2 = re.split('시|군', tmp)
        else:
            tmp2 = [tmp[:-1], '']
        
        if len(tmp2[1]) == 2:
            tmp3 = tmp2[0] + ' ' + tmp2[1]
        elif len(tmp2[1]) >= 3:
            tmp3 = tmp2[0] + ' ' + tmp2[1][:-1]
        else:
            tmp3 = tmp2[0]
            
        ID.append(tmp3)
        
    else:
        ID.append('세종')

election_result['ID'] = ID

먼저 아까 얻은 데이터를 pandas[바로가기]로 잡고... 그 다음 전국 지도 모양으로 그리기 위해 소개한 적이 있는 함수를 사용할 건데요.. [바로가기]에 보시면 있는 방식과 함수를 가지고 와서 사용할 겁니다. 그런데.. 그럴려면 지역별 고유 ID가 필요하거든요.. 그걸 만드는 것이 위 코드의 반복문(for)의 내용입니다. 

election_result['rate_moon'] = election_result['moon'] / election_result['pop'] * 100
election_result['rate_hong'] = election_result['hong'] / election_result['pop'] * 100
election_result['rate_ahn'] = election_result['ahn'] / election_result['pop'] * 100

..그리고.. 세 후보의 득표울을 계산해 둡니다. 이제.. [바로가기]에서 이야기한 함수를 사용할건데요.. 그럴려면...

draw_korea_data.csv

위 파일이 필요하실 겁니다.~~

draw_korea = pd.read_csv('../data/draw_korea_data.csv', encoding='utf-8', index_col=0)

election_result.loc[125, 'ID'] = '고성(강원)'
election_result.loc[233, 'ID'] = '고성(경남)'

election_result.loc[228, 'ID'] = '창원 합포'
election_result.loc[229, 'ID'] = '창원 회원'

ahn_tmp = election_result.loc[85, 'ahn']/3
hong_tmp = election_result.loc[85, 'hong']/3
moon_tmp = election_result.loc[85, 'moon']/3
pop_tmp = election_result.loc[85, 'pop']/3

rate_moon_tmp = election_result.loc[85, 'rate_moon']
rate_hong_tmp = election_result.loc[85, 'rate_hong']
rate_ahn_tmp = election_result.loc[85, 'rate_ahn']

election_result.loc[250] = [ahn_tmp, hong_tmp, moon_tmp, pop_tmp, 
                           '경기도', '부천시', '부천 소사', 
                           rate_moon_tmp, rate_hong_tmp, rate_ahn_tmp]
election_result.loc[251] = [ahn_tmp, hong_tmp, moon_tmp, pop_tmp, 
                           '경기도', '부천시', '부천 오정', 
                           rate_moon_tmp, rate_hong_tmp, rate_ahn_tmp]
election_result.loc[252] = [ahn_tmp, hong_tmp, moon_tmp, pop_tmp, 
                           '경기도', '부천시', '부천 원미', 
                           rate_moon_tmp, rate_hong_tmp, rate_ahn_tmp]

election_result.drop([85], inplace=True)

.그리고 나면.. 웹에서 수집한 데이터와 지도를 그리기 위한 draw_korea라는 파일에서 읽은 데이터를 합쳐야하는데요.. 그 기준으로 지역별 ID를 이용할 겁니다. 그래서 두 데이터의 ID가 일치하도록 작업을 해줘야 하는데요... 일단~ 강원도와 경남에 있는 고성을 구분했구요.. 아참.. .loc 바로 위의 index num은 그러니까.. 위 코드에서 125, 233 등은 혹시 이 코드를 실행하신다면 바뀔 수도 있으니 확인을 해보셔야합니다. 조건문으로 이쁘게 코드를 짤 수 있었겠지만... 급한 마음에.ㅠㅠ. 흠흠.. 뭐 아무튼.. 그렇게 고성을 배려하고...^^

다음은 경남 창원인데요.. 창원은 전국의 다른 지역과 달리 정~~말 긴 구이름을 가지고 있습니다. 창원시 마산합포구, 창원시 마산회원구인데요... 지도에 표기할 칸이 작아서 그냥 창원 합포, 창원 회원... 으로 해두겠습니다. 그다음이... 휴~ 부천이네요...

부천은 2016년 7월 부터.. 구가 폐지되고 책임동(?^^)이라는 제도가 시작되었다고 합니다.ㅠㅠ. 그런데 지도 만드는 방법을 소개할 당시는 부천이 소사구 오정구, 원미구가 있었거든요.. 그래서 (역시 급한 마음에.. 뭐 귀찮아서^^) 그냥 부천시의 데이터를 3등분해서 각 구에 배정해 버렸습니다. 전체 득표수에는 영향을 주지 않고, 단지 지도에 표기할때 살짝 칸이 넓어보이긴 하는데 85만명의 부천을 한칸으로 취급할 수도 없어서 이렇게 하겠습니다.^^.

final_elect_data = pd.merge(election_result, draw_korea, how='left', on=['ID'])

final_elect_data['moon_vs_hong'] = final_elect_data['rate_moon'] - final_elect_data['rate_hong']
final_elect_data['moon_vs_ahn'] = final_elect_data['rate_moon'] - final_elect_data['rate_ahn']
final_elect_data['ahn_vs_hong'] = final_elect_data['rate_ahn'] - final_elect_data['rate_hong']

.이제 드디어 두 데이터를 병합(merge)하네요.. 당연히 ID를 기준으로 했구요.. 추가로 세 후보간 득표율을 빼서 (왜 뺴는냐면 지도그릴때 color map의 적용이 쉽게 하기 위해서입니다.) 서로간 대결 구도로 봤습니다. 이제.. 데이터 다듬기도 끝났네요~~~^^

데이터 시각화하기~~~

[바로가기]에서 소개한 적이 있는 우리나라 지도 형상으로 시각화할건데요... 애초 그 글에서 살짝 더 확장했습니다. 광역시가 아닌데 구가 있는 시들도 표기를 했거든요~ 그리고, color map을 적용하는 부분도 살짝 변경했습니다. 세 후보간 격차를 득표율을 빼서 정해서 그렇구요~~~

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

    whitelabelmin = 20.

    datalabel = targetData

    vmin = -50
    vmax = 50

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

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

    # 지역 이름 표시
    for idx, row in blockedMap.iterrows():
        # 광역시는 구 이름이 겹치는 경우가 많아서 시단위 이름도 같이 표시한다. (중구, 서구)
        if len(row['ID'].split())==2:
            dispname = '{}\n{}'.format(row['ID'].split()[0], row['ID'].split()[1])
        elif row['ID'][:2]=='고성':
            dispname = '고성'
        else:
            dispname = row['ID']

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

        annocolor = 'white' if np.abs(row[targetData]) > whitelabelmin else 'black'
        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=2)

    plt.gca().invert_yaxis()

    plt.axis('off')

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

    plt.tight_layout()
    plt.show()

...아무튼.. 위 함수를 사용하면 됩니다.^^

drawKorea('moon_vs_hong', final_elect_data, 'RdBu')
drawKorea('moon_vs_ahn', final_elect_data, 'RdBu')
drawKorea('ahn_vs_hong', final_elect_data, 'RdBu')

.위 명령이 각각 문재인 대통령대 홍준표 후보... 그 다음이 문재인 대통령대 안철수 후보, 마지막이 안철수 후보대 홍준표 후보의 지역별 득표율의 차이를 보여줍니다.

다 했다~~~ 감상하기...

이제.. 그 결과를 보죠~

먼저 문재인 대통령대 홍준표 후보의 결과입니다. 흠... 서울, 인천, 대전, 광주, 부산, 울산에서 문재인 대통령이 이겼네요... 특히 광주와 전라도의 지지는 엄청나네요... 반면 대구 경북의 지지는 홍준표 후보가 얻었네요... 그러나 부산과 대전 충청도에서 홍준표 후보의 실패가 보입니다. 위 지도는 한 칸이 인구에 (정확히는 아니고 대충.ㅠㅠ) 비슷하게 맞춰져 있으니 저 지지율의 시각화 방식으로 보면 문재인 대통령이 얼마나 많은 격차로 홍준표 후보를 이겼는지를 알 수 있습니다.

다음은 문재인 대통령대 안철수 후보의 결과인데요... 화면이 다 파란색이라는건 그냥 문재인후보가 전 지역에서 안철수 후보를 이겼다는 겁니다. 전국적으로 단 한곳도 예외없이 문재인 대통령이 안철수 후보를 이겼네요.^^

이번에는 안철수 후보 대 홍준표 후보의 결과입니다. 안철수 후보는 홍준표후보와의 전투에서도 열세네요... 선거기간 동안 안철수 후보는 보수층과 보수 지역을 공략하는데 많은 공을 들였다고 들었는데 그 전략이 실패한 모양입니다.

이렇게해서 세 후보간 득표율의 차이를 단순히 일등만 지역별로 표기하는 방식이 아니라... 득표율을 빼서 +나 -방향의 끝으로 갈 수록 더 많은 격차로 이긴걸로 표기하도록 해서 후보간 지역별 격차에 대해 표현하고자 했습니다. (라고 쓰고.. 재미있게 놀았습니다. 데이터를 가지고 노는건 항상 그렇듯 재미있습니다.^^)

신고