EDA - 인구분석

BlackLabel·2023년 10월 19일
0

EDA, 크롤링 기초 

목록 보기
12/13

인구소멸위기지역 파악하기 프로젝트 개요

목표

  • 인구 소멸 위기 지역 파악

  • 인구 소멸 위기 지역의 지도 표현

  • 지도 표현에 대한 카르토그램 표현

  • 65세 이상 노인 인구와 20 ~ 39세 여성 인구를 비교해 젊은 여성 인구가 노인 인구의 절반에 미달할 경우 '소멸 위험 지역'으로 분류하는 방식

데이터 읽기 및 인구 소멸 지역 계산

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import rc
import warnings

warnings.filterwarnings(action='ignore')

plt.rcParams['axes.unicode_minus'] = False
rc('font',family='Malgun Gothic')
%matplotlib inline
population = pd.read_excel('../data/07_population_raw_data.xlsx',header=1)
population.fillna(method='pad',inplace=True)
population.head()

  • pad : 결측값(NaN)을 이전값(바로 위의 값)으로 대체
  • 결측값(NaN)이 난 이유: 엑셀을 보면 전국, 소계가 여러 칸을 잡아먹고 있기 때문

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

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

fillna_df.fillna(method="pad")

컬럼 이름 변경

population.rename(
    columns={
        '행정구역(동읍면)별(1)': '광역시도',
        '행정구역(동읍면)별(2)':'시도',
        '계':'인구수'
    },inplace=True
)
population = population[population['시도'] != '소계']
population.head()

  • 일부 컬럼의 이름 변경
  • 소계 컬럼 제거
population.is_copy = False

population.rename(
    columns={
        '항목':'구분'
    }, inplace=True
)
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['95 - 99세'] + 
    population['100+']
)
population.tail()

  • 가임기 여성의 수, 65세 이상 노인의 수로 각각 합쳐서 새로운 컬럼
  • 소멸 지역을 조사하기 위한 데이터

pivot_table

pop = pd.pivot_table(
    data=population,
    index=['광역시도','시도'],
    columns=['구분'],
    values=['인구수','20-39세','65세 이상']
)
pop

  • 가로에 지역, 세로에 연령대별, 남여구분이 모두 위치

소멸 비율 계산

pop['소멸 비율'] = pop['20-39세','여자'] / (pop['65세 이상','합계'] / 2)
pop.head()

  • 가임기 여성수 / 65세 이상 노인수 / 2
  • 소멸비율 컬럼 추가
  • 1보다 작으면 소멸위기 지역

소멸위기지역 컬럼 생성

pop['소멸위기지역'] = pop['소멸 비율'] < 1
pop

소멸위기지역 조회

pop[pop['소멸위기지역']].index.get_level_values(1)

인덱스 수정

pop.reset_index(inplace=True)
pop.head()

컬럼명 정리

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

인구현황데이터에 시각화를 위한 지도 ID 만들기

  • 행정구, 광역시 구, 도 밑에 있는 시/군
  • 서울 중구, 서울 서초, 통영, 남양주, 포항 북구, 인천 남동, 안양 만안 등 이런 식으로 ID를 만들어야 한다
si_name = [None] * len(pop)

tmp_gu_dict = {
    '수원':['장안구','권선구','팔달구','영통구'],
    '성남':['수정구','중원구','분당구'],
    '안양':['만안구','동안구'],
    '안산':['상록구','단원구'],
    '고양':['덕양구','일산동구','일산서구'],
    '용인':['처인구','기흥구','수지구'],
    '청주':['상당구','서원구','흥덕구','청원구'],
    '천안':['동남구','서북구'],
    '전주':['완산구','덕진구'],
    '포항':['남구','북구'],
    '창원':['의창구','성산구','진해구','마산합포구','마산회원구'],
    '부천':['오정구','원미구','소사구']
}
  • 자치구는 어차피 자료에서 나타나니, 행정구를 가진 지역만 신경쓰면 된다
pop.head()

일반 시 이름과 세종시, 광역시도 일반 구 정리

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]
  • 강릉시 => 강릉, 춘천시 => 춘천 이런 식으로 정리
  • 세종시는 특별 관리
  • 중구 => 중구, 강남구 => 강남 이런 식으로 정리

행정구 정리

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]
  • 광역시나 특별시, 자치시가 아닌 경우의 행정구에 대해서만 적용
  • 행정구를 지정한 dict형 자료에 있는 지역인지 검색
  • 분당구 => 분당, 북구 => 북구
  • 특별히 긴 이름의 구는 짧게 처리

고성군 정리

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

지도에 그리기 위한 ID 생성!

pop['ID'] = si_name

pop.drop(['65세 이상남자','65세 이상여자'],axis=1,inplace=True)
del pop['20-39세남자']

pop.head()

카르토그램으로 인구현황 시각화하기

  • 불러올 엑셀 데이터(엑셀로 그린 그림)
  • 엑셀 + pivot_table
draw_korea_raw = pd.read_excel('../data/07_draw_korea_raw.xlsx')
draw_korea_raw

  • 빈 곳은 NaN으로 표시
  • 지도 모양 엑셀을 읽어온다
draw_korea_stacked = pd.DataFrame(draw_korea_raw.stack())
draw_korea_stacked

  • stack : 쉽게 말해서 pivot_tabel의 반대, 행과 열로 풀어버린다
  • 각 지역별 위치가 나타난다
