
【Cartogram 시각화】 인구 현황 데이터 분석
카르토그램은 지리적 공간을 나타내는 지도에서 지역의 상대적 크기를 표현하는 방식을 말한다.
일반적으로 지도에서는 지역의 면적이나 크기가 실제 지리적 크기와 일치하지만, 카르토그램은 특정 변수의 값을 반영하여 지역의 크기를 변형하여 나타낸다.
카르토그램은 통계적 혹은 시각적으로 중요한 데이터를 강조하거나 특정 지역 간의 비교를 용이하게 하기 위해 사용된다.
일반적으로 인구, 경제적 측면, 환경적 특성 등과 같은 변수를 나타내는 카르토그램이 흔히 사용된다.
import numpy as np
import pandas as pd
population = pd.read_excel("../data/07_population_raw_data.xlsx")
population.head()

population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1)
fillna() : NaN 값 채우기 population.fillna(method="pad", inplace=True)
population


소멸위험지수는?
65세 고령인구 대비 20-39세 여성인구 비중으로 비율이 0.5이하이면 30년 내 소멸 가능성이 크다고 판단한다.
소멸위험지수 = (20-39세 여성 인구수) / (65세 이상 인구수)
소멸위험지수가 0.5미만이면 인구 소멸 위험 지역으로 구분한다.


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+"]
)
popul_pivot = pd.pivot_table(
data=population,
index=["광역시도", "시도"],
columns=["구분"],
values=["인구수", "20-39세", "65세이상"]
)
popul_pivot["소멸지수"]
= popul_pivot["20-39세", "여자"] / popul_pivot["65세이상", "합계"]
popul_pivot["소멸위기지역"] = popul_pivot["소멸지수"] < 0.5

// 인덱스 재정렬
popul_pivot.reset_index(inplace=True)
popul_pivot.head()
// 컬럼 더하기
tmp_cols = [
popul_pivot.columns.get_level_values(0)[n]
+ popul_pivot.columns.get_level_values(1)[n]
for n in range(len(popul_pivot.columns.get_level_values(0)))
]
popul_pivot.columns = tmp_cols

지도 시각화를 위해 지역별 ID가 필요하다.
일반 시에 있는 행정구 데이터를 정리한다.
admin_gu = {
"수원": ["장안구", "권선구", "팔달구", "영통구"],
"성남": ["수정구", "중원구", "분당구"],
"안양": ["만안구", "동안구"],
"안산": ["상록구", "단원구"],
"고양": ["덕양구", "일산동구", "일산서구"],
"용인": ["처인구", "기흥구", "수지구"],
"청주": ["상당구", "서원구", "흥덕구", "청원구"],
"천안": ["동남구", "서북구"],
"전주": ["완산구", "덕진구"],
"포항": ["남구", "북구"],
"창원": ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
"부천": ["오정구", "원미구", "소사구"],
}
일반 시
세종특별자치시는 하나밖에 없다.
일반 구
행정구
def find_key_by_value(dictionary, value):
for key, val in dictionary.items():
if value in val:
return key
return None // 값이 없는 경우
for idx, row in popul_pivot[["광역시도", "시도"]].iterrows():
if row["광역시도"][-3:] not in ["광역시", "특별시", "자치시"]:
# 행정구
if row["시도"][-1:] == "구":
key = find_key_by_value(admin_gu, row["시도"])
if len(key) > 0:
if len(row["시도"]) == 2:
sidoID[idx] = key + " " + row["시도"]
elif row["시도"] in ["마산합포구", "마산회원구"]:
sidoID[idx] = key + " " + row["시도"][2:-1]
else:
sidoID[idx] = key + " " + row["시도"][:-1]
# 고성군
elif row["시도"] == "고성군":
if row["광역시도"] == "강원도":
sidoID[idx] = "고성(강원)"
else: sidoID[idx] = "고성(경남)"
# 일반시
else:
sidoID[idx] = row["시도"][:-1]
# 세종특별자치시
elif row["광역시도"] == "세종특별자치시":
sidoID[idx] = "세종"
# 특별시/광역시 구
else:
if len(row["시도"]) == 2: // 서구, 중구, 북구, ...
sidoID[idx] = row["광역시도"][:2] + " " + row["시도"]
else:
sidoID[idx] = row["광역시도"][:2] + " " + row["시도"][:-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
# Hangul OK in your Windows!!!
draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")
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)] # 제주도
]
stack() draw_kr_carto = pd.DataFrame(draw_korea_raw.stack())
draw_kr_carto.reset_index(inplace=True)
draw_kr_carto.rename(
columns={"level_0": "y", "level_1": "x", 0: "ID"},
inplace=True
)
|

