[EDA] mini project 3 _ 인구소멸 위기지역 파악

jaam._.mini·2023년 12월 13일
0
post-thumbnail

프로젝트 목적/목표

  1. 인구소멸 위기 지역 파악
  2. 인구 소멸 위기 지역의 지도 표현
  3. 지도 표현에 대한 카르토그램 표현

📌 카르토그램 ex.

  • 각 칸의 넓이가 인구에 비례하도록 지역을 나눔
  • 양 쪽의 bias가 큼
  • 따라서 인구수 대비 '카르토그램'을 그리게 되면 정확히 파악이 가능해짐

📌 fillna ( ) | 기능 알아보기

  • EX. 1~45 중 8개 출력
datas = {
    "A" : np.random.randint(1,45,8),
    "B" : np.random.randint(1,45,8),
    "C" : np.random.randint(1,45,8)
}
datas

{'A': array([14, 42, 22, 24, 4, 43, 41, 38]),
'B': array([20, 14, 18, 6, 33, 3, 29, 23]),
'C': array([ 3, 22, 10, 14, 26, 43, 44, 28])}

fillna_df = pd.DataFrame(datas)
fillna_df
  • nan 값 주기
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
  • value 를 이용해서 채우기
fillna_df.fillna(value=0)
  • method='pad' 를 이용해서 채우기

    • pad : 앞에 있는 데이터를 가져와서 똑같이 채워라

    • method : {'backfill', 'bfill', 'ffill', None}, default None
      Method to use for filling holes in reindexed Series:

      • ffill: propagate last valid observation forward to next valid.
      • backfill / bfill: use next valid observation to fill gap.
fillna_df.fillna(method='pad')







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

📌 엑셀 불러오기

# , header=1 : 엑셀 상단 항목(구분)이 병합되어 있어 1번 줄 부터 불러오기 싶음

population = pd.read_excel('../data/07_population_raw_data.xlsx', header=1)
population.head(2)


📌 nan 값을 없애고 싶음

population.fillna(method='pad', inplace=True)
population.head(2)


📌 컬럼이름 변경

population.info()

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


📌 소계 제거

  • 특정 데이터(컬럼)가 아닌 것들만 출력
# population 중에서 시도 컬럼 안에서 소계 데이터가 아닌 것들만 선택
population[population['시도']!='소계']

# population에 저장
population = population[population['시도']!='소계']
population.tail(2)


📌항목 → 구분 : 컬럼 변경ㅍ

population.is_copy = False

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

population.tail(2)


📌소멸 지역을 조사하기 위한 데이터

  • 20-39세, 65세 이상 컬럼 추가
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(2)


📌피벗 테이블

  • index(인덱스), columns(컬럼), 밸류(value) 변경
  • columns(컬럼), 밸류(value) 을 지정하지 않으면 평균(mean)이 밸류값이 됨

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


📌소멸 비율 계산 & 컬럼 추가

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


📌소멸 위기 지역 컬럼 추가

# True, False 로 반환됨
pop["소멸위기지역"] = pop["소멸비율"] < 1.0
pop.tail()


📌소멸위기지역 조회

pop['소멸위기지역'] == True
# 마스킹을 하면, pop DataFrame에서 True 값만 출력해서 볼 수 있음
# index=["광역시도", "시도"] : 시도가 index이기 때문에 .index
pop[pop['소멸위기지역'] == True].index.get_level_values(1)


📌컬럼/인덱스 재설정

  • reset_index를 통해
    - "광역시도", "시도" 가 columns으로 잡힘 = DataFrame column이 됨
    - '구분'은 index가 됨
pop.reset_index(inplace=True)
pop.head()


📌컬럼 칸수 정리

  • 광역시도/시도 밑에 빈 컬럼을 더해주면 칸수가 깔끔하게 정리 됨
# 빈 변수를 설정해줘야 함 (이름은 마음대로)
tmp_columns = [

    # get_level_values(0) : 광역시도 ~ 소멸위기지역
    # get_level_values(1) : 남자 ~ 합
    pop.columns.get_level_values(0)[n] + pop.columns.get_level_values(1)[n]

    # 0(광역시도) ~ 컬럼 끝까지(소멸위기지역)
    for n in range(0, len(pop.columns.get_level_values(0)))
]
tmp_columns
pop.columns = tmp_columns
pop.head()







2. 지역별 ID 만들기


pop.info()

pop['시도'].unique()


📌si_name 빈 리스트 생성 x pop(인구현황데이터) 길이 만큼

si_name = [None] * len(pop)

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

pop.head()


📌만들고자 하는 ID의 형태

서울 중구
서울 서초
통영
남양주
포항 북구
인천 남동
안산 단원


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

