본문 바로가기
Projects/⛴️ Ship Waiting Time Prediction

[선박 대기시간 예측] 1차 전처리 : PORT-MIS 항만 데이터 결합

by ISLA! 2023. 12. 15.

 

 

항만 대기시간 예측 및 운영시스템 개선을 위한 1차 전처리

🎯 목표 : 항구별 대기율 산정을 위한 입출항 개별 건에 대한 대기시간 & 서비스 시간 도출 

🎯 사용 데이터 : PORT-MIS 입출항 현황 + PORT-MIS 시설사용허가현황 (총 2종)
                          (하단 ERD에서 빨간색으로 표시된 부분)

 

 

 

01.  Target 항만 선정을 위한 대기율

어떤 항만에 대한 대기시간 예측 모델을 만들것인지부터 결정해야했는데, 이를 위한 지표로 '대기율'을 사용했다.

후보 항만은 2022년 물동량이 가장 많은 순으로 부산, 광양, 울산, 인천을 선정했다.

대기시간과 대기율의 개념은 다음과 같다.
대기시간이 선박이 단순히 정박지에 접안하기까지 기다리는 시간이라면,  
대기율은 '서비스 시간'대비 얼마나 대기하는지를 측정하는 지표이다.

 

 

 

입출항 프로세스 및 시점 재정의

 

정확한 대기시간과 서비스 시간을 도출하기 위해서는 선박 현황(상태)별로 구체적인 시점과 지속시간을 알아야 한다.

과거 진행된 연구에서 사용한 데이터에는 이러한 정보가 포함되어 있었지만, 프로젝트 진행 시점 해당 정보를 확보할 수 없었다.

따라서 일반적으로 8단계로 구성된 입출항 프로세스를 대기시간과 서비스 시간을 중심으로 4단계로 압축하고 케이스를 나누어 대기/서비스 지속 시간을 추출하기로 했다.

 

 

👉 기본적으로 입항하여 별도 접안 상태가 없으면(대기시간 0), 바로 양적하 서비스를 수행하고 출항(또는 출항대기)하는 것으로 간주하며 이때 지속시간을 '서비스 시간'으로 정의

👉  접안 대기가 발생할 경우, 입항부터 접안 대기까지의 시간을 '대기시간'으로, 이후 출항(또는 출항대기)까지의 시간을 '서비스 시간'으로 간주

 

 


02.  항구별 입출항 데이터 전처리(4개 항구, 3년)

데이터 병합

 

  • 4개 항구별로 2022년의 시설사용허가현황과 입출항 현황 데이터를 merge

 

1) 시설사용허가현황

  • 월별로 데이터를 다운로드 할 수 있음
  • 연도/월별 개별 파일을 불러와 concat 후, 컬럼명을 변경

 

 

 

2) 입출항 현황

  • 1년 단위로 데이터 다운로드가 가능
  • 연도별 개별 파일을 불러와 concat 후, 컬럼명을 변경

 

 

컬럼 제거 및 필터링

 

  • 시설허가(combined_df, 입출항 : dfs) 컬럼 제거 
columns_to_drop = ['신청시설_코드', '신청시설_번호', '신청시설명', '신청일시(FROM)', '신청일시(TO)', '선사/대리점_코드', '선사/대리점명']
combined_df.drop(columns=columns_to_drop, inplace=True)

columns_to_drop_dfs = ['외내', 'CIQ수속일자', 'MRN 번호', '차항지', '전출항지','외항:한국인선원수/내항:해기사선원수','외항:외국인선원수/내항:보통선원수', '승객', '예선', '도선', '부선호출부호1', '부선호출부호2']
dfs.drop(columns=columns_to_drop_dfs,inplace=True)

 

  • 허가현황 중 '허가'만 사용  ▶︎ 미허가 제외
  • 입출항 중 '출항'만 사용
  • 사용목적명 중 '적하, 양하, 접안대기, 양적하, 출항대기'만 사용
    👉 대기/서비스 시간 지점 추출에 필요한 사용목적만 사용함
combined_df = combined_df[combined_df['허가유무'] != '미허가']
dfs = dfs[dfs['입출'] == '출항']
combined_df = combined_df[combined_df['사용목적명'].isin(['적하', '양하', '양적하', '접안대기', '출항대기'])]

 

 

"호출부호, 선박명, 입항횟수_횟수, 입항횟수_연도" 기준 Merge

 

  • 두 데이터프레임의 merge를 위해 데이터 타입 int 로 통일 후, Merge
  • 고유한 선박 1건(호출부호, 이름)의 입항 횟수와 연도를 기준으로 Merge
dfs['입항횟수_횟수'] = dfs['입항횟수_횟수'].astype(int)
dfs['입항횟수_연도'] = dfs['입항횟수_연도'].astype(int)
combined_df['입항횟수_횟수'] = combined_df['입항횟수_횟수'].astype(int)
combined_df['입항횟수_연도'] = combined_df['입항횟수_연도'].astype(int)

merged_left = pd.merge(dfs, combined_df, on= ['호출부호','선박명','입항횟수_횟수','입항횟수_연도'], how='left') # 입출항, 시설사용허가 left merge
merged_left.head()

 

 

중복행 제거

 

# 중복된 행, unique값만 남기기
unique_values = merged_left.drop_duplicates()

 

 

 

접안_대기시간, 출항_서비스시간 컬럼 생성

 

  • 지정일시(TO)와 지정일시(FROM)이 대기와 서비스가 '지속'된 정도를 측정하기 위한 핵심 컬럼이다.
  • 위에서 정의한 네 가지 케이스에 따라서 조건을 만들어, 사용목적에 따라 접안_대기시간출항_대기시간 컬럼을 만들었다.
