항만 대기시간 예측 및 운영시스템 개선을 위한
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
'Projects > ⛴️ Ship Waiting Time Prediction' 카테고리의 다른 글
[선박 대기시간 예측] 예측 결과 활용 예시 & 기대 효과 (0) | 2023.12.15 |
---|---|
[선박 대기시간 예측] 시계열/선석 기반 EDA (0) | 2023.12.15 |
[선박 대기시간 예측] 선박 기반 EDA (0) | 2023.12.15 |
[선박 대기시간 예측] 3차 전처리 : 울산항 모델용 데이터셋 도출(결측치 처리) (1) | 2023.12.15 |
[선박 대기시간 예측] 2차 전처리 : 중복값 처리 (0) | 2023.12.15 |