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

[선박 대기시간 예측] 파생변수 생성 + 선박 대기시간 예측 모델(LightGBM)

by ISLA! 2023. 12. 15.

 

항만 대기시간 예측 및 운영시스템 개선을 위한
Feature Engineering
+ Model 구축

Feature Engineering

 

선박, 선석 기준으로 피쳐를 파악하고 나서 유의미한 파생변수를 만들고자 했다.

도메인 지식이 완전하지 않다고 여겨 논문들을 참고하여 총 6개 변수를 만들어 보았다.

변수 생성은 '선박'과 '선석'으로 기준을 나누어 진행했다.

 

 

1. 선박 일 별 누적입항 건수

 

  • 선석 기준 변수로, 선박이 입항하고자 하는 선석에 그 전 시간대까지 몇 개의 선박이 입항했는지를 count 한다.
df['이전_입항선박수'] = df.groupby(['ETA_Year', 'ETA_Month', 'ETA_Day', '계선장소명'])['호출부호'].cumcount()

# 확인용
df[df['계선장소명']=='가스부두'][['Datetime', '계선장소명', '호출부호', '이전_입항선박수']].head(60)

 

 

 

2. 3년 평균 선석 점유율

 

  • 선석 기준 변수로, 데이터를 수집한 3년 동안 선석별로 평균 점유율을 계산했다.
  • 선석별로 working day가 달라, 선석이 이용된 날짜를 추출하여 점유율을 계산
# 선석점유율 계산시 워킹 데이 계산 다시함(3년 각각)
df['Datetime'] = pd.to_datetime(df['Datetime'])
df['Month_Day'] = df['Datetime'].dt.strftime('%m-%d')

# 연도별/계선장소별 서비스 시간 확인 필요 : 3년 각각
temp2 = df.groupby(['ETA_Year', '계선장소명']).agg({'Service_Time_분':'sum', 'Month_Day':'nunique'}).reset_index()

# 연도별/계선장소별, 선석점유율계산
temp2['연도별_선석점유율'] = temp2['Service_Time_분'] / (temp2['Month_Day'] * 24 * 60)

# 3년 평균 선석점유율
temp3 = temp2.groupby('계선장소명')['연도별_선석점유율'].mean().to_frame().reset_index()

 

 

 

3. 선석의 3년 평균 톤 처리량

 

  • 선석 기준 변수로, 데이터를 수집한 3년 동안 선석별로 평균 톤 처리량을 계산
temp = df.copy()

# 연도별, 계선장소별 재화중량톤수 평균 계산
temp_mean = temp.groupby(['ETA_Year', '계선장소명'])['재화중량톤수'].mean().reset_index()

# 각 계선장소별로 3년 평균 계산
result = temp_mean.groupby('계선장소명')['재화중량톤수'].mean().to_frame().reset_index()

result.columns = ['계선장소명', '시설연평균_재화중량톤수']

 

 

4. 선석의 3년 평균 입항 척수

 

  • 선석 기준 변수로, 개별 선석에 3년 동안 평균 몇 척이나 입항했는지 계산
  • 연도/선석별로 입항일시의 건수를 count()
ship_cnt = df.groupby(['ETA_Year', '계선장소명'])['입항일시'].count().reset_index()
ship_cnt = ship_cnt.rename(columns = {'입항일시':'연간_총입항건수'})

# 각 계선장소별로 3년 평균 계산
ship_cnt_result = ship_cnt.groupby('계선장소명')['연간_총입항건수'].mean().to_frame().reset_index()
ship_cnt_result = ship_cnt_result.rename(columns = {'연간_총입항건수':'연평균_총입항건수'})
ship_cnt_result

 

 

 

5. 선박의 3년 평균 서비스 시간

 

  • 선박 기준 변수로, 입항 선박의 총 접안(서비스)시간 합과 접안 척수를 계산 👉 개별 선박의 연평균 서비스 시간
temp = df.groupby(['ETA_Year', '호출부호']).agg({'Service_Time_분' : ['sum', 'count']}).reset_index()
temp['선박_평균서비스시간'] = temp['Service_Time_분']['sum'] / temp['Service_Time_분']['count']
temp = temp.groupby('호출부호')['선박_평균서비스시간'].mean().to_frame().reset_index().rename(columns = {'선박_평균서비스시간':'선박_연평균_서비스시간'})
temp.head()

 

 

 

6. 선박 1건당 3년 평균 대기시간

 

  • 선석 기준 변수로, 개별 선박의 평균 대기시간(건수와 대기시간 합)
temp = df.groupby(['ETA_Year', '호출부호']).agg({'접안_대기시간_분' : ['sum', 'count']}).reset_index()
temp['선박_평균대기시간'] = temp['접안_대기시간_분']['sum'] / temp['접안_대기시간_분']['count']
temp = temp.groupby('호출부호')['선박_평균대기시간'].mean().to_frame().reset_index().rename(columns = {'선박_평균대기시간':'선박_연평균_대기시간'})

 


 

Model Fitting

 

라이브러리 불러오기

 

  • 주요 모델인 LightGBM과 하이퍼 파라미터 설정을 위한 베이지안 옵티마이제이션 불러오기에 유의한다
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
pd.set_option('display.max.columns', None)

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

import lightgbm as lgb
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split, KFold
from bayes_opt import BayesianOptimization

import matplotlib.pyplot as plt
plt.rc("font", family="NanumGothic") # 라이브러리 불러오기와 함께 한번만 실행

 

 