draw_korea_stacked.reset_index(inplace=True)
draw_korea_stacked

  • 인덱스로 나타난 좌표를 데이터로 사용하기 위해 reset_index
draw_korea_stacked.rename(columns={'level_0':'y','level_1':'x',0:'ID'}, inplace=True)
draw_korea_stacked

  • 컬럼 이름을 x,y, ID로 변경
draw_korea = draw_korea_stacked

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)]
]
  • 변수명 변경 후 경계선을 노가다 수작업으로 쓴다

시도의 이름을 표현하는 함수

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']
        
        # 글자가 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,
            linespacing = linespacing,
            ha='center', # 수평 정렬
            va='center', # 수직 정렬
        )
  • annotate : 주석 다는 기능
  • 지역명이 split으로 나눴을 때 2개이면 줄바꿈 하기
  • 고성은 고성으로 쓰기
  • 자치구의 이름이 3글자 이상이면 폰트를 줄이고 나머지는 키우기
  • 경계선에 글자가 오면 안되서 살짝 띄워쓰기
def simpleDraw(draw_korea):
    plt.figure(figsize=(8,11))
    
    plot_text_simple(draw_korea)
    
    for path in BORDER_LINES:
        ys, xs = zip(*path)
        plt.plot(xs,ys, c='black',lw=1.5)
    plt.gca().invert_yaxis() # 엑셀은 거꾸로라서 거꾸로 설정을 해줘야 정상적인 모양이 나온다
    plt.axis('off')
    plt.tight_layout()
    plt.show()

  • 간단히 경계선과 시도 이름만 표시
  • zip함수로 좌표를 합치기
  • invert_yaxis() : 지도가 거꾸로 나오는 걸 방지하기 위해

데이터 검증 과정

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

  • 이런식으로 차집합은 없는게 정상임
set(pop['ID'].unique()) - set(draw_korea['ID'].unique())

  • 다른쪽 차집합을 봤을때는 데이터가 있음
  • 광역시가 아닌데 행정구를 가지고 있던 지역들
  • 이미 인구현황을 정리 다 했기 때문에 필요 X

데이터 삭제

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()))

지도를 그리기 위한 데이터와 인구현황 데이터를 합치기

pop = pd.merge(pop,draw_korea,how='left',on='ID')
pop.head()

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

def get_data_info(targetData, blockedMap):
    whitelabelmin = (
        max(blockedMap[targetData]) - min(blockedMap[targetData])
    ) * 0.25 + min(blockedMap[targetData])
    vmin = min(blockedMap[targetData])
    vmax = max(blockedMap[targetData])
    
    mapdata = blockedMap.pivot_table(index='y',columns='x',values=targetData)

    return mapdata,vmax,vmin, whitelabelmin
    
def get_data_info_for_zero_center(targetData, blockedMap):
    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
  • 색상을 만들 때 최소값을 흰색으로 한다
  • blockedMap은 인구현황(pop), targetData는 그리고 싶은 컬럼
  • 초기에 엑셀로 있던 지도모양 엑셀에서 지역이름이 들어가야할 자리에 targetData로 지정된 컬럼의 숫자가 들어간다
  • whitelabelmin : 배경색에 따라 글자색을 다르게 해야하는데 그 경계선을 지정한다
def plot_text(targetData, blockedMap, whitelabelmin):
    for idx, row in blockedMap.iterrows():
        if len(row['ID'].split()) == 2:
            dispname = "{}/{}".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
            
        
        anncolor = 'white' if np.abs(row[targetData]) > whitelabelmin else 'black'
        plt.annotate(
            dispname,
            (row['x'] + 0.5, row['y'] + 0.5),
            weight='bold',
            color = anncolor,
            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))
    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)
        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)
    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)

  • 수치가 0에 가까우면 남녀 비율이 비슷하다
  • 수치가 0보다 크면 여성이 많다
  • 수치가 0보다 작으면 남성이 많다
  • 이 데이터에선 군 밀집지역 제외하면 남녀 비율이 비슷해보이지만 함정이다
pop['2030여성비'] = (pop['20-39세여자'] / pop['20-39세합계']-0.5)*100
drawKorea('2030여성비',pop, 'RdBu', zeroCenter=True)

  • 20 ~ 39세 사이의 여성 비율
  • 대부분의 지역이 남성이 많다
import folium
import json

pop_folium = pop.set_index('ID')
pop_folium.head()

  • 인덱스를 json파일이랑 맞춘다
geo_path = '../data/07_skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding='utf-8'))

mymap = folium.Map(location=[36.2002, 127.054], zoom_start=7)
mymap.choropleth(
    geo_data = geo_str,
    data=pop_folium['인구수합계'],
    key_on='feature.id',
    columns=[pop_folium.index,pop_folium['인구수합계']],
    fill_color = 'YlGnBu'
)
mymap

  • 인구수합계
mymap = folium.Map(location=[36.2002, 127.054], zoom_start=7)
mymap.choropleth(
    geo_data = geo_str,
    data=pop_folium['소멸위기지역'],
    key_on='feature.id',
    columns=[pop_folium.index,pop_folium['소멸위기지역']],
    fill_color = 'PuRd'
)
mymap

  • 소멸위기 시각화
  • 절반 이상의 지역이 인구소멸지역에 포함된다
profile
+database

0개의 댓글