본문 바로가기
머신러닝 대회 풀이

[ML 대회 해설] 데이콘 부동산 허위매물 분류 해커톤 10등 풀이 - Feature Engineering

by 미역청 2025. 4. 1.

이제 거의 다 왔습니다!
이번 포스트에서는 이전 글의 EDA를 바탕으로 진행한 최종 Feature Engineering 를 보여드리겠습니다.
 
이전 글:

 

[ML 대회 해설] 데이콘 부동산 허위매물 분류 해커톤 10등 풀이 - EDA

본격적으로 데이터의 주요 Feature의 개요를 살피고,EDA를 수행해 feature engineering을 위한 insight를 얻어보겠습니다. 이번 대회는 feature가 상당히 많은 관계로 주요 feature와 그 engineering 방법만을 짚

here-lives-mummy.tistory.com

 

Overview

본 대회는 일반적인 데이터분석 대회들과 달리
train이 약 2,400건으로 데이터 크기가 매우 적습니다.
따라서 feature engineering 시 public score은 참고용으로만 확인하고, 중요한 의사결정은 오로지 본인의 감을 믿고 해야합니다.
물론, 건수가 적기 때문에 hyperparameter tuning을 통한 성능개선은 기대할 수 없습니다.
 
지난 시간 EDA를 통해 얻은 insight를 정리해보면 다음과 같은데요,
이 중에서 최종버전에 반영한 것만 굵은 글씨로 표시해보겠습니다:

Insight 1-1. Threshold를 조절하여 모델을 보정할 수 있을 것이다.
Insight 1-2. LGBM 모델이 가장 좋아보이지만, LGBM, XGBoost, CatBoost 모두를 실험한다.
Insight 2-1. '총주차대수'의 결측치는 0으로 채울 수 있을 것이다.
Insight 2-2. '총주차대수'가 결측치인지 아닌지를 나타내는 새 feature '주차가능여부'를 만들 수 있다.
Insight 2-3. '관리비'로 target encoding을 할 수 있을 것이다. 
Insight 2-4. '관리비'의 소숫점을 rounding 할 수 있을 것이다.
Insight 2-5. 관리비 + 월세 = '월납부액' feature를 만들 수 있다.
Insight 2-6. '게재일' feature를 다음과 같이 처리할 수 있다.
  1. 'yyymmdd' 형식 int값으로 만들기
  2. '게재일' feature를 연도, 월, 날짜로 나누어 각각 파생 feaure '게재년' '게재월', '게재일자'를 만든다.
  3. 가장 옛 데이터 기준 몇주차인지를 계산한 파생 feature '게재주차'를 만든다.
Insight 2-7. '매물확인방식'으로 target encoding을 할 수 있다.
Insight 2-8. '방향'으로 target encoding을 할 수 있다.
Insight 2-9. '주차가능여부'로 target encoding을 할 수 있다.
Insight 2-10. '제공플랫폼'을 target encoding할 수 있을 것이다.
Insight 2-11. '제공플랫폼'의 건수가 작은 플랫폼들을 하나로 그룹핑해줄 수 있다.
Insight 2-12. '중개사무소'가 G52Iz8V2B9인지 여부에 따른 파생 feature 'G52'를 만들 수 있다.

 

1. Object type feature 추가 - 매물확인방식, 방향, 주차가능여부, 제공플랫폼

가장 먼저, 게재일을 제외한 모든 object type feature를 한꺼번에 넣어주었습니다.
Testset의 수가 적어 Valid.Score의 신뢰성이 떨어지기 때문에
feature를 하나씩 넣었다 빼며 미세한 성능차를 비교하는 것보다
여러 feature를 한 번에 넣은 후 feature 중요도를 비교하며 추후에 불필요한 feature를 빼는 편이 더 효율적이기 때문입니다.
 
Insight 2-7. '매물확인방식'으로 target encoding을 할 수 있다.
Insight 2-8. '방향'으로 target encoding을 할 수 있다.
Insight 2-9. '주차가능여부'로 target encoding을 할 수 있다.
Insight 2-10. '제공플랫폼'을 target encoding할 수 있을 것이다.
를 따라, 네 가지 feature 모두 한꺼번에 target encoding을 하여, numeric feature들과 함께 모델에 넣고 학습시켰습니다.

  LightGBM XGBoost CatBoost
이전 Valid.Score 0.22933 0.23773 0.25613
Valid.Score 0.47059 0.48941 0.49029

그 결과, 점수가 0.2 이상 향상하였네요.
 

2. 게재일

두 번째로, 게재일을 처리하여 feature에 넣어주었습니다.
제가 떠올렸던 접근법은 다음과 같았는데요,

Insight 2-6. '게재일' feature를 다음과 같이 처리할 수 있다.
  1. 'yyymmdd' 형식 int값으로 만들기
  2. '게재일' feature를 연도, 월, 날짜로 나누어 각각 파생 feaure '게재년' '게재월', '게재일자'를 만든다.
  3. 가장 옛 데이터 기준 몇주차인지를 계산한 파생 feature '게재주차'를 만든다.

위의 세 가지 방법 중에서 가장 좋은 성능을 보인 것은 두 번째 방법과 첫 번째 방법의 조합이었습니다.
 
게재일을 참조하여 세 가지 파생 feature '게재년', '게재월', '게재연월'을 만들었는데요,
여기서 '게재연월'은 'yyyymm'형태로 시간을 나타낸 것으로,
Insight 2-6 (1) 대로 'yyyymmdd'로 표기했을 때 데이터가 너무 세분화되는 것을 방지하고 어느정도 그룹핑 해주기 위해 날짜를 자른 것입니다.

