[Zero-Base DS]스터디노트_인구소멸위기 지역 파악(카르토그램)

HAHAHAEUN·2024년 4월 16일
post-thumbnail

인구 소멸 지역 계산하기

목표

  • 인구 소멸 위기 지역 파악

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

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

🤔

사용한 데이터는 제로베이스 데이터스쿨에서 제공한 데이터로, 현재 인구 상황과는 다를 수 있음!

인구 소멸 지역 계산하기

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import set_matplotlib_hangul
import warnings

warnings.filterwarnings(action = "ignore")
%matplotlib inline

1. 데이터 가져오기(읽어오기)

population = pd.read_excel("../data/07_population_raw_data.xlsx", header = 1)

2. 병합된 데이터(NaN값으로 들어가있음) 수정

  • pad/ffill = 앞에 있는 값으로 채움
  • backfill = 뒤에 있는 값으로 채움
population.fillna(method = "pad", inplace = True)
population.head()

3. 데이터 정리

3-1) 컬럼 이름 변경
population.rename(
    columns= {
        "행정구역(동읍면)별(1)":"광역시도",
        "행정구역(동읍면)별(2)":"시도",
        "계":"인구수"
    }, inplace = True
)
3-2) 소계 제거
population = population[population["시도"] != "소계"]
3-3) col 이름 변경
population.is_copy = False

population.rename(
    columns = {"항목": "구분"}, inplace=True
)

# loc[행,열]

population.loc[population["구분"] == "총인구수 (명)", "구분"] = "합계"
population.loc[population["구분"] == "남자인구수 (명)", "구분"] = "남자"
population.loc[population["구분"] == "여자인구수 (명)", "구분"] = "여자"

4. 소멸지역을 조사하기 위한 데이터 추가

4-1) column추가
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+"] 
)
4-2) 피봇테이블 생성
pop = pd.pivot_table(
    data=population,
    index = ["광역시도", "시도"],
    columns = ["구분"],
    values = ["인구수", "20~39세", "65세이상"]
)
pop

4-3) 소멸 비율 & 소멸 위기지역 column 추가
pop["소멸비율"] = pop["20~39세", "여자"] / (pop["65세이상", "합계"] / 2)

pop["소멸위기지역"] = pop["소멸비율"] < 1.0
4-4) 소멸위기지역 조회 & 이중컬럼 한줄로 만들어주기
pop[pop["소멸위기지역"] == True].index.get_level_values(1)
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()

5. 지도 시각화를 위한 지역별 id 만들기

  • 여기서부터 약간의 노가다..?가 들어간다..
  • 교수님께서 실제로 현업을 진행하다보면 비슷하게 라벨링 지옥을 많이 경험하게 될거라고 하셨는데, 현업에서도 이렇게 수기로 작업을 많이 진행하게 되는건가..? 싶었다
tmp_gu_dict = {
    "수원": ["장안구", "권선구", "팔달구", "영통구"],
    "성남": ["수정구", "중원구", "분당구"],
    "안양": ["만안구", "동안구"],
    "안산": ["상록구", "단원구"],
    "고양": ["덕양구", "일산동구", "일산서구"],
    "용인": ["처인구", "기흥구", "수지구"],
    "청주": ["상당구", "서원구", "흥덕구", "청원구"],
    "천안": ["동남구", "서북구"],
    "전주": ["완산구", "덕진구"],
    "포항": ["남구", "북구"],
    "창원": ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
    "부천": ["오정구", "원미구", "소사구"],
}
5-1) 일반 시 이름과 세종시, 광역시도 일반 구 정리
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]
5-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]
5-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] = "고성(경남)"
5-4) id column생성
pop["ID"] = si_name

5-5) 필요없는 colum 삭제
del pop["20~39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]

6 지도그리기(카르토그램)

🤦‍♂️

  • 시별로 라인을 그리는 작업이 너무 어렵게 느껴졌다.. 엑셀과 대조해보며 코드를 봐도 잘 이해가 되지 않았다..
  • 이렇게 또 부족한 실력을 느꼈다...😂
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.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

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"]

        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"  # 수직정렬
        )
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()

6-1) merge전 검증
# 공통된 id merge 작업 전 검증 단계

set(draw_korea["ID"].unique()) - set(pop["ID"].unique())
# should be = 0 

set(pop["ID"].unique()) - set(draw_korea["ID"].unique())
# should be = 0 

tmp_list = list(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))
tmp_list

for tmp in tmp_list:
    pop = pop.drop(pop[pop['ID']==tmp].index)

print(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))
6-2 merge
pop = pd.merge(pop, draw_korea, how="left", on = "ID")

pop

  • 그림을 그리기 위한 데이터를 계산하는 함수
    • 색상을 만들 때, 최소값을 흰색으로
    • blockedMap : 인구현황(pop)
    • targetData : 그리고 싶은 컬럼
6-3) 시각화
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


# 위에서 만들어둔 module 다시 가져옴

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

        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,
            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)
        # zeroCenter면 여기 통과시키
    if not zeroCenter:
        masked_mapdata, vmax, vmin, whitelabelmin = get_data_info(targetData, blockedMap)
        # 아니면 여기 통
    
    plt.figure(figsize = (8, 11))
    plt.pcolor(masked_mapdata, vmax=vmax, vmin=vmin, 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()
    
    
pop.head()

# 1)
drawKorea("인구수합계", pop, "Blues")

# 2)
pop["소멸위기지역"] = [1 if con else 0 for con in pop["소멸위기지역"]]
drawKorea("소멸위기지역", pop, "Reds")

# 3)
pop["여성비"] = (pop["인구수여자"]/pop["인구수합계"]-0.5)*100
drawKorea("여성비", pop, "RdBu", zeroCenter=True)

# 4)
# 2030여성 기준

pop["2030여성비"] = (pop["20~39세여자"]/pop["20~39세합계"]-0.5)*100
drawKorea("2030여성비", pop, "RdBu", zeroCenter=True)
12
34

7 지도그리기(folium)


import folium
import json

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

7-1) 인구수 합계 지도 시각화
geo_path = "../data/07_skorea_municipalities_geo_simple.json"
geo_str= json.load(open(geo_path, encoding="utf-8"))

mymap = folium.Map(location=[36.002, 127.054], zoom_start = 7)

folium.Choropleth(
    geo_data=geo_str,
    data = pop_folium["인구수합계"],
    key_on="feature.id",
    columns= [pop_folium.index, pop_folium["인구수합계"]],
    fill_color = "YlGnBu"
).add_to(mymap)

mymap

7-2) 소멸위기지역 지도 시각화
mymap = folium.Map(location=[36.002, 127.054], zoom_start = 7)

folium.Choropleth(
    geo_data = geo_str,
    data = pop_folium["소멸위기지역"],
    key_on="feature.id",
    columns = [pop_folium.index, pop_folium["소멸위기지역"]],
    fill_color = "PuRd"
).add_to(mymap)
mymap

[자료 출처]https://zero-base.co.kr/

profile
할 거면 제대로 하자

0개의 댓글