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

[ML 대회 해설] Dacon 아파트 실거래가 예측 AI 경진대회 2등 풀이 - Feature Engineering (실패편)

by 미역청 2025. 3. 10.

이번 포스트에선 Feature Engineering (성공편)에서 다루지 않았던,
실패한 Feature Engineering들과 실패한 이유에 대해 얘기해보겠습니다.
 
지난 글:

 

[ML 대회 해설] Dacon 아파트 실거래가 예측 AI 경진대회 2등 풀이 - Model Tuning

그간 따라오시느라 고생 많으셨습니다! 이제 거의 다 왔습니다.  지난 글에서는 EDA로 얻었던 insight를 기반으로 Feature Engineering을 진행했습니다.지난 글: [ML 대회 해설] Dacon 아파트 실거래가 예

here-lives-mummy.tistory.com

 

개요

우리는 EDA 단계에서 얻은 다양한 insight를 기반으로 feature engineering을 수행했습니다.
기존에 있던 feature에서 노이즈를 제거하거나 그룹핑을 하기도 했고, 반면 
 
이 중 Feature Engineering에서 실제로 활용했던 아이디어만 굵은 글씨로 표시할게요.
 
Numeric Feature의 EDA에서 얻은 insight (EDA 포스트 참고) :

Insight 1. 데이터는 특별한 규칙으로 정렬되어있을 것이다.
Insight 2-1. target에 log를 씌우면 성능이 개선될 수도 있다.
Insight 2-2. exclusive_use_area를 적절한 자리에서 반올림/버림/올림 하여 노이즈를 제거한다.
Insight 2-3. Target Feature / exclusive_use_area = 평당가격 feature을 만들어볼 수 있을 것이다.
Insight 2-4. transaction_year - year_of_completion = 아파트연식 을 파생 feature로 만들 수 있을 것이다.
Insight 2-5. apartment_id는 target feature 기준으로 그룹핑된 것이 아니기에 상대적으로 성능을 대폭 하락시킬 확률이 매우 낮다.
Insight 2-6. transaction_year_month를 각각 transaction_year, transaction_month로 나눌 수 있을 것이다.

 
Object Type Feature의 EDA에서 얻은 insight (EDA 포스트 참고):

Insight 3-1. City는 서울을 1로, 부산을 0으로 encoding한다.
Insight 3-2. Dong을 City + Dong 형식으로 바꾸어, 동명이동이 하나로 묶이는 것을 방지한다.
Insight 3-3. Jibun은 addr_kr과 공유하는 속성이 있어, 둘 중 하나를 골라야 할 수도 있다.
Insight 3-4. Addr_kr 는 dong + jibun + apt와 거의 똑같은 패턴을 가진 feature이다. addr_kr과 다른 세 feature 중 하나를 택해야 할 수 있다.
Insight 3-5. apt는 속성값이 다양하고 count가 1인 것들이 많아, 비슷한 값들끼리 그룹핑을 해줄 필요가 있다.
Insight 3-6. Transaction_date는 1~10, 11~20, 21~말일로 이루어져있으나, 매달 말일이 달라 실제보다 많은 unique값을 갖게 되었으니 이를 하나로 그룹핑해줄 수 있다.

 
 
여기서 선택받지 못한 아이디어는 다음과 같습니다:

Insight 1. 데이터는 특별한 규칙으로 정렬되어있을 것이다.
Insight 2-1. target에 log를 씌우면 성능이 개선될 수도 있다.
Insight 2-4. transaction_year - year_of_completion = '아파트연식' 을 파생 feature로 만들 수 있을 것이다.
Insight 2-5. apartment_id는 target feature 기준으로 그룹핑된 것이 아니기에 상대적으로 성능을 대폭 하락시킬 확률이 매우 낮다.
Insight 2-6. transaction_year_month를 각각 transaction_year, transaction_month로 나눌 수 있을 것이다.

Insight 3-3. Jibun은 addr_kr과 공유하는 속성이 있어, 둘 중 하나를 골라야 할 수도 있다.
Insight 3-4. Addr_kr 는 dong + jibun + apt와 거의 똑같은 패턴을 가진 feature이다. addr_kr과 다른 세 feature 중 하나를 택해야 할 수 있다.
Insight 3-6. Transaction_date는 1~10, 11~20, 21~말일로 이루어져있으나, 매달 말일이 달라 실제보다 많은 unique값을 갖게 되었으니 이를 하나로 그룹핑해줄 수 있다.

 
이들은 대부분 아이디어를 토대로 만든 feature가 성능을 좀먹거나, 원본 feature 자체가 회생불가인 경우입니다.
하나씩 살펴보며 실패 원인을 분석해봅시다.
 

2-1. Target에 log씌우기

Target값에 log1p를 씌우고 싶다면, 되도록 feature engineering 가장 첫 단계에서 하는 것이 좋습니다.
Logscale일 때와 그렇지 않을 때 각각 성능을 향상시키는 방법이 조금씩 달라지기 때문입니다.
Target이 일반 scale인 모델에서 유용했던 feature가, logscale 모델에서는 힘을 발휘하지 못하는 경우도 비일비재합니다.
 
