🇰🇷

어떤 나라 사람들이 한국을 찾을까 - 공공데이터 시각화

코로나로 인해 외국인 출입이 극도로 제한되었다곤 하지만, 의외로 눈을 돌리면 발견되는 다양한 국적의 사람들이 있습니다. 그래서(?) 공공데이터로서 문체부가 제공한 외국인의 국내 입국 데이터를 시각화해봅니다.
한국관광정보 시스템 웹사이트는 무려 1984년부터 국가별 외국인의 입국 (이하 '방한객') 데이터를 정리한 엑셀 파일을 다운 받을 수 있습니다.
단, 2018년 이후 데이터는 한 파일에 모아서 정리되지 않고 자체 구축한 시스템을 통해 열람이 가능했습니다.
인터렉티브하게 그래프 형식으로 열람할 수 있는 동적인 페이지를 제작한 모양입니다.
*그래서 이번 포스팅엔 코로나 정보가 포함되지 않은 2018년까지만 다룹니다.
파일 직접 다운로드 하기
1984-2018출입국국가별월별통계.xlsx
850.0KB

STEP 1. 데이터 둘러보기

본 데이터는 대중적으로 많은 사람들이 열람해보고, 실무적으로 편하게 사용할 수 있도록 구축되어 있습니다.
따라서 데이터 제작진이 직접 엑셀에 서식도 넣고, 테두리와 음영 + 열고정까지 넣어 가면서 예쁘게 작업해 두었습니다.
다만, 200여개 국가에서 30여년 간의 입국자 수를 모두 모으다 보니 데이터 량이 방대하긴 합니다.
이런 엑셀을 코드 기반으로 정리하고 시각화 하는 작업은 순탄치 않습니다.
데이터 구조가 코드로 간편하게 데이터를 조작할 수 있는 RDBMS 구조와 거리가 멀고
엑셀 상에서 시각적인 편리함을 위해 작업한 구조이기 때문입니다.
판다스 패키지를 이용해 데이터를 가져오면 다음과 같습니다.
# 초기 세팅 코드 import pandas as pd import numpy as np # 화면에 출력하는 데이터 프레임의 최대 row 수를 100으로 설정합니다. pd.set_option('display.max_rows', 100) # 화면에 출력하는 데이터 프레임의 최대 column 수를 500으로 설정합니다. pd.set_option('display.max_columns', 500) # 나라가 200개가 넘어요! df = pd.read_excel("1984-2018출입국국가별월별통계.xlsx", header=2)
Python
hㅏ...
예상은 했지만, 예상보다 더 큰 인내심을 갖고 전처리를 해야할 것 같습니다.
데이터를 꼼꼼하게 둘러봅니다. 30여년 간 동일한 프로토콜로 데이터를 구축했다면 좋겠지만 긴 시간인 만큼 데이터가 균일하게 관리되지 않았을 수 있습니다.
아니나 다를까 1997년까지는 연 단위로 방한객이 적혀있는데 1998년부터는 월 단위 칼럼이 추가되었습니다.
심지어 1998년 총합 칼럼 하나, 그 다음부터는 1998년 1월, 2월 ... 식으로 구성되어 있습니다.
월별 방한객 추이는 성수기 / 비수기 간의 차이를 볼 수 있는 재미있는 자료가 될 수 있으므로, 월별 데이터가 없는 1998년 이전 데이터는 이번 분석에서 사용하지 않아야겠습니다. (아까워라...)
데이터 관리 좀 잘 해줘요!

STEP 2. 데이터 전처리