for df in [train, test]:
    df['게재년'] = df['게재일'].str.split('-').str[0].astype(int)
    df['게재월'] = df['게재일'].str.split('-').str[1].astype(int)
    df['게재연월'] = (
        df['게재일'].str.split('-').str[0].astype(int) * 100 +
        df['게재일'].str.split('-').str[1].astype(int)
    )
  LightGBM XGBoost CatBoost
이전 Valid.Score 0.47059 0.48941 0.49029
Valid.Score 0.80142 0.79144 0.76262

 
그 결과, 모든 모델에서 성능이 두 배 가까이 뛰는 기염을 토하였습니다. 

 

3. 중개사무소

Insight 2-12. '중개사무소'가 G52Iz8V2B9인지 여부에 따른 파생 feature 'G52'를 만들 수 있다.
를 따라, 파생 feature 'G52' 를 적용시켜주었습니다.

for df in [train, test]:
    df['G52'] = 0
    df.loc[df.중개사무소 == 'G52Iz8V2B9', 'G52'] = 1
  LightGBM XGBoost CatBoost
이전 Valid.Score 0.80142 0.79144 0.79144
Valid.Score 0.86010 0.85962 0.86713

전체적으로 약 0.06씩 성능이 증가하였습니다.
 
그리고, 이 버전을 기점으로 기존의 LGBM > XGBoost > CatBoost였던 성능이 CatBoost  > LGBM > XGBoost 순으로 변화하였습니다. 
 

4. Threshold 조정

다음으로 Threshold를 조정해주었습니다.
적절한 Threshold값은 모델종류, 들어간 feature 등에 따라 매번 달라집니다.
 
따라서 Threshold 조정은 모든 feature engineering이 끝난 후 마지막에 수행해주어야 합니다. 
 
y_pred와 y_test를 이용해 모델에 다양한 Threshold를 실험해보고, 그 중 가장 적합한 것을 고르는 코드입니다: 

def find_optimal_threshold(y_probs, y_test, step=0.01):
    thresholds = np.arange(0.0, 1.0, step)
    results = []

    for threshold in thresholds:
        y_pred = (y_probs >= threshold).astype(int)
        acc = accuracy_score(y_test, y_pred)
        f1 = f1_score(y_test, y_pred)
        results.append((threshold, acc, f1))

    results = np.array(results)

    best_index_acc = np.argmax(results[:, 1])
    best_threshold_acc = round(results[best_index_acc, 0], 5)
    best_accuracy = round(results[best_index_acc, 1], 5)
    best_f1_score_acc = round(results[best_index_acc, 2], 5)

    best_index_f1 = np.argmax(results[:, 2])
    best_threshold_f1 = round(results[best_index_f1, 0], 5)
    best_f1_score = round(results[best_index_f1, 2], 5)
    best_accuracy_f1 = round(results[best_index_f1, 1], 5)

    df = pd.DataFrame({
        "Criterion": ["Best by Accuracy", "Best by F1-Score"],
        "Threshold": [best_threshold_acc, best_threshold_f1],
        "Accuracy": [best_accuracy, best_accuracy_f1],
        "F1-Score": [best_f1_score_acc, best_f1_score]
    })
    return df

optimal_thresholds_df = find_optimal_threshold(vld_pred_L, train[TARGET])
display(optimal_thresholds_df)

 
저는 이 때, CatBoost에 가장 적합한 threshold로 0.33를  찾아 모델에 대입시켜보았습니다.

  CatBoost
이전 Valid.Score 0.86713
Valid.Score 0.88215

 
그 결과, Valid.score에서 성능이 소폭 상승하였습니다.
 
※ 주의 Threshold 역시 하이퍼파라미터의 일종이기 때문에, Threshold 변화에 따라 Valid.Score 역시  크게 변화할 수 있습니다. 따라서, Threshold 조정 시 수시로 결과를 제출하여 public score를 확인해야합니다.


마무리

이로써 Feature Engineering을 마무리하고, 모델을 최종제출하여 대회를 마무리하였습니다.

최종 제출버전

 
아마 몇몇 분들께선 이번 대회에서 유난히 feature engineering에 힘을 뺐다고 느끼실 것 같습니다.
실제로 이 버전은 대회기간에는 feature engineering을 더 이상 진행하지 않은 탓에 성능이 그리 좋지 않았습니다.

대회 public score. 전체 채점 데이터의 30%만 채점한 결과입니다.

하지만 그럼에도 불구하고 더 이상 feature engineering을 추가로 수행하지 않은 명확한 이유가 있었습니다.
 
머신러닝 대회에서 데이터가 작을 경우, feature engineering은 높은 확률로 과적합의 위험을 수반합니다.
따라서 public score를 올리겠다며 공격적으로 feature engineering을 수행하는 것 보다, 적당한 시점에서 feature engieering을 중단하는 것이 오히려 private score를 올리는 지름길이 됩니다.
 
저는 현재 버전에서 수많은 feature engineering을 시도해보았으나 그렇다 할 성능개선이 나오지 않았는데요,
여기서 이 모델이 더 이상의 성능개선이 어려운 상태인 것으로 판단, 과적합을 피하기 위해 과감하게 더 이상의 디벨롭을 종료하였습니다.
그 결과 최종 private score에서 오히려 최종 10등 을 차지하게 되었습니다. :)
 


 
이로써 대회 풀이를 마무리하겠습니다.
지금까지 따라오시느라 고생 많으셨습니다!
 
도움이 되었다면 하트를 눌러주세요 :)
구독하시면 더 많은 데이터사이언스 정보와 대회풀이를 보실 수 있습니다!