[EDA] 인구 분석

박미영·2023년 4월 8일
0

DataSchool StudyNote - EDA

목록 보기
18/19

1. 배경

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





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

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import platform
import warnings

path="C:/Windows/Fonts/malgun.ttf"

if platform.system() == "Darwin":
    rc("font", family="Arial Unicode MS")
elif platform.system() == "Windows":
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc("font", family=font_name)
else:
    print("Unknown system. sorry~")


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



📍excel 데이터 가져오기

엑셀에서 그린 지도 모양을 읽어온다.

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


  • 원본 데이터 형태



- header=1

원본 데이터는 년도, 나이로 컬럼이 지정되어있다. 따라서 header 지정 없이 데이터를 불러오면 제일 첫번째 행이 년도가 컬럼으로 지정된다.
따라서 header=1을 통해 년도 컬럼이 아닌 나이를 컬럼으로 지정했다.

population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1) # 1행을 컬럼으로 사용하겠다. 
population



- fillna()

원본 파일에서 행정 구역 컬럼의 행들은 최소 3칸씩 차지 하고 있다. 그래서 한 칸의 나머지 비어있는 부분은 데이터를 불러왔을 때 NaN값으로 채워졌다.


population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1) # 1행을 컬럼으로 사용하겠다. 
population.fillna(method="pad", inplace=True)



📍컬럼 이름 변경

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



📍소계 제거

소계 부분은 필요 없으므로 제거한다.

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



📍구분 컬럼 행 이름 변경

population.is_copy = False  # copy 했을 때 warning 나오지 않게 해달라

population.rename(
    columns={"항목": "구분"}, inplace=True
)
population.loc[population["구분"]=="총인구수 (명)", "구분"]



  • 행 이름 변경
population.loc[population["구분"]=="총인구수 (명)", "구분"] = "합계" # loc[행, 열]
population.loc[population["구분"]=="남자인구수 (명)", "구분"] = "남자" # loc[행, 열]
population.loc[population["구분"]=="여자인구수 (명)", "구분"] = "여자" # loc[행, 열]



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

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

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



📍pivot_table

# pivot_table

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

pop



📍소멸 비율 계산

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



📍소멸위기지역 컬럼 생성

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



📍 소멸위기지역 조회

  • 도, 시/군 기준
pop[pop["소멸위기지역"] == True].index

  • 시/군 기준
pop[pop["소멸위기지역"] == True].index.get_level_values(1)



  • 광역시도가 컬럼으로 변경되었다. (구분 index 생성됨)
pop.reset_index(inplace=True)
pop.head()



📍컬럼 합치기

2개의 컬럼을 하나로 만들자

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



💡fillna 테스트

datas = {
    "A": np.random.randint(1, 45, 8),
    "B": np.random.randint(1, 45, 8),
    "C": np.random.randint(1, 45, 8),
}

datas


fillna_df = pd.DataFrame(datas)
fillna_df


fillna_df.loc[2:4]


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=0
    NaN을 0으로 채운다.
fillna_df.fillna(value=0)



  • method="pad" (=ffill)
    NaN 값에 앞에 있는 값을 가져와 채운다.
fillna_df.fillna(method="pad")



  • method="backfill"
    NaN 값에 뒤에 있는 값을 가져와 채운다.
    끝에 있는 데이터가 NaN 값이면 NaN이 그대로 있다.
fillna_df.fillna(method="backfill")





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

si_name = [None] + len(pop)
si_name



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

- 만들고자하는 ID 형태



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

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

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




📍ID 컬럼 추가

pop "ID" 컬럼으로 생성하여 앞서 만든 si_name을 저장해준다.

pop["ID"] = si_name
pop



📍필요 없는 컬럼 제거

현재 구해야하는 데이터는 여자 인구 소멸 위기 지역을 구하는 것이기 필요 없는 컬럼은 제거하자.

del pop["20-39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]
pop.head()



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

📍엑셀 파일 가져오기

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




📍DataFrame 생성

- stack

각 지역별 위치가 나타난다.
0인덱스에 0-6은 NaN, 7-철원, 8-화천, 9-양구, 10-고성(강원), 11-13 NaN이라는 의미를 나타낸다.

draw_korea_raw.stack()



- DataFrame

draw_korea_raw_Stacked = pd.DataFrame(draw_korea_raw.stack())
draw_korea_raw_Stacked



- reset_index()

인덱스로 나타난 좌표를 데이터로 사용하기 위해

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_raw_Stacked


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), (13, 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)]
]
BORDER_LINES



📍Text 설정

