목표
- 인구 소멸 위기 지역 파악
- 인구 소멸 위기 지역의 지도 표현
- 지도 표현에 대한 카르토그램 표현
인구 소멸 위기 지역이란?
65세 이상 노인 인구와 20~39세 여성 인구를 비교해서 젊은 여성 인구가 노인 인구의 절반보다 적을 경우
인구 소멸 위기 지역이라고 한다.
카르토그램
의석수나 선거인단수, 인구 등의 특정한 데이터 값의 변화에 비례하게 지도의 면적을 나타낸 것
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
#import set_matplotlib_hangul
from matplotlib import rc
rc("font", family="Malgun Gothic")
warnings.filterwarnings(action="ignore") # 경고문구 무시
%matplotlib inline
#get_ipython().run_line_magic("matplotlib", "inline")
population = pd.read_excel("../data/07_population_raw_data.xlsx")
#초기 데이터에서 연도가 컬럼으로 잡혀있고 나이는 value값으로 들어가있음
#연도를 날려줌
population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1)
#nan값을 채워줌
#nan값이 있는 것은 원본 데이터를 보면 전국에 해당하는 셀이 여러셀로 병합되어 나타나져 있어서 한 셀을 제외한 나머지 셀에 대해서는 nan값으로 출력된다.
population.fillna(method="pad", inplace=True)
population
value : scalar, dict, Series, or DataFrame
Value to use to fill holes (e.g. 0), alternately a
dict/Series/DataFrame of values specifying which value to use for
each index (for a Series) or column (for a DataFrame). Values not
in the dict/Series/DataFrame will not be filled. This value cannot
be a list.
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() 예시
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)
위 데이터 프레임에 임의로 nan값을 주고
nan값을 채워본다.
value
fillna_df.fillna(value=0)

method="pad" , axis=0
fillna_df.fillna(method="pad")

method="pad", axis=1
fillna_df.fillna(method="pad",axis=1)

method="backfill"
fillna_df.fillna(method="backfill")

method로 채우는 방법은 기준이 되는 값이 없으면 그대로 nan

# 컬럼 이름 변경
population.rename(
columns={
"행정구역(동읍면)별(1)" : "광역시도",
"행정구역(동읍면)별(2)" : "시도",
"계" : "인구수"
},inplace=True
)
population.tail()
### 광역시도, 시도 컬럼에서 전국, 소개 내용은 필요가 없음
### 소계 제거
population = population[population["시도"] != "소계"]
#copy 했을 때, warning을 내보내지 말라는 옵션
population.is_copy = False
#항목 컬럼 이름 변경
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+"]
)

# pivot_table
"""
values 값을 지정하면 그 값이 그대로 들어가고
지정하지 않으면 평균값이 들어감
"""
pop = pd.pivot_table(
data=population,
index=["광역시도", "시도"],
columns=["구분"],
values=["인구수", "20~39세", "65세이상"]
)
# 소멸 비율 계산
pop["소멸비율"] = pop["20~39세", "여자"] / (pop["65세이상", "합계"] / 2)
pop.tail()
# 소멸위기지역 컬럼 생성
pop["소멸위기지역"] = pop["소멸비율"] < 1.0 #True or False 값

# 소멸위기지역 조회
pop[pop["소멸위기지역"] == True].index.get_level_values(1)
#소멸위기지역의 시도 명을 알고 싶은데, 시도는 인덱스 값으로 들어가있고,
#인덱스는 광역시도, 시도 두개로 멀티인덱스이므로 get_level_values(인덱스값)으로 시도를 불러옴
출력결과
Index(['고성군', '삼척시', '양양군', '영월군', '정선군', '평창군', '홍천군', '횡성군', '가평군', '양평군',
'연천군', '거창군', '고성군', '남해군', '밀양시', '산청군', '의령군', '창녕군', '하동군', '함안군',
'함양군', '합천군', '고령군', '군위군', '문경시', '봉화군', '상주시', '성주군', '영덕군', '영양군',
'영주시', '영천시', '예천군', '울릉군', '울진군', '의성군', '청도군', '청송군', '동구', '영도구',
'강화군', '옹진군', '강진군', '고흥군', '곡성군', '구례군', '담양군', '보성군', '신안군', '영광군',
'영암군', '완도군', '장성군', '장흥군', '진도군', '함평군', '해남군', '화순군', '고창군', '김제시',
'남원시', '무주군', '부안군', '순창군', '임실군', '장수군', '정읍시', '진안군', '공주시', '금산군',
'논산시', '보령시', '부여군', '서천군', '예산군', '청양군', '태안군', '홍성군', '괴산군', '단양군',
'보은군', '영동군', '옥천군'],
dtype='object', name='시도')
광역시도, 시도를 인덱스에서 빼준다
pop.reset_index(inplace=True)