# 인구현황 데이터(pop)에서 iterrows로 하나씩 받아오는데,
# row : pop['광역시도'] 
for idx, row in pop.iterrows():

    # 광역시도 컬럼에 있는 마지막 3글자, 광역시, 특별시, 자치시가 아니라면 
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        # 뒤에서 마지막 한글자를 빼고 저장해 (ex. 강릉시 → 강릉)
        si_name[idx] = row["시도"][:-1]

    # 세종은 방법이 없어서 직접 설정
    elif row["광역시도"] == "세종특별자치시":
        si_name[idx] = "세종"
    
    else:
        # 2글자 이면 그냥 두고, (ex. 중구)
        if len(row["시도"]) == 2:
            # 광역시면, 앞에 두글자만 따오고 뒤에 시도를 붙여라 (ex. 서울 중구)
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"]
    
        # 2글자 아니면
        else:
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"][:-1]
si_name

📌(2) 행정구 정리

  • ⭐dict에서 key/value 를 받는 건 : .items() 함수
  • ⭐DataFrame에서 key(idx)/value(row) 를 받는 건 : .iterrow() 함수
for idx, row in pop.iterrows():
    
    #광역시가 아닌 경우에
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        
        # tmp_gu_dict 에서 1개씩 가져와라
        # key : 행정구를 가진 시, value : 구 이름
        for keys, values in tmp_gu_dict.items():

            # 행정구를 지정한 dict형 자료에 있는(in values) 지역이라면
            if row["시도"] in values:

                # 두글자 이면
                if len(row["시도"]) == 2:
                 
                    # si_name 에 저장
                    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] = "고성(경남)"
si_name

📌ID 컬럼을 만들어 si_name 컬럼 추가

pop["ID"] = si_name
pop.tail()


📌여성의 data만 추출하고싶음

  • columns 삭제 (del)
del pop["20-39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]
pop.tail()







3. 시각화



📌필요한 데이터 가져오기

  • 엑셀에 우리나라 지도 모양을 표현한 자료를 가져 옴
draw_korea_raw = pd.read_excel('../data/07_draw_korea_raw.xlsx')
draw_korea_raw




(1) 1차 카르토그램 시각화



📌nan 값을 확인하기위해 '값들이 들어 있는 index만 확인'

  • .stack() : index 값들을 확인할 수 있음
  • DataFrame 으로 만들어주고
  • draw_korea_raw_stacked 변수로 저장
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 : 컬럼명 바꾸기

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

draw_korea_raw_stacked
draw_korea = draw_korea_raw_stacked


📌엑셀 BORDER_LINES(경계선) 쓰기

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

📌 plot_text_simple(draw_korea) 그래프 그리는 함수 작성

  • splitlines() : 문자열을 줄바꿈 기준으로 쪼개기
  • plt.annotate : matplotlib에서 주석을 달기 위한 기능
  • ha="center" : 수평 정렬
  • va="center" : 수직 정렬
# 시/도 이름을 표현하는 함수
def plot_text_simple(draw_korea):

    # draw_korea 에서 
    for idx, row in draw_korea.iterrows():

        # ID(지역이름)을 띄워쓰기 한 다음, 두글자 이면(=광역시)
        if len(row["ID"].split()) == 2:
            
            # \n(줄바꾸기를 해라) 첫 번째 칸{}은 split()[0], 두 번째 칸{}은 split()[1]
            dispname = "{}\n{}".format(row["ID"].split()[0], row["ID"].split()[1])

        # 고성은 그냥 고성으로 해라
        elif row["ID"][:2] == "고성":
            dispname = "고성"
        # 다 아니면, 그대로 써라
        else:
            dispname = row["ID"]
        
        # 자치구의 이름이 3글자 이상이면
        # splitlines() : 문자열을 줄바꿈 기준으로 쪼개기
        if len(dispname.splitlines()[-1]) >= 3:
            # 폰트를 줄여라
            fontsize, linespacing = 9.5, 1.5
        else:
            # 폰트를 키워라
            fontsize, linespacing = 11, 1.2

        # plt.annotate : matplotlib에서 주석을 달기 위한 기능 (엑셀에 글자를 쓰고 싶을 때 사용)
        plt.annotate(
            dispname, # 글자를 쓰고 싶음
            (row["x"] + 0.5, row["y"] + 0.5), #경계선에서 0.5씩 띄워서
            weight="bold",
            fontsize=fontsize, # 위에서 설정함
            ha="center", # 수평 정렬
            va="center", # 수직 정렬
            linespacing=linespacing # 위에서 설정함
        )

📌 simpleDraw(draw_korea) | plot_text_simple(draw_korea) 그래프 출력 명령

# 함수(DataFrame): ← 이 모양이 되야 출력 됨
def simpleDraw(draw_korea):
    plt.figure(figsize=(8, 11)) # 사이즈 설정
    
    plot_text_simple(draw_korea) # 위 명령 함수(DataFrame) 가져오
    
    for path in BORDER_LINES: # 위에서 작업한 것
        ys, xs = zip(*path) # 각 변수에 넣어 줌
        plt.plot(xs, ys, c="black", lw=1.5) # x끼리, y끼리 모아줌, 검정 색상, 1.5두께
    
    plt.gca().invert_yaxis() # matplotlib에서 y는 밑에서 위로 증가하나 excel은 반대라서 바꿔줌
    plt.axis("off")
    plt.tight_layout()
    plt.show()
    
simpleDraw(draw_korea)




(2) 2차 카르토그램 데이터 준비



📌 ID 를 기준으로 merge 하기 위해 [데이터 검증] & [정리]

  • set() : 집합으로 만드는 명령어
  • draw_korea["ID"] :지도 현황
  • pop["ID"] : 인구 현황

📌 차집합(지도 현황 - 인구 현황) : 없음

# 빼준다
set(draw_korea["ID"].unique()) - set(pop["ID"].unique())

#출력 값 : set()
# = 공집합 이라는 얘기
# 즉, pop에 draw_korea의 모든 ID가 포함되어 있다는 애기

set()


📌 차집합(인구 현황 - 지도 현황) : 있음

# 빼준다
set(pop["ID"].unique()) - set(draw_korea["ID"].unique())

#출력 값 : {'고양', '부천', '성남', '수원', '안산', '안양', '용인', '전주', '창원', '천안', '청주', '포항'}
# 행정구를 가지고 있는 도시들 임
# 삭제 Go GO !!

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


📌차집합(인구 현황 - 지도 현황) : 행정구 삭제

# list 형태로 만들고 싶으니까, list()로 다 묶음
tmp_list = list(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))

