인구분석 1 ~ 3

yeoni·2023년 5월 11일
0

01. 목표

  1. 인구 소멸 위기 지역 파악
  2. 인구 소멸 위기 지역의 지도 표현
  3. 지도 표현에 대한 카르토그램 표현
    • 카토그램은 의석수나 선거인단수, 인구 등의 특정한 데이터 값의 변화에 따라 지도의 면적이 왜곡되는 그림을 말한다. 변량비례도, 왜상 통계 지도라고도 한다.(출처: 위키백과)

02. 데이터 정리

1) 엑셀 데이터

  • 컬럼 이름 변경
  • 불필요한 소계 항목 제거
  • 구분 컬럼 정리
  • 소멸지역를 조사하기 위한 연령대 컬럼 생성
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

population = pd.read_excel('../data/07_population_raw_data.xlsx', header=1)
population.fillna(method='pad', inplace=True) # 병합된 곳 -> NaN 값 존재해서 fillna

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

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

population.is_copy = False # copy시 warning 표시 x
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+"]
)
population.head()

2) pivot_table로 데이터 정리

  • pivot_table 생성
  • 소멸비율 계산
  • 소멸위기지역 컬럼 생성
  • MultiIndex 정리
# 인덱스만 지정하면 보통 평균, values 지정하면 그 값
pop = pd.pivot_table(
    data=population,
    index=["광역시도", "시도"],
    columns=["구분"],
    values=["인구수", "20-39세", "65세이상"]
) 

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

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

pop.reset_index(inplace=True)

# MultiIndex 정리
# len(pop.columns.get_level_values(0)) -> 컬럼의 길이만큼 
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 만들기

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

  • 자치구는 자료에서 사용
  • 예시) '서울 중구', '서울 종로' '강릉' 등으로 정리
si_name = [None] * len(pop)

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

for idx, row in pop.iterrows():
    # 마지막 3글자 != "광역시", "특별시", "자치시"
    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]

2) 행정구

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

for idx, row in pop.iterrows():
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        for keys, values in tmp_gu_dict.items():
            if row["시도"] in values:
            	# 2글자 그대로 표출
                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]

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

4) 데이터 정리

  • ID 컬럼 생성
  • 불필요 컬럼 삭제
pop["ID"] = si_name

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

pop.head()



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

  • 엑셀로 그린 지도
  • stack(): 열을 피벗하여 하위 인덱스로 변환하는 메서드, 데이터 프레임의 축 변환을 위해 사용함
  • reset_index → 인덱스로 나타난 좌표를 데이터로 사용하기 위해
  • columns rename
  • 경계선 직접 입력
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)]
]

2) 검증작업과 merge

  • 합치기 전 pop, draw_korea의 unique 같은지 확인
  • 값이 다른 결과가 나와서 지우는 과정 필요
  • merge
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 = pd.merge(pop, draw_korea, how="left", on="ID")
pop.head()

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

  • 색상을 만들 때, 최솟값을 흰색
  • blockedMap: 인구현황(pop)
  • targetData: 그리고 싶은 컬럼
  • 주석
  • pcolor
  • colorbar
  • zip(): 두 개의 리스트를 서로 묶어줄 때 사용
  • 축 뒤집기: plt.gca().invert_yaxis()
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_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_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()

소멸위기지역

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

여성비

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

4) folium으로 표현

인구수합계 시각화

import folium
import json

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

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_folim["인구수합계"],
    key_on="feature.id",
    columns=[pop_folim.index, pop_folim["인구수합계"]],
    fill_color="YlGnBu"
)
mymap

소멸위기지역 시각화

mymap = folium.Map(location=[36.2002, 127.054], zoom_start=7)
mymap.choropleth(
    geo_data=geo_str,
    data=pop_folim["소멸위기지역"],
    key_on="feature.id",
    columns=[pop_folim.index, pop_folim["소멸위기지역"]],
    fill_color="PuRd"
)
mymap


fillna()함수

  • method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None
    Method to use for filling holes in reindexed Series
    • pad / ffill: propagate last valid observation forward to next valid
    • backfill / bfill: use next valid observation to fill gap.
datas = {
    "A":np.random.randint(1, 45, 8),
    "B":np.random.randint(1, 45, 8),
    "C":np.random.randint(1, 45, 8),
}
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

# fillna_df.fillna(value=0) # 기본
fillna_df.fillna(method='pad') # 앞의 값을 가져와서 데이터를 채워줌
fillna_df.fillna(method='pad', axis=0) # 가로축

Referece
1) 제로베이스 데이터스쿨 강의자료

profile
데이터 사이언스 / just do it

0개의 댓글