# 컬럼명이 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()

pop.info()로 데이터 확인, 문제 없어 보인다.
ID값이 될 리스트를 none값으로 채워서 만들어 둔다.
si_name = [None] * len(pop)
si_name = [none, none, ... none] 형태

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]
결과
['강릉','고성','동해','삼척', ... , '남양주',
'단원', ... ,'마산합포', '마산회원', ..., '부산 남구', ... ,'서울 송파', ... ]
tmp_gu_dic = {}
tmp_gu_dic = {
"수원" : ["장안구", "권선구", "팔달구", "영통구"],
"성남" : ["수정구", "중원구", "분당구"],
"안양" : ["만안구", "동안구"],
"안산" : ["상록구", "단원구"],
"고양" : ["덕양구", "일산동구", "일산서구"],
"용인" : ["처인구", "기흥구", "수지구"],
"청주" : ["상당구", "서원구", "흥덕구", "청원구"],
"천안" : ["동남구", "서북구"],
"전주" : ["완산구", "덕진구"],
"포항" : ["남구", "북구"],
"창원" : ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
"부천" : ["오정구", "원미구", "소사구"]
}
for idx, row in pop.iterrows():
if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
for keys, values in tmp_gu_dic.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] = "고성(경남)"
pop["ID"] = si_name
ID컬럼을 추가해서 si_name 값으로 채워준다
소멸위기지역 판단에 필요없는 컬럼 제거한다.
del pop["20~39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]

1) 엑셀에서 지도모양으로 작성된 파일을 불러온다
draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")
draw_korea_raw

2) 좌표값 데이터프레임 draw_korea
nan값을 제외한 값들로 데이터프레임을 만들어준다.
draw_korea = pd.DataFrame(draw_korea_raw.stack())
stack() : 컬럼을 인덱스로 보내는 기능, 원래 인덱스도 있기 때문에 stack() 후에는 멀티인덱스가 됨

여기서 멀티인덱스로 설정된 값들은 카르토그램의 x,y좌표가 될 값들이므로 이 값들은 인덱스에서 제외시키고, 컬럼명을 재설정 해준다.
draw_korea.reset_index(inplace=True)
draw_korea.rename(
columns={
"level_0" : "y",
"level_1" : "x",
0 : "ID"
}, inplace=True
)
draw_korea

3) 지역별 경계좌표 값 설정
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), (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)]
]
4) 카르토그램에 글자를 표현하는 함수
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:
#일산동, 일산서, 동대문 같은 곳
fontsize, linespacing = 9.5, 1.5 #글자크기, 줄간격 조정
else: #3글자보다 적은 곳
fontsize, linespacing = 11, 1.2
#plt.annotate() : 주석 달아주는 기능, 여기서는
위에서 dispname을 그래프에 표시해주는 역할
plt.annotate(
dispname,
(row["x"]+0.5 , row["y"]+0.5),
#경계선에 글자가 나오지 않도록 위치조정
weight="bold", #글자 진하게
fontsize=fontsize, #위에서 설정함
linespacing=linespacing, #위에서 설정함
ha="center", #수평정렬
va="center" #수직정렬
)
5) 카르토그램을 그리는 함수

for path in BORDER_LINES:
ys, xs = zip(*path)
#경계좌표로 그래프를 그리려고 하는데,
plot을 시키려면 x끼리, y끼리 모아야하므로 이렇게 써준다.
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)
#경계좌표로 그래프를 그리려고 하는데,
plot을 시키려면 x끼리, y끼리 모아야하므로 이렇게 써준다.
plt.gca().invert_yaxis()
#matplotlib에서 y가 증가하는 방향과 엑셀에서 y가 증가하는 방향이 반대라서 이를 바꿔줘야함
plt.axis("off")
#그래프의 모든 축과 라벨을 제거
plt.tight_layout()
#주석들이 모여있지 않고 좀 널널하게
plt.show()
4, 5번을 적용하여 기본형태를 그리면 다음과 같다.

