스터디 노트🖊️_Day 26(EDA)

정설령·2023년 4월 6일

EDA

목록 보기
9/11
post-thumbnail

✔️ 인구분석

1. 배경

  • 목표
    • 인구 소멸 위기 지역 파악
    • 인구 소멸 위기 지역의 지도 표현
    • 지도 표현에 대한 카르토그램 표현

2. 데이터 읽고 인구 소멸 위기지역 계산하기

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings(action='ignore')

import matplotlib.pyplot as plt
import seaborn as sns
import platform
from matplotlib import font_manager, rc
%matplotlib inline

path = 'C:/Windows/Fonts/malgun.ttf'

if platform.system == 'Darwin':
    rc('font', family='Arial Unicode MS')
    print('Hangul OK in your MAC!!')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
    print('Hangul OK in your Windows!!')
else:
    print('Unkown system. sorry')
  • fillna()
datas = {
    'a':np.random.randint(1, 45, 8),
    'b':np.random.randint(1, 45, 8),
    'c':np.random.randint(1, 45, 8),
}
datas

fillna_df = pd.DataFrame(datas)

fillna_df.loc[2:4, ['a']] = np.nan
fillna_df.loc[3:5, ['b']] = np.nan
fillna_df.loc[4:7, ['c']] = np.nan

fillna_df

# method : {'backfill', 'bfill', 'pad', 'ffill', None}
# pad: 앞에 있는 데이터 값으로 nan을 채우기
# axis 지정 가능
fillna_df.fillna(method='pad', axis=0) 

  • 인구 데이터 불러오기
population = pd.read_excel('../data/07_population_raw_data.xlsx', header=1)
population.fillna(method='pad', inplace=True)
population

population.info()

  • 컬럼이름 변경 후 '소계' 데이터 제거
population.rename(
    columns={
        '행정구역(동읍면)별(1)':'광역시도',
        '행정구역(동읍면)별(2)':'시도',
        "계":'인구수',
        '항목':'구분'
    }, inplace=True
)

population = population[population['시도'] != '소계']
population.head()

  • '구분' 데이터 재정의
population.loc[population['구분']=='총인구수 (명)', '구분'] = '합계'
population.loc[population['구분']=='남자인구수 (명)', '구분'] = '남자'
population.loc[population['구분']=='여자인구수 (명)', '구분'] = '여자'
population.head()

  • 소멸지역을 조사하기 위한 데이터
population['20~39세'] = (
    population['20 - 24세'] + population['25 - 29세'] + population['30 - 34세'] + population['35 - 39세']
)

population['65세 이상'] = (
    population['65 - 69세'] + population['70 - 74세'] + population['75 - 79세'] + population['80 - 84세']
    + population['85 - 89세'] + population['90 - 94세'] + population['100+']
)
population.head()

  • pivot_table을 이용하여 광역시도와 시도 별 '인구수', '20~39세', '65세 이상' 데이터프레임 생성
# value값을 별도로 지정해주지 않으면 평균값으로 입력되는 반면, value 값을 지정해주면 원 데이터 그대로 입력됨
pop = pd.pivot_table(
    data=population, 
    index=['광역시도', '시도'], 
    columns='구분', 
    values=['인구수', '20~39세', '65세 이상']
)
pop

  • 소멸 비율 계산 ('20~39세' 여성을 대상으로 나눔)
pop['소멸비율'] = pop['20~39세', '여자'] / (pop['65세 이상', '합계']/2)
pop.tail()

  • 1보다 작은 소멸비율을 기준으로 소멸 위기 지역 컬럼 생성
pop['소멸위기지역'] = pop['소멸비율'] < 1.0
pop.head()

  • 멀티인덱스 제거
pop.reset_index(inplace=True)

tmp_columns = [
    pop.columns.get_level_values(0)[n] + pop.columns.get_level_values(1)[n]
    for n in range(0, len(pop.columns.get_level_values(0)))
]

pop.columns = tmp_columns
pop.head()

3. 지도 시각화를 위한 지역별 ID 만들기

  • 행정구를 담은 임시 딕셔너리 생성
tmp_gu_dict = {
    '수원':['장안구', '권선구', '팔달구', '영통구'],
    '성남':['수정구', '중원구', '분당구'],
    '안양':['만안구', '동안구'],
    '안산':['상록구', '단원구'],
    '고양':['덕양구', '일산동구', '일산서구'],
    '용인':['처인구', '기흥구', '수지구'],
    '청주':['상당구', '서원구', '흥덕구', '청원구'], 
    '천안':['동남구', '서북구'],
    '전주':['완산구', '덕진구'],
    '포항':['남구', '북구'],
    '창원':['의창구', '성산구', '진해구', '마산합포구', '마산회원구'],
    '부천':['오정구', '원미구', '소사구']
}

(1) 일반 시 이름과 세종시, 광역시도의 일반 구 정리

pop['광역시도'].unique()

pop['시도'].unique()

si_name = [None] * len(pop)

for idx, row in pop.iterrows():
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        si_name[idx] = row["시도"][:-1]
    
    elif row["광역시도"] == "세종특별자치시":
        si_name[idx] = "세종"
    
    else:
        if len(row["시도"]) == 2:
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"]
        else:
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"][:-1]
            
