5. 기차역간 수송량 분석 및 시각화를 통한 마케팅 방안 제시

2016년 기차역간 수송량 데이터를 Lucypark님이 만드신 catogram을 이용하여 시각화하여 기차역사이에 수송량을 알아보기 쉽게 나타낸다.

[main code]

2016년 1년간 기차역간 수송량 분석 및 마케팅 활용 방안

기차역간 수송량이 나와 있는 데이터를 이용하여 알아보기 편하게 catogram으로 시각화 한 후 서로 수송량이 높은 역들을 알아내어 코레일에서 어떻게 해야 효율적으로 마케팅을 할 수 있을지 알아본다

import pandas as pd
import numpy as np

import platform
import matplotlib.pyplot as plt

%matplotlib inline

path = "c:/Windows/Fonts/malgun.ttf"
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry~~~~')    

plt.rcParams['axes.unicode_minus'] = False
raw_data = pd.read_excel("../data/한국철도공사_2016년_역간_OD자료.xlsx",header=3)
raw_data.head()
착 역 Destination Station 발 역 Origin StationUnnamed: 1Unnamed: 2서울영등포수원평택천안아산조치원신탄진...함안진주하동광양보성화순광명울산포항합계
0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1NaNNaNNaNSeoulYeongdeungpoSuwonPyeongtaekCheonanAsanJochiwonSintanjin...HamanJinjuHadongGwangyangBoseongHwasunGwangmyeongUlsanPohangNaN
2NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNTotal
3NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
4서울NaNSeoul012170513357376864911895049234574102706...49657863NaNNaN5063825NaN114902451489623793106

5 rows × 109 columns

  • 필요없는 부분을 제거한다.
del raw_data['Unnamed: 1']
del raw_data['Unnamed: 2']
raw_data.drop([0],inplace=True)
raw_data.drop([1],inplace=True)
raw_data.drop([2],inplace=True)
raw_data.drop([3],inplace=True)
raw_data.rename(columns={raw_data.columns[0]:"ID"}, inplace=True)
raw_data.reset_index(inplace=True)
del raw_data['index']
raw_data.head()
ID서울영등포수원평택천안아산조치원신탄진대전옥천...함안진주하동광양보성화순광명울산포항합계
0서울012170513357376864911895049234574102706290092125100...49657863NaNNaN5063825NaN114902451489623793106
1영등포128006NaN1105671687801104499628979511147192288122288...32315606NaNNaN4523757NaNNaNNaN8549279
2수원1376031108117404165427086503102729490668472617408...631742NaNNaN3250750NaN68833546856996
3평택676230671865436359085608102991350092444326625...105324NaNNaN539180NaNNaNNaN2679023
4천안아산1877918104341374360210238302925509045079206714809...1342226NaNNaN1055155NaN150636658297292509

5 rows × 107 columns

  • NaN 인 값을 0으로 바꿔준다.

  • 값들의 최대값과 최소값이 차이가 너무 크기때문에 비교하기가 힘들어 모든 값에 1을 더하고(log0은 -inf이기 때문에 0을 1로 바꿔준다.) log를 씌운다.

raw_data.fillna(0, inplace=True)
raw_data.rename(columns = {'합계':'탑승'}, inplace=True)
raw_data['하차']= list(raw_data.iloc[105,:])[1:]
raw_data.drop([105],inplace=True)
raw_data['탑승-하차']= raw_data['탑승']-raw_data['하차']
raw_data.head()
ID서울영등포수원평택천안아산조치원신탄진대전옥천...하동광양보성화순광명울산포항탑승하차탑승-하차
0서울012170513357376864911895049234574102706290092125100...005063825011490245148962379310624290985-497879
1영등포12800601105671687801104499628979511147192288122288...00452375700085492798559289-10010
2수원1376031108117404165427086503102729490668472617408...0032507500688335468569966917681-60685
3평택676230671865436359085608102991350092444326625...005391800002679023266716611857
4천안아산1877918104341374360210238302925509045079206714809...0010551550150636658297292509727491317596

5 rows × 109 columns

  • 역의 탑승량과 하차량을 비교하기 위하여 ‘탑승-하차’ 데이터도 만든다.
