2023.11.15~2023.12.11까지 진행되었던 나와 팀원의 첫 데이콘 대회.
 Public에서 6등으로 마무리 하였지만 과적합에 때문에 Private은 48등으로 아쉽게 마무리하였다..
 성능을 높이기 위해 노력했던 우리 팀의 방법들과 다른 상위팀의 아이디어를 종합하여 최고의 모형을 만들어보려 한다.

데이터 설명

https://dacon.io/competitions/official/236193/data

 

대구 교통사고 피해 예측 AI 경진대회 - DACON

분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.

dacon.io

위 사이트의 데이터 설명란 참고

 

우리팀 전략

우리 팀이 사용했던 아이디어들은 다음과 같다.

 

1. 평가 기준이 RMSLE임을 고려하여 ECLO가 20이상인 데이터를 제거
2. 사고유형을 기준으로 데이터를 3분할하여 학습시키고 예측하는 것
3. 공휴일 칼럼 추가
4. 계절 칼럼 추가
5. 출퇴근 칼럼 추가
6. ECLO 로그 변환

Private 2등 code '비타민 13기'

핵심 아이디어

1. ECLO 로그 변환
2. 이상치 처리(상위 0.1% 제거)
3. 외부 데이터 활용
-> sun(일출, 일몰), 총통행량(택시), 평균통행량(택시), 평균속도(택시)
4. 파생변수 생성
-> 주말, 도로형태1, 도로형태2, 가해운전자 평균연령, 피해운전자 평균 연령, 가해운전자 평균성별, 피해운전자 평균성별, ride_dangerous, accident_case_dangerous
5. 전국 데이터 활용(전국 18개의 도시)
6. 가중치 기반 앙상블


교통사고 인사이트

인사이트 1: 지역별 택시의 평균 속도, 통행량은 ECLO에 영향을 미친다.
-> 구별 택시 평균속도/총통행량/평균통행량 변수를 생성하여 지역별 교통화견 차이를 예측해 반영
인사이트 2: 지역별 사고유형별 사고 빈도와 가해운전자 차종별 사고 빈도는 ECLO와 연관성을 보인다.
-> 구별 위험도 파생변수를 생성하여 지역별 위험도 차이를 예측해 반영
인사이트 3: 구별 피해 운전자 평균 연령과 ECLO는 양의 상관관계를 보인다.
-> 구별 피해 운전자의 평균 연령 파생변수를 생성하여 지역별 운전자의 인구 통계학적 특성을 예측해 반영
인사이트 4: 야간의 시인성 저하는 교통사고 발생에 영향을 미친다.
-> 일출 일몰 시각 파생 변수를 추가하여 야간의 시인성 저하에 의한 교통사고 발생확률을 예측해 반영
인사이트 5: 인명 피해 정도는 평일보다 주말에 심해진다.
-> 주말 변수를 추가하여 ECLO 예측 정확성을 높임.


모델

Xgboost Regressor, CatBoost Regressor, LightGBM Regressor를 앙상블
이후 feature importance를 기반으로 교통사고 주요 위험요인을 선정 후 이에 대한 해결책을 제시


위 내용을 어떻게 코드로 구현해보았는지 알아보자.


데이터 전처리
1. 파생변수 생성 1. 날짜, 시간정보 생성
'사고 일시' 컬럼으로부터 연도, 월, 일, 시간 정보 추출 및 변환
2. 파생변수 생성 2. 공간 정보 생성 
'시군구' 컬럼으로부터 도시, 구, 동 정보 추출 및 변환
3. 파생변수 생성 3. 도로 형태 정보 추출
'도로형태' 칼럼은 두 개의 정보로 이루어져 이를 분리 도로형태 1과 도로형태2로 분리
> 준비하면서 도로형태 2에서 다른 도로형태 1에서 오는 기타를 하나로 묶는 것보다 다르게 두는 것이 낫다고 생각해 도로형태를 도로형태 1과 도로형태 2로 분리 후 도로 형태 2를 드랍하는 것을 생각해봄.
4. 가해운전자 연령 -> 변경
5. 외부 데이터 - 전국 데이터 사고 전처리 및 이상치 제거
-> 대구 데이터와 전체 도시 데이터에서 object column을 확인하고
-> 대구 데이터에 없는 object형 변수 값을 가지고 있는 전체 도시 데이터의 인덱스를 파악
-> 이를 제거함.
차량단독에서 ('노면상태', '가해운전자 연령', '도시', '구', '동') column에 대한 결측치 drop
차량단독의 경우 피해운전자 차종은 미분류나 결측치임. 따라서 없음으로 대치
차량단독이 아닌 경우에 대해서는 결측치 모두 drop
-> 결측지 제거 이유는 뭐지? 그냥 형식상의 처리인듯
+ 이상치 제거 코드

