EDA_7 Population

이병찬·2024년 3월 13일

EDA

목록 보기
7/7

fillna를 통한 NaN값 채우기

  • value = : NaN값을 value에서 지정한 값으로 채워라
  • method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None
    pad / ffill: 바로 직전 데이터를 가져와서 채워줘라
    backfill / bfill: 바로 직후 데이터를 가져와서 채워줘라

인구현황 데이터 프레임 확인, read_excel

population = pd.read_excel("../data/Population data/07_population_raw_data.xlsx", header=1)
population.fillna(method="pad", inplace=True) # NaN 값은 제외하겠다
population.head()

컬럼 이름 변경, rename

population = pd.read_excel("../data/Population data/07_population_raw_data.xlsx", header=1)
population.fillna(method="pad", inplace=True) # NaN 값은 제외하겠다
population.head()

조건에 따른 '시도' 컬럼 안의 소계 제거

# 소계 제거 
population = population[population["시도"] != "소계"] 
# ["시도"] != "소계", population 중 '시도' 컬럼 안에 '소계' 라는 데이터 값이 아닌 것들만 출력해라
population.head()

'항목' 컬럼명 변경

population.is_copy = False 

population.rename(
    columns={"항목": "구분"}, inplace=True
)

population.head()

조건에 따른 '구분' 컬럼의 데이터값 변경

population.loc[population["구분"] == "총인구수 (명)", "구분"] = "합계"
# loc[행, 열]
# 행 = population["구분"] == "총인구수 (명)", "구분" 컬럼 중에서 "총인구수 (명)" 부분만 설정
# 열 = "구분", 행에서 설정한 부분에서 "구분"들만 설정 
# population.loc[행, 열] = "합계", loc으로 설정한 부분을 "합계" 로 바꿔라 
population.loc[population["구분"] == "남자인구수 (명)", "구분"] = "남자"
population.loc[population["구분"] == "여자인구수 (명)", "구분"] = "여자"

소멸 지역 조사를 위한 컬럼간 계산을 통한 추가 컬럼('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()

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.get_level_values(1)

인덱스 초기화(reset_index) 후 get_level_values에 따른 컬럼 재설정

print(pop.columns.get_level_values(0)),# level(0) value 출력
len(pop.columns.get_level_values(0))

print(pop.columns.get_level_values(1)),# level(1) value 출력
len(pop.columns.get_level_values(1))

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 만들기

pop 데이터프레임 인덱스만큼의 si_name 빈리스트를 만들고 행정구 딕셔너리 만들기

si_name = [None] * len(pop) # len(pop) = 264
si_name

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

만들고자 하는 ID 형태 정의

  • 서울 중구, 서울 서초, 남양주, 안양 만안, 안양 동안...

슬라이싱을 통한 확인

text1 = '서울특별시'
text1[2:] # 2번째부터 끝까지 

text2 = '서울특별시'
text2[-3:] # 맨끝부터 역순으로 세글자만

text3 = '임실군'
text3[:-1] # 마지막 한글자를 빼고 처음부터 끝까지 출력

text4 = "서울특별시"
text5 = "경산시"
text4[:2] + " " + text5[:-1]

for문과 iterrows를 통하여 si_name 리스트 append

for idx, row in pop.iterrows():
    # 광역시도 컬럼 데이터에서 광역시, 특별시, 자치시가 포함되지 않은 데이터는 
    # 시도 컬럼 데이터에서 마지막 한글자만 빼고 처음부터 출력
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
        si_name[idx] = row["시도"][:-1]
    
    elif row["광역시도"] == "세종특별자치시": #유일한 데이터로 별도 설정 필요
        si_name[idx] = "세종"
    
    # 광역시도 컬럼 데이터에서 광역시, 특별시, 자치시가 포함된 데이터 중 
    else:
        # 시도 컬럼 데이터가 2글자이면
        if len(row["시도"]) == 2:
            # 광역시도 컬럼 데이터에서 앞 2글자만, 시도 컬럼 데이터를 합쳐서 출력
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"]
        else:
            # 광역시도 컬럼 데이터에서 앞 2글자만, 시도 컬럼 데이터에서 마지막 한글자만 뺴고 처음부터 출력
            si_name[idx] = row["광역시도"][:2] + " " + row["시도"][:-1]

앞에서 정의한 행정구 딕셔너리를 고려하여 for문 재설정

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

for idx, row in pop.iterrows(): # interrow() : 데이터프레임에서 key와 value로 나눠 받는 함수 
    if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
    	# 행정구 딕셔너리에 포함되는 지역인지 확인 
        for keys, values in tmp_gu_dict.items():# items() : dict 형태({:})를 key와 value로 나눠 받는 함수
            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' 컬럼 추가 및 필요없는 컬럼 삭제

사전에 준비된 엑셀의 카르토그램 가져오기

stack()을 이용한 데이터 재구조화(위에서 아래로 데이터 쌓기) 후 데이터프레임 생성

draw_korea_raw.stack()

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

reset_index 후 x, y 좌표값으로 재설정 후 변수에 지정

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

각 지역을 x, y좌표로 표현하기 위한 리스트 만들기

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():
        # 띄어쓰기로 split 했을 시 단어가 2개 이상인 것
        if len(row["ID"].split()) == 2:
            dispname = "{}\n{}".format(row["ID"].split()[0], row["ID"].split()[1]) 
                        # \n 다음줄로 내려가라, 개행하라
        elif row["ID"][:2] == "고성":
            dispname = "고성"
        else:
            dispname = row["ID"]
            
        if len(dispname.splitlines()[-1]) >= 3: # dispname의 이름이 3글자 이상이면(ex.남양주)
            fontsize, linespacing = 9.5, 1.5 # 글자크기를 줄이자
        else:
            fontsize, linespacing = 11, 1.2 # 그렇지 않다면 글자크기를 조금 키우자

        # plt.annotate = 주석달기, 그래프 안에 단어 넣기
        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)
        # BORDER_LINES에서 리스트 안의 (y, x) 값들을 path로 하나씩 보내면서 
        # zip함수를 통해 y값끼리 x값끼리 묶어주는 방법
        plt.plot(xs, ys, c="black", lw=1.5)
    
    plt.gca().invert_yaxis()