raw_data_p1 = raw_data.iloc[:,1:].applymap(lambda x : np.log(float(x)+1))
raw_data = pd.concat([raw_data.iloc[:,0:1],raw_data_p1],axis = 1)
raw_data.head()
C:\Users\study\Anaconda3\lib\site-packages\ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in log
  """Entry point for launching an IPython kernel.
ID서울영등포수원평택천안아산조치원신탄진대전옥천...하동광양보성화순광명울산포항탑승하차탑승-하차
0서울0.00000011.70936414.10499513.43935014.45475612.36553111.53963614.88053910.130663...0.00.08.5299126.7165950.013.95442413.15172216.98490617.005616NaN
1영등포11.7598400.00000013.91596413.44125613.85952512.57693311.62152913.73525710.011849...0.00.08.4171526.6306830.00.0000000.00000015.96135815.962528NaN
2수원14.13471513.8935590.00000012.93974513.47111812.64520811.46065313.4367759.764743...0.00.08.0867186.6214060.011.1394534.00733315.74078015.749591NaN
3평택13.42429013.41781412.9862230.00000011.35754611.54240710.46338912.4066978.798757...0.00.06.2915695.1984970.00.0000000.00000014.80096314.7965279.380758
4천안아산14.44567513.85800913.51926311.5364860.00000012.58639411.41256413.5824039.603058...0.00.06.9622435.0498560.011.92262811.09483115.80235815.7999439.775484

5 rows × 109 columns

station = list(raw_data["ID"])
  • github에서 미리 받아온 catogram을 받아온다.
draw_korea_raw = pd.read_excel('../data/05. draw_korea_raw.xlsx', 
                               encoding="EUC-KR")
draw_korea_raw.head()
012345678910111213
0NaNNaNNaNNaNNaNNaNNaN철원화천양구고성(강원)NaNNaNNaN
1NaNNaNNaN양주동두천연천포천의정부인제춘천속초NaNNaNNaN
2NaNNaNNaN고양 덕양고양 일산동서울 도봉서울 노원남양주홍천횡성양양NaNNaNNaN
3NaNNaN파주고양 일산서김포서울 강북서울 성북가평구리하남정선강릉NaNNaN
4NaNNaN부천 소사안양 만안광명서울 서대문서울 종로서울 동대문서울 중랑양평태백동해NaNNaN
  • 이제 각 행정 구역의 화면상 좌표를 얻기 위해 pivot_table의 반대 개념으로 .stack() 명령을 사용한다.
draw_korea_raw_stacked = pd.DataFrame(draw_korea_raw.stack())
draw_korea_raw_stacked.reset_index(inplace=True)
draw_korea_raw_stacked.rename(columns={'level_0':'y', 'level_1':'x', 0:'ID'}, 
                              inplace=True)

draw_korea = draw_korea_raw_stacked
draw_korea.head()
yxID
007철원
108화천
209양구
3010고성(강원)
413양주
  • 실제역의 이름과 지명은 다르기 때문에 실제역이 있는 지역의 이름을 역이름으로 바꿔주기 위한 dict를 만들어준다.
set(station) - set(draw_korea['ID'].unique())

changename = {'서울 노원':'광운대', "광주 광산":'광주송정', "곡성":'구례구', "부산 북구":'구포', '남양주':'금곡', '고양 덕양':'능곡', '대전 동구':'대전',
 '보령':'대천', '삼척':'도계', '대구 동구':'동대구', '창원 회원':'마산', '파주':'문산', '양양':'민둥산', '부산 동구':'부산',
 '부산 부산진':'부전', '부천 원미':'부천', '인천 부평':'부평', '서울 용산':'서울', '용인 수지':'석포', '서울 은평':'수색', '수원 팔달':'수원',
 '서울 관악':'신림', '연천':'신탄리', '대전 대덕':'신탄진', '영덕':'쌍룡', '청도':'안강', '여주':'양동', '여수':'여수 엑스포',
 '서울 영등포':'영등포', '횡성':'예미', '시흥':'오이도', '아산':'온양온천', '칠곡':'왜관', '울산 중구':'울산', '인천 중구':'인천',
 '서천':'장항', '전주 덕진':'전주', '문경':'점촌', '세종':'조치원', '부산 중구':'좌천', '김해':'진영', '창원 성산':'창원',
 '천안 동남':'천안아산', '평창':'철암', '서울 동대문':'청량리', '청주 흥덕':'청주', '영양':'춘양', '하남':'팔당', '포항 북구':'포항',
 '군위':'풍기', '성주':'하양', '합계':'합계'}
for (key,value) in changename.items():
    draw_korea.loc[draw_korea['ID'] == key, 'ID'] = value
draw_korea.head()
yxID
007철원
108화천
209양구
3010고성(강원)
413양주
set(station) - set(draw_korea['ID'].unique())
{'여수엑스포'}
  • 먼저 ID 컬럼에서 지도에 표기할때 시 이름 구 이름으로 줄을 나누기 위해 분리한다
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)], #울산
#     [(9,14), (9,15)], 
    [(27,5), (27,6), (25,6)],
]
plt.figure(figsize=(8, 11))

# 지역 이름 표시
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']

    # 서대문구, 서귀포시 같이 이름이 3자 이상인 경우에 작은 글자로 표시한다.
    if len(dispname.splitlines()[-1]) >= 3:
        fontsize, linespacing = 9.5, 1.5
    else:
        fontsize, linespacing = 11, 1.2

    plt.annotate(dispname, (row['x']+0.5, row['y']+0.5), weight='bold',
                 fontsize=fontsize, ha='center', va='center', 
                 linespacing=linespacing)
    
# 시도 경계 그린다.
for path in BORDER_LINES:
    ys, xs = zip(*path)
    plt.plot(xs, ys, c='black', lw=1.5)

plt.gca().invert_yaxis()
#plt.gca().set_aspect(1)

plt.axis('off')

plt.tight_layout()
plt.show()

png

raw_data = pd.merge( draw_korea,raw_data, how='outer', on=['ID'])
raw_data.fillna(0, inplace=True)
raw_data.head()
yxID서울영등포수원평택천안아산조치원신탄진...하동광양보성화순광명울산포항탑승하차탑승-하차
00.07.0철원0.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.08.0화천0.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
20.09.0양구0.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
30.010.0고성(강원)0.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
41.03.0양주0.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0

5 rows × 111 columns

  • 이제 위 raw_data 데이터에서 지도에 표현하고자 하는 데이터가 탑승이라면 이 값들이 아까 만든 각 해당 기차역에 위치하면 된다.
mapdata = raw_data.pivot_table(index='y', columns='x', values='탑승')
masked_mapdata = np.ma.masked_where(np.isnan(mapdata), mapdata)
  • 지도로 시각화 하기 위한 함수를 만든다
def drawKorea(targetData, blockedMap, cmapname):
    gamma = 0.75

    whitelabelmin = (max(blockedMap[targetData]) - 
                                     min(blockedMap[targetData]))*0.25 + \
                                                                min(blockedMap[targetData])

    datalabel = targetData

    vmin = min(blockedMap[blockedMap[targetData]>0][targetData])
    vmax = max(blockedMap[targetData])

    mapdata = blockedMap.pivot_table(index='y', columns='x', values=targetData)
    masked_mapdata = np.ma.masked_where(np.isnan(mapdata), mapdata)
    
    plt.figure(figsize=(9, 11))
    plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmapname, 
               edgecolor='#aaaaaa', linewidth=0.5)

    # 지역 이름 표시
    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']

        # 서대문구, 서귀포시 같이 이름이 3자 이상인 경우에 작은 글자로 표시한다.
        if len(dispname.splitlines()[-1]) >= 3:
            fontsize, linespacing = 10.0, 1.1
        else:
            fontsize, linespacing = 11, 1.

        annocolor = 'white' if row[targetData] > whitelabelmin else 'black'
        plt.annotate(dispname, (row['x']+0.5, row['y']+0.5), weight='bold',
                     fontsize=fontsize, ha='center', va='center', color=annocolor,
                     linespacing=linespacing)

    # 시도 경계 그린다.
    for path in BORDER_LINES:
        ys, xs = zip(*path)
        plt.plot(xs, ys, c='black', lw=2)

    plt.gca().invert_yaxis()

    plt.axis('off')

    cb = plt.colorbar(shrink=.1, aspect=10)
    cb.set_label(datalabel)

    plt.tight_layout()
    plt.show()

데이터 시각화

전체 기차역 탑승량 비교

drawKorea('탑승', raw_data, 'YlGnBu')

png

  • 광역시, 도 별 주요 거점 역들이 존재하여 그 역을 기준으로 주위 역의 이용이 높고 전체적으로 서울, 부산, 대구, 대전 등 광역시의 이용량이 높고 인구수 대비 강원도 지역의 수송량또한 전반적으로 많은 편으로 보인다.

전체 기차역 하차량 비교

drawKorea('하차', raw_data, 'YlGnBu')

png

  • 승차량과 하차량이 거의 비슷하다는 것을 알 수 있다. 따라서 코레일에서 기차표를 판매할때 왕복권을 할인하여 파는 것과 같은 마케팅을 한다면 소비자들의 니즈를 더 충족 시킬 수 있을 것이다.

주요 거점 역별 출발지 비교

1. 서울

drawKorea('서울', raw_data, 'YlGnBu')

png

2. 부산

drawKorea('부산', raw_data, 'YlGnBu')

png

3. 광주

drawKorea('광주', raw_data, 'YlGnBu')

png

4. 인천

drawKorea('인천', raw_data, 'YlGnBu')

png

5. 대전

drawKorea('대전', raw_data, 'YlGnBu')

png

결론

위의 자료를 이용하여 우리 지역에 많이 오는 지역을 조사하여 해당 지역와 같이 마케팅을 하면 더 큰 이율을 창출 할 수 있을 것이다.


© 2020. All rights reserved.