# 이상치 제거(0.1%)
def set_limit(column):
    return np.quantile(column, 0.999)

country_copy = country_df.copy()

country_copy = country_copy[['기상상태', '노면상태', '사고유형', '사고유형 - 세부분류', '도로형태1', '도로형태2', 'ECLO']]
country_copy.reset_index(inplace=True, drop=True)

outlier_idxs = []
for col in country_copy.columns[:-1]:
    temp_df = pd.DataFrame(country_copy.groupby(col)['ECLO'].agg(set_limit)).reset_index()
    for j in range(len(temp_df)):
        s_idxs = country_df[(country_df[col] == temp_df.loc[j, col]) & (country_df['ECLO'] > temp_df.loc[j, 'ECLO'])].index.to_list()
        outlier_idxs = outlier_idxs + s_idxs
outliers = list(set(outlier_idxs))

print('outlier 수 : ', len(outliers))

country_df = country_df.drop(outliers, axis=0)
country_df.reset_index(inplace=True, drop=True)

print(len(country_df))



6. 파생변수 4. 지역별 가해운전자 & 피해운전자 평균 연령 추출
train에 대해서는 도시, 구, 동으로 groupby, 전국 데이터는 도시로 groupby
7. 파생변수 5. 지역별 가해운전자 & 피해운전자 평균 성별 추출
train에 대해서는 도시, 구, 동으로 groupby, 전국 데이터는 도시로 groupby
8. 파생변수 6. 주말 변수 추가
9. 전체 데이터 concat
10. 파생변수 7. 외부데이터(일출일몰시각 : 해 떴는지(1) 안떴는지(0))
11. 파생변수 8. 지역별 위험도 변수 생성
ride dangerous: 가해운전자 차종별 위험도를 측정 후(가해운전자 차종 별 ECLO의 평균 구함.) 구 별 가해운전자 차종 사고 발생 비율을 반영하여 가중치 생성
전체 train에서 가해운전자 차종별 위험도 평균를 측정
특정 구에 대해서 ((각 차종별 사고 회수)x(가해운전자 차종별 위험도 평균))/(구의 전체 사고 횟수)
accident case dangerous: 사고유형별 위험도를 측정 후 구 별로 사고 유형 발생 비율을 반영하여 가중치 생성
12. 파생변수 9. 외부데이터 - 택시 통행량, 택시 속도
도시별 총통행량 평균, 도시별 평균통행량 평균
도시별 평균속도 평균 -> 모두 train에서 데이터 mearge하고 train의 결측치 채우고 train의 데이터로 test 채움.
13. 기상상태, 노면상태, 사고유형, 도로형태1, 도로형태2 원핫 인코딩
테스트에 없는 원핫 인코딩을 버리기 보단 test에 새로운 컬럼 생성(0으로)

for i in train_oh.columns:
    if i not in test_oh.columns:
        test_oh[i]=0


14. 요일, 도시, 구 레이블 인코딩

for case in np.unique(test_x[i]):
    if case not in le.classes_:
        print('test case is not in classes')
        le.classes_ = np.append(le.classes_, case)


모델 돌리기

from xgboost import XGBRegressor
from lightgbm import LGBMRegressor, early_stopping
from catboost import CatBoostRegressor
import optuna

from sklearn.metrics import mean_squared_log_error as msle
from sklearn.model_selection import train_test_split


XGBOOST

