🚚 물류 유통량 예측 경진대회_2 [코드 및 최종 모델]

2023. 10. 12. 17:35데이터분석

https://uncoolpark.tistory.com/2

 

🚚 물류 유통량 예측 경진대회_1 [베이스라인 모델, EDA]

Baseline Model LGBM 사용 ['물품_카테고리'] 라벨 인코딩 Measure Metric: RMSE # 라이브러리 임포트 import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from lightgbm impor

uncoolpark.tistory.com

EDA Code 

# 라이브러리 임포트 
import pandas as pd

# 데이터 로드 
train = pd.read_csv('train.csv')

# 데이터 변형_격자공간고유번호 앞 다섯 자리만 남기기 
df_five = train.copy()
df_five['송하인_격자공간고유번호'] = df_five['송하인_격자공간고유번호'].astype(str).str[:5].astype(int)
df_five['수하인_격자공간고유번호'] = df_five['수하인_격자공간고유번호'].astype(str).str[:5].astype(int)

# 데이터 변형_'송하인_격자공간고유번호' 상위 다섯 개만 남기기 
categories_to_include = [50110, 50130, 41480, 41410, 41590]
df_only_five = df_five[df_five['송하인_격자공간고유번호'].isin(categories_to_include)]

# 데이터 변형_'물품_카테고리' 상위 다섯 개만 남기기 
categories_to_include = ['농산물', '문화컨텐츠', '음료', '수산', '가공식품']
df_top5 = df_only_five[df_only_five['물품_카테고리'].isin(categories_to_include)]
df_top5.shape

# Group by '송하인_격자공간고유번호', '물품_카테고리'; index 기준 
result2 = df_top5.groupby('송하인_격자공간고유번호').apply(lambda x: x.groupby('물품_카테고리').agg({'index':'count'}).sort_values('index', ascending=False)).reset_index().sort_values(['송하인_격자공간고유번호', 'index'], ascending=[False, False])

# Group by '물품_카테고리' 기준 
result3 = df_top5.groupby('물품_카테고리').apply(lambda x: x.groupby('송하인_격자공간고유번호').agg({'index':'count'}).sort_values('index', ascending=False)).reset_index().sort_values(['물품_카테고리', 'index'], ascending=[False, False])

# Group by '송하인_격자공간고유번호' 기준 '운송장_건수' 평균 확인 
result_mean = df_top5.groupby('송하인_격자공간고유번호').apply(lambda x: x.groupby('물품_카테고리').agg({'운송장_건수':'mean'}).sort_values('운송장_건수', ascending=False))

# Group by '물품_카테고리' 기준 '운송장_건수' 평균 확인
result_mean = df_top5.groupby('물품_카테고리').apply(lambda x: x.groupby('송하인_격자공간고유번호').agg({'운송장_건수':'mean'}).sort_values('운송장_건수', ascending=False))

앞선 글에서 내린 결론처럼 '격자공간고유번호'를 앞 다섯 자리만 남기기로 했습니다. 

 

각 격자공간 고유번호를 상위 다섯 종류만 남겨주고, '물품 카테고리' 또한 상위 다섯 개 카테고리를 남겨봤습니다.

 

이후 Group by를 사용해 각 변수들간의 비율을 확인했습니다.

 

 

Modeling


EDA를 바탕으로 전처리 과정 및 코드를 알려드리겠습니다.

3. 전처리

  • ‘격자공간고유번호’ 앞 5자리만 남기기
  • ‘격자공간고유번호’, ‘물품_카테고리’ 라벨 인코딩
  • 오버 샘플링 필요할까?
  • ‘운송장_건수’ 기준으로 오른쪽 0.05 날리기?
  • train-val-test split

3.1. 데이터 합치고 ‘격자공간고유번호’ 앞 5자리 남기기

# 데이터 로드 
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train_df = train.copy()
test_df = test.copy()
 
# 데이터 결합
# 타겟 컬럼 지정 
target = train_df['운송장_건수']

# 타겟 컬럼 드랍
train_df = train_df.drop('운송장_건수', axis=1)

# train + test 결합
merged_df = pd.concat([train_df, test_df], axis=0)

# 결합데이터 인덱스 초기화
merged_df = merged_df.reset_index(drop=True)

# 결합 데이터 '격자공간고유번호' 앞 5자리만 남기기 
mdf = merged_df.copy()
mdf['송하인_격자공간고유번호'] = mdf['송하인_격자공간고유번호'].astype(str).str[:5].astype(int)
mdf['수하인_격자공간고유번호'] = mdf['수하인_격자공간고유번호'].astype(str).str[:5].astype(int)
  • 라벨 인코딩을 위해서 우선 train, test 데이터를 합쳐준다.
  • 결합한 데이터에서 격자공간고유번호를 앞의 5자리만 남긴다.