set(draw_kr_carto["ID"].unique()) - set(population["ID"].unique())
//==> set()
tmp_list = list(
set(population["ID"].unique()) - set(draw_kr_carto["ID"].unique())
)
//==> ['수원', '전주', '청주', '천안', '성남', '부천',
// '고양', '포항', '안양', '용인', '창원', '안산']
for item in tmp_list:
population.drop(
population[population["ID"] == item].index,
inplace=True
)
set(population["ID"].unique()) - set(draw_kr_carto["ID"].unique())
//==> set()
population = pd.merge(
population, draw_kr_carto, how="left", on="ID"
)
def get_data_info(targetData, blockedMap):
vmin = min(blockedMap[targetData])
vmax = max(blockedMap[targetData])
white_label_min = (vmax - vmin) * 0.25 + vmin
map_data = blockedMap.pivot_table(
index="y", columns="x", values=targetData
)
return map_data, vmin, vmax, white_label_min
def get_data_info_for_zero_center(targetData, blockedMap):
white_label_min = 5
tmp_max = max(
[np.abs(min(blockedMap[targetData])),
np.abs(max(blockedMap[targetData]))]
)
vmin, vmax = -tmp_max, tmp_max
map_data = blockedMap.pivot_table(
index="y", columns="x", values=targetData
)
return map_data, vmin, vmax, white_label_min
def plot_text(targetData, blockedMap, white_label_min):
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
annocolor = "white" if np.abs(row[targetData]) > white_label_min 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 drawKoreaCartogram(targetData, blockedMap, cmapName, zeroCenter=False):
if zeroCenter:
masked_mapdata, vmin, vmax, white_label_min
= get_data_info_for_zero_center(targetData, blockedMap)
else:
masked_mapdata, vmin, vmax, white_label_min
= 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, white_label_min)
for coord in BORDER_LINES:
ys, xs = zip(*coord)
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()
drawKoreaCartogram(
targetData="인구수합계", blockedMap=population, cmapName="Blues"
)

population["소멸위기지역"] = [
1 if val else 0 for val in population["소멸위기지역"]
]
drawKoreaCartogram("소멸위기지역", population, "Reds")

import pandas as pd
import folium
import json
geo_path = "../data/07_skorea_municipalities_geo_simple.json"
geo_json = json.load(open(geo_path, encoding="utf-8"))
popmap = folium.Map(location=[36.3002, 127.554], zoom_start=7)
folium.Choropleth(
geo_data=geo_json,
data=popul_folium["인구수합계"],
key_on="feature.id",
columns=[popul_folium.index, popul_folium["인구수합계"]],
fill_color="YlGnBu"
).add_to(popmap)
popmap

popmap = folium.Map(location=[36.3002, 127.554], zoom_start=7)
folium.Choropleth(
geo_data=geo_json,
data=popul_folium["인구수합계"],
key_on="feature.id",
columns=[popul_folium.index, popul_folium["인구수합계"]],
fill_color="YlGnBu"
).add_to(popmap)
popmap

|
|
👉 카트토그램으로 확인하는 것 보다 지도로 표현된 소멸 위험 지역의 크기가 훨씬 더 넓어 보인다.