si_name

(2) 행정구 정리

for idx, row in pop.iterrows():
    if row['광역시도'][-3:] not in ['광역시', '특별시', '자치시']:
        for keys, values in tmp_gu_dict.items():
            if row['시도'] in values:
                if len(row['시도']) == 2:
                    si_name[idx] = keys + ' ' + row['시도']
                elif row['시도'] in ['마산합포구', '마산회원구']:
                    si_name[idx] = keys + ' ' + row['시도'][2:-1]
                else:
                    si_name[idx] = keys + ' ' + row['시도'][:-1]

si_name

(3) 고성군 정리

  • 강원도 고성과 경남 고성을 구분
for idx, row in pop.iterrows():
    if row['광역시도'][-3:] not in ['광역시', '특별시', '자치시']:
        if row['시도'][:-1] == '고성' and row['광역시도'] == '강원도':
            si_name[idx] = '고성(강원)'
        elif row['시도'][:-1] == '고성' and row['광역시도'] == '경상남도':
            si_name[idx] = '고성(경남)'
            
si_name

  • DataFrame에 'ID'컬럼 추가 후 필요없는 컬럼 삭제
pop['ID'] = si_name

del pop['20~39세남자']
del pop['65세 이상남자']
del pop['65세 이상여자']

pop

4. 지도 그리기(카르토그램)

  • 우리나라 지도 형태로 표현되어있는 엑셀파일

  • 데이터 불러오기
    - stack() : pivot_table()의 반대 역할 수행

draw_korea_raw = pd.read_excel('../data/07_draw_korea_raw.xlsx')

draw_korea_raw_stacked = pd.DataFrame(draw_korea_raw.stack())
draw_korea_raw_stacked

  • 좌표 값을 주기위해 인덱스 설정
draw_korea_raw_stacked.reset_index(inplace=True)

draw_korea_raw_stacked.rename(
    columns={
        'level_0':'y', 
        'level_1':'x', 
        0 : 'ID'
    }, inplace=True
)
draw_korea = draw_korea_raw_stacked
draw_korea

  • 경계선 입력
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)],
    [(27, 5), (27, 6), (25, 6)]
]
  • test 함수작성
def plot_text_simple(draw_korea):
    for idx, row in draw_korea.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']
        
        # 경계선에 겹치지 않게 글자 사이즈 조절(세글자 이상일 경우 더 줄임)
        if len(dispname.splitlines()[-1]) >= 3:
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2

            
        # annotate() : matplotlib에서 주석 다는 기능
        plt.annotate(
            dispname,  # text
            (row['x']+0.5, row['y']+0.5),  # xy
            weight='bold',    
            fontsize=fontsize,
            linespacing=linespacing,
            ha='center',  # 수평정렬
            va='center',  # 수직정렬
            
        )

def simpleDraw(draw_korea):
    plt.figure(figsize=(8,11))
    
    plot_text_simple(draw_korea)
    
    for path in BORDER_LINES:
        ys,xs = zip(*path)  
                # zip함수에 *args를 인수로 넣음, *를 붙여서 입력하면 col끼리 서로 엮어준다.
        plt.plot(xs, ys, c='black', lw=1.5)
    plt.gca().invert_yaxis()
    plt.axis('off')
    plt.tight_layout()
    plt.show()
    
simpleDraw(draw_korea)

검증작업

set(draw_korea['ID'].unique()) - set(pop['ID'].unique())

# set()
set(pop['ID'].unique()) - set(draw_korea['ID'].unique())

# {'고양', '부천', '성남', '수원', '안산', '안양', '용인', '전주', '창원', '천안', '청주', '포항'}

→ 결괏값들은 광역시가 아니지만 행정구를 가지고 있던 도시이며, 앞서 행정 구 별로 정리를 해놨기 때문에 삭제 해도 괜찮은 데이터값들

  • 필요없는 데이터 삭제
tmp_list = list(set(pop['ID'].unique()) - set(draw_korea['ID'].unique()))

for tmp in tmp_list:
    pop = pop.drop(pop[pop['ID']== tmp].index)
print(set(pop['ID'].unique()) - set(draw_korea['ID'].unique())) 

# set()
  • pop과 draw_korea 데이터프레임 합치기 - merge() 이용
pop = pd.merge(pop, draw_korea, how='left', on='ID')
pop.head()

그림을 그리기 위한 데이터를 계산하는 함수

  • 색상을 만들때, 최소값을 흰색으로 지정
  • blockedMap : 인구현황(pop) 데이터
  • targetData : 표현하고자 하는 컬럼명
                # pop의 column  # pop 데이터
def get_data_info(targetData, blockedMap):
    
    # 값에 따라 달라지는 바탕 색상으로 인해 글자가 보이지 않게 되는 현상을 막기위해 설정
    whitelabelmin = (
        max(blockedMap[targetData]) - min(blockedMap[targetData])
    ) * 0.25 + min(blockedMap[targetData])   # 25% 지점으로 설정
    vmin = min(blockedMap[targetData])
    vmax = max(blockedMap[targetData])
    
    # stack 했던 데이터를 pivot_table을 이용하여 다시 되돌림
    mapdata = blockedMap.pivot_table(index='y', columns='x', values=targetData)
    
    return mapdata, vmax, vmin, whitelabelmin