def xgb_modeling(X_train, y_train, X_valid, y_valid):
  def objective(trial):
    params = {
        'learning_rate': trial.suggest_float('learning_rate', 0.0001, 0.1),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 20),
        'gamma': trial.suggest_float('gamma', 0.01, 1.0),
        'reg_alpha': trial.suggest_float('reg_alpha', 0.01, 1.0),
        'reg_lambda': trial.suggest_float('reg_lambda', 0.01, 1.0),
        'seed':42,
        'max_depth': trial.suggest_int('max_depth', 3, 15), # Extremely prone to overfitting!
        'n_estimators': trial.suggest_int('n_estimators', 300, 3000, 200), # Extremely prone to overfitting!
        'eta': trial.suggest_float('eta', 0.007, 0.013), # Most important parameter.
        'subsample': trial.suggest_discrete_uniform('subsample', 0.3, 1, 0.1),
        'colsample_bytree': trial.suggest_discrete_uniform('colsample_bytree', 0.4, 0.9, 0.1),
        'colsample_bylevel': trial.suggest_discrete_uniform('colsample_bylevel', 0.4, 0.9, 0.1),
    }

    model = XGBRegressor(**params, random_state=42, n_jobs=-1, objective='reg:squaredlogerror')
    bst_xgb = model.fit(X_train,y_train, eval_set = [(X_valid,y_valid)], eval_metric='rmsle', early_stopping_rounds=100,verbose=False)

    preds = bst_xgb.predict(X_valid)
    if (preds<0).sum()>0:
      print('negative')
      preds = np.where(preds>0,preds,0)
    loss = msle(y_valid,preds)

    return np.sqrt(loss)

  study_xgb = optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=100))
  study_xgb.optimize(objective,n_trials=30,show_progress_bar=True)

  xgb_reg = XGBRegressor(**study_xgb.best_params, random_state=42, n_jobs=-1, objective='reg:squaredlogerror')
  xgb_reg.fit(X_train,y_train,eval_set = [(X_valid,y_valid)], eval_metric='rmsle', early_stopping_rounds=100,verbose=False)

  return xgb_reg,study_xgb


LGBM

def lgbm_modeling(X_train, y_train, X_valid, y_valid):
  def objective(trial):
    param = {
        'objective': 'regression',
        'verbose': -1,
        'metric': 'rmse',
        'num_leaves': trial.suggest_int('num_leaves', 2, 1024, step=1, log=True),
        'colsample_bytree': trial.suggest_uniform('colsample_bytree', 0.7, 1.0),
        'reg_alpha': trial.suggest_uniform('reg_alpha', 0.0, 1.0),
        'reg_lambda': trial.suggest_uniform('reg_lambda', 0.0, 10.0),
        'max_depth': trial.suggest_int('max_depth',3, 15),
        'learning_rate': trial.suggest_loguniform("learning_rate", 1e-8, 1e-2),
        'n_estimators': trial.suggest_int('n_estimators', 100, 3000),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'subsample': trial.suggest_loguniform('subsample', 0.4, 1),
    }

    model = LGBMRegressor(**param, random_state=42, n_jobs=-1)
    bst_lgbm = model.fit(X_train,y_train, eval_set = [(X_valid,y_valid)], eval_metric='rmse',callbacks=[early_stopping(stopping_rounds=100)])

    preds = bst_lgbm.predict(X_valid)
    if (preds<0).sum()>0:
      print('negative')
      preds = np.where(preds>0,preds,0)
    loss = msle(y_valid,preds)

    return np.sqrt(loss)

  study_lgbm = optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=100))
  study_lgbm.optimize(objective,n_trials=30,show_progress_bar=True)

  lgbm_reg = LGBMRegressor(**study_lgbm.best_params, random_state=42, n_jobs=-1)
  lgbm_reg.fit(X_train,y_train,eval_set = [(X_valid,y_valid)], eval_metric='rmse', callbacks=[early_stopping(stopping_rounds=100)])

  return lgbm_reg,study_lgbm


Catboost

