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
엑셀에서 그린 지도 모양을 읽어온다.
population = pd.read_excel("../data/07_population_raw_data.xlsx")
population
원본 데이터는 년도, 나이로 컬럼이 지정되어있다. 따라서 header 지정 없이 데이터를 불러오면 제일 첫번째 행이 년도가 컬럼으로 지정된다.
따라서 header=1을 통해 년도 컬럼이 아닌 나이를 컬럼으로 지정했다.
population = pd.read_excel("../data/07_population_raw_data.xlsx", header=1) # 1행을 컬럼으로 사용하겠다.
population
원본 파일에서 행정 구역 컬럼의 행들은 최소 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
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)
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()
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
fillna_df.fillna(value=0)
fillna_df.fillna(method="pad")
fillna_df.fillna(method="backfill")
si_name = [None] + len(pop)
si_name
tmp_gu_dict = {
"수원": ["장안구", "권선구", "팔달구", "영통구"],
"성남": ["수정구", "중원구", "분당구"],
"안양": ["만안구", "동안구"],
"안산": ["상록구", "단원구"],
"고양": ["덕양구", "일산동구", "일산서구"],
"용인": ["처인구", "기흥구", "수지구"],
"청주": ["상당구", "서원구", "흥덕구", "청원구"],
"천안": ["동남구", "서북구"],
"전주": ["완산구", "덕진구"],
"포항": ["남구", "북구"],
"창원": ["의창구", "성산구", "진해구", "마산합포구", "마산회원구"],
"부천": ["오정구", "원미구", "소사구"],
}
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] = "고성(경남)"
pop "ID" 컬럼으로 생성하여 앞서 만든 si_name을 저장해준다.
pop["ID"] = si_name
pop
현재 구해야하는 데이터는 여자 인구 소멸 위기 지역을 구하는 것이기 필요 없는 컬럼은 제거하자.
del pop["20-39세남자"]
del pop["65세이상남자"]
del pop["65세이상여자"]
pop.head()
draw_korea_raw = pd.read_excel("../data/07_draw_korea_raw.xlsx")
draw_korea_raw
각 지역별 위치가 나타난다.
0인덱스에 0-6은 NaN, 7-철원, 8-화천, 9-양구, 10-고성(강원), 11-13 NaN이라는 의미를 나타낸다.
draw_korea_raw.stack()
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_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
시도의 이름을 표현하는 함수 생성
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,
)
한 셀의 이름을 두 줄로 만들어주기 위한 작업
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)
공통된 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())
데이터가 지워진 모습을 볼 수 있다.
지도를 그리기 위한 데이터와 인구현황 데이터를 합친다.
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:
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", # 수직 정렬
)
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)
pop["2030여성비"] = (pop["20-39세여자"] / pop["20-39세합계"] - 0.5) * 100
drawkorea("2030여성비", pop, "RdBu", zeroCenter=True)
수도권 밖을 지날 수록 비율이 무너지는 모습을 볼 수 있다.
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
draw_korea.to_csv("../data/07_draw_korea.csv", encoding="utf-8", sep=",")
2주간 재밌었지만 생각보다 쉽지 않았다. 아직도 사용할땐 어려워서 이것 저것 계속 검색을 해봐야하고 배운거 이외의 작업을 하는 것은,,, 아직 먼 세상이야기 같다.
빠른 진도를 따라가느라 쉽진 않았지만 언젠가는 익숙해질꺼라고 믿으며🤣
"이 글은 제로베이스 데이터 취업 스쿨 강의를 듣고 작성한 내용으로 제로베이스 데이터 취업 스쿨 강의 자료 일부를 발췌한 내용이 포함되어 있습니다."