아무튼 1998년 이전 데이터는 버립니다.
다만 1984년부터 칼럼을 하나 하나 버리는 코드를 짜는 것 역시 고역이므로 적당한 꼼수를 써줍니다.
표에다가 직접 1984년부터 쭉 드래그를 해서 CTRL C V 를 해주면 화살표로 연결되어 있지만 그 텍스트를 고스란히 가져올 수 있습니다.
[GIF] 주피터 노트북에서는 "1984년→1985년→1986년" 이런 식으로 구성된 텍스트로 붙여진다.
그 구성된 텍스트를 바로 리스트로 지정하면 리스트 타입으로 저장됩니다.
이제 df.drop() 메소드로 1997년까지의 데이터를 버려줍니다.
계속 진행해줍니다. 사용하지 않을 데이터가 많습니다.
데이터프레임 상 보면 Country 칼럼과 국가명 칼럼을 따로 지정되어 있고 정체 불명의 total과 NaN 행이 보이는 등 조금 이상하게 생겨있습니다.
이는 원래 의도한 바랑 다르게, 데이터프레임으로 엑셀을 억지로 열다 보니 찌그러져 생긴 문제이므로, 적당히 버릴 것은 버리고 사용할 예정입니다.
우선, 연도 + 월 로 구성된 데이터를 깔끔하게 처리해줍시다.
year_month_list = df.columns.tolist() # 1999.1월, 1998.2월 같은 칼럼을 리스트로 담아줄 예정 year_month_list.remove('Country') # 필요 없는 항목은 임시로 제거 year_month_list.remove('국가명') # 필요 없는 항목은 임시로 제거 new_year_month_list = ['Country', '국가명'] # 데이터 크기 (인덱스)를 기존 데이터 맞추기 위해 country, 국가명을 일단 둡니다. 나중에 제거할 예정. for item in year_month_list: year = item.split()[0] year = year.split(".")[0] year = year.split("년")[0] year = year[-2:] month = item.split()[0] if "."in(month): month = month.split(".")[1] else: month = month.split("년")[1] month = month[:-1] new_item = str(year) + "_" +str(month) new_year_month_list.append(new_item)
Python
상단부 (G.TOTAL, NaN, Foreign Visitors, Overseas Korean, NaN 가 있는 5개 행) 데이터들은 합계 데이터로 이번 분석에서 사용되지 않으나 유용할 수 있으니 분리 취급합니다.
# 상단부 토탈 합계를 나타내는 데이터는 분리 취급 df_total_info = df.iloc[:4].drop(1, axis=0).reset_index().drop('index', axis=1)
Python

STEP 3. 데이터 관리 오류 찾아내기

이제 전처리도 대충 마무리되고, 시각화 함수를 짜려던 찰나 계속해서 이상한 오류가 났습니다.
코드에는 문제가 없는데 오류가 발생했다...?!
이는 전처리를 완료했던 데이터에게 배신 당하는 케이스일 확률이 높습니다.
앞서 말했듯 30년 간 사람 손으로, 엑셀로 관리된 데이터는 오탈자관리규칙을 어기는 등 다양한 변수가 존재할 수 있습니다.
그렇다면... 데이터를 셀 단위로 하나하나 반복문으로 돌려가며 이상하게 기입된 데이터를 찾아봅시다.
df = df.fillna(0) # 비어있는 셀은 0을 넣어준다. for index in df.index.tolist(): # 모든 인덱스, 즉 모든 행을 다 돌려 볼 것이다. for col in range(2, len(df.columns)): # 행을 돌리는 동시에 열(칼럼, col)도 다 돌려 볼 것이다. if type(df.iloc[index, col]) == str: print("==========") print("내용 :", df.iloc[index, col]) print("데이터타입 :", type(df.iloc[index, col])) print("index, col (주소) :", index, ",", col) print("국가 :", df.loc[df.index[index],'국가명']) print("연도 :", df.columns[col]) df.iloc[index, col] = 0.0 elif type(df.iloc[index, col]) == np.float64: df.iloc[index, col].astype(int) else: pass
Python
역시나 공백을 스페이스바 (" ") 로 채워넣거나 "-" 로 채워넣는 등 담당자가 실수로 null을 null로 두지 않는 케이스가 검출되었습니다. 코드를 통해 값을 0으로 바꿔주었습니다.
이럴 땐... 코드로 바꿔줄 수도 있지만 엑셀파일로 가서 손으로 수정해주는 게 빠를 수 있습니다. 셀의 주소를 알아냈으니까요. 반면 코드를 통해 바꿔주는 코드까지 짜준다면 남들도 해당 코드를 돌려 원본 공공데이터에 쉽게 대응할 수 있는 장점이 생깁니다. (재생산성이 높아진다) 자신의 상황에 맞게 대처합시다!
엑셀에서 직접 확인할 수 있었습니다.

STEP 4. 데이터 재편하기