시도의 이름을 표현하는 함수 생성

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: # 3글자 이상인 경우 폰트 사이즈 줄이기
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2


        plt.annotate( # matplotlib에서 주석을 달기 위한 기능
            dispname, 
            (row["x"], row["y"]), # 경계선에서 조금 띄우기 위해
            weight="bold", 
            fontsize = fontsize, 
            linespacing = linespacing,
        )
  • 첫번째 for문


한 셀의 이름을 두 줄로 만들어주기 위한 작업



📍지도 그리기 연습

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)	# 검은색, 1.5 두께
    
    # plt.gca().invert_yaxis()
    # plt.axis("off")
    # plt.tight_layout()
    plt.show
simpleDraw(draw_korea)

  • 서귀포가 아래 있어야 하는데 밑에 있다. 엑셀은 위에서 아래로 그리고 지도는 아래에서 위로 그리기 때문이다. (위 아래가 뒤집혔다는 뜻)

-> 해결 코드

    # plt.gca().invert_yaxis()



  • 글자가 선에 맞춰지지 않았을 때 추가한 코드

plt.annotate( # matplotlib에서 주석을 달기 위한 기능
            dispname, 
            (row["x"] + 0.5, row["y"] + 0.5), 
            weight="bold", 
            fontsize = fontsize, 
            linespacing = linespacing,
            ha="center", # 수평 정렬
            va="center", # 수직 정렬
        )
plt.axis("off")  
    plt.tight_layout()



최종 지도 그리기 코드

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: # 3글자 이상인 경우 폰트 사이즈 줄이기
            fontsize, linespacing = 9.5, 1.5
        else:
            fontsize, linespacing = 11, 1.2


        plt.annotate( # matplotlib에서 주석을 달기 위한 기능
            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)



💡merge

- 검증 작업

공통된 ID 컬럼으로 merge() 하기 위해 데이터 검증 작업


차집합이 비어있어야 정상

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




반대는 차집합이 비어있지 않다. 따라서 제거해줘야 한다.
-> 비어있지 않은 이유: 광역시는 아니지만 행정구를 가지고 있는 도시

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)
set(pop["ID"].unique()) - set(draw_korea["ID"].unique())

데이터가 지워진 모습을 볼 수 있다.



- merge

지도를 그리기 위한 데이터와 인구현황 데이터를 합친다.

pop = pd.merge(pop, draw_korea, how="left", on="ID")
pop.head()



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

- get_data_info()

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

- get_data_info_for_zero_center()

  • 그림을 그리기 위한 데이터를 계산하는 함수
  • 색상을 만들때 중간값을 흰색으로 한다.
  • blockedMap: 인구현황 데이터, targetData: 그리고 싶은 컬럼
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()
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: # 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( # matplotlib에서 주석을 달기 위한 기능
            dispname, 
            (row["x"] + 0.5, row["y"] + 0.5), 
            weight="bold", 
            color=annocolor,
            fontsize = fontsize, 
            linespacing = linespacing,
            ha="center", # 수평 정렬
            va="center", # 수직 정렬
        )

  • drawkorea()
def drawkorea(targetData, blockedMap, cmapname, zeroCenter=False):

    if zeroCenter:	# 0이 센터에 위치하고 음수부터 양수의 값을 가짐
        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")




- 여성비 지도

여성이 많은 경우 파란색, 남성이 많은 경우 빨간색

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)


수도권 밖을 지날 수록 비율이 무너지는 모습을 볼 수 있다.





5. 지도 시각화(folium)

import folium
import json

pop_folium = pop.set_index("ID")
pop_folium

ID가 인덱스로 들어갔다.

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



- 인구수합계 지도 시각화

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_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)
mymap.choropleth( # 경계선
    geo_data=geo_str,
    data=pop_folium["소멸위기지역"],
    key_on="feature.id",
    columns=[pop_folium.index, pop_folium["소멸위기지역"]],
    fill_color="PuRd",
)
mymap





6. 데이터 저장

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




😆느낀점

2주간 재밌었지만 생각보다 쉽지 않았다. 아직도 사용할땐 어려워서 이것 저것 계속 검색을 해봐야하고 배운거 이외의 작업을 하는 것은,,, 아직 먼 세상이야기 같다.
빠른 진도를 따라가느라 쉽진 않았지만 언젠가는 익숙해질꺼라고 믿으며🤣


"이 글은 제로베이스 데이터 취업 스쿨 강의를 듣고 작성한 내용으로 제로베이스 데이터 취업 스쿨 강의 자료 일부를 발췌한 내용이 포함되어 있습니다."

0개의 댓글