[제로베이스] 인구소멸위기지역 시각화

한결·2024년 1월 16일
0
post-thumbnail

카르토그램? 카르토그램(Cartogram)은 지리적 공간을 나타내는 지도이며, 특정 지역의 통계적인 정보를 시각적으로 표현하는 데 사용됩니다. 그러나 일반적인 지도와는 달리, 카르토그램은 지역의 크기를 그 지역에 해당하는 특정 통계 데이터와 연관시켜 조절하여 나타냅니다. 기존의 지도는 지역의 크기가 실제 지리적 크기와 일치하지만, 카르토그램은 특정 변수(예: 인구, GDP, 범죄율 등)를 반영하여 지역의 상대적 크기를 변형합니다. 이렇게 함으로써, 사용자는 지역 간의 통계적 차이를 시각적으로 파악할 수 있습니다.

이번 글은 카르토그램을 통해 우리나라 인구소멸 위기 지역을 파악해보겠습니다.


데이터 준비

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
from matplotlib import rc
rc('font', family='Arial Unicode MS')

population = pd.read_excel("data/07_population_raw_data.xlsx", header=1)
population.fillna(method="pad", inplace=True)
population.head(3)
행정구역(동읍면)별(1) 행정구역(동읍면)별(2) 항목 20 - 24세 25 - 29세 30 - 34세 35 - 39세 65 - 69세 70 - 74세 75 - 79세 80 - 84세 85 - 89세 90 - 94세 95 - 99세 100+
0 전국 소계 총인구수 (명) 51696216.0 3541061.0 3217367.0 3517868 4016272.0 2237345.0 1781229.0 1457890 909130.0 416164.0 141488.0 34844 17562.0
1 전국 소계 남자인구수 (명) 25827594.0 1877127.0 1682988.0 1806754 2045265.0 1072395.0 806680.0 600607 319391.0 113221.0 32695.0 7658 4137.0
2 전국 소계 여자인구수 (명) 25868622.0 1663934.0 1534379.0 1711114 1971007.0 1164950.0 974549.0 857283 589739.0 302943.0 108793.0 27186 13425.0

# 열 이름 바꾸기
population.rename(
    columns={"행정구역(동읍면)별(1)":"광역시도","행정구역(동읍면)별(2)":"시도", "계":"인구수"}, inplace=True
)

# 소계 열 제거
population = population[(population["시도"]!="소계")]

population.is_copy = False
population.rename(columns={"항목":"구분"}, inplace=True)
population.loc[population["구분"] == "총인구수 (명)", "구분"] = "합계"
population.loc[population["구분"] == "남자인구수 (명)", "구분"] = "남자"
population.loc[population["구분"] == "여자인구수 (명)", "구분"] = "여자"

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

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


# 소멸비율 추가
pop["소멸비율"] = pop["20 - 39세", "여자"] / (pop["65세이상", "합계"] / 2)

# 소멸위기지역 추가
pop["소멸위기지역"] = pop["소멸비율"] <1.0

pop[pop["소멸위기지역"]==True].index.get_level_values(1)
Index(['고성군', '삼척시', '양양군', '영월군', '정선군', '평창군', '홍천군', '횡성군', '가평군', '양평군',
       '연천군', '거창군', '고성군', '남해군', '밀양시', '산청군', '의령군', '창녕군', '하동군', '함안군',
       '함양군', '합천군', '고령군', '군위군', '문경시', '봉화군', '상주시', '성주군', '영덕군', '영양군',
       '영주시', '영천시', '예천군', '울릉군', '울진군', '의성군', '청도군', '청송군', '동구', '영도구',
       '강화군', '옹진군', '강진군', '고흥군', '곡성군', '구례군', '담양군', '보성군', '신안군', '영광군',
       '영암군', '완도군', '장성군', '장흥군', '진도군', '함평군', '해남군', '화순군', '고창군', '김제시',
       '남원시', '무주군', '부안군', '순창군', '임실군', '장수군', '정읍시', '진안군', '공주시', '금산군',
       '논산시', '보령시', '부여군', '서천군', '예산군', '청양군', '태안군', '홍성군', '괴산군', '단양군',
       '보은군', '영동군', '옥천군'],
      dtype='object', name='시도')

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(0, len(pop.columns.get_level_values(0)))
]
pop.columns = tmp_columns
pop.head()


si_name = [None] * len(pop)

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

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]

# 고성 경남, 고성 강원
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] = "고성(경남)"

pop["ID"] = si_name

del pop["20 - 39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]

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


draw_korea= pd.DataFrame(draw_korea_raw.stack())
draw_korea.head(3)


draw_korea.reset_index(inplace=True)
draw_korea.head(3)


draw_korea.rename(
    columns={"level_0":"y", "level_1":"x", 0:"ID"},inplace=True
)
draw_korea.head(3)


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

simpleDraw(draw_korea)


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 = 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

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",
            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))
    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")

profile
낭만젊음사랑

0개의 댓글