LightGBM + BayesianOptimization 베이스 모델 코드

 

  • 파생변수 중, 종속변수가 포함된 피쳐를 제외하여 x_cols에 저장
  • 하이퍼 파라미터는 6종 선택하여 범위 지정 : num_leaves, learning_rate, feature_fraction, max_depth, min_child_samples, reg_lambda
👉 최적 하이퍼파라미터: {'feature_fraction': 0.8004190309699286, 'learning_rate': 0.10702215053413637, 'max_depth': 12.070914571518188, 'min_child_samples': 47.09880576148448, 'num_leaves': 55.66683069702394, 'reg_lambda': 0.22413298533545556}

👉 평균 검증 RMSE (최적 모델): 1044.9487936444125
x_cols = ['ETA_Year', 'ETA_Month', 'ETA_Day', 'ETA_Hour', 
       'Service_Time_분','총톤수', '재화중량톤수',
       '선박_총길이', '선박_너비', '선박_만재흘수', '선박_깊이', '선박_길이1', '풍속',
       '풍향', 'GUST풍속', '현지기압', '습도', '기온', '수온',
       '최대파고', '유의파고', '평균파고', '파주기', '파향', '이전_입항선박수',
       '시설연평균_재화중량톤수', '연평균_총입항건수', '선박_연평균_서비스시간',
       '호출부호_encoded', '계선장소명_encoded', '선박용도_encoded']

# 데이터 불러오기 (주어진 데이터 사용)
data = df.copy()

# 특성과 타겟 변수 분리
X = data[x_cols]
y = data['접안_대기시간_분']

# 베이지안 옵티마이제이션 범위 설정
pbounds = {
    'num_leaves': (2, 100),           # num_leaves의 범위 설정
    'learning_rate': (0.01, 0.3),    # learning_rate의 범위 설정
    'feature_fraction': (0.1, 0.9),  # feature_fraction의 범위 설정
    'max_depth': (3, 30),
    'min_child_samples': (1, 50),     # min_child_samples의 범위 설정
    'reg_lambda': (0, 1)              # reg_lambda의 범위 설정
}

# 베이지안 옵티마이제이션 함수 정의
def lgb_cv(num_leaves, learning_rate, feature_fraction, max_depth, min_child_samples, reg_lambda):
    params = {
        'objective': 'regression',  # 회귀 문제 설정
        'metric': 'rmse',          # 평가 지표 (Root Mean Squared Error)
        'num_leaves': int(num_leaves),
        'learning_rate': max(min(learning_rate, 1), 0),  # learning_rate를 0과 1 사이로 제한
        'feature_fraction': max(min(feature_fraction, 1), 0),  # feature_fraction을 0과 1 사이로 제한
        'max_depth':int(max_depth),
        'min_child_samples': int(min_child_samples),
        'reg_lambda': max(min(reg_lambda, 1), 0)
    }

    kf = KFold(n_splits=5, random_state=42, shuffle=True)
    rmses = []

    for train_index, val_index in kf.split(X):
        X_train, X_val = X.iloc[train_index], X.iloc[val_index]
        y_train, y_val = y.iloc[train_index], y.iloc[val_index]

        model = lgb.LGBMRegressor(**params)

        # 모델 학습
        model.fit(X_train, y_train)

        # 검증 데이터로 예측
        y_pred = model.predict(X_val)

        # 모델 평가 (RMSE 계산)
        rmse = np.sqrt(mean_squared_error(y_val, y_pred))
        rmses.append(rmse)

    return -np.mean(rmses)  # 목적 함수는 최소화해야 하므로 음수로 반환

# BayesianOptimization 객체 생성
optimizer = BayesianOptimization(
    f=lgb_cv,  # 최적화할 함수 지정
    pbounds=pbounds,  # 변수 범위 지정
    random_state=42,  # 랜덤 시드 설정
    verbose=2  # 로그 출력 레벨 설정
)

# 최적화 실행
optimizer.maximize(init_points=5, n_iter=10)

# 최적 하이퍼파라미터 출력
best_params = optimizer.max['params']
print("최적 하이퍼파라미터:", best_params)

# 최적 하이퍼파라미터로 모델 학습 및 평가
kf = KFold(n_splits=5, random_state=42, shuffle=True)
test_rmses = []

for train_index, val_index in kf.split(X):
    X_train, X_val = X.iloc[train_index], X.iloc[val_index]
    y_train, y_val = y.iloc[train_index], y.iloc[val_index]

    best_model = lgb.LGBMRegressor(
        objective='regression',
        metric='rmse',
        num_leaves=int(best_params['num_leaves']),
        learning_rate=best_params['learning_rate'],
        feature_fraction=best_params['feature_fraction'],
        min_child_samples=int(best_params['min_child_samples']),
        reg_lambda=best_params['reg_lambda']
    )

    # 모델 학습
    best_model.fit(X_train, y_train)

    # 검증 데이터로 예측
    y_pred = best_model.predict(X_val)

    # 모델 평가 (RMSE 계산)
    rmse = np.sqrt(mean_squared_error(y_val, y_pred))
    test_rmses.append(rmse)


# 예측 결과값의 범위 출력
print("예측 결과값 범위:")
print(f"최소 예측값: {np.min(y_pred)}")
print(f"최대 예측값: {np.max(y_pred)}")

print(f'평균 검증 RMSE (최적 모델): {np.mean(test_rmses)}')

# feature importance 시각화
plt.figure(figsize=(15, 12))
lgb.plot_importance(best_model, max_num_features=20, importance_type='split')  # split 기준으로 시각화 / importance_type='split': 노드에서 해당 특성을 분할하는 데 사용된 횟수
plt.show()

 

728x90