본문 바로가기
Project/Ship Waiting Time Prediction

PORT MIS 대기시간 예측 1. 항구별 대기율 산정(1차 전처리)

by skwkiix 2023. 11. 13.
728x90

 

 

항만 대기시간 예측 및 운영시스템 개선 을 위한 첫번째 전처리 단계인, 항구별 대기율 산정에 관한 내용을 담고 있다.

 

 

1.  사용 데이터 

데이터명 사용 목적 merge 기준 컬럼
PORT MIS 입출항 현황 입출항 데이터 사용 > 서비스시간 산출 호출부호, 선박명 ,입항횟수_횟수, 입항횟수_연도
PORT MIS 시설사용허가현황 접안대기, 출항대기 데이터 파악 및 대기시간 산출

 


2.  전처리 설계

2.1. 선박 입출항 관련 서비스 지표

대기율 (WR) = TW/TS

TW : 대기시간 (Waiting Time)

TS : 서비스시간 (Service Time)

 

선박 입출항 관련 서비스 지표인 대기율(대기시간/서비스시간) 을 기준으로 대기율이 가장 높은 항구를 선정한다.

22년 기준 물동량이 가장 많은 항구인 부산, 인천, 광양, 울산을 대상으로 진행한다.

 


 

2.2. 대기율 산출 프로세스


선박 입출항 프로세스 : 입항 > 투묘&양묘 > 선석 접안 > 이안 > 출항

대기시간 :  선박이 입항에서 선석 접안 전까지 대기한 시간
서비스시간 : 선박이 선석 접안 후 양적하 등 서비스를 마치고 선석을 떠나기 전까지의 시간

 

이슈 : 접안, 이안, 출항 등 명확한 시점에 대한 데이터 부재 > 따라서, 4가지 경우로 나누어 서비스 시간을 산출했다.

 

대기시간 산출 방법 : 시설사용허가 중 사용 목적명이 '대기시간'인 지정일시(FROM ~ TO) 시간

서비스시간 산출 방법 : merge 후, 출항대기와 접안대기가 발생한 경우, 발생하지 않은 경우로 나누어 서비스시간 산출 방식을 다르게 함

 

  • 입항 > 접안 > 출항
    • 입항일시를 접안일시로 파악, 출항 - 입항 = 서비스 시간
  • 입항 > 접안 > 출항대기 > 출항
    • 출항 대기(처음) - 입항 = 서비스 시간
  • 입항 > 접안 대기 > 접안 > 출항
    • 출항 - 접안 대기 일시(마지막 시점) = 서비스 시간
  • 입항 > 접안 대기 > 접안 > 출항대기 > 출항
    • 출항 대기 (처음) - 접안 대기 일시(마지막 시점) = 서비스 시간

 


 

3.  항구별 대기율 산정_전처리

각각 항구별로 2022년 입출항, 시설사용허가 데이터를 merge 해서 사용했으며, 부산의 경우 약 8만건의 row 값을 확인했다.

예시코드는 대기율이 가장 높은 울산항 기준이며, 3년치 데이터(14만건) 를 불러와서 전처리한 코드이다.

 

3.0. 데이터 불러오기

  • 시설사용허가현황 : 월별로 분리되어있어, 개별 파일을 불러온 후 이중 for 문(연도, 월)으로 concat > 컬럼명 지정
import pandas as pd

# 데이터 파일이 저장된 디렉토리 경로
directory = '../data/울산'

# 데이터프레임을 저장할 리스트 생성
dfs = []

# 202001부터 202212까지의 월을 대상으로 반복문 실행
for year in range(2020, 2023):
    for month in range(1, 13):
        # 파일 경로 생성
        file_path = f'{directory}/울산_시설_{year}{month:02d}.xlsx'
        
        # 엑셀 파일을 읽어와 데이터프레임 생성 후 리스트에 추가
        try:
            df = pd.read_excel(file_path)
            dfs.append(df)
        except FileNotFoundError:
            print(f"파일을 찾을 수 없습니다: {file_path}")

# 모든 데이터프레임을 하나로 합치기
result_df = pd.concat(dfs, ignore_index=True)
combined_df = result_df.copy() # copy()

# 제목 삭제
combined_df = combined_df.iloc[3:]