def cat_modeling(X_train, y_train, X_valid, y_valid):
  def objective(trial):
    param = {
        'iterations':trial.suggest_int("iterations", 1000, 20000),
        'od_wait':trial.suggest_int('od_wait', 500, 2300),
        'learning_rate' : trial.suggest_uniform('learning_rate',0.01, 1),
        'reg_lambda': trial.suggest_uniform('reg_lambda',1e-5,100),
        'subsample': trial.suggest_uniform('subsample',0,1),
        'random_strength': trial.suggest_uniform('random_strength',10,50),
        'depth': trial.suggest_int('depth',1, 15),
        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf',1,30),
        'leaf_estimation_iterations': trial.suggest_int('leaf_estimation_iterations',1,15),
        'bagging_temperature' :trial.suggest_loguniform('bagging_temperature', 0.01, 100.00),
        'colsample_bylevel':trial.suggest_float('colsample_bylevel', 0.4, 1.0),
    }


    model = CatBoostRegressor(**param, random_state=42)
    #task_type="GPU",devices='0:1'
    bst_cat = model.fit(X_train,y_train, eval_set = [(X_valid,y_valid)], early_stopping_rounds=100,verbose=False)

    preds = bst_cat.predict(X_valid)
    if (preds<0).sum()>0:
      print('negative')
      preds = np.where(preds>0,preds,0)
    loss = msle(y_valid,preds)

    return np.sqrt(loss)

  study_cat = optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=100))
  study_cat.optimize(objective,n_trials=30,show_progress_bar=True)

  cat_reg = CatBoostRegressor(**study_cat.best_params, random_state=42)
  cat_reg.fit(X_train,y_train,eval_set = [(X_valid,y_valid)], early_stopping_rounds=100,verbose=False)

  return cat_reg,study_cat

수정한 코드

 파생변수는 위의 코드와 비슷하게 만들어주고 대구 데이터에 없는 object형 변수 값을 갖고 있는 전체 도시 데이터의 인덱스를 파악해 drop해준 다음 사고유형에 따라 데이터를 분리하였다.

train1=country_df[country_df["사고유형"]=="차량단독"]
train2=country_df[country_df["사고유형"]=="차대차"]
train3=country_df[country_df["사고유형"]=="차대사람"]
test1=test_df[test_df["사고유형"]=="차량단독"]
test2=test_df[test_df["사고유형"]=="차대차"]
test3=test_df[test_df["사고유형"]=="차대사람"]

display(train1.shape)
display(train2.shape)
display(train3.shape)

결과

(26620, 29)
(489871, 29)
(113105, 29)


이렇게 나눈 후 이상치 1% 제거, 사고 유형 drop, 지역별 가해운전자 & 피해운전자 평균 연령 추출, 지역별 가해운전자 & 피해운전자 평균 성별 추출, 가해운전자 & 피해운전자 차종 별 위험도 추출
+ 피해 & 가해 노인운전자 위험도(차량단독, 차대사람만)
+ 원핫 인코딩("노면상태", "도로형태1")
+ 레이블 인코딩("요일", "도시", "구", "동", "도로형태")
을 진행하였고 3개의 데이터에 대해 optuna를 사용하여 lgbm 하이퍼파라미터 튜닝, catboost 하이퍼파라미터 튜닝을 진행하였다.
마지막 앙상블은 lgbmx0.2+catboostx0.8로 하였을 때 가장 성능이 높게 나왔다.


그런데 생각보다 성과가 나오지 않고 제출 횟수도 제한되어 있어서 더 이상의 진행은 그렇게 도움이 되지 않을 것 같아 개선을 멈추려고 한다.(튜닝하는데 시간을 너무 보내서..)
수정한 코드의 ipynb는 다음 링크에 있다.
https://github.com/ParkSeokwoo/Dacon_Daegu-Traffic-Accident-Prediction-AI/blob/main/%EB%B8%94%EB%A1%9C%EA%B7%B8_%EC%9E%91%EC%84%B1%EC%9A%A9.ipynb

 

Dacon_Daegu-Traffic-Accident-Prediction-AI/블로그_작성용.ipynb at main · ParkSeokwoo/Dacon_Daegu-Traffic-Accident-Predict

A project predicting the risk of traffic accidents in Daegu - ParkSeokwoo/Dacon_Daegu-Traffic-Accident-Prediction-AI

github.com

 

+ Recent posts