따라서, 이전 톺아보기 포스트 에서 실험했던 XGBoost 모델을 baseline으로, logscale을 붙여 성능을 비교해보겠습니다.

 

  Baseline Logscale 붙인 것
n_estimator=1000 일 때
Validation Score
6,990.93174 7,693.12412
n_estimator=5000 일 때
Validation Score
5,377.72821 5,508.73725
n_estimator=1000 일 때
Public Score
13,303.27830 13,450.03321

 

Logscale을 붙인 모델의 성능이 훨씬 떨어집니다.
n_estimator를 5000까지 늘려서 학습시켜봐도 결과는 같습니다.
 
이상합니다.
분명히 log1p를 씌웠을 때 target이 더 정규분포에 가까운 모습을 보였고,
소숫점 아래 작은 값을 증폭시켰을텐데 왜 오히려 성능이 떨어졌을까요?
 
이는 대회 규칙인 eval metric과 관련이 있습니다.
 
log1p를 씌웠을 때, target 값이 정규분포에 가까워지면 성능향상을 노려볼 수 있는 것은 사실입니다.
하지만 언제 그런 것은 아닙니다.

특히 대회에서는 eval metric이 logscale일 때만 사용하는 것이 좋습니다.

 
Eval metric이 MSLE, RMSLE, Poisson Loss였다면 사용해볼만한 방법이었겠으나,
이 대회는 RMSE였기 때문에 제 힘을 발휘하지 못했던 거죠.
 

2-6. 'transaction_year', 'transaction_month' 추가하기

거래년월을 뜻하는 'transaction_year_month' feature는 모델에서 적지 않은 기여를 하고 있습니다.
이는 Feature 중요도를 확인해보면 알 수 있습니다.

XGBoost Baseline에서의 Feature 중요도

그렇다면 transcation_year_month를 transaction_year과 transaction_month로 나눠, 총 2개의 파생 feature를 만든다면 더 좋은 성능을 기대할 수도 있지 않을까요?

for df in [train, test]:
  df['transaction_year'] = df.transaction_year_month // 100
  df['transaction_month'] = df.transaction_year_month % 100

 
단, 이 경우 주의할 것이 있습니다.
 
완성한 두 feature와 다른 feature 간의 상관계수를 분석해보겠습니다.

transaction_year_month와 transaction_year의 상관계수가 1입니다. 

(transaction_month가 target과 매우낮은 상관관계를 갖는 것 또한 좋은 신호는 아니지만, 일단은 넘어가겠습니다.)
 
이 경우, transaction_year와 transaction_year_month 두 feature를 모두 모델에 적용시키면 과적합이 발생할 수 있습니다.
 
따라서 저는 다음 경우의 수를 모두 시도해보며 성능 비교를 해봤습니다.

  1. transaction_year_month, transaction_year, transaction_month 선택
  2. transaction_year, transaction_month 선택
  3. transaction_year_month, transaction_month 선택
  4. transaction_year_month, transaction_year 선택
  5. transaction_year_month, transaction_year에 target encoding
  6. transaction_year_month 선택, transaction_year로 target encoding

결과는 모두 성능이 저하하거나, 변동이 없었습니다.
 

2-4. '아파트연식' feature 만들기

'transaction_year' - 'year_of_completion' 을 하여 아파트연식에 해당하는 파생 feature를 만들어볼 수 있습니다.
보통 신축 아파트일수록 더 비싼 가격에 거래된다고 알려져있으니 이 feature는 꽤나 유의미해보입니다.
 
하지만 우리의 통념과 달리, 이걸 추가해보면 모델 성능이 살짝 떨어지는 것을 확인할 수 있습니다.
왜 그런 걸까요?
 
저는 이것이 원본 feature인 'transaction_year''year_of_completion' 때문이라고 생각합니다.
 
이전 글에서 완성한 최종 XGBoost 모델의 Feature 중요도를 보겠습니다.

이전 글에서 완성한 최종 XGBoost model의 Feature 중요도

'year_of_completion'의 feature 중요도가 많이 낮아졌습니다.
그 사이 다른 feature들에 밀려 힘을 쓰지 못하게 되었네요.
 
일반적으로 target과의 상관계수가 크거나, 중요도가 높은 feature를 활용해 만든 파생 feature가 그렇지 않은 feature보다 좋은 결과를 낳습니다.
이말인즉, 상관계수가 낮거나 중요도가 낮은 feature를 활용한 파생 feature는 모델에서 큰 활약을 하기 어려울 수 있다는 뜻입니다.
 
'아파트연식'은 상관계수가 낮은 feature 'transaction_year'와 중요도가 낮은 feature 'year_of_completion'로부터 나온 파생 feature이기 때문에 모델에 좋은 영향을 끼치기 어려웠던 것입니다.