국가만 200여개... 너무 많고 낯선 나라들이 많습니다.
데이터 분석이라는 막막한 상황 속에서 첫 단추는 분류입니다.
마침 원본 데이터에서 대륙별로 국가들을 구분해 나열하였습니다. 대륙별로 데이터를 쪼개서 분석해봅시다.
우리가 직접 대륙별로 찾지 않아도 되게끔 엑셀 상 구분 지어 주었다.
데이터 작성자가 엑셀에 대륙 간 구분으로 쪼개 놓은 행을 찾으면 대륙들이 어떻게 나타나는지 쉽게 검출할 수 있을 것입니다.
특히 대륙별로 쪼갤 때 위 아래 행을 디자인적으로 비워두었는데, 그걸 찾아내면 되겠죠?
country_null_index = df.loc[df['Country'].isnull()].index.tolist() country_null_index_below_index = [item + 1 for item in country_null_index] country_null_index_upper_index = [item - 1 for item in country_null_index] df.loc[df.index.isin(country_null_index_below_index) | df.index.isin(country_null_index_upper_index) ]
Python
돌려보니 대륙을 표기한 행의 넘버를 찾을 수 있었습니다.
# 쭉 살펴보니... 찾았다 대륙 인덱스들 continental_index = [5, 41, 67, 126, 189, 222, 287] # 대륙 이름도 직접 표기할 필요 없이 데이터로부터 그대로 따옵시다. continental_list = df.loc[df['Country'].index.isin(continental_index), 'Country'].tolist()
Python
[대륙 - 대륙 구분용 행이 있던 인덱스] 를 따로 묶어 딕셔너리(dict)에 저장해둡시다.
# 대륙 - 인덱스 를 표기한 dict 데이터 형성 mix_list = [None]*(len(continental_index)+len(continental_list)) mix_list[::2] = continental_index mix_list[1::2] = continental_list mix_dict = [] for i in range(0,len(mix_list),2): mix_dict.append((mix_list[i+1], mix_list[i])) mix_dict = dict(mix_dict) continental_dict=mix_dict print("아시아의 인덱스는? ", continental_dict['ASIA']) print("인덱스 5의 표기는? ", df.loc[df.index.isin([5]), 'Country' ])
Python
아시아는 5번 행부터 시작인 것입니다.
왜 대륙별 구분행의 인덱스를 찾았느냐면, 데이터에 제대로된 대륙 표기를 해둔 칼럼을 만들기 위함입니다.
# 새롭게 작업을 할 것이니 new_df 라는 카피 데이터를 하나 만들고 합시다. new_df = df.iloc[:, 2:].astype(int) new_df['Country'] = df['Country'] new_df['국가명'] = df['국가명'] new_df['cont'] = "" # 소속 대륙 칼럼을 만들자! for key, value in continental_dict.items(): new_df.iloc[value : , -1] = key
Python
그리고 데이터 제작자가 친절하게 월별로 나오다가 12월이 끝나면 연간 총합 열을 만들어 두셨습니다.
이는 월별 추이를 볼 땐 도움이 되지 않으니 일단 배제하고 다른 목적으로 쓰도록 합시다.
# 연도별 합은 별도의 리스트로 만들어서 열람하고자 할 때만 열어서 관리 except_year_columns = ["99_", "00_"] for num in range(10): num_ = "0" + str(num) + "_" except_year_columns.append(num_) for num in range(10,19): num_ = str(num) + "_" except_year_columns.append(num_) # 연도별 합을 통해 보고 싶다면 #new_df_asia_year = new_df_asia[except_year_columns] #를 통해 연도별 집계만 볼 수 있다 #월별 추이를 다 보고 싶다면 #except_year_columns를 drop시켜야함 # 연도별 합만 보기 new_df_year = new_df[except_year_columns] new_df_year['국가명'] = new_df['국가명'] new_df_year['cont'] = new_df['cont'] new_df_year = new_df_year.set_index('국가명') new_df_year = new_df_year.iloc[5:] # 월별추이 보기 new_df_month = new_df.drop(except_year_columns, axis=1) new_df_month = new_df_month.set_index('국가명') new_df_month = new_df_month.iloc[5:]
Python

STEP 5. 시각화 함수 짜기