3.2. 라벨 인코딩

범주형 변수들은 라벨 인코딩을 해줍니다.

# 범주형 피처 라벨 인코딩 
cat_cols = ['송하인_격자공간고유번호', '수하인_격자공간고유번호', '물품_카테고리']
encoded_df = mdf.copy()
le = LabelEncoder()
for col in cat_cols:
    encoded_df[col] = le.fit_transform(mdf[col])

3.3. ‘운송장_건수’ 기준 30이상? 50이상? 날리기

‘운송장_건수’ > 10 = 1613 (31684 대비 5.09%)

df_under_ten = 30071개 (94.90%)

3.4. Data split

# 인코딩 이후 다시 train/test 분리 
train_data = encoded_df.iloc[:31684, :]
train_data['운송장_건수'] = target
train_data = train_data.set_index(train_data.columns[0])

test_data = encoded_df.iloc[31684:, :]
test_data = test_data.set_index(test_data.columns[0])

# train - validation 데이터 split 
train_X = train_data.drop('운송장_건수', axis=1)
train_y = train_data['운송장_건수']
X_train, X_val, y_train, y_val = train_test_split(train_X, 
                                                 train_y, test_size=0.3, random_state=42)
  • train-test 데이터가 결합되어 있기에 overfitting 방지를 위해 분리해준다.
  • train 데이터를 다시 7:3의 비율로 train과 validation set으로 나눠준다.

4. 모델링

  • reg 모델 몇 개 성능 비교
  • reg val-loss plot
  • 최종 알고리즘 고르기
  • cv해서 오버피팅 방지
  • 성능 고도화

4.1. 모델 성능 비교

# 기초 모델 설정 
rf = RandomForestRegressor(random_state=42)
rf.fit(X_train, y_train)
pred = rf.predict(X_val)

# 기초 모델 rmse 도출 
rmse = mean_squared_error(y_val, pred, squared=False)
print('Baseline Root Mean Squared Error Score:', rmse)

4.5. 최종 모델 성능 고도화

# 하이퍼 파라미터 튜닝을 위한 GridSearch 
# 파라미터 그리드 세팅
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# GridSearch 실행 
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid, cv=5, n_jobs=-1)

# 모델 피팅 
grid_search.fit(X_train, y_train)

# 최적 모델 적용
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_val)

# rmse 계산
rmse = mean_squared_error(y_val, y_pred, squared=False)
print("Root Mean squared error:", rmse)

# 최종모델 하이퍼파라미터
best_params = grid_search.best_params_
print("Best hyperparameters:", best_params)

1차 고도화 최종 모델 코드

# 라이브러리 임포트 
import pandas as pd
import numpy as np 
import seaborn as sns
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV
import matplotlib.pyplot as plt

# 데이터 로드 
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train_df = train.copy()
test_df = test.copy()
 
# 데이터 결합
# 타겟 컬럼 지정 
target = train_df['운송장_건수']

# 타겟 컬럼 드랍
train_df = train_df.drop('운송장_건수', axis=1)

# train + test 결합
merged_df = pd.concat([train_df, test_df], axis=0)

# 결합데이터 인덱스 초기화
merged_df = merged_df.reset_index(drop=True)

# 결합 데이터 '격자공간고유번호' 앞 5자리만 남기기 
mdf = merged_df.copy()
mdf['송하인_격자공간고유번호'] = mdf['송하인_격자공간고유번호'].astype(str).str[:5].astype(int)
mdf['수하인_격자공간고유번호'] = mdf['수하인_격자공간고유번호'].astype(str).str[:5].astype(int)

# 범주형 피처 라벨 인코딩 
cat_cols = ['송하인_격자공간고유번호', '수하인_격자공간고유번호', '물품_카테고리']
encoded_df = mdf.copy()
le = LabelEncoder()
for col in cat_cols:
    encoded_df[col] = le.fit_transform(mdf[col])

# 인코딩 이후 다시 train/test 분리 
train_data = encoded_df.iloc[:31684, :]
train_data['운송장_건수'] = target
train_data = train_data.set_index(train_data.columns[0])

test_data = encoded_df.iloc[31684:, :]
test_data = test_data.set_index(test_data.columns[0])

# train - validation 데이터 split 
train_X = train_data.drop('운송장_건수', axis=1)
train_y = train_data['운송장_건수']
X_train, X_val, y_train, y_val = train_test_split(train_X, 
                                                 train_y, test_size=0.3, random_state=42)

# 기초 모델 설정 
rf = RandomForestRegressor(random_state=42)
rf.fit(X_train, y_train)
pred = rf.predict(X_val)