Q. 상관계수가 크거나, 중요도가 높은 feature을 활용해 만든 파생 feature는 언제나 좋은 feature가 될 수 있을까?
A. 예외도 존재합니다. 대표적인 예시가 addr_kr입니다.
addr_kr처럼 값이 데이터에 대해 너무 세분화되어있는 feature는 target과 밀접한 상관관계를 갖는 것 처럼 보이지만, test에서는 무용지물이 됩니다.
실제로 addr_kr을 label encoding해 target과의 상관관계가 1로 나오지만, 이걸로 모델을 학습시켜 validation score를 뽑아보면 성능이 대폭 하락합니다.
이는 addr_kr이 너무 세분화되어 모델의 과적합을 적극 유도했기 때문입니다.

 

2-5. 'apartment_id' drop하기

apartment_id는 drop하지 않았습니다.
 
머신러닝에서 feature의 drop은 곧 데이터 유실을 뜻합니다.
따라서 데이터의 row든 column이든, drop할 때에는 아주 신중하게 선택해야합니다.
 

파생 feature가 아니라는 가정하에, 어떤 feature가 있든 없든 성능에 차이가 없다면 되도록 feature를 drop하지 않는 것이 좋습니다.

 
따라서, 저는 apartment_id를 drop하지 않았습니다.
 
 

3-3, 3-4. 'jibun'과 'addr_kr'

'jibun'과 'addr_kr'은 feature engineering을 수행하지 않았습니다.
'jibun'은 target과 매우 낮은 상관관계를 보이고, 모델 학습 시 유의미한 결과를 보이지 않습니다. 이는 target encoding을 하였을 때도 마찬가지입니다. 때문에 feature에서 아예 제외하였습니다.
 
'addr_kr'은 위에서 언급한 것과 같이 너무 세분화되어있어 모델에 넣을 경우 과적합을 초래할 수 있습니다.
 
'addr_kr'을 사용해서 각 아파트의 위도/경도를 찾아 파생 feature로 넣어주는 방법도 있습니다.
 
지도 API를 불러와 (12만건의 train + 5,500건의 test) = 약 12.5만 row의 주소를 일일이 검색하고, 검증하여 넣어주면 되는데요,
이는 상당한 computation cost와 노동력을 요하므로 다루지 않겠습니다.
 

다만 이 방법을 활용하면 현 1등도 충분히 쟁취할 수 있을 것이라고 생각합니다.

 

3-6. 'transaction_date' 그룹

transaction_date는 1~10, 11~20, 21~말일 중 하나의 값을 가집니다.
하지만, 달별로 말일 날짜가 다른 탓에 '21~말일' 값이 '21~28', '21~29', '21~30', '21~31'로 나뉘었습니다.
 
따라서 이 모든 값을 '21~30'로 변경해 노이즈를 줄여주었습니다.
 
하지만 이렇게 만든 feature 'transcation_date'는 모델에서 큰 중요도를 차지하지 못했을 뿐더러,
적용 시 오히려 모델의 성능을 떨어뜨리는 모습까지 보였습니다.
 

1. 데이터 정렬의 규칙

수차례 EDA를 수행해보았으나, train의 데이터 정렬에서 특별한 규칙을 찾을 수 없었습니다.
'transaction_year_month', 'addr_kr', 'dong' 등 다양한 기준을 염두에 두고 관찰하였으나 끝내 찾지 못했습니다.

 

 

++) Floor은 왜 좋은 Feature가 되지 못했는가?

Floor과 target값 간의 상관계수는 0.11로, transaction_id를 제외하면 세 번째로 상관관계가 높은 feature입니다.

때문에 이론 상으로는 모델에서 큰 활약을 할 수 있는 feature입니다.

하지만, 막상 모델에서 floor는 제 구실을 하지 못합니다.

 

왜 그럴까요?

 

저는 이것이 지역, 아파트에 따라 최고/최저층이 다르기 때문이라고 생각합니다.

 

서울 OO동에서는 법적으로 아파트를 6층까지밖에 올릴 수 없지만, XX동에는 제약이 없어 고층 아파트가 많다고 가정합시다.

만약 서울 사람들이 고층건물을 선호한다면, OO동에서는 6층 매물이 제일 비싸겠지만 XX동에서 6층 매물은 상대적으로 저렴할 것입니다.

때문에 저는 'apartmet_id' 별로 데이터를 묶은 후 거기서 floor의 최댓값인 'max_floor'를 찾아

각 매물이 최고층대비 얼마나 높은지를 표현하는 'relative_floor' feature를 만들어보았는데요,

이는 오히려 모델 성능을 저하시켰습니다.

from df in [train, test]:
  df['relative_floor'] = (df.floor / df.max_floor).round(2)

 

이는 우리가 가진 dataset에 각 아파트의 최고층 매물이 있을거란 보장이 없기 때문으로 보입니다.

쉽게 말해, 우리가 찾은 'max_floor'가 실제로 그 아파트의 최고층 아닐 수도 있으므로 'relative_floor' 역시 신뢰성이 떨어지는 것입니다.

 

 



지금까지 데이콘에서 개최한 아파트 실거래가 예측 AI 경진대회 2등 풀이였습니다.
마지막까지 포기하지 않고 따라와주신 모든 분들께 박수 보내드립니다♥
 
도움이 되었다면 하트를 눌러주세요 :)
구독하시면 더 많은 데이터사이언스 정보와 대회풀이를 보실 수 있습니다!