6) pop, draw_korea를 합치기 전에 문제없는지 검증
set(draw_korea["ID"].unique()) set(pop["ID"].unique())
결과 set()
set(pop["ID"].unique()) - set(draw_korea["ID"].unique())
{'고양', '부천', '성남', '수원', '안산', '안양', '용인', '전주', '창원', '천안', '청주', '포항'}
차집합은 교환법칙이 성립하지 않기 때문에 위 결과가 다르게 나온다.
광역시는 아니지만 행정구를 갖고 있는 지역명이 pop["ID"]에 남아 있다는 것이므로 이를 제거 해준다.
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()
7) 두 데이터프레임을 병합

8) 그림을 그리기 위한 데이터를 계산하는 함수
카르토그램 배경색에 맞춰서 글자 색상을 다르게 만들어주는 함수를 만들어보자
#데이터에 따라 배경색이 달라질때, 글씨 색상도 맞춰서 바꿔주는 진한 배경-> 흰색 글자, 연한 배경-> 검은색 글자
def get_data_info(targetData, bloackedMap):
# 글씨 색상을 결정하는 경계값
whitelabelmin = (
max(bloackedMap[targetData]) - min(bloackedMap[targetData])
) * 0.25 + min(bloackedMap[targetData])
vmin = min(bloackedMap[targetData])
vmax = max(bloackedMap[targetData])
#지역이름이 적히는 자리에 숫자데이터(예를 들어 인구수합계)가 들어감
mapdata = bloackedMap.pivot_table(index="y", columns="x", values=targetData)
return mapdata, vmax, vmin, whitelabelmin
그리려는 데이터 범위가 음수~양수인 경우, 0을 중간값으로 두는 함수
# 데이터 분포가 음수 ~ 양수 범위인 경우, 0을 중간값으로
def get_data_info_for_zero_center(targetData, bloackedMap):
whitelabelmin = 5
tmp_max = max(
[np.abs(min(bloackedMap[targetData])), np.abs(max(bloackedMap[targetData]))]
)
vmin, vmax = -tmp_max, tmp_max
mapdata = bloackedMap.pivot_table(index="y", columns="x", values=targetData)
return mapdata, vmax, vmin, whitelabelmin
9) 실제 그래프 그리는 함수
4번에서 만들었던 글자 표현 함수를 사용해보자
def plot_text(targetData, bloackedMap, whitelabelmin):
for idx, row in bloackedMap.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" #수직정렬
)
5번에서 만들었던 함수를 사용해보자
def drawKorea(targetData, bloackedMap, cmapname, zeroCenter=False):
#0을 중간값으로 둘지 말지
if zeroCenter:
masked_mapdata, vmax, vmin, whitelabelmin = get_data_info_for_zero_center(targetData, bloackedMap)
if not zeroCenter:
masked_mapdata, vmax, vmin, whitelabelmin = get_data_info(targetData, bloackedMap)
plt.figure(figsize=(8,11))
#그래프 채우는 색상
plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmapname, edgecolor="#aaaaaa", linewidth=0.5)
#그래프에 글자 표현
plot_text(targetData, bloackedMap, 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["소멸위기지역"]] #True-> 1 False -> 0
drawKorea("소멸위기지역", pop, "Reds")

전라도와 경북, 충남, 강원도에서 소멸위기지역이 많이 보인다.
여성비 카르토그램
pop["여성비"] = (pop["인구수여자"] / pop["인구수합계"] - 0.5) * 100
drawKorea("여성비", pop, "RdBu", zeroCenter=True)

파란색에 가까울수록 여성비가 높은 곳인데 진한 파란색은 보이지 않지만, 남/여 비율은 비슷해보인다.

일부지역을 제외하고 대부분 지역에서 남성 인구수가 더 많은 것 같다.
import folium
import json
geo_path = "../data/07_skorea_municipalities_geo_simple.json"
geo_str = json.load(open(geo_path, encoding="utf-8"))
pop_folium = pop.set_index("ID")
#json파일에도 ID가 인덱스이므로 맞춰주면 그리기 편함
pop_folium.head()
인구수합계로 시각화
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

한반도의 절반 이상이 소멸위기지역으로 나타났다.