# 데이터복사
temp = unique_values.copy()

# 데이트 타입으로 변경 , 잘못된 값 제거
temp['지정일시(TO)'] = pd.to_datetime(temp['지정일시(TO)'], errors='coerce')
temp['지정일시(FROM)'] = pd.to_datetime(temp['지정일시(FROM)'], errors='coerce')
temp['입항일시'] = pd.to_datetime(temp['입항일시'], errors = 'coerce')

# 접안 대기시간,출항 대기시간을 위한 빈 컬럼 생성
temp['접안_대기시간'] = pd.NaT
temp['출항_대기시간'] = pd.NaT

temp.loc[temp['사용목적명'] == '접안대기', '접안_대기시간'] = temp['지정일시(TO)'] - temp['지정일시(FROM)']
temp.loc[temp['사용목적명'] == '출항대기', '출항_대기시간'] = temp['지정일시(TO)'] - temp['지정일시(FROM)']

 

 

서비스 종료 시점(Service_Time_End) 컬럼 생성

 

  • 서비스 시간 산출을 위해 '서비스 시간 종료지점' 컬럼을 생성
    👉 사용목적명이 '출항대기'라면, 서비스가 종료되는 시점이 출항대기가 끝나는 지점인 지정일시(FROM)이다.
    👉 사용목적에 '출항대기'가 없는, 즉 '출항_대기시간' 컬럼이 null인 경우라면 서비스가 종료되는 시점은 단순 출항일시(출항 시작하는 시점)이 된다.
import numpy as np

# 'Service_Time_End' 열 생성
temp['Service_Time_End'] = np.nan

# '사용목적명'이 '출항대기'인 경우 'Service_Time_End' 값 할당
temp.loc[temp['사용목적명'] == '출항대기', 'Service_Time_End'] = temp['지정일시(FROM)']

# '출항_대기시간'이 null인 경우 'Service_Time_End' 값 할당
temp.loc[temp['출항_대기시간'].isnull(),'Service_Time_End'] = temp['출항일시']

 

 

대기 종료시점(end_of_Anchor) 컬럼 생성

 

  • 대기 시간 산출을 위해 '대기 시간 종료지점' 컬럼을 생성
    👉 사용목적명이 '접안대기'인 경우, 대기가 끝나는 시점은 지정일시(TO)가 된다. (대기 시작시점은 입항일시)
  • 이때, 서비스 시작지점은
    👉 접안대기가 발생하지 않은 경우(end_of_anchor 컬럼이 null인 경우) 입항일시가 되고
    👉 접안대기가 발생한 경우는 대기가 끝나는 시점이 된다.
temp['end_of_Anchor'] = pd.NaT

# '사용목적명'이 '출항대기'인 경우 'Anchor' 값 할당
temp.loc[temp['사용목적명'] == '접안대기', 'end_of_Anchor'] = temp['지정일시(TO)']

temp['Service_Time_Start'] = np.where(temp['end_of_Anchor'].isnull(), temp['입항일시'], temp['end_of_Anchor'])

 

 

 

  • 서비스 시작 시점과 끝 시점의 차이를 '서비스 시간' 컬럼으로 생성
# 문자열을 datetime으로 변환
temp['Service_Time_End'] = pd.to_datetime(temp['Service_Time_End'])
temp['Service_Time_Start'] = pd.to_datetime(temp['Service_Time_Start'])
temp['Service_Time'] = temp['Service_Time_End'] - temp['Service_Time_Start']

 

 

4개 항구 대기율 산출

 

  • 접안 대기시간의 결측치는 0으로 채운다.
  • 대기시간과 서비스 시간은 '지속시간'이기 때문에 Timedelta로 형식 변경
  • timedelta 값을 일, 시, 분 단위로 환산
  • '분'으로 단위를 통일하고, 대기율 구하기
waiting = temp.copy()

# 접안_대기시간이 null 값인 경우 대기 0
waiting['접안_대기시간'].fillna(0, inplace = True)

# 데이터프레임에서 '접안_대기시간'과 'Service_Time' 컬럼을 timedelta 형식으로 변환
waiting['접안_대기시간'] = pd.to_timedelta(waiting['접안_대기시간'])
waiting['Service_Time'] = pd.to_timedelta(waiting['Service_Time'])

# timedelta 값에서 일(day)로 변환
waiting['접안_대기시간_NumDays'] = waiting['접안_대기시간'].dt.days
waiting['Service_Time_NumDays'] = waiting['Service_Time'].dt.days

# 'Service_Time' 컬럼에서 'days', 'hours', 'minutes'를 분리하여 새로운 열로 추가
waiting['Service_Time_일'] = waiting['Service_Time'].dt.days
waiting['Service_Time_시간'] = waiting['Service_Time'].dt.components.hours
waiting['Service_Time_분'] = waiting['Service_Time'].dt.components.minutes

# 'Service_Time'을 분으로 변환하여 'Service_Time_분' 열에 추가
waiting['Service_Time_분'] = waiting['Service_Time_일'] * 24 * 60 + waiting['Service_Time_시간'] * 60 + waiting['Service_Time_분']

# '접안_대기시간'을 분으로 변환하여 '접안_대기시간_분' 열에 추가
waiting['접안_대기시간_분'] = waiting['접안_대기시간_일'] * 24 * 60 + waiting['접안_대기시간_시간'] * 60 + waiting['접안_대기시간_분']

 

waiting['대기율'] = waiting['접안_대기시간_분'] / waiting['Service_Time_분']

 

 


 

03.  항구별 대기율 결과 확인 ▶︎ 울산항 target 선정

728x90