# 기초 모델 rmse 도출 
rmse = mean_squared_error(y_val, pred, squared=False)
print('Baseline Root Mean Squared Error Score:', rmse)


# 하이퍼 파라미터 튜닝을 위한 GridSearch 
# 파라미터 그리드 세팅
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# GridSearch 실행 
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid, cv=5, n_jobs=-1)

# 모델 피팅 
grid_search.fit(X_train, y_train)

# 최적 모델 적용
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_val)

# rmse 계산
rmse = mean_squared_error(y_val, y_pred, squared=False)
print("Root Mean squared error:", rmse)

# 최종모델 하이퍼파라미터
best_params = grid_search.best_params_
print("Best hyperparameters:", best_params)

# 튜닝에 따른 성능 변화 시각화
results_df = pd.DataFrame(grid_search.cv_results_)
results_df = results_df[['params', 'mean_test_score', 'std_test_score']]
results_df = results_df.sort_values(by='mean_test_score', ascending=False)

plt.figure(figsize=(8, 6))
plt.errorbar(x=range(len(results_df)), y=results_df['mean_test_score'], yerr=results_df['std_test_score'])
plt.xticks(range(len(results_df)), results_df['params'], rotation=90)
plt.xlabel('Parameter settings')
plt.ylabel('Mean cross-validated score (RMSE)')
plt.title('Grid search development sequence')
plt.tight_layout()
plt.show()

# 최종모델 저장 및 결과 파일 생성 
final_model = best_model
final_pred = final_model.predict(X_val)
final_score = mean_squared_error(y_val, final_pred, squared=False)
print('Final Score:', final_score)

# 예측 결과인 array를 데이터 프레임, csv로 저장 
test_pred = final_model.predict(test_data)
result_df = pd.DataFrame({'운송장_건수': test_pred})
final_result = pd.concat([test, result_df], axis=1)
submit = final_result.set_index(final_result.columns[0])
submit_f = submit.iloc[:,-1]
submit_f.to_csv('submit.csv')

Public Score = RMSE: 5.46013, 104등

Best Hyperparameters

🤖**Best hyperparameters: {'max_depth': 5, 'min_samples_leaf': 4, 'min_samples_split': 2, 'n_estimators': 50}**

🌳RandomForestRegressor 알고리즘 설명

  • 트리기반 모델
  • 트리를 랜덤하게 여러 개로 째는 방식 (그래서 랜덤 + 포레스트)
  • 각 노드를 랜덤하게 구성하여 decision tree 모델 보다 과적합에 강하고, 우수한 성능을 보임 ↔ dt는 단순히 변수의 순서에 따라 노드를 구성하고 째는 방식이기 때문에 성능 저하 및 과적합이 매우 잘 발생함.
  • 데이터의 크기 및 변수의 수, 하이퍼 파라미터 튜닝 방식에 따라 학습 시간이 오래걸리거나 과적합이 발생하는 문제 발생
  • 비선형 모델답게 선형 모델에 비해서 변수 추론이 불편함
  • 하지만 신경망 모델에 비해서는 모델의 전개과정을 이해할 수 있고, 특정 변수가 모델에서 갖는 영향력은 해석할 여지가 있기에 보편적으로 사용되는 알고리즘.

하이퍼 파라미터 해석

  • max_depth: tree의 최대 깊이 (default = None). None일 경우, 노드들은 모든 leaf들이 순수할 때 까지 확장되거나 min_samples_split 샘플보다 적게 담고 있을 때 까지 확장된다.
    • 깊이를 너무 깊게 설정하면 overfitting
  • min_samples_leaf: leaf 노드를 생성하기 위한 최소 샘플의 수 (default=1). 어떤 depth에서도 스플릿 지점은 좌 우측 가지들의 최소 학습 샘플의 수인지 만을 고려한다. (min_samples_leaf에 지정된 수 보다 크면 새로운 스플릿을 한다는 뜻)
    • imbalanced한 데이터의 경우 특정 클래스의 데이터가 적게 들어 있을 수 있기 때문에 값을 작게 설정 필요
  • min_samples_split: 초기 노드로 스플릿하기 위한 최소 샘플의 수 (default=2).
    • int일 때, min_samples_split을 최소 수로 간주한다.
    • float일 때, min_samples_split은 특정 공식의 일부로서, 각 스플릿의 최소 수를 도출한다.
    • overfitting을 제어. 값이 작으면 분할 노드가 많아지고 overfitting 가능성 증가
  • n_estimators: forest 안의 tree의 개수 (default: 100)
    • 트리의 수가 늘어나면 computing 시간이 오래 걸리고, 성능이 좋아질 수 있지만 그만큼 overfitting 가능성 증가