데이터 전처리 & 재편... 정말 오래 걸렸습니다.
사실상 데이터 분석 작업에 쏟아 붓는 에너지와 시간의 90%는 전처리 단에서 소모됩니다.
# 시각화 기본 세팅 코드 import matplotlib import matplotlib.pyplot as plt # 파이플롯 사용 import seaborn as sns from IPython.display import set_matplotlib_formats set_matplotlib_formats('retina') # 한글코드를 더 선명하게 해주는 조치, 레티나 설정 matplotlib.rc('font', family='AppleGothic') # 폰트 설정 matplotlib.rc('axes', unicode_minus=False) # 마이너스 폰트가 깨지는 경우가 있으므로 조치
Python
어떻게 그래프를 그릴것인가를 따져봐야겠죠?
연간, 월별 입국 변동 추이를 확인할 계획이므로 꺾은선 그래프
연간, 월별을 쉽게 선택하면 자동으로 그려주게 만들자! → 함수화
월별인 경우 x축은 1998년 1월부터 2018년 12월까지가 들어가야하므로 가로로 긴 그래프가 될 듯
한 국가만 그래프로 표현하면 심심하니까, 여러 국가를 한꺼번에 그리되 선의 색으로 구분
200여개 국가들의 명단을 쉽게 열람할 수 있으면 편할테니 명단을 만들자
대륙별 명단으로
함수화는 어떻게?
변수 1 : 연간으로 볼 것인가 월별로 볼 것인가?
변수 2 : 어느 대륙인가? 대륙 굳이 선택 안 하고 싶다면?
변수 3 : 어느 나라들을 볼 것인가? (리스트로 한꺼번에 여러 국가 추이 확인)
def show_visitors(data, cont, country_list): # 원하는 대륙 선택. 굳이 선택 안 하고 싶다면 'global'로 처리 if cont != 'global': # cont = continent (대륙) if data == 'year': # 연간 입국자로 볼까? new_df_cont = new_df_year.loc[new_df_year['cont'] == cont] # 트랜스포즈해야 lineplot을 찍을 수 있다 plot_data = new_df_cont.loc[new_df_cont.index.isin(country_list)].T #쓸데 없는 country, 대륙 파트 커팅 + 가끔 내용물이 이상하게 Object로 되어있더라? plot_data_cut = plot_data.iloc[:-1,:].astype(int) plt.figure(figsize=(18, 9)) # 그래프 사이즈 조절 elif data == 'month': # 월별 입국자로 볼까? new_df_cont = new_df_month.loc[new_df_month['cont'] == cont] # 트랜스포즈해야 lineplot을 찍을 수 있다 plot_data = new_df_cont.loc[new_df_cont.index.isin(country_list)].T #쓸데 없는 country, 대륙 파트 커팅 + 가끔 내용물이 이상하게 Object로 되어있더라? plot_data_cut = plot_data.iloc[:-2,:].astype(int) plt.figure(figsize=(50, 9)) # 그래프 사이즈 조절 else: print('연도 보기 혹은 월별 보기를 다시 선택해주세요') else: if data == 'year': plot_data = new_df_year.loc[new_df_year.index.isin(country_list)].T plot_data_cut = plot_data.iloc[:-1,:].astype(int) plt.figure(figsize=(18, 9)) elif data == 'month': plot_data = new_df_month.loc[new_df_month.index.isin(country_list)].T plot_data_cut = plot_data.iloc[:-2,:].astype(int) plt.figure(figsize=(50, 9)) else: print('연도 보기 혹은 월별 보기를 다시 선택해주세요') plt.title('방한객 증감 추이', fontsize=20) plt.ylabel('방한객', fontsize=15) plt.xlabel('시간', fontsize=15) plt.xticks(rotation=90) for country in country_list: plt.plot(plot_data_cut.index, plot_data_cut[country], linestyle='--', linewidth=2) # 'dashed' # linestyle='--', '-', ':' (dashed, solid, dotted) # linewidth=1 plt.legend(country_list ,fontsize=14, loc='best') plt.show()
Python
대륙별 국가 명단 만들기
asia_list = new_df_year.loc[new_df_year['cont'] == 'ASIA'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() europe_list = new_df_year.loc[new_df_year['cont'] == 'Europe'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() americas_list = new_df_year.loc[new_df_year['cont'] == 'Americas'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() mid_east_asia_list = new_df_year.loc[new_df_year['cont'] == 'Middle East Asia'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() oceania_list = new_df_year.loc[new_df_year['cont'] == 'Oceania'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() africa_list = new_df_year.loc[new_df_year['cont'] == 'AFRICA'].iloc[:,:-1].sum(axis = 1, skipna = True).sort_values(ascending=False).index.tolist() cont_list = [asia_list, europe_list, americas_list, mid_east_asia_list, oceania_list, africa_list] for cont in cont_list: cont.remove(0)
Python
→ 쉽게 나라 이름을 파악하고 시각화에 활용할 수 있다
이제 일본과 미국의 입국객 추이를 연도별로 비교하고 싶다면...?!
show_visitors('year', 'global', ['일본', '미국'])
Python
다 만들었으니 다양하게 그래프를를 뽑아봅니다.
목표 달성!

STEP 6. 입국자 수 TOP n 뽑아보기

# 총 입국자 수를 파악해야 하므로 total 데이터를 구축해줍시다. new_df['total'] = new_df.iloc[:,:-2].sum(axis = 1, skipna = True) new_df = new_df.set_index('국가명')
Python
top_list = new_df.sort_values(by='total',ascending=False).index.tolist() remove_list = [0,0,0,0, '구주', '아프리카', '대양주', '미주', 'GCC6개국'] for item in remove_list: top_list.remove(item)
Python
김문과의 데이터