# tmp_list 에서 tmp 변수 하나씩 받아와서
for tmp in tmp_list:
    #pop ID 컬럼을 조회해서 tmp와 이름이 같은게 있으면 drop해라
    pop = pop.drop(pop[pop["ID"] == tmp].index) 

print(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))

set()


📌 ID 를 기준으로 merge
  • pop DataFrame 가져오고
  • draw_korea DataFrame 가져오고
  • how="left" : 왼쪽에 있는 데이터를 기준으로 하고
  • on=["ID"] : ID 컬럼을 기준으로 DataFrame을 합침
pop = pd.merge(pop, draw_korea, how="left", on=["ID"])
pop.head()




(3) 2차 카르토그램 시각화


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

  • 받아지는 인자
    • blockedMap : 인구현황(pop)
    • targetData : 인구현황(pop) 중에서 그리고 싶은 column
  • whitelabelmin :
    • 바탕이 흰색일 때는 검정색 글씨를 사용해야함
    • 그 경계선을 지정하는 것
    • 색상을 만들 때, 최소값을 흰색으로 하는 기능을 담음
  • pivot_table 시킴
  • columns="x" : simple로 했을때 엑셀 자리에 [지역이름]이 들어갔는데, targetData로 지정된 컬럼의 숫자를 넣어 줌
  • get_data_info_for_zero_center :
    • 색상을 만들 때 "중간값"을 흰색으로 하는 함수
    • 음수, 양수 값이 있어서 0을 센터에 두고 싶을 때 사용
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

📌위에 plot_text_simple (1차 그래프)를 가져 옴

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

        # 크면 white, 작으면 black
        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,
            ha="center",
            va="center",
            linespacing=linespacing
        )

📌 simpleDraw(draw_korea) (1차 그래프 출력 함수)를 가져옴

def drawKorea(targetData, blockedMap, cmapname, zeroCenter=False):
    if zeroCenter:
        masked_mapdata, vmaxm, 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=(9,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=2)

    plt.gca().invert_yaxis()
    plt.axis('off')
    cb = plt.colorbar(shrink=0.1, aspect=10)
    cb.set_label(targetData)
    plt.tight_layout()
    plt.show()

📌 인구수 합계 그래프

drawKorea('인구수합계', pop, 'Blues')


📌 소멸위기지역 그래프

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




(4) 소멸위기 지역 지도 시각화


import folium
import json
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'))
mymap = folium.Map(location=[36.2002, 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'
)

mymap
mymap = folium.Map(location=[36.2002, 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'
)

mymap
# 데이터 저장

draw_korea.to_csv('../data/07_drae_korea_1213.csv', encoding='utf-8', sep=',')

제로베이스 데이터 스쿨
profile
비전공자의 데이터 공부법

0개의 댓글