# 엑셀파일을 받아왔기 때문에 y축의 증가 방향이 엑셀은 위, 
# matplotlib은 아래 이므로 엑셀과 같은 y축 증가뱡향을 만들어줘야 함에 따라 
# plt기능 중 .gca().invert_yaxis() y축 방향을 뒤집어 줘야된다

    plt.axis("off") # 그래프의 x, y축과 이름들을 제거 
# plt.axis() = 그래프의 x축, y축 옵션 설정

    plt.tight_layout()
#  각 요소(subplot)들이 겹치지 않게 최소한의 여백을 만들어주는 역할,그래프 출력 전 맨 마지막에 설정
    plt.show()

pop데이터와 draw_korea 데이터를 merge 시 빈값이 없는지 검증

  • 두 데이터의 ID가 unique값으로 존재하여 set()을 이용하여 차집합 검증
  • draw_korea 에서 pop 차집합 시 공집합 확인
  • 하지만 한 식의 차집합이 공집합이라도 전치했을 때 공집합을 증명해주진 않기 때문에 pop에서 draw_korea 차집합 시 공집합도 확인

draw_korea 에서 pop 차집합

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

pop에서 draw_korea 차집합

  • {'고양', '부천', '성남', '수원', '안산', '안양', '용인', '전주', '창원', '천안', '청주', '포항'} 존재
  • 차집합 시 존재하는 시들은 광역시가 아닌데 행정구를 가지고 있던 도시들로 이미 행정구별로 pop데이터 프레임에서 인구 현황을 잡아놨기 때문에 전체 시에 대한 인구 현황 데이터는 필요 없으므로 제거 필요
set(pop["ID"].unique()) - set(draw_korea["ID"].unique())

pop에서 차집합 시 존재 했던 전체 시에 대한 인구 현황 삭제 후 차집합 확인

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)
    # pop['ID'] 중 tmp_list와 같은 값들로 설정된 인덱스들을 제거해라
print(set(pop["ID"].unique()) - set(draw_korea["ID"].unique()))

pop, draw_korea merge

pop = pd.merge(pop, draw_korea, how="left", on="ID")
# pop 및 ID를 기준으로 병합해라
pop.head()

get_data_info(targetData, blockedMap)

  • 그림을 그리기 위한 데이터를 계산하는 함수
  • 색상을 만들 때 최솟값을 흰색으로 한다.
  • 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(targetData, blockedMap):

  • 그림을 그리기 위한 데이터를 계산하는 함수
  • 색상을 만들때 중간값을 흰색으로 상하 극단값에 색상을 다르게 해주는 함수
  • 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]))]
        # np.abs() : 절대값 함수
    )
    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

        # 상기에서 지정한 whitelabelmin에 따라 글자 색상 설정
        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)
    # colorbar = 각 데이터의 기준 표시bar
    cb.set_label(targetData)
    plt.show()

인구수합계 지도 시각화

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

소멸위기지역 지도 시각화

pop["소멸위기지역"] = [1 if con else 0 for con in pop["소멸위기지역"]]
                       # pop["소멸위기지역"]에서 하나씩 con 이란 변수에 담아준다, 
                       # con = true = 1, con = false = 0 으로 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.head()

folium을 이용한 인구수합계 지도시각화

geo_path = "../data/Population 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)
# choropleth : 경계선 그리는 기능
folium.Choropleth(
    geo_data=geo_str,
    data=pop_folium["인구수합계"],
    key_on="feature.id",
    columns=[pop_folium.index, pop_folium["인구수합계"]],
    fill_color="YlGnBu"
).add_to(mymap)

mymap

folium을 이용한 소멸위기지역 지도시각화

# 소멸위기지역 지도 시각화
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"
).add_to(mymap)

mymap

profile
비전공 데이터 분석가 도전

0개의 댓글