관리 메뉴

log.Sehee

[데이터 취업 스쿨 스터디 노트] Cartogram (카르토그램) / EDA 인구분석 1 - 3 본문

Zerobase DS School

[데이터 취업 스쿨 스터디 노트] Cartogram (카르토그램) / EDA 인구분석 1 - 3

Sehe_e 2024. 8. 15. 21:39

 


 

인구 분석

 

 

개요

- 인구 소멸 위기 지역 파악

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

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

 

 

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

import numpy as np, pandas as pd, matplotlib as plt, set_matplotlib_hangul, warnings

warnings.filterwarnings(action='ignore')
%matplotlib inline
population = pd.read_excel('../data/07_population_raw_data.xlsx', header = 1)
# fillna(method='pad') pad 옵션은 null값 바로 앞 값을 가져와서 채움 / backfill: 뒷 값을 가져와서 채움
population.fillna(method='pad', inplace=True)
population.head()

 

컬럼 정리

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

 

소계 제거 및 컬럼 이름 변경

# 소계 제거
population = population[population['시도'] != '소계']
population.is_copy = False
population.rename(
    columns = {'항목':'구분'}, inplace = True
)
population.head()

 

구분 column의 총인구수, 남자인구수, 여자인구수 각각의 행 내용 변경하기

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

 

20 - 39세, 65세 이상 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+"]
)
population.head()

 

pivot table 생성

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

 

소멸 비율 column 생성

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

 

소멸 위기 지역 column 생성

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

 

소멸 위기 지역 True인 '시도' column 보기

# 소멸위기지역 조회
pop[pop["소멸위기지역"] == True].index.get_level_values(1)

 

인덱스 리셋

# index 리셋
pop.reset_index(inplace=True)
pop.head()

 

0번째 column과 1번째 column을 문자열로 더해 하나로 통합하기

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

 

 


 

지도 시각화를 위한 지역별 ID 만들기

 

지역구 정보 딕셔너리 만들기

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

 

특별시, 광역시, 자치시 등 column

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

 

시, 군, 구 등 column

pop['시도'].unique()

 

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

# 데이터 길이만큼의 시 정보 넣을 리스트 생성
si_name = [None] * len(pop)

for idx, row in pop.iterrows():
    # 서울특별시[-3:] => 특별시
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        # 임실군[:-1] => 임실
        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]

 

ID column 생성

pop['ID'] = si_name
pop.head()

 

불필요한 column 제거

# 불필요한 컬럼 제거
del pop["20 - 39세남자"]
del pop["65세 이상남자"]
del pop["65세 이상여자"]

 

 


 

지도 그리기 (Catogram)

 

# 지도 데이터 불러오기
draw_korea_raw = pd.read_excel('../data/07_draw_korea_raw.xlsx')
draw_korea_raw

 

위 엑셀 파일의 null값 제외, 값의 row 인덱스 정보와 column 인덱스 정보 정리

# 엑셀 위치를 나타내는 값 데이터프레임에 저장
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

 

컬럼명 정리

# 컬럼명 변경
draw_korea_raw_stacked.rename(
    columns = {
        "level_0" : "y",
        "level_1" : "x",
        0 : "ID"
    }, inplace=True
)
draw_korea = draw_korea_raw_stacked
draw_korea

 

위치 정보 튜플로 리스트에 정리

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():
        # 지역이름 dispname에 저장하기
        
        # ID가 두 단어이면
        if len(row["ID"].split()) == 2:
            r = row['ID'].split()
            dispname = f"{r[0]}\n{r[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", # 수직 정렬
        )

 

지역 라인 그리기 위한 함수

import matplotlib.pyplot as plt

def simpleDraw(draw):
    plt.figure(figsize=(8,11))
    plot_text_simple(draw)

    # x좌표끼리, y좌표끼리 묶어주기
    for path in BORDER_LINES:
        ys, xs = zip(*path)
        plt.plot(xs, ys, c="black", lw=1.5)
    
    plt.gca().invert_yaxis() # y좌표 상하전환
    # 모여 있는 텍스트 풀어주기
    plt.axis("off")
    plt.tight_layout()
    plt.show()
simpleDraw(draw_korea)

 

 


 

검증 작업

 

pop Dataframe과 draw_korea Dataframe을 merge하기 전 서로의 데이터에 문제가 없는지 확인

 

차집합으로 데이터에 문제가 없는지 검증

set(draw_korea["ID"].unique()) - set(pop["ID"].unique()) # 차집합 구하기. set이 비어야 정상

 

pop에서 차집합 구하니 결과값이 생김

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

 

Dataframe merge

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:
            r = row['ID'].split()
            dispname = f"{r[0]}\n{r[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, cmpname, zeroCenter=False):
    
    if zeroCenter:
        masked_mapdata, vmax, vmin, whitelablemin = get_data_info_for_zero_center(targetData, blockedMap)
        
    if not zeroCenter:
        masked_mapdata, vmax, vmin, whitelablemin = get_data_info(targetData, blockedMap)
        
    plt.figure(figsize=(8,11))
    plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmpname, edgecolor = "#aaaaaa", linewidth=0.5)
    plot_text(targetData, blockedMap, whitelablemin)
    
    for path in BORDER_LINES:
        ys, xs = zip(*path)
        plt.plot(xs, ys, c="black", lw=1.5) 
    
    plt.gca().invert_yaxis() # y좌표 상하전환
    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)

 

2030 여성비율 별 색상 표기

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

 

 


 

folium 시각화

import folium
import json

pop_folium = pop.set_index("ID")
pop_folium

 

인구수별 지도 시각화

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

mymap

 

소멸 위기 지역 지도 시각화

# 소멸위기지역 지도 시각화

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

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

mymap

 

데이터 저장

# 데이터 저장
draw_korea.to_csv("../data/07_draw_korea.csv", encoding="utf-8", sep=",")

 

 


내일의 학습 목표

EDA 학습과제 2

 

 

Comments