# 컬럼명 지정
combined_df.columns = ['순번', '호출부호', '입항횟수_연도', '입항횟수_횟수', '시설사용횟수', '신고톤수', '선박명', '선사/대리점_코드',
       '선사/대리점명', '신청시설_코드', '신청시설_번호', '신청시설명', '신청일시(FROM)',
       '신청일시(TO)', '지정시설_코드', '지정시설_번호', '지정시설명', '지정일시(FROM)',
       '지정일시(TO)', '사용목적명', '예보일시', '허가유무']

 

  • 입출항 현황 데이터 : 1년단위로 구분 > 반복문 하나로 concat > 컬럼명 지정
directory = '../data/울산'

dfs = []

# 20,21,22 - 연도별
for year in range(2020, 2023):
    file_path_ship = f'{directory}/울산_입출항_{year}.xlsx'
    
    # Read Excel file and append DataFrame to the list
    try:
        df_ship = pd.read_excel(file_path_ship)
        dfs.append(df_ship)
    except FileNotFoundError:
        print(f"파일을 찾을 수 없습니다: {file_path_ship}")

# concat
dfs = pd.concat(dfs, ignore_index=True)

dfs = dfs.iloc[11:]  # 상단 11행 삭제 _ 제목 부분

# 컬럼명 변경
dfs.columns = ['항명', '호출부호', '선박명', '입항횟수_연도', '입항횟수_횟수', '구분', '외내', '입출', '총톤수', '입항일시',
       '출항일시', 'CIQ수속일자', '수리일시', '항해구분', 'MRN 번호', '계선장소_코드', '계선장소_숫자', '계선장소명',
       '차항지', '전출항지', '선박용도', '외항:한국인선원수/내항:해기사선원수', '외항:외국인선원수/내항:보통선원수',
       '승객', '예선', '도선', '부선호출부호1', '부선호출부호2']

 


3.1. 컬럼 제거 및 결측치 확인

  • 컬럼 제거
## 불필요한 컬럼 제거
# 시설사용허가현황
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['입출'] == '출항']

 


 

3.2. 데이터 병합(Merge)

 

  • 기준 컬럼 타입 변경
combined_df['입항횟수_횟수'] = pd.to_numeric(combined_df['입항횟수_횟수'], errors='coerce')
combined_df['입항횟수_연도'] = pd.to_numeric(combined_df['입항횟수_연도'], errors='coerce')

dfs['입항횟수_횟수'] = dfs['입항횟수_횟수'].astype(float)
dfs['입항횟수_연도'] = dfs['입항횟수_연도'].astype(float)
combined_df['입항횟수_횟수'] = combined_df['입항횟수_횟수'].astype(float)
combined_df['입항횟수_연도'] = combined_df['입항횟수_연도'].astype(float)

 

 

  • 데이터 병합
    • 호출부호, 선박명, 입항횟수_횟수, 입항횟수_연도 기준, 입출항 데이터 기준으로 left merge
merged_left = pd.merge(dfs, combined_df, on= ['호출부호','선박명','입항횟수_횟수','입항횟수_연도'], how='left') # 입출항, 시설사용허가 left merge
merged_left

 

 


 

3.3 대기시간, 서비스시간 산출

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

 

 

  • 접안대기, 출항대기 컬럼 생성(사용목적명에 따라)
# copy
temp = unique_values.copy()

# 데이터 타입으로 변경 , 잘못된 값 제거(6311 인덱스) - 형식 불일치 값 (3022-10-02 1건)
temp['지정일시(TO)'] = pd.to_datetime(temp['지정일시(TO)'], errors='coerce')
temp['지정일시(FROM)'] = pd.to_datetime(temp['지정일시(FROM)'], 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 생성
    • 출항대기시간 null 값인 경우에는, Service_Time_End 값이 '출항일시' 값
    • 사용목적명이 '출항대기'일 경우에 Service_Time_End 값이 '지정일시(FROM)' 값
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['출항일시']

 

 

  • Anchor(= 접안대기), Service_Time_Start 생성
temp['Anchor'] = np.nan

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

# Anchor(=접안대기)가 null 인 경우, 입항 일시 값을 service time start로
temp['Service_Time_Start'] = np.where(temp['Anchor'].isnull(), temp['입항일시'], temp['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']

 

  • 대기율 산출
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_분']

 

해당 과정을 항구별로 각각 진행한다. (입항일시가 출항일시와 다른 경우에는 이상치로 판정하고 추가로 제거해주었다.)

 


4. 항구별 대기율 최종(2022)

  • 울산 0.143032
  • 부산 0.018844
  • 인천 0.110807
  • 광양 0.123489
728x90