def get_data_info_for_zero_center(targetData, blockedMap):
    # -값 ~ +값들에 대해 0을 center로 맞춰주는 함수
    whitelabelmin = 5
    tmp_max = max(
        [np.abs(min(blockedMap[targetData])), np.abs(max(blockedMap[targetData]))]
    )
    vmin, vmax = -tmp_max, tmp_max
    mapdata = blockedMap.pivot_table(index='y', columns='x', values=targetData)
    
    return mapdata, vmax, vmin, whitelabelmin
def plot_text(targetData, blockedMap, whitelabelmin):
    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']
        
        # 경계선에 겹치지 않게 글자 사이즈 조절(세글자 이상일 경우 더 줄임)
        if len(dispname.splitlines()[-1]) >= 3:
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2

            
        # annotate() : matplotlib에서 주석 다는 기능
        annocolor = 'white' if np.abs(row[targetData]) > whitelabelmin else 'black'
        plt.annotate(
            dispname,  # text
            (row['x']+0.5, row['y']+0.5),  # xy
            weight='bold',    
            color = annocolor,
            fontsize=fontsize,
            linespacing=linespacing,
            ha='center',  # 수평정렬
            va='center',  # 수직정렬       
        )
def drawKorea(targetData, blockedMap, cmapname, zeroCenter=False):
    if zeroCenter:
        masked_mapdata, vmax, vmin, whitelabelmin = get_data_info_for_zero_center(targetData, blockedMap)
        
    if not zeroCenter:
        masked_mapdata, vmax, vmin, whitelabelmin = get_data_info(targetData, blockedMap)
    
    plt.figure(figsize=(8,11))
    
    # pcolor() : matplotlib 그리드 칸 채우는 기능
    plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmapname, edgecolor='#aaaaaa', linewidth=0.5)
    
    plot_text(targetData, blockedMap, whitelabelmin)
    
    for path in BORDER_LINES:
        ys,xs = zip(*path)  
                # zip함수에 *args를 인수로 넣음, *를 붙여서 입력하면 col끼리 서로 엮어준다.
        plt.plot(xs, ys, c='black', lw=1.5)
   
    plt.gca().invert_yaxis()
    plt.axis('off')
    plt.tight_layout()
    cb = plt.colorbar(shrink=0.1, aspect=10)  # shrink: 컬러바의 크기를 곱할 비율(기본값: 1.0), aspect: 긴 치수와 짧은 치수의 비율(기본값: 20)
    cb.set_label(targetData)
    plt.show()
  • 인구수 분포 카르토그램 시각화
drawKorea('인구수합계', pop, 'Blues')

  • '소멸위기지역'컬럼 생성 후 시각화
pop['소멸위기지역'] = [1 if con else 0 for con in pop['소멸위기지역']]
drawKorea('소멸위기지역', pop, 'Reds')

→ 인구수합계로 구한 카르토그램 결과와 소멸위기지역으로 구한 카르토그램 결과가 전반적으로 대비된 모습을 보이고있다.

  • '여성비'컬럼 생성 후 시각화
pop['여성비'] = (pop['인구수여자']/pop['인구수합계'] - 0.5) * 100
drawKorea('여성비', pop, 'RdBu', zeroCenter=True)

  • '2030여성비'컬럼을 생성 후 시각화
pop['2030여성비'] = (pop['20~39세여자']/pop['20~39세합계'] - 0.5) * 100
drawKorea('2030여성비', pop, 'RdBu', zeroCenter=True)

→ 2030세대 중 주요 수도권을 제외한 대부분의 지역에서 남성 비율이 더 높은것을 알 수 있다.

5. 지도 시각화

import json
import folium

# ID를 인덱스로 잡는 이유는 ? -> 지도를 그릴때 ID로 기준을 잡아주기 위함
pop_folium = pop.set_index('ID')
pop_folium.head()

geo_path = '../data/07_skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding='utf-8'))
  • 인구수 합계 시각화
my_map = folium.Map(location=[36.2002, 127.054], zoom_start=7)
my_map.choropleth(
    geo_data=geo_str,
    data=pop_folium['인구수합계'],
    key_on='feature.id',
    columns=[pop_folium.index, pop_folium['인구수합계']],
    fill_color='YlGnBu'
)
my_map

  • 소멸위기지역 시각화
my_map = folium.Map(location=[36.2002, 127.054], zoom_start=7)
my_map.choropleth(
    geo_data=geo_str,
    data=pop_folium['소멸위기지역'],
    key_on='feature.id',
    columns=[pop_folium.index, pop_folium['소멸위기지역']],
    fill_color='PuRd'
)
my_map

  • 데이터 저장
draw_korea.to_csv('../data/07_draw_korea.csv',sep=',', encoding='utf-8')

"이 글은 제로베이스 데이터 취업 스쿨의 강의 자료 일부를 발췌하여 작성되었습니다."

0개의 댓글